Help us shape the future of Go! Take the 2026 Go Developer Survey

typesafe

package module
v0.7.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 19, 2026 License: MIT Imports: 24 Imported by: 0

README

TypeSafe AI Go SDK

Go Reference Go Report Card

Go SDK for TypeSafe AI — deterministic evaluation models that answer classification and rating questions about any text or JSON you already have.

Ask up to dozens of questions in one call, and get back probabilities, labels, and rubric scores instead of free-form text you have to parse.

  • Standard library only. net/http, encoding/json, and log/slog. No runtime dependencies.
  • Concurrency-safe by construction. A Client holds no per-request state and pools connections; every call takes a context.Context.
  • Resilient by default. Jittered exponential backoff with an overall retry budget and Retry-After support.
  • Typed everything. Typed errors you can match with errors.As, typed answers, and SystemOneAs[T] for your own response models.
  • Small and predictable. One request shape, three question kinds, no hidden global state.

Requires Go 1.26 or newer.


Contents


Installation

go get github.com/guchengod/typesafe-sdk-go
import typesafe "github.com/guchengod/typesafe-sdk-go"

Quickstart

Set your API key in the environment:

export TYPESAFE_API_KEY="ts_live_..."

Then create a client and ask questions about your content:

package main

import (
	"context"
	"fmt"
	"log"

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

func main() {
	client, err := typesafe.NewClient() // reads TYPESAFE_API_KEY
	if err != nil {
		log.Fatal(err)
	}
	defer client.Close()

	response, err := client.SystemOne(
		context.Background(),
		map[string]any{
			"channel":  "email",
			"document": "I was charged twice on my credit card for invoice #4912. Please refund immediately.",
		},
		typesafe.Questions{
			"billing": typesafe.NewNoul("Is this message about billing or payments?"),
			"category": typesafe.NewChoice(
				map[string]any{
					"billing":   "Charges, refunds, or invoices",
					"technical": "Bugs, crashes, or API problems",
					"other":     nil, // nil means the label has no description
				},
				typesafe.WithInstructions("What department should handle this ticket?"),
			),
			"urgency": typesafe.NewScore(
				[]any{"Can wait until next week", "Needs attention within 48 hours", "Requires same-day attention"},
				typesafe.WithInstructions("How urgently does this customer require a response?"),
			),
		},
	)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("billing p(true) = %.2f\n", response.Nouls()["billing"].Noul)
	fmt.Printf("category        = %s (confidence %.2f)\n", response.Choices()["category"].Choice, response.Choices()["category"].Confidence)
	fmt.Printf("urgency         = %.2f of 2\n", response.Scores()["urgency"].Score)
}

Learn what TypeSafe is, what it can do, and how to use it in the TypeSafe documentation.


What you can build

Every use case below is the same call: a state (the content) plus a map of named questions. The question kind decides the answer shape.

Use case Typical questions Answer kinds
Support ticket triage — route work to the right queue before a human reads it Which department? Is it about billing? How urgent is it? Is it a duplicate? choice, noul, score
Content moderation — screen user-generated text at ingest Is this spam or abuse? Which policy category? How severe? noul, choice, score
Intent and sentiment analysis — turn free text into fields What is the intent? What is the tone? Is the customer at churn risk? choice, noul
LLM output evaluation — score a model's answer before showing it Is it grounded in the source? Does it follow the format? How good is it? noul, score
Guardrails and routing for agent pipelines — decide what runs next Does this need a human? Is the input in scope? Which tool should handle it? noul, choice
Data labeling and enrichment — tag a corpus or a backlog Is this relevant to topic X? Project management vs. personal? noul, choice
Structured extraction — pull fields out of unstructured records Is the sender a company or an individual? Which product is named? choice, noul

A worked example, triaging tickets as they arrive:

type Ticket struct {
	ID       int
	Subject  string
	Body     string
	Customer string
}

questions := typesafe.Questions{
	"queue": typesafe.NewChoice(
		map[string]any{
			"billing":   "Charges, refunds, invoices, or payment problems",
			"technical": "Bugs, crashes, or API problems",
			"account":   "Logins, access, or account settings",
			"other":     nil,
		},
		typesafe.WithInstructions("Which queue should handle this ticket?"),
	),
	"needs_human": typesafe.NewNoul(
		"",
		typesafe.WithInstructions("Does this ticket require a human response, or can it be automated?"),
	),
	"priority": typesafe.NewScore(
		[]any{"routine", "elevated", "critical"},
		typesafe.WithInstructions("How severe is this issue?"),
	),
}

for _, ticket := range tickets {
	state := map[string]any{"subject": ticket.Subject, "body": ticket.Body, "customer": ticket.Customer}

	response, err := client.SystemOne(ctx, state, questions)
	if err != nil {
		log.Printf("ticket %d: %v", ticket.ID, err)
		continue
	}

	answer := response.Choices()["queue"]
	log.Printf("ticket %d -> %s (%.2f) escalate=%v priority=%.1f",
		ticket.ID, answer.Choice, answer.Confidence, response.Nouls()["needs_human"].Noul > 0.5, response.Scores()["priority"].Score)
}

Each answer carries the confidence the model has in it, so you can automate the confident cases and hand the uncertain ones to a person.


How it works

One call, SystemOne, evaluates a set of questions against a state:

  • State — the content the questions refer to. Any JSON-encodable value: a string, a map, a struct, or an array. Send a ticket, a message, a document, a model's answer, or a record from your database.
  • Questions — a map of names you choose to questions. The names are how you read the answers back, so name them after the decision they support ("queue", "is_spam", "urgency").
  • Answers — a response keyed by those names, one answer per question, whose type always matches the question's type. The response also reports the model that answered and the token usage.
response, err := client.SystemOne(ctx, state, questions)
if err != nil {
	return err
}
fmt.Println(response.Nouls()["is_billing"].Noul)     // 0.98
fmt.Println(response.Choices()["queue"].Choice)      // billing
fmt.Println(response.Scores()["priority"].Score)     // 1.7

To see which models your account can use, and which one model accepts:

models, err := client.Models.List(ctx)
if err != nil {
	return err
}
for _, model := range models.Models {
	fmt.Printf("%s\t%s\t%s\n", model.Name, model.ReleaseDate, model.Description)
}

Questions

Three question kinds are supported. All three accept instructions — what the model should decide — as a string, a JSON object, or an array, and all are answered with a probability distribution you can threshold yourself.

Noul — yes/no

The probability that a statement holds, in [0, 1]. Values near 1 mean yes or true, values near 0 mean no or false, and values near 0.5 mean the content does not decide it.

typesafe.NewNoul("Is this message spam?")

typesafe.NewNoul("Is this message spam?", typesafe.WithNoulCriteria(typesafe.NoulCriteria{
	True:  "Unsolicited advertising or phishing",
	False: "A legitimate conversation",
}))

NewNoul(instructions any, opts ...QuestionOption) *NoulQuestion — the instructions may be a string, a map, or a slice, and may be nil when criteria alone describe the question. Criteria are free-form: use a string, or a structured object when examples help.

typesafe.NewNoul("", typesafe.WithNoulCriteria(typesafe.NoulCriteria{
	True: map[string]any{"meaning": "Payments or invoices", "examples": []any{"charged twice", "refund"}},
}))

Threshold the answer where your decision needs it:

if response.Nouls()["is_spam"].Noul > 0.9 {
	quarantine(message)
}
Choice — categorical

The best-matching label, with the model's confidence and a probability for every label. Use it whenever the options are known ahead of time and exactly one applies.

typesafe.NewChoice(
	map[string]any{
		"billing":   "Charges, refunds, or invoices",
		"technical": "Bugs, crashes, or API problems",
		"other":     nil, // nil means "no description"
	},
	typesafe.WithInstructions("Which department should handle this request?"),
)

NewChoice(criteria map[string]any, opts ...QuestionOption) *ChoiceQuestion. Each criteria value is a label description: a string, a JSON object, an array, or nil for a label the name already describes. A choice question with no criteria is rejected before the request is sent.

answer := response.Choices()["category"]
fmt.Println(answer.Choice)         // billing
fmt.Println(answer.Confidence)     // 0.87
fmt.Println(answer.Probabilities)  // map[billing:0.87 technical:0.09 other:0.04]
Score — ordinal rubric

An expected value across an ordered rubric whose levels start at 0. Use it for priority, quality, and severity scales where "how much" matters more than "which one".

typesafe.NewScore(
	[]any{"routine", "elevated", "critical emergency"},
	typesafe.WithInstructions("Rate the severity of this issue"),
)

NewScore(criteria []any, opts ...QuestionOption) *ScoreQuestion. The score is the probability-weighted average of the rubric levels, so it may fall between two levels; the reported legend and probabilities are keyed by level, so you can always read the distribution. An empty rubric is rejected with a *typesafe.ScoreError.

answer := response.Scores()["urgency"]
fmt.Println(answer.Score)                  // 1.7
fmt.Println(answer.Legend[2])              // "critical emergency"
fmt.Println(answer.Probabilities[2])       // 0.8
Question options
Option Effect
WithInstructions(any) The question to ask: a string, a JSON object, or an array. Overrides instructions passed positionally.
WithNoulCriteria(NoulCriteria) Describes the yes and no outcomes of a noul question.
WithQuestionField(key string, value any) Adds a field this version does not model to the wire form; a colliding field replaces the modeled one.

Instructions that are nil or blank are left off the wire entirely, so typesafe.NewNoul("", typesafe.WithNoulCriteria(...)) sends a question described only by its criteria. A structured instruction (an object or an array) is always sent, even when empty.

Raw questions

RawQuestion sends a question as a plain map, for request fields this version does not model:

typesafe.Questions{
	"billing": typesafe.RawQuestion{"type": "noul", "instructions": "Is this about billing?"},
	"queue": typesafe.RawQuestion{
		"type":       "choice",
		"criteria":   map[string]any{"billing": nil, "other": nil},
		"weight":     3, // a hypothetical field a newer API accepts
	},
}

NormalizeQuestions(questions) is the same validation the client applies. It accepts Questions, map[string]Question, the typed question maps (map[string]*NoulQuestion and siblings), map[string]any, and RawQuestion, and rejects an empty set with At least one question is required.


Responses

SystemOne returns a *SystemOneResponse:

response.Model             // the model that answered; may differ from the requested alias
response.Usage             // Usage{InputTokens, OutputTokens *int} — nil when not reported
response.Answers           // map[string]Answer, keyed by question name
response.Nouls()           // map[string]*NoulAnswer
response.Choices()         // map[string]*ChoiceAnswer
response.Scores()          // map[string]*ScoreAnswer
response.RawRequestID()    // x-typesafe-request-id, or ""
response.RequestID()       // (string, error) — errors when the header is absent
response.RawBody()         // the payload exactly as received
response.RawHTTPResponse() // (*http.Response, error)

Answers carry the primitive's data: NoulAnswer{Type, Noul}, ChoiceAnswer{Type, Choice, Confidence, Probabilities}, and ScoreAnswer{Type, Score, Confidence, Legend, Probabilities}. ScoreAnswer.MarshalJSON encodes Legend and Probabilities with their integer levels as JSON object keys, so a response round-trips.

Usage is a value type with a String method, so fmt.Println(response.Usage) prints Usage{input_tokens: 492, output_tokens: 72}.

Decoding rules
  • model and usage are required. A success response missing either — or holding a field of the wrong shape, including an explicit null — is reported as an *APIResponseValidationError naming the dotted field path, such as answers.tone.confidence.
  • answers is optional and defaults to empty.
  • An answer whose type is not noul, choice, or score is dropped from Answers and the grouped accessors, so a newer server cannot break an older client. It stays reachable through RawBody().
  • client.Models.List(ctx) returns a *ListModelsResponse; every field of every model (name, description, release_date) is required.

A malformed response is never silently coerced into a plausible one: a missing probability, a null where a number belongs, or a non-integer rubric level all surface as an error naming the field, rather than a zero value you might act on.


Custom response models

SystemOneAs[T] decodes the payload into a struct you define, so the compiler knows the shape of your answers:

type TicketAnalysis struct {
	Model   string                `json:"model"`
	Usage   typesafe.Usage        `json:"usage"`
	Tone    typesafe.ChoiceAnswer `json:"tone"`    // lifted from answers
	Billing *typesafe.NoulAnswer  `json:"billing"` // optional: a pointer can be absent
	Summary string                `json:"summary,omitempty"`
}

ticket, err := typesafe.SystemOneAs[TicketAnalysis](ctx, client, state, typesafe.Questions{
	"tone":    typesafe.NewChoice(map[string]any{"calm": nil, "angry": nil}),
	"billing": typesafe.NewNoul("Is this about billing?"),
})
if err != nil {
	return err
}
fmt.Printf("tone=%s billing=%.2f\n", ticket.Tone.Choice, ticket.Billing.Noul)

Answer lifting. T may declare the envelope fields (model, usage, answers) and, in addition, one field per answer, named after the question. Each such field is populated from the matching entry of answers. A real envelope field is never displaced by an answer of the same name, and answer types this version does not model are dropped, exactly as in Client.SystemOne.

To keep every answer keyed by question name, declare Answers typesafe.Answers \json:"answers"`; typesafe.Answersdecodes each entry from itstypefield, which a baremap[string]typesafe.Answercannot do. A nested struct works too —Answers struct { Tone typesafe.ChoiceAnswer `json:"tone"` } `json:"answers"``.

Required fields. A top-level field is required unless it can represent absence — a pointer, map, slice, interface, function, or channel holds nil for "not reported" — or it is tagged omitempty/omitzero. Untagged, embedded, and unexported fields are not checked. A missing required field is reported as an *APIResponseValidationError naming the field rather than silently decoding to a zero value. Only the top level is checked; nested structures use standard encoding/json semantics.


Configuration

Client options

Explicit options win over environment variables, which win over the SDK defaults. Blank or whitespace-only environment values are treated as unset.

client, err := typesafe.NewClient(
	typesafe.WithAPIKey("ts_live_..."),
	typesafe.WithBaseURL("https://api.typesafe.ai"),
	typesafe.WithDefaultModel("jev-latest"),
	typesafe.WithTimeout(15*time.Second),
	typesafe.WithRetryPolicy(typesafe.DefaultRetryPolicy()),
	typesafe.WithHeader("X-Environment", "production"),
	typesafe.WithLogger(logger),
)
if err != nil {
	log.Fatal(err)
}
defer client.Close()
Option Purpose Environment fallback
WithAPIKey(string) Bearer credential sent as Authorization. Required. TYPESAFE_API_KEY
WithBaseURL(string) API root; trailing slashes are removed. TYPESAFE_BASE_URL
WithDefaultModel(string) Model used when a call names none. TYPESAFE_DEFAULT_MODEL
WithTimeout(time.Duration) HTTP timeout for one attempt. Must be positive. Adopted from WithHTTPClient's timeout when one is supplied and no explicit timeout is given. — (default 10s)
WithRetryPolicy(*RetryPolicy) Retry policy for every call. — (default policy)
WithHeader(key, value string) Default header sent with every request. —
WithHeaders(map[string]string) Merges several default headers. —
WithHTTPClient(*http.Client) Supplies the client and its transport. Mutually exclusive with WithTransport. —
WithTransport(http.RoundTripper) Supplies the transport only. Mutually exclusive with WithHTTPClient. —
WithLogger(*slog.Logger) Structured logger for requests, responses, and retries. TYPESAFE_LOG_LEVEL

NewClient fails with an *SDKError when no API key is available, when the timeout is not positive, when both WithHTTPClient and WithTransport are supplied, or when a retry policy fails validation. (*Client).Config() returns the resolved configuration, (*Client).HTTPClient() the underlying *http.Client, and (*Client).Close() releases the connection pool (idempotent).

Environment variables
Variable Meaning
TYPESAFE_API_KEY API key. Required unless WithAPIKey supplies one.
TYPESAFE_BASE_URL API root. Default https://api.typesafe.ai.
TYPESAFE_DEFAULT_MODEL Default model. Default jev-latest.
TYPESAFE_LOG_LEVEL debug, info, warn, warning, error, or off. Unset means the SDK logs nothing.

The per-attempt timeout has no environment variable; set it with WithTimeout or a per-call WithCallTimeout. The names above are exported as typesafe.APIKeyEnv, typesafe.BaseURLEnv, typesafe.DefaultModelEnv, and typesafe.LogLevelEnv.


Per-call options

SystemOne and Models.List accept request options that override the client for one call without mutating it:

response, err := client.SystemOne(ctx, state, questions,
	typesafe.WithModel("jev-fast"),
	typesafe.WithCallTimeout(5*time.Second),
	typesafe.WithCallRetry(policy),
	typesafe.WithCallHeader("X-Correlation-ID", "1234"),
	typesafe.WithExtraBody(map[string]any{"max_output_tokens": 512}),
)
Option Effect
WithModel(string) Overrides the model for this call.
WithCallTimeout(time.Duration) Overrides the HTTP timeout for one attempt of this call. Must be positive.
WithCallRetry(*RetryPolicy) Overrides the retry policy for this call. The client's policy is left untouched.
WithCallHeader(key, value string) Sets or overrides one request header.
WithCallHeaders(map[string]string) Merges several request headers.
WithExtraBody(map[string]any) Shallow-merges extra top-level body fields over state, model, and questions. Last write wins: a colliding key replaces the SDK's value and object values are replaced, not deep-merged.

Retry policy

Retries are on by default. DefaultRetryPolicy() returns:

Field Default Meaning
MaxRetries 2 Retries after the initial attempt. 0 disables retries.
BackoffInitial 500ms First backoff delay; doubles per attempt up to BackoffMax. 0 disables backoff.
BackoffMax 5s Cap on a single backoff delay. 0 disables backoff.
BackoffJitter 0.25 Fraction of each delay randomly subtracted, between 0 and 1.
HTTPStatuses 408, 429, and every 5xx Status codes that are retried. DefaultRetryStatuses() builds this set.
RespectRetryAfter true Honors retry-after-ms and Retry-After (seconds or HTTP date).
APIConnectionError true Retries failures that never produced an HTTP response.
APITimeoutError true Retries requests that exceeded their timeout.
RetryErrors nil Extra sentinel errors that trigger a retry; matched with errors.Is.
Predicate nil func(error) bool consulted for every failure; true triggers a retry.
Timeout 30s (DefaultRetryBudget) Total budget for one call, covering the initial attempt, retries, and delays. A retry whose delay would reach the budget is abandoned and the last error is returned. nil disables the budget.
Sleep nil Seam for tests: func(ctx, delay) error replaces the context-aware timer.
RandFloat nil Seam for tests: func() float64 in [0, 1) replaces the jitter source.

Start from the default and adjust, or set only the fields you care about:

policy := typesafe.DefaultRetryPolicy()
policy.MaxRetries = 4
policy.BackoffInitial = 250 * time.Millisecond
policy.Timeout = new(15 * time.Second)

client, err := typesafe.NewClient(typesafe.WithRetryPolicy(policy))

A cancelled or expired context is never retried. (*RetryPolicy).Validate() reports malformed policies, (*RetryPolicy).Clone() deep-copies one, and (*RetryPolicy).Execute(ctx, fn) runs a function under the policy. When a retry happens, the SDK adds the X-TypeSafe-Retry-Count header (1 for the first retry, 2 for the second, …) so the server can trace attempts.


Errors

Every error the SDK returns implements typesafe.TypeSafeError. Unsuccessful HTTP statuses map onto typed errors, and each typed error unwraps to *APIError, so errors.As finds both the specific and the general form.

Status Type
400 *BadRequestError
401 *AuthenticationError
403 *PermissionDeniedError
404 *NotFoundError
422 *UnprocessableEntityError
429 *RateLimitError (adds RetryAfterMS *float64 and RetryAfter() (time.Duration, bool))
5xx *InternalServerError
any other non-2xx *APIError

Non-HTTP failures are also typed:

Type Raised for
*SDKError Invalid configuration or input, an unencodable body, an unreadable response. Never an unsuccessful status.
*APIConnectionError The request never produced an HTTP response.
*APITimeoutError The request exceeded its timeout; carries Timeout time.Duration.
*APIResponseValidationError A success status whose body does not match the schema; carries FieldPath.
*ScoreError A score question with an empty rubric.

*APIError carries Status, Body, RawBody, Headers, Endpoint ("METHOD URL", with credentials, query, and fragment removed), RequestID, and Message. Its Error() reads <METHOD URL>: <status> <message> (request_id=<id>), omitting the parts that do not apply.

response, err := client.SystemOne(ctx, state, questions)
if err != nil {
	var rateLimit *typesafe.RateLimitError
	var validation *typesafe.APIResponseValidationError
	var apiErr *typesafe.APIError
	var timeout *typesafe.APITimeoutError

	switch {
	case errors.As(err, &rateLimit):
		if wait, ok := rateLimit.RetryAfter(); ok {
			log.Printf("rate limited, retry after %v", wait)
		}
	case errors.As(err, &validation):
		log.Printf("invalid response field %q", validation.FieldPath)
	case errors.As(err, &apiErr):
		log.Printf("http %d (request_id=%s): %s", apiErr.Status, apiErr.RequestID, apiErr.Message)
	case errors.As(err, &timeout):
		log.Printf("timed out after %v", timeout.Timeout)
	default:
		log.Printf("system one failed: %v", err)
	}
	return err
}

The same checks are available as helpers: typesafe.AsAPIError(err), typesafe.AsRateLimitError(err), typesafe.AsTimeoutError(err), and typesafe.AsValidationError(err), each returning (value, bool).


Logging and redaction

The SDK logs through log/slog. It logs nothing unless you configure it: without WithLogger and without TYPESAFE_LOG_LEVEL, records are discarded, which is what library code should do.

// Quick default: send info records to stderr.
// export TYPESAFE_LOG_LEVEL=debug

// Or own the handler:
logger := slog.New(typesafe.NewRedactingHandler(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
	Level: slog.LevelDebug,
})))
client, err := typesafe.NewClient(typesafe.WithLogger(logger))
  • ParseLogLevel(name) converts debug, info, warn, warning, error, or off into a slog.Level.
  • Requests, responses, and retries log at info; request.wire and the header and body attributes of a response log at debug.
  • NewRedactingHandler(handler) wraps any slog.Handler and replaces credential-bearing attributes with "***" before they reach it. It matches well-known credential headers (Authorization, Proxy-Authorization, X-API-Key, API-Key, Cookie, Set-Cookie) and, additionally, any attribute whose name contains token or secret. Values held in a map[string]string attribute are inspected key by key.

