typesafe-sdk-go
September 19, 2026 · View on GitHub
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, andScorequestions and their answers - Single
SystemOnecall 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.Clienttransport 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 useSystemOne) when you need to distinguish "absent" from "zero". - No metadata on success.
SystemOneAsreturns only your type, so the request ID and headers are not attached. They remain on*ResponseValidationErrorwhen decoding fails, and onSystemOne. - Unknown answer types are dropped — from both the lifted keys and any
retained
answersobject — so a field for a future answer type stays zero-valued rather than erroring. UseSystemOneto inspect unknown answers asUnknownAnswer.
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:
| Setting | Environment variable | Default |
|---|---|---|
| API key (required) | TYPESAFE_API_KEY | — |
| Base URL | TYPESAFE_BASE_URL | https://api.typesafe.ai |
| Default model | TYPESAFE_DEFAULT_MODEL | jev-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
- Package reference: pkg.go.dev
- API wire contract: TypeSafe API docs
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.