Opencode Go SDK

repository·main·Indexed 41 days ago

https://github.com/anomalyco/opencode-sdk-go

A type-safe, high-level Go library for interacting with the Opencode REST API. It features automatic pagination, retries, and a generic Field[T] type to handle nullability and zero values. The SDK includes an AgentService for managing agents, their operational modes, and permissions for bash, editing, and web-fetching.

Tokens
27.9K
Snippets
101
Records
138
Agent score
50%

What's inside Opencode

  1. How request fields and zero values work

    main

    The SDK uses a generic Field[T] type for all request parameters. This distinguishes between a zero value (like 0, false, or "") and a field that is null or omitted.

    • To send a value: Use opencode.F(value) or specific helpers like opencode.String(), opencode.Int(), or opencode.Float().
    • To send null: Use opencode.Null[T]().
    • To send a nonconforming type: Use opencode.Raw[T](value) (e.g., sending a float where an int64 is expected).

    Any field not specified in the params struct is not sent in the request.

    params := FooParams{
    	Name: opencode.F("hello"),
    
    	// Explicitly send `"description": null`
    	Description: opencode.Null[string](),
    
    	Point: opencode.F(opencode.Point{
    		X: opencode.Int(0),
    		Y: opencode.Int(1),
    
    		// In cases where the API specifies a given type, but you want to send something else:
    		Z: opencode.Raw[int64](0.01), // sends a float
    	}),
    }
  2. How response objects and the .JSON field work

    main

    Response fields are value types. If a field is null or missing in the API response, the SDK returns the Go zero value for that type.

    To distinguish between a zero value and a missing/null field, use the .JSON property on the response struct. The .JSON field provides metadata about the raw response:

    • res.JSON.Field.IsNull(): True if the field was explicitly null or not present.
    • res.JSON.Field.IsMissing(): True if the key was not present in the JSON at all.
    • res.JSON.Field.IsInvalid(): True if the data could not be coerced to the expected type. Use res.JSON.Field.Raw() to get the raw bytes for manual unmarshaling.
    • res.JSON.ExtraFields(): Returns a map[string]Field containing properties present in the JSON that were not defined in the SDK's response struct.
    if res.Name == "" {
    	// true if `"name"` is either not present or explicitly null
    	res.JSON.Name.IsNull()
    
    	// true if the `"name"` key was not present in the response JSON at all
    	res.JSON.Name.IsMissing()
    
    	// When the API returns data that cannot be coerced to the expected type:
    	if res.JSON.Name.IsInvalid() {
    		raw := res.JSON.Name.Raw()
    
    		legacyName := struct{
    			First string `json:"first"`
    			Last  string `json:"last"`
    		}{}
    		json.Unmarshal([]byte(raw), &legacyName)
    		name = legacyName.First + " " + legacyName.Last
    	}
    }
    
    // Accessing undocumented fields
    body := res.JSON.ExtraFields["my_unexpected_field"].Raw()
  3. Handle pagination

    main

    The SDK provides two ways to handle paginated list endpoints:

    1. Auto-paging: Use .ListAutoPaging() methods to iterate through all items across all pages automatically.
    2. Manual paging: Use the standard .List() method. The returned response object includes a .GetNextPage() helper to fetch the subsequent page.
  4. Configure requests with RequestOptions

    main

    The SDK uses the functional options pattern. You can provide RequestOption functions to the NewClient constructor (to apply to all requests) or to individual method calls.

    Common options include:

    • option.WithHeader(key, value): Adds or overrides an HTTP header.
    • option.WithJSONSet(path, value): Adds an undocumented field to the request body using sjson syntax.
    • option.WithMaxRetries(n): Sets the number of retries.
    • option.WithResponseInto(&http.Response): Captures the raw HTTP response.
    client := opencode.NewClient(
    	// Adds a header to every request made by the client
    	option.WithHeader("X-Some-Header", "custom_header_info"),
    )
    
    client.Session.List(context.TODO(), ...,
    	// Override the header
    	option.WithHeader("X-Some-Header", "some_other_custom_header_info"),
    	// Add an undocumented field to the request body, using sjson syntax
    	option.WithJSONSet("some.json.path", map[string]string{"my": "object"}),
    )
  5. Handle API errors

    main

    When the API returns a non-success status code, the SDK returns an error of type *opencode.Error. This error contains the StatusCode, the original *http.Request, the *http.Response, and the error body JSON.

    Use errors.As to inspect API-specific errors and access debugging information like DumpRequest or DumpResponse.

    _, err := client.Session.List(context.TODO(), opencode.SessionListParams{})
    if err != nil {
    	var apierr *opencode.Error
    	if errors.As(err, &apierr) {
    		println(string(apierr.DumpRequest(true)))  // Prints the serialized HTTP request
    		println(string(apierr.DumpResponse(true))) // Prints the serialized HTTP response
    	}
    	panic(err.Error())
    }
  6. Make undocumented requests

    main

    If you need to interact with endpoints or parameters not explicitly defined in the SDK, you can use the following patterns:

    Undocumented Endpoints

    Use the client's HTTP verb methods (e.g., client.Get, client.Post). These respect client-level RequestOptions like retries.

    Undocumented Parameters

    Use option.WithQuerySet() for query parameters or option.WithJSONSet(path, value) for request body parameters (using sjson syntax).

    Undocumented Response Properties

    • Access the raw JSON string via result.JSON.RawJSON().
    • Access a specific field's raw JSON via result.JSON.FieldName.Raw().
    • Access extra fields via result.JSON.ExtraFields().
    // Undocumented POST request
    var result *http.Response
    err := client.Post(context.Background(), "/unspecified", params, &result)
    
    // Undocumented parameter via JSONSet
    client.Foo.New(context.Background(), params, option.WithJSONSet("data.last_name", "Doe"))
  7. Implement Middleware

    main

    You can inject custom logic into the request lifecycle using option.WithMiddleware. Middleware functions follow this signature:

    func(req *http.Request, next option.MiddlewareNext) (res *http.Response, err error)

    • Middleware is applied left-to-right when multiple are provided.
    • Middleware provided at the client level runs before middleware provided at the method level.
    • You can replace the underlying transport by using option.WithHTTPClient(client).
    func Logger(req *http.Request, next option.MiddlewareNext) (res *http.Response, err error) {
    	// Before the request
    	start := time.Now()
    
    	// Forward the request
    	res, err = next(req)
    
    	// After the request
    	end := time.Now()
    	fmt.Printf("Duration: %v\n", start-end)
    	return res, err
    }
    
    client := opencode.NewClient(option.WithMiddleware(Logger))
  8. Upload files

    main

    File uploads are handled via parameters typed as param.Field[io.Reader].

    • Default behavior: The io.Reader is sent as a multipart form part with the filename "anonymous_file" and content-type "application/octet-stream".
    • Customizing filename/content-type: If the io.Reader implementation has Name() string or ContentType() string methods (like os.File), those values will be used.
    • Helper: Use opencode.FileParam(reader, filename, contentType) to wrap any io.Reader with specific metadata.
    // Example using the helper
    fileParam := opencode.FileParam(myReader, "image.png", "image/png")
  9. Install the Opencode Go SDK

    main

    To use the Opencode Go library, import it into your project. You can pin a specific version using go get.

    Requirements

    • Go 1.22 or higher
    import (
    	"github.com/sst/opencode-sdk-go" // imported as opencode
    )
    go get -u 'github.com/sst/opencode-sdk-go@v0.19.2'
  10. Configure timeouts and retries

    main

    Timeouts

    Requests do not time out by default. Use context.Context to set a lifecycle timeout.

    Note: If a request is retried, the context timeout does not reset. To set a timeout for each individual retry attempt, use option.WithRequestTimeout(duration).

    Retries

    By default, the SDK retries certain errors 2 times with exponential backoff. Retried errors include connection errors, 408 Request Timeout, 409 Conflict, 429 Rate Limit, and any error >= 500.

    You can configure or disable retries using option.WithMaxRetries(n) at the client level or per-request.

    // Set a global timeout for the entire operation (including retries)
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
    defer cancel()
    
    client.Session.List(
    	ctx,
    	opencode.SessionListParams{},
    	// Set a per-retry timeout
    	option.WithRequestTimeout(20*time.Second),
    )
    
    // Configure max retries
    client := opencode.NewClient(option.WithMaxRetries(0)) // Disable retries
  11. Process Session Error Unions

    main

    The EventListResponseEventSessionErrorPropertiesError type uses a union pattern to provide type-safe access to different error data structures. To handle these, use the AsUnion() method and type-assert to the specific error implementation.

    Possible runtime types for the error union include:

    • shared.ProviderAuthError
    • shared.UnknownError
    • EventListResponseEventSessionErrorPropertiesErrorMessageOutputLengthError
    • shared.MessageAbortedError
    • EventListResponseEventSessionErrorPropertiesErrorAPIError
    // Example of handling a session error union
    if errProp, ok := event.Properties.Error.AsUnion().(EventListResponseEventSessionErrorPropertiesErrorAPIError); ok {
        fmt.Printf("API Error: %s (Status: %f)\n", errProp.Data.Message, errProp.Data.StatusCode)
    }
  12. Configure Agent settings (Build, General, and Plan)

    main

    Opencode uses a hierarchical agent configuration model. The ConfigAgent struct contains sub-configurations for different agent contexts: Build, General, and Plan.

    Each agent configuration (e.g., ConfigAgentBuild) supports:

    • Model: The specific model to use (e.g., anthropic/claude-3-opus).
    • Permission: Controls what actions the agent can take (Bash, Edit, Webfetch).
    • Prompt: Custom system prompts for that specific agent role.
    • Temperature & TopP: LLM sampling parameters.
    • Tools: A map of available tools and their enabled status.
    • Mode: Defines if the agent is a subagent, primary, or all.