Headers are redacted; bodies are not. Keep credentials out of state, and prefer log levels below debug in production.


Security

  • Credential redaction. The SDK redacts well-known credential headers, plus any header whose name contains token or secret, from every record it logs. NewRedactingHandler extends the guarantee to records you route through the SDK's logger.
  • URL sanitization. Error endpoints are rendered as "METHOD URL" with userinfo, query parameters, and fragments removed, so a credential embedded in a URL never reaches an error message or a log line.
  • Protected headers. Authorization, Accept, User-Agent, X-TypeSafe-SDK, and X-TypeSafe-Runtime are applied after your default and per-call headers and cannot be displaced, whatever case you spell them in. A caller-supplied X-TypeSafe-Retry-Count is stripped, and Content-Type is set only when the request has a body.
  • No redirects. The SDK's own client does not follow redirects, so the API key is never replayed to another host. A 3xx response surfaces as an *APIError. A client you supply is used as-is.
  • Bounded responses. Response bodies are read through a 16 MB limit (typesafe.MaxResponseBodyLimit), and an oversized payload is rejected rather than truncated into a confusing decode failure. Error messages include at most MaxErrorBodyLength (200) bytes of an unrecognized body.
  • Strict decoding. Missing fields, null where a value belongs, and wrong-typed fields in a success response are reported as errors naming the field, never silently decoded into a zero value that reads as a real answer.
  • TLS 1.2+. The SDK's own transport refuses TLS versions below 1.2, enables HTTP/2 when available, and pools connections. A transport you supply is used as-is.

