typesafe-sdk-go

September 19, 2026 · View on GitHub

CI Go Reference Go Report Card

An idiomatic Go client for the TypeSafe AI System One API.

Important

Unofficial and unaffiliated. This project is not created, maintained, endorsed by, or associated with TypeSafe AI in any way. It is an independent, community-maintained SDK that aims for 1:1 feature parity with the official JavaScript and Python SDKs — the same wire contract, environment configuration, retry semantics, request metadata, and typed errors — adapted to idiomatic Go. All product names, logos, and brands are property of their respective owners.

Features

  • Typed Noul, Choice, and Score questions and their answers
  • Single SystemOne call with typed answer lookup helpers
  • Generic SystemOneAs[T] to decode responses into your own typed structs
  • Model discovery via client.Models.List
  • Environment-based configuration with explicit overrides
  • Configurable per-attempt timeouts and exponential-backoff retries
  • Request IDs and raw response metadata on every call
  • Injectable *http.Client transport for framework integration and tests
  • Status-specific typed errors that work with errors.As
  • Context-aware cancellation throughout

Install

go get github.com/valksor/typesafe-sdk-go

Requires Go 1.25 or newer.

Quick start

Set TYPESAFE_API_KEY, then ask typed questions:

package main

import (
	"context"
	"fmt"
	"log"

	typesafe "github.com/valksor/typesafe-sdk-go"
)

func main() {
	client, err := typesafe.NewClient()
	if err != nil {
		log.Fatal(err)
	}

	response, err := client.SystemOne(context.Background(), typesafe.SystemOneRequest{
		State: map[string]any{"document": "I was charged twice. Please fix this ASAP."},
		Questions: map[string]typesafe.Question{
			"category": typesafe.Choice("What is this ticket about?", map[string]any{
				"billing": nil, "technical": nil, "other": nil,
			}),
		},
	})
	if err != nil {
		log.Fatal(err)
	}

	category, ok := response.Choice("category")
	if !ok {
		log.Fatal("category was not a choice answer")
	}
	fmt.Println(category.Choice, category.Confidence)
}

Noul, Choice, and Score create the three question types. Responses keep all answers in SystemOneResponse.Answers and provide typed lookup helpers (Noul, Choice, Score). List available models with client.Models.List(ctx).

Typed responses (SystemOneAs)

SystemOneAs[T] answers the same request as SystemOne but decodes the response into a type you define. Each answer is lifted from answers.{name} to a top-level key, so you can declare typed answer fields directly:

type Triage struct {
	Model    string                `json:"model"`
	Urgent   typesafe.NoulAnswer   `json:"urgent"`
	Category typesafe.ChoiceAnswer `json:"category"`
}

triage, err := typesafe.SystemOneAs[Triage](context.Background(), client, typesafe.SystemOneRequest{
	State: map[string]any{"document": "I was charged twice."},
	Questions: map[string]typesafe.Question{
		"urgent":   typesafe.Noul("Is this urgent?"),
		"category": typesafe.Choice("What is this about?", map[string]any{"billing": nil, "other": nil}),
	},
})
if err != nil {
	log.Fatal(err)
}
fmt.Println(triage.Category.Choice)

Three behaviors differ from SystemOne, by design:

  • Presence is not validated. Decoding uses encoding/json, which checks field types but not presence. A missing answer or field decodes to its zero value with no error. Declare pointer fields and nil-check them (or use SystemOne) when you need to distinguish "absent" from "zero".
  • No metadata on success. SystemOneAs returns only your type, so the request ID and headers are not attached. They remain on *ResponseValidationError when decoding fails, and on SystemOne.
  • Unknown answer types are dropped — from both the lifted keys and any retained answers object — so a field for a future answer type stays zero-valued rather than erroring. Use SystemOne to inspect unknown answers as UnknownAnswer.

T may also keep the pruned answers object alongside lifted fields by declaring a nested Answers map[string]json.RawMessage (or a struct of known answer types). The full Answers interface map and the Noul/Choice/Score lookup helpers are available only on SystemOne (an interface map cannot be JSON-decoded).

Configuration

NewClient accepts an optional Config. Explicit values take precedence over environment variables:

SettingEnvironment variableDefault
API key (required)TYPESAFE_API_KEY
Base URLTYPESAFE_BASE_URLhttps://api.typesafe.ai
Default modelTYPESAFE_DEFAULT_MODELjev-latest

The default timeout is 10 seconds per attempt. The client retries HTTP 408, 429, and 5xx responses, connection errors, and timeouts twice with exponential backoff. Pass Config.Retry for client-wide behavior or per-call RequestOptions to override it for a single request.

Error handling

HTTP failures support errors.As with *typesafe.APIError and status-specific types:

resp, err := client.SystemOne(ctx, req)
if err != nil {
	var rateLimit *typesafe.RateLimitError
	var apiErr *typesafe.APIError
	switch {
	case errors.As(err, &rateLimit):
		time.Sleep(rateLimit.RetryAfter)
	case errors.As(err, &apiErr):
		log.Printf("api error %d (request %s): %s", apiErr.StatusCode, apiErr.RequestID, apiErr.Message)
	default:
		log.Fatal(err)
	}
}

Status codes map to *BadRequestError (400), *AuthenticationError (401), *PermissionDeniedError (403), *NotFoundError (404), *ConflictError (409), *UnprocessableEntityError (422), *RateLimitError (429), and *InternalServerError (5xx). Transport-level failures surface as *APIConnectionError, *APITimeoutError, and *APIUserAbortError.

A successful HTTP response whose body cannot be decoded surfaces as *ResponseValidationError, which carries the request metadata (.Meta) and the raw body (.Body) and wraps ErrInvalidResponse. Non-2xx responses still return a typed *APIError carrying .RequestID for both SystemOne and SystemOneAs; what SystemOneAs omits is metadata on a successful decode (use SystemOne when you need the request ID on success). Avoid logging .Body unredacted in production — it echoes the request state, which may contain sensitive data.

Documentation

Testing

go test -race -cover ./...

Live integration tests are opt-in and read-only (they exercise GET /v1/models):

TYPESAFE_RUN_LIVE_TESTS=1 TYPESAFE_API_KEY=... go test -run Integration ./...

Releases

This SDK tracks the same release version line as the official JavaScript and Python SDKs. Version, the latest changelog entry, and the Git tag must all match exactly — for example, release 0.6.0 with tag v0.6.0. See docs/changelog.md.

CI tests Go 1.25 and 1.26. The publish workflow can be run manually as a dry run; pushing the matching vX.Y.Z tag from the default branch creates the GitHub Release, and the Git tag itself publishes the Go module version.

License

MIT