Concurrency

A Client is safe for concurrent use by any number of goroutines. It holds no per-request state, its configuration is read-only after construction, and the underlying http.Client pools connections (MaxIdleConnsPerHost: 100). Create one client, share it, and pass a context.Context per call for cancellation and deadlines:

var wg sync.WaitGroup
for _, ticket := range tickets {
	wg.Add(1)
	go func(ticket string) {
		defer wg.Done()
		response, err := client.SystemOne(ctx, ticket, questions)
		if err != nil {
			log.Printf("%s: %v", ticket, err)
			return
		}
		fmt.Printf("%s -> %s\n", ticket, response.Choices()["category"].Choice)
	}(ticket)
}
wg.Wait()

Use a semaphore or a worker pool if you need to bound how many calls are in flight at once. Close releases idle connections; call it once, when the program is done with the client.


Examples

Runnable programs live in this repository:

Example Shows
examples/quickstart One SystemOne call with all three primitives, printing every answer.
examples/advanced SystemOneAs with a custom model, Models.List, per-call options, WithExtraBody, typed error handling, and a redacting logger.
export TYPESAFE_API_KEY="ts_live_..."
go run ./examples/quickstart
go run ./examples/advanced

License

MIT License. See LICENSE for details.

Documentation

Overview

Package typesafe is the official Go SDK for TypeSafe AI (https://typesafe.ai).

The TypeSafe AI platform evaluates deterministic classification and rating primitives through System One models. A Client sends one request that answers any number of named questions about a state, and returns every answer keyed by the name you supplied.

Primitives

Three question kinds are supported:

  • Noul: the probability that a statement holds, from 0 to 1.
  • Choice: the best-matching criteria key, its confidence, and a probability per key.
  • Score: an expected value across an ordered rubric whose levels start at 0.

Quickstart

client, err := typesafe.NewClient() // reads TYPESAFE_API_KEY
if err != nil {
	log.Fatal(err)
}
defer client.Close()

response, err := client.SystemOne(ctx,
	map[string]any{"document": "I was charged twice. Please fix this ASAP."},
	typesafe.Questions{
		"billing": typesafe.NewNoul(
			"Is this ticket about billing?",
			typesafe.WithNoulCriteria(typesafe.NoulCriteria{
				True:  "Charges, refunds, or invoices",
				False: "Anything else",
			}),
		),
		"category": typesafe.NewChoice(
			map[string]any{"billing": nil, "technical": nil, "other": nil},
			typesafe.WithInstructions("What is this ticket about?"),
		),
		"urgency": typesafe.NewScore(
			[]any{"can wait", "this week", "today"},
			typesafe.WithInstructions("How urgent is this issue?"),
		),
	},
)
if err != nil {
	log.Fatal(err)
}

fmt.Printf("billing: %.2f\n", response.Nouls()["billing"].Noul)
fmt.Printf("category: %s (%.2f)\n", response.Choices()["category"].Choice, response.Choices()["category"].Confidence)
fmt.Printf("urgency: %.2f\n", response.Scores()["urgency"].Score)

Custom Response Models

SystemOneAs decodes the payload into a caller-defined struct, so the compiler knows the shape of your answers. The struct may declare the envelope fields and one field per answer, named after the question:

type Ticket struct {
	Model   string                 `json:"model"`
	Usage   typesafe.Usage         `json:"usage"`
	Billing typesafe.NoulAnswer    `json:"billing"`
	Tone    *typesafe.ChoiceAnswer `json:"tone"` // optional: a pointer can be absent
}

result, err := typesafe.SystemOneAs[Ticket](ctx, client, state, questions)

A top-level field that cannot represent absence — anything but a pointer, map, slice, interface, function, or channel — is required, unless it is tagged omitempty or omitzero. A missing required field is reported as an APIResponseValidationError naming the field.

Errors

Failures are typed and match with errors.As. Unsuccessful HTTP statuses map onto APIError and its status-specific subclasses (BadRequestError, AuthenticationError, PermissionDeniedError, NotFoundError, UnprocessableEntityError, RateLimitError, InternalServerError); each unwraps to APIError, so a single check finds both forms. Transport failures are APIConnectionError or APITimeoutError, a malformed success body is APIResponseValidationError, and invalid configuration or input is SDKError. AsAPIError, AsRateLimitError, AsTimeoutError, and AsValidationError are convenience extractors.

Configuration

NewClient resolves configuration from its Option values first, then the environment (TYPESAFE_API_KEY, TYPESAFE_BASE_URL, TYPESAFE_DEFAULT_MODEL, TYPESAFE_LOG_LEVEL), then the SDK defaults. Client.SystemOne and ModelsService.List accept RequestOption values that override the client for a single call without mutating it: WithModel, WithCallTimeout, WithCallRetry, WithCallHeader, WithCallHeaders, and WithExtraBody.

Design

The SDK is engineered for high concurrency and safety:

  • Functional options: Option configures a Client, RequestOption a single call, and QuestionOption a single question.
  • Resilient retries: jittered exponential backoff with an overall budget, honoring the retry-after-ms and Retry-After headers. See RetryPolicy and DefaultRetryPolicy.
  • Security by design: credential headers are redacted from logs (NewRedactingHandler extends that to your own records), credentials in URLs are stripped from error endpoints, response bodies are bounded to 16 MB, and the SDK's transport refuses TLS below 1.2. Headers the SDK owns cannot be displaced by caller-supplied ones.
  • Concurrency: a Client holds no per-request state and is safe for concurrent use; every call takes a context.Context for cancellation and deadlines.
  • Modern Go: for range over integers, min and max, slices and maps helpers, and generics via SystemOneAs. Standard library only, and Go 1.26 or newer.

Index

Examples

Constants

View Source
const (
	// APIKeyEnv is the environment variable for the TypeSafe API key.
	APIKeyEnv = "TYPESAFE_API_KEY"

	// BaseURLEnv is the environment variable for the API base URL.
	BaseURLEnv = "TYPESAFE_BASE_URL"

	// DefaultModelEnv is the environment variable for the default model.
	DefaultModelEnv = "TYPESAFE_DEFAULT_MODEL"

	// LogLevelEnv is the environment variable for the logging level.
	LogLevelEnv = "TYPESAFE_LOG_LEVEL"
)

Environment variable names.

View Source
const (
	// DefaultBaseURL is the default API root URL.
	DefaultBaseURL = "https://api.typesafe.ai"

	// DefaultModel is the default model name.
	DefaultModel = "jev-latest"

	// DefaultTimeout is the default HTTP timeout for operations.
	DefaultTimeout = 10 * time.Second

	// DefaultRetryBudget is the default overall retry timeout budget.
	DefaultRetryBudget = 30 * time.Second

	// SDKName is the name of this SDK.
	SDKName = "typesafe-sdk-go"

	// SDKVersion is the current version of this SDK.
	SDKVersion = "0.7.0"

	// MaxErrorBodyLength limits the raw error body length included in error messages.
	MaxErrorBodyLength = 200

	// MaxResponseBodyLimit limits the maximum response body read (16 MB) to prevent OOM DOS attacks.
	MaxResponseBodyLimit = 16 * 1024 * 1024
)

Client and protocol defaults.

View Source
const (
	SystemOnePath = "/v1/systemone"
	ModelsPath    = "/v1/models"
)

API Endpoint paths.

View Source
const (
	HeaderAuthorization = "Authorization"
	HeaderAccept        = "Accept"
	HeaderContentType   = "Content-Type"
	HeaderUserAgent     = "User-Agent"
	HeaderSDK           = "X-TypeSafe-SDK"
	HeaderRuntime       = "X-TypeSafe-Runtime"
	HeaderRetryCount    = "X-TypeSafe-Retry-Count"
	HeaderRequestID     = "x-typesafe-request-id"
	HeaderRetryAfter    = "retry-after"
	HeaderRetryAfterMs  = "retry-after-ms"
	ContentTypeJSON     = "application/json"
)

Protocol headers.

Variables

This section is empty.

Functions

func DefaultRetryStatuses

func DefaultRetryStatuses() map[int]bool

DefaultRetryStatuses returns the set of HTTP status codes the SDK retries by default: 408, 429, and every 5xx.

func NormalizeQuestions

func NormalizeQuestions(questions any) (map[string]any, error)

NormalizeQuestions validates the questions before they are encoded and returns the map that is sent on the wire.

It accepts Questions, map[string]Question, map[string]*NoulQuestion and the sibling maps, map[string]any, and RawQuestion values.

Example
normalized, err := NormalizeQuestions(Questions{
	"billing": NewNoul("Is this message about billing?"),
	"tone":    NewChoice(map[string]any{"calm": nil, "angry": nil}),
})
if err != nil {
	panic(err)
}
fmt.Println(slices.Sorted(maps.Keys(normalized)))
fmt.Println(normalized["tone"].(Question).QuestionType())
Output:
[billing tone]
choice

func ParseLogLevel

func ParseLogLevel(level string) (slog.Level, bool)

ParseLogLevel converts a level name into a slog.Level. It accepts "debug", "info", "warn", "warning", "error", and "off", and reports false for any other value.

func RuntimeString

func RuntimeString() string

RuntimeString returns the runtime environment string for SDK identification, sent in the X-TypeSafe-Runtime header.

func SystemOneAs

func SystemOneAs[T any](ctx context.Context, client *Client, state any, questions any, opts ...RequestOption) (*T, error)

SystemOneAs answers questions and decodes the response into T, a caller-defined struct.

T may declare the envelope fields ("model", "usage", "answers") and, additionally, one field per answer, named after the question. Each such field is populated from the matching entry of "answers":

type Ticket struct {
	Model   string                 `json:"model"`
	Usage   typesafe.Usage         `json:"usage"`
	Billing typesafe.NoulAnswer    `json:"billing"`
	Tone    typesafe.ChoiceAnswer  `json:"tone"`
}

result, err := typesafe.SystemOneAs[Ticket](ctx, client, state, questions)

A top-level field that is not a pointer, map, slice, or interface, and that is not tagged `omitempty`, is required: its absence is reported as an APIResponseValidationError naming the field rather than silently decoding to a zero value. Answer types this SDK version does not model are dropped from the decoded document, exactly as in the Client.SystemOne path.

Types

type APIConnectionError

type APIConnectionError struct {
	// Message describes the transport failure.
	Message string

	// Cause is the underlying transport error.
	Cause error
}

APIConnectionError is returned when the request never produced an HTTP response.

func (*APIConnectionError) Error

func (e *APIConnectionError) Error() string

func (*APIConnectionError) Unwrap

func (e *APIConnectionError) Unwrap() error

Unwrap returns the underlying transport error.

type APIError

type APIError struct {
	// Status is the HTTP response status code.
	Status int

	// Body is the decoded JSON error body, the raw text for a non-JSON body, or nil for an empty body.
	Body any

	// RawBody is the exact response body as received.
	RawBody []byte

	// Headers holds the response headers.
	Headers http.Header

	// Endpoint is "METHOD URL" with credentials, query parameters, and fragments removed.
	// Empty when the response has no originating request.
	Endpoint string

	// RequestID is the x-typesafe-request-id response header, or empty when absent.
	RequestID string

	// Message is the human-readable detail shown by Error. The SDK fills it from Body when the
	// error is built; a caller constructing an APIError may leave it empty to derive it lazily or
	// set it to override the derived text.
	Message string
}

APIError is an unsuccessful HTTP response, carrying the full response metadata.

Typed subclasses such as RateLimitError embed APIError and unwrap to it, so a single

var apiErr *typesafe.APIError
errors.As(err, &apiErr)

matches every HTTP failure the SDK returns.

func AsAPIError

func AsAPIError(err error) (*APIError, bool)

AsAPIError extracts the APIError carried by any HTTP failure this SDK returns. It reports false for connection, timeout, validation-of-input, and configuration errors.

func (*APIError) Error

func (e *APIError) Error() string

Error returns the status and message with the available request context.

type APIResponseValidationError

type APIResponseValidationError struct {
	APIError

	// FieldPath is the dotted path to the offending field, such as "answers.tone.confidence".
	FieldPath string
}

APIResponseValidationError is returned for a successful HTTP status whose body does not match the expected schema.

func AsValidationError

func AsValidationError(err error) (*APIResponseValidationError, bool)

AsValidationError extracts the APIResponseValidationError when a successful response could not be decoded.

func (*APIResponseValidationError) Error

Error returns the formatted validation message.

func (*APIResponseValidationError) Unwrap

func (e *APIResponseValidationError) Unwrap() error

Unwrap exposes the embedded APIError to errors.As.

type APITimeoutError

type APITimeoutError struct {
	APIConnectionError

	// Timeout is the timeout that was in effect for the request.
	Timeout time.Duration
}

APITimeoutError is returned when a request exceeds its configured timeout.

func AsTimeoutError

func AsTimeoutError(err error) (*APITimeoutError, bool)

AsTimeoutError extracts the APITimeoutError when the request exceeded its timeout.

func (*APITimeoutError) Error

func (e *APITimeoutError) Error() string

Error returns the formatted timeout message.

type Answer

type Answer interface {
	// AnswerType returns the wire discriminator: "noul", "choice", or "score".
	AnswerType() string
}

Answer is a single answer to a single question. The concrete type is NoulAnswer, ChoiceAnswer, or ScoreAnswer, and always matches the corresponding question's type.

type Answers

type Answers map[string]Answer

Answers is a map of question names to answers, decoded by each entry's "type" discriminator.

SystemOneResponse.Answers is a plain map because its entries are already decoded. Answers exists for caller-defined response models passed to SystemOneAs, where Go needs a concrete type to decode into — a bare map[string]Answer cannot be decoded, because an interface type carries no information about which concrete answer to build:

type Analysis struct {
	Model   string         `json:"model"`
	Usage   typesafe.Usage `json:"usage"`
	Answers typesafe.Answers `json:"answers"`
}

func (*Answers) UnmarshalJSON

func (a *Answers) UnmarshalJSON(data []byte) error

UnmarshalJSON decodes the answers object, dispatching each entry on its "type" field.

type AuthenticationError

type AuthenticationError struct{ APIError }

AuthenticationError is an HTTP 401 response.

func (*AuthenticationError) Unwrap

func (e *AuthenticationError) Unwrap() error

Unwrap exposes the embedded APIError to errors.As.

type BadRequestError

type BadRequestError struct{ APIError }

BadRequestError is an HTTP 400 response.

func (*BadRequestError) Unwrap

func (e *BadRequestError) Unwrap() error

Unwrap exposes the embedded APIError to errors.As.

type ChoiceAnswer

type ChoiceAnswer struct {
	// Type is always "choice".
	Type string `json:"type"`

	// Choice is the criteria key with the highest probability.
	Choice string `json:"choice"`

	// Confidence is the confidence in the selection, from 0 to 1.
	Confidence float64 `json:"confidence"`

	// Probabilities is the probability of each criteria key, from 0 to 1.
	Probabilities map[string]float64 `json:"probabilities"`
}

ChoiceAnswer is a selected label with its alternatives.

See the choice primitive (https://docs.typesafe.ai/primitives/choice) for details.

func (*ChoiceAnswer) AnswerType

func (a *ChoiceAnswer) AnswerType() string

AnswerType returns the wire discriminator.

type ChoiceQuestion

type ChoiceQuestion struct {
	// Type is always "choice".
	Type string `json:"type"`

	// Criteria maps each label to its description: a string, a JSON object, an array, or nil for
	// an undescribed label.
	Criteria map[string]any `json:"criteria"`

	// Instructions is the question to ask, expressed as a string, a JSON object, or an array.
	Instructions any `json:"instructions,omitzero"`
	// contains filtered or unexported fields
}

ChoiceQuestion selects between named alternatives.

See the choice primitive (https://docs.typesafe.ai/primitives/choice) for details.

func NewChoice

func NewChoice(criteria map[string]any, opts ...QuestionOption) *ChoiceQuestion

NewChoice creates a choice question.

typesafe.NewChoice(
	map[string]any{"calm": nil, "angry": "An upset or hostile message"},
	typesafe.WithInstructions("What is the tone of this message?"),
)
Example
question := NewChoice(
	map[string]any{"calm": nil, "angry": "An upset or hostile message"},
	WithInstructions("What is the tone of this message?"),
)
encoded, err := json.Marshal(question)
if err != nil {
	panic(err)
}
fmt.Println(string(encoded))
Output:
{"type":"choice","criteria":{"angry":"An upset or hostile message","calm":null},"instructions":"What is the tone of this message?"}

func (*ChoiceQuestion) MarshalJSON

func (q *ChoiceQuestion) MarshalJSON() ([]byte, error)

MarshalJSON encodes the question, including any fields added by WithQuestionField.

func (*ChoiceQuestion) QuestionType

func (q *ChoiceQuestion) QuestionType() string

QuestionType returns the wire discriminator.

func (*ChoiceQuestion) Validate

func (q *ChoiceQuestion) Validate(name string) error

Validate reports a question that has no criteria.

type Client

type Client struct {

	// Models provides access to the Models API resource.
	Models *ModelsService
	// contains filtered or unexported fields
}

Client is the TypeSafe AI API client.

A Client is safe for concurrent use by multiple goroutines: it holds no per-request state, and the underlying http.Client pools connections across calls. Create one Client and share it. All request methods take a context.Context, which is the only cancellation and concurrency control the SDK needs.

func NewClient

func NewClient(opts ...Option) (*Client, error)

NewClient creates a client for the TypeSafe AI API.

Configuration is resolved from the functional options first, then the environment, then the SDK defaults:

client, err := typesafe.NewClient(typesafe.WithTimeout(30*time.Second))

TYPESAFE_API_KEY is required unless WithAPIKey supplies the key.

func (*Client) Close

func (c *Client) Close() error

Close releases the connection pool held by the client's transport.

A transport supplied through WithTransport or WithHTTPClient is closed when it implements io.Closer, so the client owns the resources it was given. Close is idempotent: later calls return the first call's error without touching the transport again.

func (*Client) Config

func (c *Client) Config() *Config

Config returns the resolved client configuration.

func (*Client) HTTPClient

func (c *Client) HTTPClient() *http.Client

HTTPClient returns the underlying *http.Client. A client constructed with WithHTTPClient returns that value; otherwise it returns the SDK's own client.

func (*Client) SystemOne

func (c *Client) SystemOne(ctx context.Context, state any, questions any, opts ...RequestOption) (*SystemOneResponse, error)

SystemOne answers named questions about text or structured state.

See System One (https://docs.typesafe.ai/concepts/system-one) for details.

The response carries every answer keyed by the question name supplied here, alongside the model that answered and the token usage for the evaluation:

response, err := client.SystemOne(ctx,
	map[string]any{"message": "I was charged twice. Please help."},
	typesafe.Questions{
		"billing": typesafe.NewNoul("Is this message about billing?"),
		"tone": typesafe.NewChoice(
			map[string]any{"calm": nil, "angry": nil},
			typesafe.WithInstructions("What is the tone?"),
		),
	},
)
if err != nil {
	return err
}
fmt.Println(response.Nouls()["billing"].Noul)

Errors are typed: SDKError for invalid input, APIError and its status-specific subclasses for unsuccessful responses, APIConnectionError and APITimeoutError for transport failures, and APIResponseValidationError for a success response that could not be decoded.

type Config

type Config struct {
	// APIKey is the credential sent as a bearer token.
	APIKey string

	// BaseURL is the API root, without a trailing slash.
	BaseURL string

	// DefaultModel is the model used when a call does not name one.
	DefaultModel string

	// Timeout is the HTTP timeout for one attempt, in effect for every call.
	Timeout time.Duration

	// DefaultHeaders holds headers sent with every request. Client identification and credentials
	// are applied after these, so a default header cannot displace them.
	DefaultHeaders http.Header

	// Logger receives the SDK's structured log records.
	Logger *slog.Logger

	// RetryPolicy is the retry policy in effect for every call.
	RetryPolicy *RetryPolicy

	// HTTPClient is the caller-supplied HTTP client, or nil when the SDK owns its client.
	HTTPClient *http.Client

	// Transport is the caller-supplied transport, or nil when the SDK owns its transport.
	Transport http.RoundTripper
}

Config is the resolved client configuration.

type InternalServerError

type InternalServerError struct{ APIError }

InternalServerError is an HTTP 5xx response.

func (*InternalServerError) Unwrap

func (e *InternalServerError) Unwrap() error

Unwrap exposes the embedded APIError to errors.As.

type ListModelsResponse

type ListModelsResponse struct {
	// Models holds the available models.
	Models []ModelMetadata `json:"models"`
	// contains filtered or unexported fields
}

ListModelsResponse holds the models available to the account.

func (*ListModelsResponse) MarshalJSON

func (r *ListModelsResponse) MarshalJSON() ([]byte, error)

MarshalJSON encodes only the API payload, leaving HTTP metadata out of the result.

func (*ListModelsResponse) RawBody

func (r *ListModelsResponse) RawBody() []byte

RawBody returns the response payload exactly as received.

func (*ListModelsResponse) RawHTTPResponse

func (r *ListModelsResponse) RawHTTPResponse() (*http.Response, error)

RawHTTPResponse returns the underlying *http.Response.

func (*ListModelsResponse) RawRequestID

func (r *ListModelsResponse) RawRequestID() string

RawRequestID returns the request ID, or the empty string when absent.

func (*ListModelsResponse) RequestID

func (r *ListModelsResponse) RequestID() (string, error)

RequestID returns the x-typesafe-request-id response header. It reports an error when the response did not carry one.

type ModelMetadata

type ModelMetadata struct {
	// Name is the model name or alias accepted by a request's model field.
	Name string `json:"name"`

	// Description describes the model and its capabilities.
	Description string `json:"description"`

	// ReleaseDate is the model's release date as the API reports it. The published schema
	// describes a YYYY-MM-DD date; the live API currently returns a full timestamp.
	ReleaseDate string `json:"release_date"`
}

ModelMetadata describes a model available to the account.

type ModelsService

type ModelsService struct {
	// contains filtered or unexported fields
}

ModelsService exposes the Models API resource. Reach it through Client.Models.

func (*ModelsService) List

List returns the models available to the account, with each model's name, description, and release date.

response, err := client.Models.List(ctx)
for _, model := range response.Models {
	fmt.Println(model.Name, model.ReleaseDate)
}

type NotFoundError

type NotFoundError struct{ APIError }

NotFoundError is an HTTP 404 response.

func (*NotFoundError) Unwrap

func (e *NotFoundError) Unwrap() error

Unwrap exposes the embedded APIError to errors.As.

type NoulAnswer

type NoulAnswer struct {
	// Type is always "noul".
	Type string `json:"type"`

	// Noul is the probability of a yes answer or a true statement, from 0 to 1.
	Noul float64 `json:"noul"`
}

NoulAnswer is a yes/no answer.

See the noul primitive (https://docs.typesafe.ai/primitives/noul) for details.

func (*NoulAnswer) AnswerType

func (a *NoulAnswer) AnswerType() string

AnswerType returns the wire discriminator.

type NoulCriteria

type NoulCriteria struct {
	// True describes what counts as a yes answer.
	True any `json:"true,omitzero"`

	// False describes what counts as a no answer.
	False any `json:"false,omitzero"`
}

NoulCriteria describes what counts as a yes or no answer. Both fields accept a string, a map, or a slice, and may be left nil.

type NoulQuestion

type NoulQuestion struct {
	// Type is always "noul".
	Type string `json:"type"`

	// Instructions is the question to ask, expressed as a string, a JSON object, or an array.
	Instructions any `json:"instructions,omitzero"`

	// Criteria describes what counts as a yes or no answer.
	Criteria *NoulCriteria `json:"criteria,omitzero"`
	// contains filtered or unexported fields
}

NoulQuestion is a yes/no question or statement.

See the noul primitive (https://docs.typesafe.ai/primitives/noul) for details.

func NewNoul

func NewNoul(instructions any, opts ...QuestionOption) *NoulQuestion

NewNoul creates a yes/no question. The instructions may be a string, a map, or a slice, and may be nil when criteria alone describe the question.

typesafe.NewNoul("Is this message spam?")
typesafe.NewNoul("Is this spam?", typesafe.WithNoulCriteria(typesafe.NoulCriteria{
	True:  "Unsolicited advertising",
	False: "A legitimate conversation",
}))

Options refine the question: WithInstructions overrides the instructions given positionally.

Example
question := NewNoul("Is this message about billing?")
fmt.Println(question.QuestionType())
fmt.Println(question.Instructions)
Output:
noul
Is this message about billing?

func (*NoulQuestion) MarshalJSON

func (q *NoulQuestion) MarshalJSON() ([]byte, error)

MarshalJSON encodes the question, including any fields added by WithQuestionField.

func (*NoulQuestion) QuestionType

func (q *NoulQuestion) QuestionType() string

QuestionType returns the wire discriminator.

func (*NoulQuestion) Validate

func (q *NoulQuestion) Validate(string) error

Validate accepts every noul question; the server interprets a bare statement with no criteria.

type Option

type Option func(*configOptions)

Option configures a Client at construction time.

func WithAPIKey

func WithAPIKey(apiKey string) Option

WithAPIKey sets the API key, overriding the TYPESAFE_API_KEY environment variable.

func WithBaseURL

func WithBaseURL(baseURL string) Option

WithBaseURL sets the API root, overriding the TYPESAFE_BASE_URL environment variable.

func WithDefaultModel

func WithDefaultModel(model string) Option

WithDefaultModel sets the model used when a call does not name one, overriding the TYPESAFE_DEFAULT_MODEL environment variable.

func WithHTTPClient

func WithHTTPClient(client *http.Client) Option

WithHTTPClient supplies the http.Client the SDK sends through, together with its transport. Mutually exclusive with WithTransport.

func WithHeader

func WithHeader(key, value string) Option

WithHeader sets a default header sent with every request. Client identification and credentials are applied afterwards and cannot be displaced.

func WithHeaders

func WithHeaders(headers map[string]string) Option

WithHeaders sets default headers sent with every request, merging with any set by WithHeader.

func WithLogger

func WithLogger(logger *slog.Logger) Option

WithLogger sets the structured logger. The SDK logs requests, responses, and retries at info level, and headers and bodies at debug level, with credential headers redacted.

func WithRetryPolicy

func WithRetryPolicy(policy *RetryPolicy) Option

WithRetryPolicy sets the retry policy for every call.

func WithTimeout

func WithTimeout(timeout time.Duration) Option

WithTimeout sets the HTTP timeout for one attempt. It must be positive.

The timeout has no environment variable. When WithHTTPClient supplies both a client and no explicit timeout, the client's own Timeout is adopted instead.

func WithTransport

func WithTransport(transport http.RoundTripper) Option

WithTransport supplies the http.RoundTripper the SDK sends through. Mutually exclusive with WithHTTPClient.

type PermissionDeniedError

type PermissionDeniedError struct{ APIError }

PermissionDeniedError is an HTTP 403 response.

func (*PermissionDeniedError) Unwrap

func (e *PermissionDeniedError) Unwrap() error

Unwrap exposes the embedded APIError to errors.As.

type Question

type Question interface {
	// QuestionType returns the wire discriminator: "noul", "choice", or "score".
	QuestionType() string

	// Validate reports whether the question is well formed under the given name.
	Validate(name string) error
}

Question is implemented by every question the SDK can send: NoulQuestion, ChoiceQuestion, ScoreQuestion, and RawQuestion.

type QuestionOption

type QuestionOption func(*questionOptions)

QuestionOption refines a question constructed by NewNoul, NewChoice, or NewScore.

func WithInstructions

func WithInstructions(instructions any) QuestionOption

WithInstructions sets the question's instructions: the question to ask, expressed as a string, a JSON object, or an array.

func WithNoulCriteria

func WithNoulCriteria(criteria NoulCriteria) QuestionOption

WithNoulCriteria sets the criteria describing the yes and no outcomes of a noul question.

Example
question := NewNoul("Is this spam?", WithNoulCriteria(NoulCriteria{
	True:  "Unsolicited advertising",
	False: "A legitimate conversation",
}))
encoded, err := json.Marshal(question)
if err != nil {
	panic(err)
}
fmt.Println(string(encoded))
Output:
{"type":"noul","instructions":"Is this spam?","criteria":{"true":"Unsolicited advertising","false":"A legitimate conversation"}}

func WithQuestionField

func WithQuestionField(key string, value any) QuestionOption

WithQuestionField adds a field to a question's wire form. It exists for forward compatibility with request fields this SDK version does not model; a field that collides with a modeled one replaces it.

Example
question := NewNoul("Is this message about billing?", WithQuestionField("language", "en"))
encoded, err := json.Marshal(question)
if err != nil {
	panic(err)
}
fmt.Println(string(encoded))
Output:
{"instructions":"Is this message about billing?","language":"en","type":"noul"}

type Questions

type Questions map[string]any

Questions maps question names to questions. The names identify the answers in the response.

type RateLimitError

type RateLimitError struct {
	APIError

	// RetryAfterMS is the wait the server requested in milliseconds, or nil when the
	// response carried no usable Retry-After or retry-after-ms header.
	RetryAfterMS *float64
}

RateLimitError is an HTTP 429 response.

func AsRateLimitError

func AsRateLimitError(err error) (*RateLimitError, bool)

AsRateLimitError extracts the RateLimitError when the failure was an HTTP 429.

func (*RateLimitError) RetryAfter

func (e *RateLimitError) RetryAfter() (time.Duration, bool)

RetryAfter returns the server-requested wait and whether the response provided one.

func (*RateLimitError) Unwrap

func (e *RateLimitError) Unwrap() error

Unwrap exposes the embedded APIError to errors.As.

type RawQuestion

type RawQuestion map[string]any

RawQuestion is a question sent as a plain map, for request fields this SDK version does not model:

typesafe.RawQuestion{"type": "noul", "instructions": "Is this about billing?"}

func (RawQuestion) QuestionType

func (q RawQuestion) QuestionType() string

QuestionType returns the "type" field, or the empty string when absent.

func (RawQuestion) Validate

func (q RawQuestion) Validate(name string) error

Validate applies the structural checks the SDK can make without duplicating the server's schema: the type must be a non-empty string, and a choice or score question must carry criteria.

type RedactingHandler

type RedactingHandler struct {
	// contains filtered or unexported fields
}

RedactingHandler wraps an slog.Handler and redacts credential-bearing attributes before they reach it.

The SDK already redacts the headers it logs itself; this handler extends that guarantee to records the caller routes through the SDK's logger:

logger := slog.New(typesafe.NewRedactingHandler(slog.NewJSONHandler(os.Stdout, nil)))
client, err := typesafe.NewClient(typesafe.WithLogger(logger))

Values held in a map[string]string under any attribute are inspected key by key.

func NewRedactingHandler

func NewRedactingHandler(handler slog.Handler) *RedactingHandler

NewRedactingHandler wraps handler so that credential-bearing attributes are redacted.

func (*RedactingHandler) Enabled

func (r *RedactingHandler) Enabled(ctx context.Context, level slog.Level) bool

Enabled reports whether the wrapped handler records the level.

func (*RedactingHandler) Handle

func (r *RedactingHandler) Handle(ctx context.Context, record slog.Record) error

Handle redacts the record's attributes and forwards it to the wrapped handler. Attributes that carry no credential are passed through unchanged.

func (*RedactingHandler) WithAttrs

func (r *RedactingHandler) WithAttrs(attrs []slog.Attr) slog.Handler

WithAttrs returns a handler whose records carry the given attributes.

func (*RedactingHandler) WithGroup

func (r *RedactingHandler) WithGroup(name string) slog.Handler

WithGroup returns a handler that groups subsequent attributes under name.

type RequestOption

type RequestOption func(*requestOptions)

RequestOption overrides client configuration for a single call.

func WithCallHeader

func WithCallHeader(key, value string) RequestOption

WithCallHeader sets or overrides a request header for a single call.

func WithCallHeaders

func WithCallHeaders(headers map[string]string) RequestOption

WithCallHeaders sets or overrides request headers for a single call, merging with any set by WithCallHeader.

func WithCallRetry

func WithCallRetry(policy *RetryPolicy) RequestOption

WithCallRetry overrides the retry policy for a single call.

func WithCallTimeout

func WithCallTimeout(timeout time.Duration) RequestOption

WithCallTimeout overrides the HTTP timeout for a single call. It must be positive.

func WithExtraBody

func WithExtraBody(extra map[string]any) RequestOption

WithExtraBody adds top-level fields to a request body, shallow-merged over "state", "model", and "questions" after they are set. Merging is last-write-wins: a colliding key replaces the SDK's value, and object values are replaced rather than deep-merged.

func WithModel

func WithModel(model string) RequestOption

WithModel overrides the model for a single call.

type RetryPolicy

type RetryPolicy struct {
	// MaxRetries is the number of retries after the initial attempt. Zero disables retries.
	MaxRetries int

	// BackoffInitial is the first backoff delay; it doubles per attempt up to BackoffMax.
	// Zero disables backoff.
	BackoffInitial time.Duration

	// BackoffMax caps the backoff delay. Zero disables backoff.
	BackoffMax time.Duration

	// BackoffJitter is the fraction of each backoff delay that is randomly subtracted,
	// between zero and one.
	BackoffJitter float64

	// HTTPStatuses holds the response status codes that are retried.
	HTTPStatuses map[int]bool

	// RespectRetryAfter honors the retry-after-ms and Retry-After response headers.
	RespectRetryAfter bool

	// APIConnectionError retries [APIConnectionError] failures.
	APIConnectionError bool

	// APITimeoutError retries [APITimeoutError] failures.
	APITimeoutError bool

	// RetryErrors holds additional error values that trigger a retry. A failure is retried when
	// errors.Is reports a match against any entry.
	RetryErrors []error

	// Predicate is consulted for every failure. Returning true triggers a retry, in addition to
	// the rules above.
	Predicate func(error) bool

	// Timeout is the total budget for one SDK call, covering the initial attempt, retries, and
	// delays. A retry whose delay would reach or exceed the remaining budget is abandoned and the
	// last error is returned. Nil disables the budget.
	Timeout *time.Duration

	// Sleep waits between attempts. It is a seam for tests; nil uses a context-aware timer.
	Sleep func(ctx context.Context, delay time.Duration) error

	// RandFloat returns a value in [0, 1) used for backoff jitter. It is a seam for tests;
	// nil uses the global math/rand/v2 source.
	RandFloat func() float64
}

RetryPolicy configures how the client retries failed requests.

The zero value is not usable; start from DefaultRetryPolicy and adjust, or set the fields you care about and leave the rest zero:

policy := typesafe.DefaultRetryPolicy()
policy.MaxRetries = 3
policy.Timeout = nil // no overall budget

A non-nil policy passed to WithRetryPolicy or WithCallRetry is validated when it is used.

func DefaultRetryPolicy

func DefaultRetryPolicy() *RetryPolicy

DefaultRetryPolicy returns a fresh copy of the retry policy the SDK uses when none is supplied.

func (*RetryPolicy) Clone

func (p *RetryPolicy) Clone() *RetryPolicy

Clone returns a deep copy of the policy, so a per-call override can be adjusted without mutating the client's policy.

func (*RetryPolicy) Execute

func (p *RetryPolicy) Execute(ctx context.Context, fn func(attempt int) error) error

Execute runs fn under this policy, retrying per the policy's rules.

fn receives the zero-based attempt index. Execute stops early, returning the last failure, when the retry budget would be exceeded, when the failure is not retryable, or when the caller's context ends. A cancelled or expired context is never retried.

func (*RetryPolicy) Validate

func (p *RetryPolicy) Validate() error

Validate reports whether the policy is well formed.

type SDKError

type SDKError struct {
	// Message describes the failure.
	Message string

	// Cause is the wrapped error, when one exists.
	Cause error
}

SDKError is a client-side failure: invalid configuration, an unencodable body, or a response that could not be read. It never represents an unsuccessful HTTP status.

func (*SDKError) Error

func (e *SDKError) Error() string

func (*SDKError) Unwrap

func (e *SDKError) Unwrap() error

Unwrap returns the wrapped cause so errors.Is and errors.As traverse it.

type ScoreAnswer

type ScoreAnswer struct {
	// Type is always "score".
	Type string `json:"type"`

	// Score is the probability-weighted average of the rubric levels; it may fall between levels.
	Score float64 `json:"score"`

	// Confidence is the confidence in the rating, from 0 to 1.
	Confidence float64 `json:"confidence"`

	// Legend maps each rubric level to the criteria description supplied in the request.
	Legend map[int]any `json:"legend"`

	// Probabilities is the probability of each rubric level, keyed as in Legend.
	Probabilities map[int]float64 `json:"probabilities"`
}

ScoreAnswer is a rating against an ordered rubric.

See the score primitive (https://docs.typesafe.ai/primitives/score) for details.

func (*ScoreAnswer) AnswerType

func (a *ScoreAnswer) AnswerType() string

AnswerType returns the wire discriminator.

func (*ScoreAnswer) MarshalJSON

func (a *ScoreAnswer) MarshalJSON() ([]byte, error)

MarshalJSON encodes the rubric maps with their integer levels as JSON object keys.

type ScoreError

type ScoreError struct {
	// Name is the question name that failed validation.
	Name string
}

ScoreError reports a score question with an empty rubric.

func (*ScoreError) Error

func (e *ScoreError) Error() string

type ScoreQuestion

type ScoreQuestion struct {
	// Type is always "score".
	Type string `json:"type"`

	// Criteria is the ordered rubric: each entry's position is its score, starting at zero. Each
	// entry is a string, a JSON object, or an array.
	Criteria []any `json:"criteria"`

	// Instructions is the question to ask, expressed as a string, a JSON object, or an array.
	Instructions any `json:"instructions,omitzero"`
	// contains filtered or unexported fields
}

ScoreQuestion rates content using an ordered rubric.

See the score primitive (https://docs.typesafe.ai/primitives/score) for details.

func NewScore

func NewScore(criteria []any, opts ...QuestionOption) *ScoreQuestion

NewScore creates a score question with an ordered rubric.

typesafe.NewScore(
	[]any{"can wait", "needs attention this week", "needs attention today"},
	typesafe.WithInstructions("How urgent is this message?"),
)
Example
question := NewScore(
	[]any{"can wait", "needs attention this week", "needs attention today"},
	WithInstructions("How urgent is this message?"),
)
encoded, err := json.Marshal(question)
if err != nil {
	panic(err)
}
fmt.Println(string(encoded))
Output:
{"type":"score","criteria":["can wait","needs attention this week","needs attention today"],"instructions":"How urgent is this message?"}

func (*ScoreQuestion) MarshalJSON

func (q *ScoreQuestion) MarshalJSON() ([]byte, error)

MarshalJSON encodes the question, including any fields added by WithQuestionField.

func (*ScoreQuestion) QuestionType

func (q *ScoreQuestion) QuestionType() string

QuestionType returns the wire discriminator.

func (*ScoreQuestion) Validate

func (q *ScoreQuestion) Validate(name string) error

Validate reports a score question whose rubric has no levels.

type SystemOneResponse

type SystemOneResponse struct {
	// Model is the name of the model that answered the questions; it may differ from the alias
	// supplied in the request.
	Model string `json:"model"`

	// Usage reports the token counts for this evaluation.
	Usage Usage `json:"usage"`

	// Answers holds every answer keyed by the question name supplied in the request. Answer types
	// this SDK version does not model are omitted here and remain reachable through RawBody.
	Answers map[string]Answer `json:"answers"`
	// contains filtered or unexported fields
}

SystemOneResponse holds the answers to a System One request, grouped by question kind.

See System One (https://docs.typesafe.ai/concepts/system-one) for details.

func (*SystemOneResponse) Choices

func (r *SystemOneResponse) Choices() map[string]*ChoiceAnswer

Choices returns the choice answers keyed by question name.

func (*SystemOneResponse) MarshalJSON

func (r *SystemOneResponse) MarshalJSON() ([]byte, error)

MarshalJSON encodes only the API payload, leaving HTTP metadata out of the result.

func (*SystemOneResponse) Nouls

func (r *SystemOneResponse) Nouls() map[string]*NoulAnswer

Nouls returns the yes/no answers keyed by question name.

func (*SystemOneResponse) RawBody

func (r *SystemOneResponse) RawBody() []byte

RawBody returns the response payload exactly as received, including any answer types this SDK version does not model.

func (*SystemOneResponse) RawHTTPResponse

func (r *SystemOneResponse) RawHTTPResponse() (*http.Response, error)

RawHTTPResponse returns the underlying *http.Response.

func (*SystemOneResponse) RawRequestID

func (r *SystemOneResponse) RawRequestID() string

RawRequestID returns the request ID, or the empty string when absent.

func (*SystemOneResponse) RequestID

func (r *SystemOneResponse) RequestID() (string, error)

RequestID returns the x-typesafe-request-id response header. It reports an error when the response did not carry one.

func (*SystemOneResponse) Scores

func (r *SystemOneResponse) Scores() map[string]*ScoreAnswer

Scores returns the score answers keyed by question name.

func (*SystemOneResponse) String

func (r *SystemOneResponse) String() string

String implements fmt.Stringer for convenient debugging of a response.

type TypeSafeError

type TypeSafeError interface {
	error
	// contains filtered or unexported methods
}

TypeSafeError is the marker interface implemented by every error this SDK returns.

Match it with errors.As:

if err != nil {
	var sdkErr typesafe.TypeSafeError
	if errors.As(err, &sdkErr) {
		// the failure came from the SDK
	}
}

type UnprocessableEntityError

type UnprocessableEntityError struct{ APIError }

UnprocessableEntityError is an HTTP 422 response.

func (*UnprocessableEntityError) Unwrap

func (e *UnprocessableEntityError) Unwrap() error

Unwrap exposes the embedded APIError to errors.As.

type Usage

type Usage struct {
	// InputTokens is the number of billable input tokens, or nil when the API did not report it.
	InputTokens *int `json:"input_tokens,omitzero"`

	// OutputTokens is the number of output tokens, or nil when the API did not report it.
	OutputTokens *int `json:"output_tokens,omitzero"`
}

Usage reports the token counts for a request.

func (Usage) String

func (u Usage) String() string

String renders the token counts, showing an unreported count as "-".

Directories

Path Synopsis
examples
advanced command
Command advanced exercises the parts of the TypeSafe AI Go SDK a production caller reaches for: per-call overrides, a custom retry policy, a typed response model, a second API resource, and the typed error taxonomy.
Command advanced exercises the parts of the TypeSafe AI Go SDK a production caller reaches for: per-call overrides, a custom retry policy, a typed response model, a second API resource, and the typed error taxonomy.
quickstart command
Command quickstart is the smallest useful TypeSafe AI Go SDK program: one System One request that answers a noul, a choice, and a score question about a structured state.
Command quickstart is the smallest useful TypeSafe AI Go SDK program: one System One request that answers a noul, a choice, and a score question about a structured state.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL