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

typesafe

package module
v0.1.1 Latest Latest
Warning

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

Go to latest
Published: Sep 30, 2026 License: Apache-2.0 Imports: 27 Imported by: 0

README

typesafe-sdk-go

typesafe-sdk-go is a Go client for the TypeSafe System One API, a port of typesafe-sdk-python 0.7.1. It asks questions about a state (a support ticket, a review, any JSON) and returns typed answers: a probability (noul), a choice among options, or a score on a scale.

Status: v0.1.1, the second release (typesafe.Version is 0.1.1). Before v1.0.0 a minor release may change the API, as Semantic Versioning allows.

Install

go get github.com/zchee/typesafe-sdk-go
  • The SDK supports Go 1.27.x on amd64 and arm64, the releases the newest github.com/bytedance/sonic tag supports. Its go.mod says go 1.27, so with GOTOOLCHAIN=auto, the default, go get from Go 1.21 to 1.26 switches to the newest Go 1.27 release and writes go 1.27 into your go.mod. Every go command after it in your module, as in a checkout of this repository, then switches to go1.27.0, except that Go 1.21.0 to 1.21.10 and 1.22.0 to 1.22.3 stop with toolchain not available. Go 1.17 to 1.20 fail on the standard-library packages they lack. On any other GOARCH, or on Go 1.28 and later, the build fails on purpose with the error undefined: typesafe_sdk_go_requires_go1_17_to_go1_27_on_amd64_or_arm64 (docs/support.md gives each case, explains why, and holds the Go 1.28 bump procedure).

Quick start

Set TYPESAFE_API_KEY in your environment, then build a client and ask a question. NewClient without options reads the key from TYPESAFE_API_KEY and, when they are set, the API's address from TYPESAFE_BASE_URL and the default model from TYPESAFE_DEFAULT_MODEL.

// Copyright 2026 The typesafe-sdk-go Authors.
// SPDX-License-Identifier: Apache-2.0

// Command quickstart asks the TypeSafe API one question about a support
// ticket and prints the answer. The client reads the API key from the
// TYPESAFE_API_KEY environment variable.
package main

import (
	"context"
	"fmt"
	"log"

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

func main() {
	if err := run(context.Background()); err != nil {
		log.Fatal(err)
	}
}

func run(ctx context.Context) error {
	client, err := typesafe.NewClient()
	if err != nil {
		return err
	}
	defer client.Close()

	questions, err := typesafe.NewQuestions().
		Choice("category", typesafe.Choice{
			Instructions: typesafe.Text("What is this ticket about?"),
			Options:      typesafe.Options{{Label: "billing"}, {Label: "technical"}, {Label: "other"}},
		}).
		Prepare()
	if err != nil {
		return err
	}

	state := map[string]any{"document": "I was charged twice. Please fix this ASAP."}
	response, err := client.SystemOne(ctx, state, questions)
	if err != nil {
		return err
	}

	category, _ := response.Answers().Choice("category")
	fmt.Println(category.Choice())
	return nil
}

Run it with go run ./examples/quickstart from a checkout of this repository.

Typed answers

A struct can declare the questions in its field tags and receive the answers in its fields. Ask[T] sends the questions T declares and decodes the answers into a T; PreparedFor[T] and DecodeAs[T] are the two halves, for a caller that also wants the response (its request id, usage or raw body). The tag's name is the question's name on the wire; without it the name is the Go field name as written.

// Copyright 2026 The typesafe-sdk-go Authors.
// SPDX-License-Identifier: Apache-2.0

// Command typed declares its questions as a struct: each tagged field is a
// question, and receives its answer.
package main

import (
	"context"
	"fmt"
	"log"

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

// Ticket is the question set. The tag's name key is the question's name on
// the wire (the field name when left out); optional marks an answer that
// may be missing from the response, which Present then reports.
type Ticket struct {
	Billing typesafe.NoulAnswer   `typesafe:"kind=noul;name=billing;instructions=Is this ticket about billing?"`
	Tone    typesafe.ChoiceAnswer `typesafe:"kind=choice;name=tone;instructions=What is the customer's tone?;options=calm|frustrated|angry"`
	Urgency typesafe.ScoreAnswer  `typesafe:"kind=score;name=urgency;instructions=How urgent is this ticket?;levels=can wait|this week|today"`
	Spam    typesafe.NoulAnswer   `typesafe:"kind=noul;name=spam;optional;instructions=Is this spam?"`
}

func main() {
	if err := run(context.Background()); err != nil {
		log.Fatal(err)
	}
}

func run(ctx context.Context) error {
	client, err := typesafe.NewClient()
	if err != nil {
		return err
	}
	defer client.Close()

	state := map[string]any{
		"subject": "Charged twice this month",
		"body":    "I see two charges of $49. Please fix this ASAP.",
	}

	// One step: Ask sends the questions Ticket declares and returns the
	// answers as a Ticket.
	ticket, err := typesafe.Ask[Ticket](ctx, client, state)
	if err != nil {
		return err
	}
	fmt.Printf("billing: %.2f\n", ticket.Billing.Noul())
	fmt.Printf("tone: %s (confidence %.2f)\n", ticket.Tone.Choice(), ticket.Tone.Confidence())
	fmt.Printf("urgency: %.2f\n", ticket.Urgency.Score())
	if ticket.Spam.Present() {
		fmt.Printf("spam: %.2f\n", ticket.Spam.Noul())
	}

	// Two steps, when the response itself is needed too (its request id,
	// usage or raw body): the questions Ask would send, the call, and the
	// typed decode of its answers.
	questions, err := typesafe.PreparedFor[Ticket]()
	if err != nil {
		return err
	}
	response, err := client.SystemOne(ctx, state, questions)
	if err != nil {
		return err
	}
	again, err := typesafe.DecodeAs[Ticket](response)
	if err != nil {
		return err
	}
	id, _ := response.Meta().RequestID()
	fmt.Printf("request %s: tone %s\n", id, again.Tone.Choice())
	return nil
}

Retries and errors

A RetryPolicy is a value; its zero value is DefaultRetry, the Python SDK's RetryPolicy(). WithRetry sets a client's policy and Retry one call's. A successful response is billed, so a 2xx is never retried by its status.

Errors are pointers: *APIError, *ConnectionError, *TimeoutError, *ResponseValidationError, *ResponseTooLargeError, *ConfigError and *InvalidRequestError, each an Error. Ask for them with errors.AsType[*typesafe.APIError](err), or with errors.As and a **typesafe.APIError (var e *typesafe.APIError; errors.As(err, &e)); errors.As panics on a value target (var e typesafe.APIError), since only the pointer type is an error. A cancelled context returns context.Canceled itself.

// Copyright 2026 The typesafe-sdk-go Authors.
// SPDX-License-Identifier: Apache-2.0

// Command retries sets a retry policy for a client and for one call, and
// tells the errors of a call apart once its retries are spent.
package main

import (
	"context"
	"errors"
	"fmt"
	"log"
	"time"

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

func main() {
	if err := run(context.Background()); err != nil {
		log.Fatal(err)
	}
}

func run(ctx context.Context) error {
	// A policy is a value; each setter returns a changed copy. The zero
	// policy is DefaultRetry: 2 retries on 408, 429 and 5xx, on connection
	// errors and on timeouts, with a backoff from 500 ms to 5 s and a
	// budget of 30 s per call.
	patient := typesafe.DefaultRetry().
		MaxRetries(4).
		Backoff(time.Second, 10*time.Second, 0.25).
		Budget(time.Minute)
	client, err := typesafe.NewClient(typesafe.WithRetry(patient))
	if err != nil {
		return err
	}
	defer client.Close()

	questions, err := typesafe.NewQuestions().
		Noul("refund", typesafe.Noul{Instructions: typesafe.Text("Does the customer ask for a refund?")}).
		Prepare()
	if err != nil {
		return err
	}
	state := "I was charged twice; please refund one of the charges."

	// This call retries nothing; the client's policy applies to the others.
	response, err := client.SystemOne(ctx, state, questions, typesafe.Retry(typesafe.NoRetry()))
	report("without retries", response, err)

	// A successful response is billed, so one that fails validation is
	// never retried unless a predicate asks for it.
	validating := patient.Predicate(func(err error) bool {
		_, ok := errors.AsType[*typesafe.ResponseValidationError](err)
		return ok
	})
	response, err = client.SystemOne(ctx, state, questions, typesafe.Retry(validating))
	report("with a predicate", response, err)
	return nil
}

// report prints the answer, or what kind of error the call returned.
func report(label string, response *typesafe.SystemOneResponse, err error) {
	if err == nil {
		refund, _ := response.Answers().Noul("refund")
		fmt.Printf("%s: refund %.2f\n", label, refund.Noul())
		return
	}
	if apiErr, ok := errors.AsType[*typesafe.APIError](err); ok {
		wait, _ := apiErr.RetryAfter()
		fmt.Printf("%s: the API answered %d (%s, authentication %t, retry after %s)\n",
			label, apiErr.StatusCode, apiErr.Kind, apiErr.IsAuthentication(), wait)
		return
	}
	if timeoutErr, ok := errors.AsType[*typesafe.TimeoutError](err); ok {
		fmt.Printf("%s: no response within %s\n", label, timeoutErr.Timeout)
		return
	}
	if connErr, ok := errors.AsType[*typesafe.ConnectionError](err); ok {
		fmt.Printf("%s: no connection (through a proxy: %t): %v\n", label, connErr.Proxy(), err)
		return
	}
	if errors.Is(err, context.Canceled) {
		fmt.Printf("%s: cancelled\n", label)
		return
	}
	fmt.Printf("%s: %v\n", label, err)
}

Logging and redaction

The client writes to the log/slog logger given by WithLogger, and discards its records without one.

// Copyright 2026 The typesafe-sdk-go Authors.
// SPDX-License-Identifier: Apache-2.0

// Command logging gives the client a log/slog logger. At INFO the client
// writes one record per attempt; at DEBUG it adds the request and response
// headers, with credentials shown as ***, and the body lengths; at
// typesafe.LevelTrace it adds the bodies themselves, as sent and received.
package main

import (
	"context"
	"fmt"
	"log"
	"log/slog"
	"os"

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

func main() {
	if err := run(context.Background()); err != nil {
		log.Fatal(err)
	}
}

func run(ctx context.Context) error {
	logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelDebug}))
	client, err := typesafe.NewClient(typesafe.WithLogger(logger))
	if err != nil {
		return err
	}
	defer client.Close()

	questions, err := typesafe.NewQuestions().
		Noul("greeting", typesafe.Noul{Instructions: typesafe.Text("Is this a greeting?")}).
		Prepare()
	if err != nil {
		return err
	}
	response, err := client.SystemOne(ctx, "Hello there!", questions)
	if err != nil {
		return err
	}
	greeting, _ := response.Answers().Noul("greeting")
	fmt.Printf("greeting: %.2f\n", greeting.Noul())
	return nil
}

What the SDK redacts, in its errors and in its log records alike:

  • Header-derived values are redacted. The value of a credential header (Authorization, Proxy-Authorization, X-Api-Key, Api-Key, Cookie, Set-Cookie, or a name that contains token or secret), and any header value that holds the client's API key (a key of at least 8 bytes), is shown as ***. This covers the request headers of the DEBUG records, the response header an *APIError, *ResponseValidationError or *ResponseTooLargeError keeps, and the request id, in RequestID(), in Error() and in the INFO record.
  • Body-derived text is shown as received. A server's error message, a field path and the name of a skipped answer at WARN are the server's text, as the Python SDK shows them: a key the server echoes there is visible. They are escaped and cut (messages at 200 characters, names and request ids at 128, field paths at 320).
  • LevelTrace records are the bodies. The request and response bodies are logged as they are; enable that level only where bodies may be logged.

A transport error's text shows its credentials as ***; when the text of its cause printed a credential, a stand-in holding the redacted text takes the cause's place.

More examples

docs/examples.md shows every program under examples/: the four above, and the client and call options, custom transports, and one client shared by goroutines. Each Go block in this README and in docs/ is one of those programs, byte for byte, and CI checks that it is.

Differences from the Python SDK

The port keeps the Python SDK's behaviour except where noted. The main differences:

  • One *Client for synchronous and concurrent use; every call takes a context.Context, and each attempt has one deadline.
  • On an https base URL the client speaks HTTP/2 only, on one connection, and refuses an API server that does not negotiate h2 (ErrHTTP2NotNegotiated); WithHTTPVersion(HTTPAuto) lets ALPN choose.
  • Typed answers come from struct tags (Ask[T]) instead of pydantic models. The typed decode refuses what the question set does not declare (an unknown option label, a level beyond the levels) and requires usage; its error paths keep the Python SDK's form (tone.choice). It writes the answers at their fields' offsets through unsafe, in one file of the package, so that it does not allocate.
  • The error types keep a redacted copy of the response header, and the request id is redacted in the INFO record too (see above).
  • RetryPolicy holds time.Duration values, and the budget counts from the first attempt; a deadline that ends a wait returns a *TimeoutError.
  • A response is read under a 16 MiB limit (WithMaxResponseBytes).
  • TYPESAFE_LOG_LEVEL is not read; bodies are logged at typesafe.LevelTrace only.

docs/deviations.md is the full table, each row naming the Python behaviour, the Go behaviour and why, and the upstream tests it replaces; docs/port-test-matrix.md maps every one of the Python SDK's 129 tests to a Go test or to a row of that table.

Tests

go test ./... runs every test offline, the examples included (against a local stand-in for the API). The tests against the live API are opt-in: they compile only with the build tag live, and each fails before it calls the API unless both TYPESAFE_LIVE_TESTS=1 and TYPESAFE_API_KEY are set. With the key already in your environment:

TYPESAFE_LIVE_TESTS=1 go test -tags live -count=1 -v ./livetest/

The key is read from the environment and is never printed. The API bills each System One call; CI does not run these tests (docs/support.md says where they run).

CI, coverage and benchmarks

  • CI runs the linters (golangci-lint, which runs modernize, vet and staticcheck and checks every Go file's formatting; govulncheck; go mod tidy -diff), the port test matrix, the docs-snippets check, the check that every example is a package, and the compile-time refusal off the support matrix; then the tests, the import-confinement tests among them, with -race on ubuntu-26.04, xcode-27 and windows-2025, and the allocation budgets without it.
  • Codecov receives each image's coverage; its project status blocks at the 85 % target (the 90 % goal and the patch status are informational), and every block the tests leave unrun has a reason in docs/uncovered-lines.md.
  • CodSpeed runs every benchmark in walltime mode on ubuntu-26.04. The job fails when a whole call is not faster than a naive sonic client's in the same run (the step "AC-P7 gate" compares BenchmarkCall/sdk's mean with BenchmarkCall/naive's); every absolute time is reported, not enforced (docs/perf/codspeed.md).

License

Licensed under the Apache License, Version 2.0 (LICENSE). Third-party licence texts are available from each module's source in the Go module cache (go mod download -json <module> gives the directory).

Documentation

Overview

Package typesafe is a Go client for the TypeSafe System One API, a port of typesafe-sdk-python 0.7.1.

A Client asks questions about a state and returns the answers: a question set built with NewQuestions and sent by Client.SystemOne, or one declared by a struct's tags and asked by Ask. The repository's examples directory holds complete programs, docs/deviations.md lists where the port behaves differently from the Python SDK, and docs/support.md the supported Go releases and platforms.

Index

Examples

Constants

View Source
const (
	// APIKeyEnv names the variable holding the API key.
	APIKeyEnv = "TYPESAFE_API_KEY" //nolint:gosec // G101: the name of the variable that holds the key, not a key.

	// BaseURLEnv names the variable holding the API base URL.
	BaseURLEnv = "TYPESAFE_BASE_URL"

	// DefaultModelEnv names the variable holding the model a request names
	// when the call names none.
	DefaultModelEnv = "TYPESAFE_DEFAULT_MODEL"
)

The environment variables a client reads for a setting its options leave unset, as typesafe-sdk-python names them. A value is trimmed of leading and trailing whitespace, and a variable that is unset or blank counts as unset.

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

	// DefaultModel is the model a request names when the call names none.
	DefaultModel = "jev-latest"

	// DefaultTimeout is the deadline of each attempt of a request.
	DefaultTimeout = 10 * time.Second

	// DefaultConnectTimeout is the deadline for opening a connection,
	// inside the deadline of the attempt that opens it.
	DefaultConnectTimeout = 10 * time.Second

	// DefaultMaxResponseBytes is the largest response body a request reads:
	// 16 MiB.
	DefaultMaxResponseBytes = 16 << 20
)

The settings a client uses when neither an option nor the environment gives one.

View Source
const LevelTrace = slog.LevelDebug - 4

LevelTrace is the log level of the records that carry request and response bodies, below slog.LevelDebug: a body holds the caller's state, which may be personal data, so a handler shows it only when set to this level or lower. typesafe-sdk-python logs bodies at DEBUG.

View Source
const Version = "0.1.1"

Version is the version of this SDK. Every request names it in the User-Agent and X-TypeSafe-SDK headers as typesafe-sdk-go/<Version>.

Variables

View Source
var ErrClientClosed = errors.New("typesafe: the client is closed")

ErrClientClosed reports a call on a client that was closed. The SDK returns it wrapped in a *ConfigError: errors.Is(err, ErrClientClosed) tells it apart.

View Source
var ErrHTTP2NotNegotiated = errors.New("typesafe: the API host did not negotiate HTTP/2")

ErrHTTP2NotNegotiated reports that the API host did not speak HTTP/2 under HTTP2Only: its TLS handshake chose another protocol or none, it refused h2, a caller TLS dialer returned a connection without h2, or a response came back over HTTP/1.1. The SDK returns it wrapped in a *ConfigError, which retrying cannot fix: errors.Is(err, ErrHTTP2NotNegotiated) tells it apart. HTTPAuto allows HTTP/1.1.

Functions

func Ask

func Ask[T any](ctx context.Context, c *Client, state any, opts ...CallOption) (T, error)

Ask asks the questions the struct type T declares about state and returns the answers as a T: it is Client.SystemOne with the question set PreparedFor builds for T, followed by DecodeAs of the response.

t, err := typesafe.Ask[Ticket](ctx, c, state)

A T that PreparedFor refuses fails with its *ConfigError before any request is made. Every error of the call itself, an *APIError, a *ConnectionError, a *TimeoutError, a *ResponseValidationError of the response as a whole and the others Client.SystemOne lists, is returned as SystemOne returns it. A response whose answers do not fit T fails as DecodeAs says, with the call's endpoint in the error and its header redacted as the call's other errors redact theirs (APIError.Header).

Ask returns only the answers. A caller that also needs the response, its request id, usage, model or raw body, makes the two calls Ask makes:

qs, err := typesafe.PreparedFor[Ticket]()
resp, err := c.SystemOne(ctx, state, qs)
t, err := typesafe.DecodeAs[Ticket](resp)
id, ok := resp.Meta().RequestID()
Example
package main

import (
	"context"
	"fmt"
	"io"
	"net/http"
	"strings"

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

// cannedAPI answers every request with one status and body. It stands in
// for the API, so that these examples run without the network.
type cannedAPI struct {
	status int
	body   string
}

// RoundTrip implements http.RoundTripper.
func (c cannedAPI) RoundTrip(req *http.Request) (*http.Response, error) {
	if req.Body != nil {
		_, _ = io.Copy(io.Discard, req.Body)
		_ = req.Body.Close()
	}
	return &http.Response{
		StatusCode: c.status,
		Proto:      "HTTP/2.0",
		ProtoMajor: 2,
		Header: http.Header{
			"Content-Type":          {"application/json"},
			"X-Typesafe-Request-Id": {"req_example"},
		},
		Body:          io.NopCloser(strings.NewReader(c.body)),
		ContentLength: int64(len(c.body)),
		Request:       req,
	}, nil
}

// answersBody is an answer to the questions of the examples below.
const answersBody = `{"model":"jev-latest","usage":{"input_tokens":12,"output_tokens":3},"answers":{` +
	`"spam":{"type":"noul","noul":0.02},` +
	`"tone":{"type":"choice","choice":"friendly","confidence":0.9,"probabilities":{"friendly":0.9,"hostile":0.1}},` +
	`"quality":{"type":"score","score":1.7,"confidence":0.8,"legend":{"0":"bad","1":"ok","2":"great"},"probabilities":{"0":0.1,"1":0.1,"2":0.8}}}}`

// exampleClient returns a client whose requests the canned API answers.
func exampleClient(status int, body string) *typesafe.Client {
	c, err := typesafe.NewClient(
		typesafe.WithAPIKey("example-key"),
		typesafe.WithRoundTripper(cannedAPI{status: status, body: body}),
	)
	if err != nil {
		panic(err)
	}
	return c
}

// Review is a question set declared as a struct: each tagged field is a
// question, and receives its answer.
type Review struct {
	Spam    typesafe.NoulAnswer   `typesafe:"kind=noul;name=spam;instructions=Is this review spam?"`
	Tone    typesafe.ChoiceAnswer `typesafe:"kind=choice;name=tone;instructions=What is the tone?;options=friendly|hostile=rude or threatening"`
	Quality typesafe.ScoreAnswer  `typesafe:"kind=score;name=quality;instructions=How useful is the review?;levels=bad|ok|great"`
}

func main() {
	client := exampleClient(http.StatusOK, answersBody)
	defer client.Close()

	review, err := typesafe.Ask[Review](context.Background(), client, "Great product, fast shipping.")
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Printf("spam %.2f\n", review.Spam.Noul())
	fmt.Printf("tone %s (%.1f)\n", review.Tone.Choice(), review.Tone.Confidence())
	fmt.Printf("quality %.1f\n", review.Quality.Score())
}
Output:
spam 0.02
tone friendly (0.9)
quality 1.7

func DecodeAs

func DecodeAs[T any](resp *SystemOneResponse) (T, error)

DecodeAs returns the answers of resp as a T, the struct type whose fields declare the questions (PreparedFor has the grammar): each answer field holds the answer that resp carries under the field's question name. resp may answer a question set built by hand, such as one from NewQuestions; the names are what DecodeAs matches, so a field tagged name=spam reads the answer to the question "spam".

DecodeAs reads the answers as SystemOneResponse.Answers holds them: when the body repeats a name, the last answer under it is the one read, and an answer of a type this version does not model is not there. The fields hold copies that share resp's slices and bytes, which must not be modified; resp itself is not changed, and must not be nil. Answers that no field of T names are ignored. Each field read has Present true; an optional field without an answer is left as the zero answer, whose Present is false and which marshals as null, as the Python SDK holds and dumps None there.

A T that PreparedFor refuses fails with its *ConfigError. Otherwise the fields are read in their order in T, and the first that does not fit fails the whole decode: DecodeAs returns the zero T, never one holding the fields read before the failure, and a *ResponseValidationError whose FieldPath names the answer. T's fields are the answers lifted out of the response's "answers" member, so the paths are the ones the Python SDK reports for such a response model, a SystemOneResponse subclass with one field per answer: the answer's name, then the member and the key. An answer of a type this version does not model has been skipped, as that model skips it, so under a required field's name it fails as absent and under an optional field's name it leaves the field absent, and a response without an "answers" member fails at T's first required field:

the answer is absent and the field is not optional  <name>
the answer is of another kind than the field        <name>.type
a choice picks an option T does not list            <name>.choice
a choice gives a probability to such an option      <name>.probabilities.<option>
a score's legend has a level beyond T's levels      <name>.legend.<level>
a score gives a probability to such a level         <name>.probabilities.<level>

A failure of the response as a whole, a missing model or usage or an answer without a type, comes from the decoder before T is filled, at the decoder's own path, such as usage or answers.spam.noul, as in Python, which validates the base model's fields first.

A choice's options and a score's levels are the ones T's tag lists: a level counts from zero, so a score with three levels has the levels 0, 1 and 2, and a level is written in decimal in the path. An option or a level the answer does not mention, and the score's value, are not checked. Python checks an option only when the response model's own type says so, with a Literal, and then reports the pick at "tone.choice", as DecodeAs does.

The error carries resp's HTTP metadata, none of it for a response read back with SystemOneResponse.UnmarshalJSON, whose Meta is empty. Its Header is redacted as APIError.Header describes, with the credentials of the call resp came from, however long resp is kept; a response read back with UnmarshalJSON came from no client and is redacted by header name alone. FieldPath is not redacted: the answer's name and the option or level it names are shown as they arrived, as the SDK's other errors show a path. Its Endpoint is empty; Ask fills it in.

Example
package main

import (
	"fmt"

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

// answersBody is an answer to the questions of the examples below.
const answersBody = `{"model":"jev-latest","usage":{"input_tokens":12,"output_tokens":3},"answers":{` +
	`"spam":{"type":"noul","noul":0.02},` +
	`"tone":{"type":"choice","choice":"friendly","confidence":0.9,"probabilities":{"friendly":0.9,"hostile":0.1}},` +
	`"quality":{"type":"score","score":1.7,"confidence":0.8,"legend":{"0":"bad","1":"ok","2":"great"},"probabilities":{"0":0.1,"1":0.1,"2":0.8}}}}`

// Review is a question set declared as a struct: each tagged field is a
// question, and receives its answer.
type Review struct {
	Spam    typesafe.NoulAnswer   `typesafe:"kind=noul;name=spam;instructions=Is this review spam?"`
	Tone    typesafe.ChoiceAnswer `typesafe:"kind=choice;name=tone;instructions=What is the tone?;options=friendly|hostile=rude or threatening"`
	Quality typesafe.ScoreAnswer  `typesafe:"kind=score;name=quality;instructions=How useful is the review?;levels=bad|ok|great"`
}

func main() {
	// A response stored earlier with MarshalJSON, read back.
	var stored typesafe.SystemOneResponse
	if err := stored.UnmarshalJSON([]byte(answersBody)); err != nil {
		fmt.Println(err)
		return
	}
	review, err := typesafe.DecodeAs[Review](&stored)
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println(review.Tone.Choice(), review.Spam.Present())
}
Output:
friendly true

Types

type APIError

type APIError struct {
	// Kind classifies StatusCode.
	Kind APIErrorKind
	// StatusCode is the HTTP status code.
	StatusCode int
	// Header is the response header as a new map in which each value of a
	// header that carries a credential is "***": a header named
	// Authorization, Proxy-Authorization, X-Api-Key, Api-Key, Cookie or
	// Set-Cookie, or whose name contains "token" or "secret" (compared
	// without regard to case), a header with a value that holds the
	// client's API key when the key is at least 8 bytes long, and, in the
	// answer to a plain-HTTP request through a proxy, which the proxy may
	// have written itself, one with a value that holds the proxy's whole
	// password, as it is or as the URL escapes it, or its Basic token, 8
	// bytes or longer ([WithProxy]); a shorter one, or a word of the
	// password, is shown, as a shorter key is. Every other header,
	// Retry-After, Retry-After-Ms and X-Typesafe-Request-Id included, is as
	// the server sent it, so [APIError.RetryAfter] and [APIError.RequestID]
	// read it. The values of those other headers are the response's own and
	// must not be modified. typesafe-sdk-python keeps the headers as they
	// arrived and redacts them only in its logs.
	Header http.Header
	// Body is the response body as it arrived, or nil when it was empty or
	// over the size limit. It is shared, not copied, and must not be
	// modified.
	Body []byte
	// Endpoint is the request's method and URL without credentials, query
	// or fragment, such as "GET https://api.typesafe.ai/v1/models", or
	// empty when it is not known.
	Endpoint string
	// Message is the message Error prints after the status; empty prints
	// the status alone. A message the server composes is not redacted: when
	// it echoes the client's API key, Message, Error and %+v show the key,
	// as the Python SDK's error does. Header and the request id show "***"
	// for a value that holds the key, in the error and in the SDK's log
	// records alike.
	Message string
	// ErrorType is the server's machine-readable name for the failure, from
	// the body's detail.error_type, or empty. It is the server's text as it
	// arrived.
	ErrorType string
}

APIError reports an unsuccessful HTTP response: a status outside 2xx, with the body and headers it arrived with.

Error renders "<Endpoint>: <StatusCode> <Message> (request_id=<id>)", each part left out when it is empty or absent, as the Python SDK's str() of TypeSafeAPIError does. The SDK builds Message from the body the way the Python SDK finds a message in it (the "error", "message" and "detail" members, or the body itself), escaped and cut at 200 characters; a message the caller sets is printed as it is.

Example
package main

import (
	"context"
	"errors"
	"fmt"
	"io"
	"net/http"
	"strings"

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

// cannedAPI answers every request with one status and body. It stands in
// for the API, so that these examples run without the network.
type cannedAPI struct {
	status int
	body   string
}

// RoundTrip implements http.RoundTripper.
func (c cannedAPI) RoundTrip(req *http.Request) (*http.Response, error) {
	if req.Body != nil {
		_, _ = io.Copy(io.Discard, req.Body)
		_ = req.Body.Close()
	}
	return &http.Response{
		StatusCode: c.status,
		Proto:      "HTTP/2.0",
		ProtoMajor: 2,
		Header: http.Header{
			"Content-Type":          {"application/json"},
			"X-Typesafe-Request-Id": {"req_example"},
		},
		Body:          io.NopCloser(strings.NewReader(c.body)),
		ContentLength: int64(len(c.body)),
		Request:       req,
	}, nil
}

// exampleClient returns a client whose requests the canned API answers.
func exampleClient(status int, body string) *typesafe.Client {
	c, err := typesafe.NewClient(
		typesafe.WithAPIKey("example-key"),
		typesafe.WithRoundTripper(cannedAPI{status: status, body: body}),
	)
	if err != nil {
		panic(err)
	}
	return c
}

func main() {
	client := exampleClient(http.StatusForbidden, `{"detail":{"error_type":"authentication_error","message":"Must supply an API key!"}}`)
	defer client.Close()
	_, err := client.Models().List(context.Background())

	// The error types are pointers: ask for *APIError (errors.As takes a
	// **APIError).
	if apiErr, ok := errors.AsType[*typesafe.APIError](err); ok {
		fmt.Println(apiErr.StatusCode, apiErr.Kind, apiErr.IsAuthentication())
		fmt.Println(apiErr.Message)
	}
}
Output:
403 permission denied true
Must supply an API key!

func (*APIError) Error

func (e *APIError) Error() string

Error returns "<Endpoint>: <StatusCode> <Message> (request_id=<id>)".

func (*APIError) IsAuthentication

func (e *APIError) IsAuthentication() bool

IsAuthentication reports whether the status or the server's error type names an authentication failure: status 401, or any status whose ErrorType is "authentication_error" (the API answers a request without a key with 403 and that error type).

func (*APIError) RequestID

func (e *APIError) RequestID() (string, bool)

RequestID returns the server's identifier for the request, from the x-typesafe-request-id response header, and whether the header was present; a repeated header's values are joined with ", ". It is the server's text as it arrived.

func (*APIError) RetryAfter

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

RetryAfter returns how long the server asked the caller to wait before trying again, and whether it asked, as the Python SDK's parse_retry_after reads it: the retry-after-ms header in milliseconds, else Retry-After in seconds or as an HTTP date (a date already past is zero); a value that is not a number, negative, NaN or infinite gives no answer from that header. The wait is truncated to whole milliseconds. It is read for any status, not only 429. A date is read in the three formats RFC 9110 names (IMF-fixdate, RFC 850, asctime; net/http.ParseTime); the Python SDK's parser also takes a numeric zone such as +0000 and a missing weekday, which give no answer here.

type APIErrorKind

type APIErrorKind uint8

APIErrorKind classifies an unsuccessful response by its status, as the Python SDK's subclasses of TypeSafeAPIError do. The mapping is by status alone, so a documented status with an undocumented body still lands in its class.

const (
	// APIErrorOther is any status without a class of its own: 408, 409 or a
	// redirect the transport did not follow, for example.
	APIErrorOther APIErrorKind = iota
	// APIErrorBadRequest is status 400: the request was invalid.
	APIErrorBadRequest
	// APIErrorAuthentication is status 401: authentication failed.
	APIErrorAuthentication
	// APIErrorPermissionDenied is status 403: access was denied. The API
	// also answers a request without a key with 403; see
	// [APIError.IsAuthentication].
	APIErrorPermissionDenied
	// APIErrorNotFound is status 404: the resource was not found.
	APIErrorNotFound
	// APIErrorUnprocessableEntity is status 422: the request failed the
	// server's validation.
	APIErrorUnprocessableEntity
	// APIErrorRateLimit is status 429: the rate limit was exceeded.
	APIErrorRateLimit
	// APIErrorInternalServer is any status from 500 up: the server failed.
	APIErrorInternalServer
)

The kinds of *APIError.

func (APIErrorKind) String

func (k APIErrorKind) String() string

String returns the kind's name in words, such as "rate limit", or "other" for a value outside the enumeration.

type Answer

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

Answer is one answer of any kind: Kind says which of Noul, Choice and Score holds it.

func (Answer) Choice

func (a Answer) Choice() (ChoiceAnswer, bool)

Choice returns the answer as a choice answer, and whether it is one. The choice answer's Present reports the same as the bool.

func (Answer) Kind

func (a Answer) Kind() AnswerKind

Kind returns the kind of the answer.

func (Answer) MarshalJSON

func (a Answer) MarshalJSON() ([]byte, error)

MarshalJSON returns the answer as the MarshalJSON of its kind writes it. The zero Answer, which is none of the kinds, is written null.

func (Answer) Noul

func (a Answer) Noul() (NoulAnswer, bool)

Noul returns the answer as a yes/no answer, and whether it is one. The yes/no answer's Present reports the same as the bool.

func (Answer) Score

func (a Answer) Score() (ScoreAnswer, bool)

Score returns the answer as a score answer, and whether it is one. The score answer's Present reports the same as the bool.

type AnswerKind

type AnswerKind uint8

AnswerKind is the kind of question an answer belongs to, which the answer's "type" member names on the wire.

const (
	// KindNoul is a yes/no answer, a [NoulAnswer] ("type": "noul").
	KindNoul AnswerKind = AnswerKind(wire.KindNoul)
	// KindChoice is a choice answer, a [ChoiceAnswer] ("type": "choice").
	KindChoice AnswerKind = AnswerKind(wire.KindChoice)
	// KindScore is a score answer, a [ScoreAnswer] ("type": "score").
	KindScore AnswerKind = AnswerKind(wire.KindScore)
)

The kinds of Answer. The zero AnswerKind is none of them.

func (AnswerKind) String

func (k AnswerKind) String() string

String returns the kind's name on the wire ("noul", "choice" or "score"), or "unknown" for a value that is none of them.

type Answers

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

Answers is the answers of one response, keyed by the question names. It reads as the Python SDK's answers dict: a name the response repeats keeps the position where it first appeared and the value of its last appearance, and an answer of a type this version does not model is not there (it stays in the raw body, ResponseMeta.RawBody). The Python SDK's nouls, choices and scores views are the Nouls, Choices and Scores iterators.

An Answers is a view of its response: it is valid as long as the response is, it shows the new answers after the response's UnmarshalJSON, and every value it yields is a copy that shares the response's slices and bytes, which must not be modified. The zero Answers is empty.

Answers is not comparable, so == does not compile on it and it cannot be a map key: as a view, it would compare equal only to a view of the same response, whatever the answers of the two held. Compare the answers themselves, with Answers.Get or Answers.All.

func (Answers) All

func (a Answers) All() iter.Seq2[string, Answer]

All yields every answer with its question's name, in the order the names first appeared in the response.

Example
package main

import (
	"fmt"

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

// answersBody is an answer to the questions of the examples below.
const answersBody = `{"model":"jev-latest","usage":{"input_tokens":12,"output_tokens":3},"answers":{` +
	`"spam":{"type":"noul","noul":0.02},` +
	`"tone":{"type":"choice","choice":"friendly","confidence":0.9,"probabilities":{"friendly":0.9,"hostile":0.1}},` +
	`"quality":{"type":"score","score":1.7,"confidence":0.8,"legend":{"0":"bad","1":"ok","2":"great"},"probabilities":{"0":0.1,"1":0.1,"2":0.8}}}}`

func main() {
	var stored typesafe.SystemOneResponse
	if err := stored.UnmarshalJSON([]byte(answersBody)); err != nil {
		fmt.Println(err)
		return
	}
	for name, answer := range stored.Answers().All() {
		fmt.Println(name, answer.Kind())
	}
}
Output:
spam noul
tone choice
quality score

func (Answers) Choice

func (a Answers) Choice(name string) (ChoiceAnswer, bool)

Choice returns the answer to the question called name, and whether there is one and it is a choice answer.

func (Answers) Choices

func (a Answers) Choices() iter.Seq2[string, ChoiceAnswer]

Choices yields the choice answers with their questions' names, in the order of All.

func (Answers) Get

func (a Answers) Get(name string) (Answer, bool)

Get returns the answer to the question called name, and whether there is one.

func (Answers) Len

func (a Answers) Len() int

Len returns the number of answers.

func (Answers) MarshalJSON

func (a Answers) MarshalJSON() ([]byte, error)

MarshalJSON returns the answers as the object the "answers" member of a response payload holds, {"<name>":<answer>,…}, in the order of All, each answer as its MarshalJSON writes it. The zero Answers is written {}.

func (Answers) Noul

func (a Answers) Noul(name string) (NoulAnswer, bool)

Noul returns the answer to the question called name, and whether there is one and it is a yes/no answer.

func (Answers) Nouls

func (a Answers) Nouls() iter.Seq2[string, NoulAnswer]

Nouls yields the yes/no answers with their questions' names, in the order of All.

func (Answers) Score

func (a Answers) Score(name string) (ScoreAnswer, bool)

Score returns the answer to the question called name, and whether there is one and it is a score answer.

func (Answers) Scores

func (a Answers) Scores() iter.Seq2[string, ScoreAnswer]

Scores yields the score answers with their questions' names, in the order of All.

type CallOption

type CallOption func(*callOptions)

CallOption configures one call, over the client's settings: Model, Timeout, Header, ExtraBody and Retry. As with ClientOption, an option only records its setting; the call checks them all before it encodes or sends anything and reports the first it cannot use as a *ConfigError, in a fixed order whatever the order of the options: a System One call checks the model, then the timeout, the retry policy and the headers; a list-models call first refuses Model and ExtraBody, then checks the timeout, the retry policy and the headers. Among options of one kind the last one wins, except that every Header and every ExtraBody counts. A nil CallOption is ignored.

func ExtraBody

func ExtraBody(key string, v any) CallOption

ExtraBody adds the member key with value v to the top level of this System One call's request body, as the Python SDK's extra_body does: a key named "state", "model" or "questions" replaces that member's value where it stands (the replaced value is not encoded at all), any other key is appended in the order the options first name it, and a key given twice sends its last value. List-models calls have no body and refuse this option.

The API refuses a System One request whose top level holds a member it does not know: the call then fails with an *APIError whose StatusCode is 400 and whose ErrorType is "api_usage_error" ("Invalid request."), so ExtraBody is for a member the API accepts.

v may be any value JSON can hold, encoded as the call's state is: a RawJSON is sent as it is after a check of its first byte, a Content as a question sends it (unset Content as null), a nil as null, and anything else by the JSON encoder, with the float spelling and map order that Client.SystemOne describes for the state. A value JSON has no form for, or a plain []byte (whose intent is ambiguous: send string(b) or RawJSON(b)), makes the call fail with an *InvalidRequestError before anything is sent.

An extra "state" follows the rules of the state, so a nil, a number or a boolean is refused where typesafe-sdk-python sends it; an extra "questions" of nil is sent as null.

func Header(name, value string) CallOption

Header sets a header this call sends, over the client's: it replaces a WithHeader of the same name, compared without regard to case, as a per-call header replaces a default one in typesafe-sdk-python, and a later Header of the same name replaces an earlier one.

The rules of WithHeader apply: the SDK's own headers and the transport's win, and a caller's value of any of them is dropped and logged at slog.LevelDebug by name; X-TypeSafe-Retry-Count, which the SDK sets on retries only, is dropped too.

The name must be a valid HTTP field name and the value a valid field value. A name that contains the API key is refused, when the key is at least 8 bytes long. Each of these failures fails the call before anything is sent, and no error repeats a value.

func Model

func Model(name string) CallOption

Model sets the model this System One call names, in place of the client's (WithModel). The model is sent as given; an empty or blank model is refused rather than sent, and so is one that is not valid UTF-8. An ExtraBody member named "model" replaces it in the body. List-models calls name no model and refuse this option.

func Retry

func Retry(policy RetryPolicy) CallOption

Retry sets the retry policy of this call, in place of the client's (WithRetry), as a whole: no setting of the client's policy carries over. NoRetry makes the call a single attempt. A policy with a setting out of range fails the call with a *ConfigError, before anything is sent (RetryPolicy).

func Timeout

func Timeout(d time.Duration) CallOption

Timeout sets the deadline of each attempt of this call, in place of the client's (WithTimeout, WithNoTimeout). It must be positive, as the Python SDK refuses a zero, negative or non-finite per-call timeout.

type Choice

type Choice struct {
	// Instructions is what the model should decide; it is optional.
	Instructions Content
	// Options are the options, sent as the "criteria" object in this order.
	// Labels must be unique. With no options an empty object is sent, and
	// the server judges it.
	Options Options
}

Choice is a question that picks one of a set of named options. See https://docs.typesafe.ai/primitives/choice.

type ChoiceAnswer

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

ChoiceAnswer is the answer to a choice question: the option picked and the probability of every option. See the choice primitive (https://docs.typesafe.ai/primitives/choice).

The zero ChoiceAnswer is not an answer a response carried: its Present is false, and MarshalJSON writes it null. DecodeAs leaves one in an optional field whose answer is absent.

func (ChoiceAnswer) Choice

func (a ChoiceAnswer) Choice() string

Choice returns the label of the option with the highest probability.

func (ChoiceAnswer) Confidence

func (a ChoiceAnswer) Confidence() float64

Confidence returns how sure the model is of the pick, from 0 to 1.

func (ChoiceAnswer) MarshalJSON

func (a ChoiceAnswer) MarshalJSON() ([]byte, error)

MarshalJSON returns the answer as the Python SDK's model_dump_json writes it, such as {"type":"choice","choice":"billing","confidence":0.9,"probabilities":{"billing":0.9,"support":0.1}}, the probabilities in the order of Probabilities. A float that arrived as -0.0 is written 0.0, where Python writes -0.0: the response reads every zero as 0. An answer that is not Present is written null, as NoulAnswer.MarshalJSON says.

func (ChoiceAnswer) Present

func (a ChoiceAnswer) Present() bool

Present reports whether the answer is one a response carried, as NoulAnswer.Present does.

func (ChoiceAnswer) Probabilities

func (a ChoiceAnswer) Probabilities() iter.Seq2[string, float64]

Probabilities yields every option's label with its probability, in the order the response lists them. Labels are unique.

func (ChoiceAnswer) Probability

func (a ChoiceAnswer) Probability(label string) (float64, bool)

Probability returns the probability of the option called label, and whether the answer lists that option.

type Client

type Client engine.Client[RetryPolicy]

Client calls the TypeSafe API. It is safe for concurrent use by multiple goroutines; build it with NewClient and release its connections with Client.Close. The zero Client is not usable.

The first attempt of every call hands the transport the client's own header map, shared by every call and never copied, so a RoundTripper given with WithRoundTripper must not modify a request, as the net/http.RoundTripper contract already requires.

func NewClient

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

NewClient builds a client from opts. Unless an option supplies the transport (WithHTTPTransport, WithRoundTripper), the client builds its own: HTTP/2 on one connection per client for an https base URL (HTTP/1.1 or HTTP/2 for http; WithHTTPVersion), through the proxy the environment names (WithProxy). Every option is checked before the client is built, and the first that cannot be used is reported as a *ConfigError; nothing is sent.

func (*Client) Close

func (c *Client) Close() error

Close releases the client's network resources: it closes the idle connections of the transport the client built or cloned (WithHTTPTransport), and calls Close once on a WithRoundTripper transport that implements io.Closer, returning its error. Calls in flight finish on their own connections. Close is idempotent: a second Close does nothing and returns nil. Every call made after Close fails with a *ConfigError wrapping ErrClientClosed.

func (*Client) Models

func (c *Client) Models() Models

Models returns the client's list-models endpoint.

func (*Client) Stats

func (c *Client) Stats() Stats

Stats returns the client's counters. A Client that NewClient did not build counts nothing.

func (*Client) SystemOne

func (c *Client) SystemOne(ctx context.Context, state any, qs *Prepared, opts ...CallOption) (*SystemOneResponse, error)

SystemOne asks the questions qs about state and returns the answers: POST /v1/systemone with {"state": state, "model": ..., "questions": ...}.

state is text, a JSON object or an array: a string, a map, a slice, a struct, a RawJSON sent as it is, or a Content. A nil, a number or a boolean is refused, as is a plain []byte, whose intent is ambiguous. The body is encoded once per call, before anything is sent, and every attempt sends the same bytes; a state that cannot be encoded fails with an *InvalidRequestError, and a missing question set with a *ConfigError. A float inside the state keeps the encoder's spelling (3.0 is sent as 3), and a map's members go out in Go's iteration order; a struct or RawJSON gives stable bytes.

opts override the client's settings for this call; see CallOption.

Each attempt has its own deadline (WithTimeout, Timeout) under ctx. A response with a status outside 2xx is an *APIError; a successful response whose body is not the answer the SDK expects is a *ResponseValidationError, and one over the size limit a *ResponseTooLargeError; an attempt that produced no response is a *ConnectionError, or a *TimeoutError when its deadline or ctx's passed. A failed attempt is tried again as the call's retry policy decides (WithRetry, Retry, RetryPolicy), and the call returns the error of its last attempt. A call whose ctx is cancelled, while a request is in flight or while it waits to retry, stops and returns ctx.Err(), context.Canceled, itself, which is not an SDK Error, as typesafe-sdk-python lets a cancellation through unwrapped; context.Cause gives a cause the canceller set.

Example
package main

import (
	"context"
	"fmt"
	"io"
	"net/http"
	"strings"
	"time"

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

// cannedAPI answers every request with one status and body. It stands in
// for the API, so that these examples run without the network.
type cannedAPI struct {
	status int
	body   string
}

// RoundTrip implements http.RoundTripper.
func (c cannedAPI) RoundTrip(req *http.Request) (*http.Response, error) {
	if req.Body != nil {
		_, _ = io.Copy(io.Discard, req.Body)
		_ = req.Body.Close()
	}
	return &http.Response{
		StatusCode: c.status,
		Proto:      "HTTP/2.0",
		ProtoMajor: 2,
		Header: http.Header{
			"Content-Type":          {"application/json"},
			"X-Typesafe-Request-Id": {"req_example"},
		},
		Body:          io.NopCloser(strings.NewReader(c.body)),
		ContentLength: int64(len(c.body)),
		Request:       req,
	}, nil
}

// answersBody is an answer to the questions of the examples below.
const answersBody = `{"model":"jev-latest","usage":{"input_tokens":12,"output_tokens":3},"answers":{` +
	`"spam":{"type":"noul","noul":0.02},` +
	`"tone":{"type":"choice","choice":"friendly","confidence":0.9,"probabilities":{"friendly":0.9,"hostile":0.1}},` +
	`"quality":{"type":"score","score":1.7,"confidence":0.8,"legend":{"0":"bad","1":"ok","2":"great"},"probabilities":{"0":0.1,"1":0.1,"2":0.8}}}}`

// exampleClient returns a client whose requests the canned API answers.
func exampleClient(status int, body string) *typesafe.Client {
	c, err := typesafe.NewClient(
		typesafe.WithAPIKey("example-key"),
		typesafe.WithRoundTripper(cannedAPI{status: status, body: body}),
	)
	if err != nil {
		panic(err)
	}
	return c
}

func main() {
	client := exampleClient(http.StatusOK, answersBody)
	defer client.Close()
	questions, err := typesafe.NewQuestions().
		Noul("spam", typesafe.Noul{Instructions: typesafe.Text("Is this review spam?")}).
		Choice("tone", typesafe.Choice{Options: typesafe.Options{{Label: "friendly"}, {Label: "hostile"}}}).
		Score("quality", typesafe.Score{Levels: []typesafe.Content{typesafe.Text("bad"), typesafe.Text("ok"), typesafe.Text("great")}}).
		Prepare()
	if err != nil {
		fmt.Println(err)
		return
	}

	state := map[string]any{"review": "Great product, fast shipping."}
	response, err := client.SystemOne(context.Background(), state, questions, typesafe.Timeout(5*time.Second))
	if err != nil {
		fmt.Println(err)
		return
	}
	answers := response.Answers()
	spam, _ := answers.Noul("spam")
	tone, _ := answers.Choice("tone")
	quality, _ := answers.Score("quality")
	level, _ := quality.Description(2)
	fmt.Printf("spam %.2f, tone %s, quality %.1f (level 2 is %q)\n", spam.Noul(), tone.Choice(), quality.Score(), level.Text())
	input, _ := response.Usage().InputTokens()
	id, _ := response.Meta().RequestID()
	fmt.Printf("model %s, %d input tokens, request %s\n", response.Model(), input, id)
}
Output:
spam 0.02, tone friendly, quality 1.7 (level 2 is "great")
model jev-latest, 12 input tokens, request req_example

func (*Client) WarmUp

func (c *Client) WarmUp(ctx context.Context) error

WarmUp lists the models once, so that the connection, and the HTTP/2 session a burst of calls then shares, is ready before the first call that matters. It returns the error of that call.

type ClientOption

type ClientOption func(*options)

ClientOption configures a client.

An option only records its setting; the client checks every setting together when it is built and reports the first one it cannot use as a *ConfigError, before anything is sent. So the order of options of different kinds never decides whether a set of them is accepted. Among options of one kind the last one wins and is the only one checked: WithTimeout(0) followed by WithTimeout(time.Second) is accepted, the reverse is not. WithHeader keeps the last value per header name, and checks every call. A nil ClientOption is ignored.

A setting that no option gives is read from its environment variable (APIKeyEnv, BaseURLEnv, DefaultModelEnv), trimmed of leading and trailing whitespace; a variable that is unset or blank counts as unset, and the SDK's default (DefaultBaseURL, DefaultModel) applies. An option always wins over the environment, even when its value is unusable: the environment is not consulted for a setting an option gave.

func WithAPIKey

func WithAPIKey(key string) ClientOption

WithAPIKey sets the API key, sent as Authorization: Bearer <key>. Without it the key is read from APIKeyEnv.

The key is trimmed of leading and trailing whitespace as Python's str.strip() trims it; what is left must be printable ASCII without whitespace, and must not be empty. A key given here that fails either rule is refused: the environment is not consulted in its place. No error repeats the key.

func WithBaseURL

func WithBaseURL(rawURL string) ClientOption

WithBaseURL sets the API base URL, such as https://api.typesafe.ai. Without it the URL is read from BaseURLEnv, and DefaultBaseURL applies when that is unset.

Trailing slashes are removed, and a path the URL keeps is a prefix of the API paths: https://example.test/prefix/ sends to https://example.test/prefix/v1/systemone. The URL must be an absolute http or https URL with a host, and without credentials, a query or a fragment; the API key is passed with WithAPIKey only. No error repeats the URL.

An http URL, here or from BaseURLEnv, sends the API key in cleartext, as the Python SDK does; use one only for a server on the same host.

func WithClientTrace

func WithClientTrace(trace *httptrace.ClientTrace) ClientOption

WithClientTrace sets the net/http/httptrace hooks every request of the client reports to; nil removes them. The hooks are copied when the option is applied and run after the SDK's own, and before those of a trace the call's context carries (httptrace.WithClientTrace).

A hook that panics, whether given here or carried by the call's context, does not unwind through net/http, which calls some hooks with its own locks held or a stream reserved: the panic is recovered inside the hook and raised again, with the same value, from the call that made the request once net/http has returned (any response body is closed first). A hook that panics after the call has returned is recovered and logged at WARN.

Hooks must return promptly. The SDK's own transport (the default one and WithHTTPTransport's clone) lets one request at a time write its headers, and a request's hooks run while it holds that turn, the first request on a new connection until its response headers arrive. How long a hook that blocks holds every other call on the client depends on where it blocks:

  • In GotConn or a later hook, until its response headers: for up to the connect timeout plus the TLS handshake timeout (20 s by default), after which the waiting calls go out without their turn.
  • In GetConn, while the client has no connection yet: for up to the wait for its first connection (the same two timeouts, and a proxy's CONNECT and handshake when a proxy may apply) plus the bound above.
  • In GetConn, once the client has its connection: until the hook returns, past the other calls' own deadlines. net/http calls GetConn with its connection pool's lock held, so a call that has waited out the bound above then waits for that lock, which its context cannot interrupt.
  • In the DNS, connect or TLS handshake hooks of a new connection (DNSStart, DNSDone, ConnectStart, ConnectDone, TLSHandshakeStart, TLSHandshakeDone): for up to the bound above plus the wait for a connection above, under WithNoTimeout too. The call that dials, and every call that waits for a connection meanwhile, then fails with a *TimeoutError while the hook runs on. The default transport's dial, which runs the DNS and connect hooks, ends at the connect timeout plus a grace of 100 ms, so the next call can dial again; a TLS handshake hook, or a hook that blocks the dialer of WithHTTPTransport's transport, keeps the client's one connection to the host from being made until it returns.

A hook the bound leaves running goes on with its dial, beside the calls that come after it: the rest of that dial's hooks run once it returns, possibly at the same time as the hooks of a later call's new connection, as net/http/httptrace allows, and a panic the hook raises then is recovered and logged at WARN, as the panic of any hook that runs after its call has returned.

func WithCompression

func WithCompression(enabled bool) ClientOption

WithCompression sets whether requests ask the API to compress its responses. The default, true, sends Accept-Encoding: gzip, as Go's transport does by default (typesafe-sdk-python's httpx asks for gzip and deflate, and for br and zstd where their decoders are installed): the transport undoes the API's gzip before the SDK reads the body, which then has no declared length. false sends no Accept-Encoding, so the API sends the body as it is with its length declared, which on this API's small bodies saves the client about 7 allocations and 10 KiB per call (docs/support.md). It configures the SDK's own transport, so, whatever its value, it cannot be combined with WithHTTPTransport, whose transport keeps its own DisableCompression, or WithRoundTripper.

func WithConnectTimeout

func WithConnectTimeout(d time.Duration) ClientOption

WithConnectTimeout sets the deadline for opening a connection, TCP and TLS together, inside the deadline of the attempt that opens it; DefaultConnectTimeout unless set. It must be positive.

func WithHTTPTransport

func WithHTTPTransport(t *http.Transport) ClientOption

WithHTTPTransport makes the client use a clone of t, which keeps t's dialers, TLS configuration, proxy and compression setting (DisableCompression); t itself is not used or changed, except that cloning runs t's own first-use setup as t's first request would. Where t leaves them zero, the SDK sets the protocols of the HTTPVersion policy, one connection per host under HTTP2Only, the HTTP/2 ping timeouts and the TLS handshake timeout (WithConnectTimeout); under HTTP2Only it always sets strict HTTP/2 stream accounting (HTTP2Config.StrictMaxConcurrentRequests), checks ALPN on the API host's handshake, and checks a connection from t's TLS dialer for h2. The SDK's cold-start gate and its one-request-at-a-time header writes (WithClientTrace) apply.

It cannot be combined with WithRoundTripper, WithProxy, WithRootCAs, WithTLSConfig or WithCompression; WithConnectTimeout still applies to what the SDK sets. A nil t is refused.

func WithHTTPVersion

func WithHTTPVersion(v HTTPVersion) ClientOption

WithHTTPVersion sets the HTTP version policy: HTTP2Only, the default for an https base URL, or HTTPAuto, the default for an http one. Any other value is refused when the client is built.

func WithHeader

func WithHeader(name, value string) ClientOption

WithHeader sets a header sent on every request. A later WithHeader of the same name, compared without regard to case, replaces an earlier one, as a per-call header replaces a default one in typesafe-sdk-python. Its default headers differ: a mapping that holds two spellings of one name, such as X-Team and x-team, sends both.

The SDK's own headers always win, as they do in typesafe-sdk-python, which writes them over the caller's: a caller's Authorization, Accept, User-Agent, X-TypeSafe-SDK, X-TypeSafe-Runtime and Content-Type are dropped, and so is X-TypeSafe-Retry-Count, which the SDK sets on retries only. WithUserAgentProduct and WithRuntimeHeader are the only ways to change what User-Agent and X-TypeSafe-Runtime carry. The headers that frame a message or manage its connection belong to the transport and are dropped too: Content-Length, Transfer-Encoding, Connection, Proxy-Connection, Keep-Alive, Upgrade, TE, Trailer and Host. Each dropped header is logged at slog.LevelDebug by name, never with its value.

The name must be a valid HTTP field name (RFC 9110, section 5.6.2) and the value a valid field value (RFC 9110, section 5.5), or the client is not built. A name that contains the API key, without regard to case, is refused too, as when the two arguments are swapped, provided the key is at least 8 bytes long: a shorter key, a test's dummy such as "test", occurs in ordinary names. No error repeats a value, which is where a caller puts a token, or a name that holds the key.

func WithLogEndpointHost

func WithLogEndpointHost(log bool) ClientOption

WithLogEndpointHost sets whether log records name the full endpoint URL. The default is true; with false they name only the API path, /v1/systemone or /v1/models, without the scheme, the host or the base URL's path prefix. An error's text, written by the network stack (a dial or DNS error), still names the host, in the error and in the record that logs it.

func WithLogger

func WithLogger(logger *slog.Logger) ClientOption

WithLogger sets the logger the client writes its records to. The default, and what a nil logger means, discards every record. The client never configures the logger's level: typesafe-sdk-python's TYPESAFE_LOG_LEVEL is not read.

func WithMaxResponseBytes

func WithMaxResponseBytes(n int64) ClientOption

WithMaxResponseBytes sets the largest response body a request reads, in bytes; DefaultMaxResponseBytes (16 MiB) unless set. It must be at least 1, since every response carries a body, and at most 1 GiB.

func WithModel

func WithModel(model string) ClientOption

WithModel sets the model a request names when the call names none. Without it the model is read from DefaultModelEnv, and DefaultModel applies when that is unset.

The model is sent as given, without trimming. An empty or blank model is refused rather than sent, where typesafe-sdk-python would send it.

func WithNoTimeout

func WithNoTimeout() ClientOption

WithNoTimeout removes the deadline of each attempt: an attempt then ends when its response is read, when the call's context is done, or when the connection fails. It cannot be used together with WithTimeout.

func WithPretouch

func WithPretouch(types ...reflect.Type) ClientOption

WithPretouch prepares the JSON encoder for each type before the first call that encodes a state or an extra body value of it, so that the call does not pay the one-time compilation the encoder makes per type (which grows with the type's size). It changes nothing on the wire. The types are prepared when the client is built, after every other option is checked; a nil type, or one the encoder refuses (a map whose keys cannot be JSON object keys, for example), fails NewClient with a *ConfigError. Types from several WithPretouch options are all prepared.

func WithProxy

func WithProxy(proxy func(*http.Request) (*url.URL, error)) ClientOption

WithProxy sets the func that chooses a proxy for each request, as http.Transport.Proxy does. The default is http.ProxyFromEnvironment (HTTPS_PROXY, HTTP_PROXY, NO_PROXY); nil disables proxies. It configures the SDK's own transport, so it cannot be combined with WithHTTPTransport or WithRoundTripper. The SDK scrubs the credentials of every proxy the func returned, the 16 most recent, from the client's transport errors, whatever their length (ConnectionError says what a short one costs), and the whole ones of 8 bytes or more from the header of a response to a plain-HTTP request that went through one, which the proxy may have written itself (APIError.Header).

func WithRetry

func WithRetry(policy RetryPolicy) ClientOption

WithRetry sets the retry policy of every call the client makes, unless a call passes its own with Retry; DefaultRetry unless set. A policy with a setting out of range fails NewClient with a *ConfigError that carries the Python SDK's message (RetryPolicy).

func WithRootCAs

func WithRootCAs(pool *x509.CertPool) ClientOption

WithRootCAs sets the certificate authorities the client trusts for the API host and an https proxy. A non-nil pool replaces the RootCAs of WithTLSConfig's configuration; nil keeps that configuration's RootCAs, which are the system's when it sets none or when there is no WithTLSConfig. It configures the SDK's own transport, so it cannot be combined with WithHTTPTransport or WithRoundTripper.

func WithRoundTripper

func WithRoundTripper(rt http.RoundTripper) ClientOption

WithRoundTripper makes the client send every request through rt as it is: no connection policy, no ALPN check, no cold-start gate. The client's per-attempt deadline and response size limit still apply. Closing the client closes rt's idle connections when rt has a CloseIdleConnections method, as an *http.Transport has, and then closes rt when it is an io.Closer, each once. rt owns its connection timeouts, so it cannot be combined with WithHTTPTransport, WithHTTPVersion, WithRootCAs, WithTLSConfig, WithProxy, WithConnectTimeout or WithCompression. A nil rt is refused.

rt must not modify a request, as the net/http.RoundTripper contract already requires: the first attempt of every call hands it the client's own header map, shared by every call and never copied, so a RoundTripper that writes to req.Header changes the headers of later calls and races with concurrent ones. Each request carries its own copy of the endpoint URL.

func WithRuntimeHeader

func WithRuntimeHeader(send bool) ClientOption

WithRuntimeHeader sets whether requests carry X-TypeSafe-Runtime, which names the Go release, operating system and architecture the program was built for, such as go/1.27.1 (linux; amd64). The default is true; false leaves the header out of every request. X-TypeSafe-SDK is sent either way.

func WithTLSConfig

func WithTLSConfig(cfg *tls.Config) ClientOption

WithTLSConfig sets the TLS configuration the client starts from; it is cloned when the option is applied. The SDK raises MinVersion to TLS 1.2 and runs its own ALPN check after the configuration's VerifyConnection; a non-nil pool given to WithRootCAs replaces RootCAs. It configures the SDK's own transport, so it cannot be combined with WithHTTPTransport or WithRoundTripper.

func WithTimeout

func WithTimeout(d time.Duration) ClientOption

WithTimeout sets the deadline of each attempt of a request, from sending the request to reading the whole response; DefaultTimeout unless set. It must be positive; WithNoTimeout removes the deadline, and the two cannot be used together.

func WithUserAgentProduct

func WithUserAgentProduct(product string) ClientOption

WithUserAgentProduct names the application in the User-Agent header, in front of the SDK's own product, the more significant product first (RFC 9110, section 10.1.5): "my-app/1.2.0" sends User-Agent: my-app/1.2.0 typesafe-sdk-go/<Version>. Unset, User-Agent is typesafe-sdk-go/<Version> alone, the form typesafe-sdk-python sends its own name in. X-TypeSafe-SDK always names the SDK alone.

The product must be name/version, both parts tokens (RFC 9110, section 5.6.2: letters, digits and !#$%&'*+-.^_`|~), with exactly one "/" and at most 64 bytes in all. A product that breaks those rules is refused by name of the rule; the error does not repeat it.

type ConfigError

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

ConfigError reports something the caller configured that the SDK cannot use: a ClientOption that fails when the client is built (a missing or malformed API key, the base URL, the model, a timeout, a header name or value, the User-Agent product), or a question set that Questions.Prepare rejects. It is returned before any request is sent, and retrying cannot fix it.

Error returns the message, which names what is wrong without repeating a credential: neither the API key nor the value of a header the caller set is ever part of it. Unwrap returns the errors it wraps, if any. A rejected question set wraps the failure behind the message, such as the syntax error in JSON content; its type is internal to the SDK, so only its text, which the message already carries, is of use to a caller. A condition a caller may want to branch on is wrapped as a sentinel error that the function returning it documents.

func (*ConfigError) Error

func (e *ConfigError) Error() string

Error returns the message.

func (*ConfigError) Unwrap

func (e *ConfigError) Unwrap() []error

Unwrap returns the wrapped errors, or nil when there are none.

type ConnectionError

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

ConnectionError reports a request that produced no HTTP response: the connection could not be made, failed or was lost, or a proxy refused it. Retrying may help.

Error returns "Connection error: <cause>", the cause's text escaped and cut at 200 characters, with every credential of the request replaced by "***": the value of each header whose name marks a credential, and the credential after the scheme of an Authorization value, the API key among them, each when it is at least 8 bytes long.

On the transport the SDK builds, which a client has unless it is given WithHTTPTransport or WithRoundTripper, the credentials of the proxies its proxy func chose (WithProxy, or the proxy environment variables) are replaced too, those of the 16 most recent: each proxy URL's password as it is, as the URL escapes it and each of its words between single spaces, and the Basic token net/http sends for it, which a proxy's answer may repeat. They are recorded when net/http asks the func; the SDK never asks it again. With WithHTTPTransport or WithRoundTripper, the transport keeps its own proxy, whose credentials the SDK does not know: they are not replaced.

Unwrap returns the transport's error, unless it, or an error it wraps, showed a credential in its text, its %+v or its %#v: then it returns a stand-in whose text has the credentials replaced and which unwraps only to the well-known errors the transport's error matched, context.DeadlineExceeded, context.Canceled, os.ErrDeadlineExceeded, io.ErrUnexpectedEOF, io.EOF, net.ErrClosed and a syscall.Errno. An error that points to the request, which only a caller's RoundTripper, dialer or response body can return, is kept, and errors.As reaches the caller's own request through it.

A proxy's credential is replaced whatever its length, where a header's is looked for only from 8 bytes. A short password, or a short word of one, may occur in the error's ordinary text, such as "443" or "dial": Error then shows "***" there too, and Unwrap returns the stand-in, so errors.As no longer reaches the *net.OpError, *net.DNSError or *url.Error in the transport's error, while errors.Is still matches the well-known errors above.

func (*ConnectionError) Error

func (e *ConnectionError) Error() string

Error returns "Connection error: <cause>".

func (*ConnectionError) Proxy

func (e *ConnectionError) Proxy() bool

Proxy reports whether the failure was on the hop to the proxy rather than to the API: the connection to the proxy, its TLS handshake, or, on the transport the SDK builds, its answer other than 200 to the CONNECT, whose status line Error shows with the proxy URL's password, and the Basic token sent for it, replaced by "***". With WithHTTPTransport, that answer is reported as net/http returns it, the status text alone as the proxy wrote it, and Proxy is false.

func (*ConnectionError) Unwrap

func (e *ConnectionError) Unwrap() error

Unwrap returns the transport's error, or its stand-in.

type Content

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

Content is text, or a JSON object or array: what the API accepts as a question's instructions, as the description of an outcome or an option, and as a score level.

Build it with Text or JSON. The zero Content is unset: an optional member holding it is left off the wire, as the Python SDK leaves off an optional member that is None.

func JSON

func JSON(raw []byte) Content

JSON returns content that is the JSON object or array raw, such as JSON([]byte(`{"subject":"refund","amount":20}`)) or a RawJSON value.

raw is kept, not copied, until Questions.Prepare checks it and writes it without its insignificant whitespace; the caller must not modify it in between. Anything but a single JSON object or array, with optional whitespace around it, makes Prepare fail with a *ConfigError; text is built with Text, never from a JSON string. As with RawJSON, a repeated member name inside an object is passed through for the server to decide.

func Text

func Text(s string) Content

Text returns content that is the text s. Text("") is set: it is sent as an empty string, unlike the zero Content.

Example
package main

import (
	"fmt"

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

func main() {
	plain, _ := typesafe.Text("payments or invoices").MarshalJSON()
	structured, _ := typesafe.JSON([]byte(`{"summary": "payments", "examples": ["charged twice"]}`)).MarshalJSON()
	fmt.Println(string(plain))
	fmt.Println(string(structured))
}
Output:
"payments or invoices"
{"summary":"payments","examples":["charged twice"]}

func (Content) IsJSON

func (c Content) IsJSON() bool

IsJSON reports whether c holds a JSON object or array rather than text.

func (Content) IsZero

func (c Content) IsZero() bool

IsZero reports whether c is unset, the zero Content.

func (Content) JSON

func (c Content) JSON() RawJSON

JSON returns the JSON bytes as they were given, or nil when c is text or unset. The slice is shared: callers must not modify it.

func (Content) MarshalJSON

func (c Content) MarshalJSON() ([]byte, error)

MarshalJSON returns c as a JSON value, written as a question writes it, so that Content nested inside a state or a request body member, such as a map[string]any value or a struct field, is sent as content rather than as an empty object: text as a JSON string, escaped as the questions are; JSON content checked and without its insignificant whitespace; unset Content as null. It fails when the text is not valid UTF-8, or when the JSON content is not a single valid JSON object or array, so that invalid JSON never reaches the network; the encoder checks the result a second time.

Nested content costs one copy per call, and JSON content one scan too: it is built in a new slice for the encoder, where a string field is written straight into the body. A Content that is the state itself, or an extra body member's whole value, is written into the body without the copy. For large values, prefer a string field or a top-level Content.

func (Content) Text

func (c Content) Text() string

Text returns the text, or "" when c holds JSON or is unset.

type Error

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

Error is implemented by every error the SDK returns, and only by them: *APIError, *ConnectionError, *TimeoutError, *ResponseValidationError, *ResponseTooLargeError, *ConfigError and *InvalidRequestError. Each is a distinct type that errors.As matches, and errors.As with a *typesafe.Error target matches any of them, as the Python SDK's TypeSafeError base class does. The one exception is a cancellation: a call whose context is cancelled returns ctx.Err() (context.Canceled) itself, as the Python SDK lets asyncio.CancelledError through.

An SDK error is a pointer to a struct that no read changes: where the Python SDK copies, pickles and rebuilds its exceptions, a Go caller shares the pointer between goroutines, or copies the struct as c := *e and uses &c. errors.As takes the address of a variable of the pointer type (var e *APIError) or of the interface (var e Error), never of a struct value.

The SDK never writes a credential into an error's text itself, and in text that others wrote it replaces the credentials it knows of with "***", under the rules each type documents (ConnectionError, APIError.Header). Text the server composed is the exception: an APIError's Message and a field path of a ResponseValidationError are shown as the server sent them, a credential the server or a proxy echoes into them included, as the Python SDK shows them. No error's text holds a request's state or a response body unescaped: text the SDK did not write is escaped and cut (the server's message at 200 characters, a name the server chose at 128, a field path at 320).

type HTTPVersion

type HTTPVersion int

HTTPVersion is the HTTP version policy of a client's transport (WithHTTPVersion). Its zero value is not a policy.

const (
	// HTTP2Only speaks HTTP/2 alone, on one connection per API host: ALPN
	// must choose h2 on https, and http uses HTTP/2 with prior knowledge. An
	// API host that does not negotiate HTTP/2 is refused with a
	// [*ConfigError] wrapping [ErrHTTP2NotNegotiated], before any byte of
	// the request is sent. It is the default for an https base URL.
	HTTP2Only HTTPVersion = iota + 1
	// HTTPAuto lets ALPN choose between HTTP/2 and HTTP/1.1 (HTTP/1.1 on
	// http), with no ALPN refusal and no cap on connections; a cold start
	// still dials once before the requests waiting on it go out. It is the
	// default for an http base URL.
	HTTPAuto
)

func (HTTPVersion) String

func (v HTTPVersion) String() string

String returns the policy's name.

type InvalidRequestError

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

InvalidRequestError reports a request the SDK refuses to send because the API cannot accept it or its body cannot be written as JSON: a state that is not text, a JSON object or an array (nil, a number, a boolean); a plain []byte, whose intent is ambiguous (send string(b) for text or RawJSON(b) for JSON); text that is not valid UTF-8; or a value JSON has no form for, such as NaN, a channel or a cyclic structure. It is returned before any request is sent, and retrying the same request cannot fix it. A request the server itself refuses, with status 400 or 422 for example, is an *APIError instead.

Error returns the message; the part of it that quotes the encoder is escaped and cut at 200 characters, so it never carries the state. Unwrap returns the cause. When the encoder failed, the chain continues to the encoder's own error, and through it to the error a caller's MarshalJSON method returned, which errors.Is and errors.As reach; the encoder's own error types are not part of the SDK's API and may change with it.

func (*InvalidRequestError) Error

func (e *InvalidRequestError) Error() string

Error returns the message.

func (*InvalidRequestError) Unwrap

func (e *InvalidRequestError) Unwrap() error

Unwrap returns the cause, or nil when there is none.

type ModelCard

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

ModelCard describes one model the account can use.

func (ModelCard) Description

func (m ModelCard) Description() string

Description returns the human-readable description of the model.

func (ModelCard) MarshalJSON

func (m ModelCard) MarshalJSON() ([]byte, error)

MarshalJSON returns the card as the Python SDK's model_dump_json writes it, {"name":…,"description":…,"release_date":…}.

func (ModelCard) Name

func (m ModelCard) Name() string

Name returns the model name or alias that a request's model accepts.

func (ModelCard) ReleaseDate

func (m ModelCard) ReleaseDate() string

ReleaseDate returns the model's release date, formatted as YYYY-MM-DD.

type Models

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

Models is the list-models endpoint of a client, from Client.Models.

func (Models) List

func (m Models) List(ctx context.Context, opts ...CallOption) (*ModelsResponse, error)

List lists the models the account can use: GET /v1/models. Of the call options it takes Timeout, Header and Retry; Model and ExtraBody are refused with a *ConfigError. Its errors are those of Client.SystemOne.

Example
package main

import (
	"context"
	"fmt"
	"io"
	"net/http"
	"strings"

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

// cannedAPI answers every request with one status and body. It stands in
// for the API, so that these examples run without the network.
type cannedAPI struct {
	status int
	body   string
}

// RoundTrip implements http.RoundTripper.
func (c cannedAPI) RoundTrip(req *http.Request) (*http.Response, error) {
	if req.Body != nil {
		_, _ = io.Copy(io.Discard, req.Body)
		_ = req.Body.Close()
	}
	return &http.Response{
		StatusCode: c.status,
		Proto:      "HTTP/2.0",
		ProtoMajor: 2,
		Header: http.Header{
			"Content-Type":          {"application/json"},
			"X-Typesafe-Request-Id": {"req_example"},
		},
		Body:          io.NopCloser(strings.NewReader(c.body)),
		ContentLength: int64(len(c.body)),
		Request:       req,
	}, nil
}

// exampleClient returns a client whose requests the canned API answers.
func exampleClient(status int, body string) *typesafe.Client {
	c, err := typesafe.NewClient(
		typesafe.WithAPIKey("example-key"),
		typesafe.WithRoundTripper(cannedAPI{status: status, body: body}),
	)
	if err != nil {
		panic(err)
	}
	return c
}

func main() {
	client := exampleClient(http.StatusOK, `{"models":[{"name":"jev-latest","description":"The latest model","release_date":"2026-09-10"}]}`)
	defer client.Close()
	models, err := client.Models().List(context.Background())
	if err != nil {
		fmt.Println(err)
		return
	}
	for _, m := range models.Models() {
		fmt.Println(m.Name(), m.ReleaseDate())
	}
}
Output:
jev-latest 2026-09-10

type ModelsResponse

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

ModelsResponse is the response to a list-models call: the models the account can use, with the HTTP response they came in.

It is immutable, except that ModelsResponse.UnmarshalJSON replaces the whole response in place: no other goroutine may read the response during the call, and a slice Models returned before it, or a copy of the response made before it, keeps the old cards.

func (ModelsResponse) MarshalJSON

func (r ModelsResponse) MarshalJSON() ([]byte, error)

MarshalJSON returns the response's payload, the object {"models":[{"name":…,"description":…,"release_date":…},…]}, as the Python SDK's model_dump_json writes it, byte for byte: the cards in the order of Models, without the HTTP response (Meta).

A caller's encoding/json escapes <, >, &, U+2028 and U+2029 in this output, as it does in every MarshalJSON's; call MarshalJSON directly for the exact bytes.

The receiver is a value, so a ModelsResponse marshals the same whether it is held by pointer or by value. ModelsResponse.UnmarshalJSON reads the payload back.

func (*ModelsResponse) Meta

func (r *ModelsResponse) Meta() ResponseMeta

Meta returns the HTTP response the models came in.

func (*ModelsResponse) Models

func (r *ModelsResponse) Models() []ModelCard

Models returns the models, in the order the response lists them. The slice is the caller's; the cards share the response's strings.

func (*ModelsResponse) UnmarshalJSON

func (r *ModelsResponse) UnmarshalJSON(data []byte) error

UnmarshalJSON sets *r to the response whose payload is data, such as one ModelsResponse.MarshalJSON returned. data is read as the body of a list-models response is, with the same checks. The response did not come from a request, so its Meta is empty, and it keeps no reference to data. It overwrites *r in place (see ModelsResponse).

A payload the decoder refuses leaves *r unchanged, its Meta included, and fails with a *ResponseValidationError, as SystemOneResponse.UnmarshalJSON does. The JSON literal null leaves *r unchanged.

type Noul

type Noul struct {
	// Instructions is the question or statement to evaluate.
	Instructions Content
	// Yes describes what counts as a yes answer.
	Yes Content
	// No describes what counts as a no answer.
	No Content
}

Noul is a yes/no question: how true is a statement about the state? See https://docs.typesafe.ai/primitives/noul.

Every member is optional; an unset one is left off the wire. Yes and No describe what counts as each outcome and are sent as the "criteria" object's "true" and "false" members; "criteria" is left off when both are unset. The Python SDK can also send an empty "criteria" object or a null outcome, which mean the same as leaving them out; a RawQuestion sends those shapes when they are wanted.

type NoulAnswer

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

NoulAnswer is the answer to a yes/no question. See the noul primitive (https://docs.typesafe.ai/primitives/noul).

The zero NoulAnswer is not an answer a response carried: its Present is false, and MarshalJSON writes it null. DecodeAs leaves one in an optional field whose answer is absent.

func (NoulAnswer) MarshalJSON

func (a NoulAnswer) MarshalJSON() ([]byte, error)

MarshalJSON returns the answer as the Python SDK's model_dump_json writes it, such as {"type":"noul","noul":0.98}. A noul that arrived as -0.0 is written 0.0, where Python writes -0.0: the response reads every zero as 0. An answer that is not Present, such as the zero NoulAnswer, is written null, as Python writes an optional answer the response did not carry (None).

func (NoulAnswer) Noul

func (a NoulAnswer) Noul() float64

Noul returns the probability, from 0 to 1, that the answer is yes or the statement is true.

func (NoulAnswer) Present

func (a NoulAnswer) Present() bool

Present reports whether the answer is one a response carried: true for an answer read from a response, through Answers or DecodeAs, and false for the zero NoulAnswer, which is what DecodeAs leaves in a field tagged optional when the response has no answer for it. The Python SDK holds None there, which it dumps as null, and so does MarshalJSON for an answer that is not present.

type Option

type Option struct {
	// Label is the option's name; the answer's choice and probabilities
	// name options by it.
	Label string
	// Description describes the option. An unset Description is sent as
	// null: the option is interpreted by its label alone.
	Description Content
}

Option is one option of a Choice.

type Options

type Options []Option

Options lists the options of a Choice in the order they are sent.

type Prepared

type Prepared engine.Prepared

Prepared is a question set validated and serialised once by Questions.Prepare, and reused as is by every call that asks it.

It is read-only and safe for concurrent use. It holds, besides the bytes a request sends as "questions", the tables the response decoder matches answers against: the names, each choice question's option labels and each score question's levels.

func PreparedFor

func PreparedFor[T any]() (*Prepared, error)

PreparedFor returns the question set that the struct type T declares: one question per answer field, in field order, prepared as Questions.Prepare prepares a set built by hand.

An answer field is an exported field of type NoulAnswer, ChoiceAnswer or ScoreAnswer whose struct tag has a "typesafe" key describing the question it answers:

type Ticket struct {
	Billing typesafe.NoulAnswer   `typesafe:"kind=noul;instructions=Is this about billing, invoices or refunds?;yes=payments or invoices"`
	Tone    typesafe.ChoiceAnswer `typesafe:"kind=choice;instructions=What is the tone?;options=calm=neutral or polite|angry"`
	Urgency typesafe.ScoreAnswer  `typesafe:"kind=score;instructions=How urgent?;levels=can wait|this week|today"`
	Spam    typesafe.NoulAnswer   `typesafe:"kind=noul;optional;instructions=Spam?"`
}

PreparedFor[Ticket] asks the same four questions, with the same bytes on the wire, as

typesafe.NewQuestions().
	Noul("Billing", typesafe.Noul{Instructions: typesafe.Text("Is this about billing, invoices or refunds?"), Yes: typesafe.Text("payments or invoices")}).
	Choice("Tone", typesafe.Choice{Instructions: typesafe.Text("What is the tone?"), Options: typesafe.Options{{Label: "calm", Description: typesafe.Text("neutral or polite")}, {Label: "angry"}}}).
	Score("Urgency", typesafe.Score{Instructions: typesafe.Text("How urgent?"), Levels: []typesafe.Content{typesafe.Text("can wait"), typesafe.Text("this week"), typesafe.Text("today")}}).
	Noul("Spam", typesafe.Noul{Instructions: typesafe.Text("Spam?")}).
	Prepare()

Tag grammar

The tag is a list of entries separated by ";". An entry is a key, "=" and a value, or the bare key optional. The keys are:

  • kind (required): noul, choice or score; it must agree with the field's type (kind=noul for a NoulAnswer field, and so on).
  • name: the name the question is asked under and its answer comes back under. Without it the name is the field's name as written, Billing for the field Billing: nothing is lowercased or converted, as encoding/json does with an untagged field. The names type, model, usage and answers are reserved.
  • instructions: the question's instructions, for every kind.
  • yes, no: what counts as a yes and as a no answer (kind=noul only), the Noul members of the same names.
  • options: the options of a choice (kind=choice only, and required there), separated by "|", in order. An option is a label, or a label, "=" and its description: options=calm=neutral or polite|angry. Labels must be unique.
  • levels: the levels of a score (kind=score only, and required there), separated by "|", lowest first. Levels must be unique.
  • optional: the answer may be absent from a response. It changes nothing on the wire; it tells the typed decoder to leave the field's Present false instead of failing when the answer is missing.

Every value is text, sent as Text content; JSON content is written with Questions instead. A value may contain any character, but four must be escaped where they would otherwise end something: "\;" is a ";", "\|" a "|", "\=" a "=" and "\\" a backslash. A ";" always ends the entry, so it is escaped in every value. A "|" ends an option or a level, so it is escaped in every option and level, descriptions included: options=calm=neutral|polite|angry asks three options, calm, polite and angry, where options=calm=neutral\|polite|angry asks two. The first "=" of an option ends its label, and a later "=" belongs to the description. In instructions, name, yes and no, "|" and "=" stand for themselves, and in a level "=" does; escaping them there is allowed but not needed. No other escape exists, keys are never escaped, and no space is trimmed.

These are errors: an unknown key, a key given twice, a key the kind does not take (yes and no belong to noul, options to choice, levels to score), an empty value, an empty entry (";;", or a ";" at either end), an empty option, level, label or description, a backslash before any other character, a backslash at the end of the tag, and a tag that is not valid UTF-8.

A struct tag is itself a Go string literal, so in source each of these backslashes is written twice: `typesafe:"kind=noul;instructions=a\\;b"` asks "a;b". A single backslash makes the tag malformed (go vet reports it), and PreparedFor refuses the field instead of ignoring the tag, as it does when the key is written with a space around its colon, is given twice, or follows a malformed pair: every tag that reflect would read as having no typesafe key, or only the first of two, while it names one.

Rejections

PreparedFor fails with a *ConfigError that names T, the field and the rule when:

  • two fields ask under the same name;
  • a choice has no options, or lists an option label twice (a choice built with Questions may have none: its empty criteria are sent for the server to judge);
  • a score lists a level twice;
  • a score has no levels;
  • an answer field's tag has no kind, or the field has no tag at all;
  • the kind is not noul, choice or score;
  • the kind does not agree with the field's type, or a field that is not an answer field has a typesafe tag;
  • a field is a pointer to an answer type (write optional instead);
  • an unexported field has a typesafe tag;
  • a struct-typed field, embedded or named, value or pointer, holds a field with a typesafe tag at any depth: the fields of a nested struct are not read, and an embedded struct's are not promoted;
  • T is not a struct type (a pointer to a struct included);
  • a question name is reserved;
  • optional is given on a field that is not an answer field;
  • the tag does not follow the grammar above;
  • T has no answer fields: "At least one question is required.", as Questions.Prepare reports for an empty set.

Fields that are neither answer fields nor tagged, and unexported fields without a typesafe tag, are ignored. A type alias of an answer type is that answer type; a type defined from one is not an answer type.

Caching

T is inspected once: the first call builds the set, or the error, and every later call for the same T returns that same *Prepared, or that same error, for the life of the process, without allocating. The set is read-only and safe for concurrent use, like every prepared set; concurrent first calls may build it more than once, but all of them return the one that was kept.

Example
package main

import (
	"fmt"

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

// Review is a question set declared as a struct: each tagged field is a
// question, and receives its answer.
type Review struct {
	Spam    typesafe.NoulAnswer   `typesafe:"kind=noul;name=spam;instructions=Is this review spam?"`
	Tone    typesafe.ChoiceAnswer `typesafe:"kind=choice;name=tone;instructions=What is the tone?;options=friendly|hostile=rude or threatening"`
	Quality typesafe.ScoreAnswer  `typesafe:"kind=score;name=quality;instructions=How useful is the review?;levels=bad|ok|great"`
}

func main() {
	questions, err := typesafe.PreparedFor[Review]()
	if err != nil {
		fmt.Println(err)
		return
	}
	for name := range questions.Names() {
		fmt.Println(name)
	}
}
Output:
spam
tone
quality

func (*Prepared) Len

func (p *Prepared) Len() int

Len returns the number of questions in the set. It is never zero for a set returned by Questions.Prepare.

func (*Prepared) Names

func (p *Prepared) Names() iter.Seq[string]

Names returns the question names in the order they are sent.

type Questions

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

Questions is a question set under construction: the questions of one call, keyed by the names their answers come back under, in the order they are sent.

Nothing is checked until Questions.Prepare, which validates and serialises the whole set once. The zero Questions is an empty set ready to use.

Adding a question copies the question value: a later change to the Noul, Choice, Score or RawQuestion variable that was added, such as assigning its Instructions, does not reach the set. The copy shares what the value refers to: the Options and Levels slices, the Fields map and the maps and slices inside it, and the bytes behind JSON content and RawJSON. Those are read by Prepare and must not change between adding the question and the return of Prepare; the prepared set shares none of them.

func NewQuestions

func NewQuestions() *Questions

NewQuestions returns an empty question set.

Example
package main

import (
	"fmt"

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

func main() {
	questions, err := typesafe.NewQuestions().
		Noul("spam", typesafe.Noul{Instructions: typesafe.Text("Is this review spam?")}).
		Choice("tone", typesafe.Choice{
			Instructions: typesafe.Text("What is the tone?"),
			Options:      typesafe.Options{{Label: "friendly"}, {Label: "hostile", Description: typesafe.Text("rude or threatening")}},
		}).
		Score("quality", typesafe.Score{
			Instructions: typesafe.Text("How useful is the review?"),
			Levels:       []typesafe.Content{typesafe.Text("bad"), typesafe.Text("ok"), typesafe.Text("great")},
		}).
		Raw("language", typesafe.RawQuestion{Type: "choice", Fields: map[string]any{
			"instructions": "Which language is the review in?",
			"criteria":     map[string]any{"en": nil, "ja": nil},
		}}).
		Prepare()
	if err != nil {
		fmt.Println(err)
		return
	}
	for name := range questions.Names() {
		fmt.Println(name)
	}
}
Output:
spam
tone
quality
language

func (*Questions) Choice

func (qs *Questions) Choice(name string, q Choice) *Questions

Choice adds a copy of the choice question q under name and returns the set; Questions says what the copy shares with q.

func (*Questions) Noul

func (qs *Questions) Noul(name string, q Noul) *Questions

Noul adds a copy of the yes/no question q under name and returns the set; Questions says what the copy shares with q.

func (*Questions) Prepare

func (qs *Questions) Prepare() (*Prepared, error)

Prepare validates the set and serialises it into the bytes every call that asks it sends.

It fails with a *ConfigError for the first problem, in the order the questions were added:

  • the set is empty: "At least one question is required.";
  • a name is added twice (the Python SDK's dictionary would keep the last question; a Go set rejects the repeat), or is not valid UTF-8;
  • a Score has no levels, or a raw "score" has empty criteria: `Score question "<name>" has no criteria; at least one score is required.`;
  • a RawQuestion has an empty Type, or Fields name "type": `Question "<name>" must be a question object or a dictionary with a nonempty string "type".`;
  • a raw "choice" or "score" has no "criteria" field: `Question "<name>" requires "criteria".`;
  • a Choice repeats an option label, or a Score level is unset;
  • content or a raw field value cannot be written: text or a string that is not valid UTF-8, JSON content that is not a single valid JSON object or array, a field value of an unsupported type, or a float that is NaN or infinite.

The first four messages are the Python SDK's. The returned set does not share memory with the questions: changing them afterwards does not change it.

Example
package main

import (
	"errors"
	"fmt"

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

func main() {
	_, err := typesafe.NewQuestions().
		Noul("spam", typesafe.Noul{}).
		Noul("spam", typesafe.Noul{Instructions: typesafe.Text("Is it spam?")}).
		Prepare()
	_, ok := errors.AsType[*typesafe.ConfigError](err)
	fmt.Println(ok)
	fmt.Println(err)
}
Output:
true
Question "spam" is added more than once; question names must be unique.

func (*Questions) Raw

func (qs *Questions) Raw(name string, q RawQuestion) *Questions

Raw adds a copy of the raw question q under name and returns the set; Questions says what the copy shares with q.

func (*Questions) Score

func (qs *Questions) Score(name string, q Score) *Questions

Score adds a copy of the score question q under name and returns the set; Questions says what the copy shares with q.

type RawJSON

type RawJSON []byte

RawJSON is one JSON value that is already encoded. The SDK never decodes it: strings keep their escape sequences and numbers their spelling. That makes it the way to send a number spelled exactly: a float in a state that is not RawJSON is written as Go's JSON encoders spell it, 3 for 3.0 and 10000000000000000 for 1e16, where the Python SDK writes 3.0 and 1e+16.

As a RawQuestion field value it is checked and written without its insignificant whitespace, as the value it holds; JSON takes one to build structured Content. As the state of a call, or as the value of a member the call adds to the request body, it is sent as it is, whitespace included, after a check of its first byte only: a state must start a string, an array or an object, a member any JSON value. That it is one valid JSON value is then the caller's contract. A *RawJSON there is sent as the RawJSON it points to; a nil one is refused. Nested inside a state or a member's value (a map value, a struct field, a slice element, through a pointer too), it is written by RawJSON.MarshalJSON: checked and written without its insignificant whitespace, as a question field is. A member name repeated inside an object (`{"a":1,"a":2}`) is passed through unchanged, which a Python dict cannot produce; the server decides which one counts.

func (RawJSON) MarshalJSON

func (r RawJSON) MarshalJSON() ([]byte, error)

MarshalJSON returns r without its insignificant whitespace, or null when r is nil, as json.RawMessage does, so that RawJSON nested inside a state or a request body member is sent as the JSON it holds, never as a base64 string. It fails when r is not exactly one valid JSON value, checked by the same scanner as a question's JSON, so that invalid JSON never reaches the network; the encoder checks the result a second time.

Nested RawJSON costs one scan and one copy of its bytes per call. A RawJSON that is the state itself, or an extra body member's whole value, is sent as it is and costs neither.

type RawQuestion

type RawQuestion struct {
	// Type is the question type, sent as the "type" member.
	Type string
	// Fields are the other members of the question object.
	Fields map[string]any
}

RawQuestion is a question of a type, or with members, that this version of the SDK does not model, sent as a JSON object: "type" first, then Fields in sorted key order.

Type must not be empty, and Fields must not name "type". A field value is nil, a bool, a string, an integer or float kind (a float must be finite, and is spelled as the Python SDK spells it: 3.0, 0.00001, 1e-6, 1e+16), []any, []string, map[string]any, map[string]string, Content (unset Content is null) or RawJSON, nested in maps and slices to any depth up to 1000 levels. Map members are sent in sorted key order, as a Go map has no order of its own. A value of any other type, such as []int, a named string type or a struct, makes Prepare fail: encode it first and pass the bytes as RawJSON, which is checked and sent without its insignificant whitespace but never decoded.

Questions.Prepare applies the checks the Python SDK applies to a raw question: a "choice" or "score" has a "criteria" field, and a "score"'s criteria is not empty (not null, false, 0, "", [] or {}). Everything else is left to the server.

type ResponseMeta

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

ResponseMeta is the HTTP side of a response, the Python SDK's raw_http_response: the status, the header and the body exactly as they arrived. The header and the body are shared with the response, not copied, and must not be modified. A response that did not come from a request, such as one read back from JSON, has an empty ResponseMeta.

It is HTTP metadata, not part of the response's payload: a response's MarshalJSON leaves it out, and it has no MarshalJSON of its own.

func (ResponseMeta) Header

func (m ResponseMeta) Header() http.Header

Header returns the response header as it arrived, or nil when there is no HTTP response. Unlike the Header of the SDK's errors, it is not redacted: a header that is a credential by its name, such as Set-Cookie, and a value that holds the API key are there as the server sent them, and fmt shows them when it prints the response. Redact the header before logging it.

func (ResponseMeta) RawBody

func (m ResponseMeta) RawBody() []byte

RawBody returns the body exactly as it was received, or nil when there is no HTTP response. It holds what the decoded response leaves out: members and answer types this version does not model.

func (ResponseMeta) RequestID

func (m ResponseMeta) RequestID() (string, bool)

RequestID returns the server's identifier for the request, from the x-typesafe-request-id response header, and whether the header was present; a repeated header's values are joined with ", ". It is the server's text as it arrived. The Python SDK raises where this reports false.

func (ResponseMeta) StatusCode

func (m ResponseMeta) StatusCode() int

StatusCode returns the HTTP status code, or zero when there is no HTTP response.

type ResponseTooLargeError

type ResponseTooLargeError struct {
	// StatusCode is the HTTP status code.
	StatusCode int
	// Header is the response header as a new map with each credential's
	// value "***", as [APIError.Header] describes; the values of the other
	// headers are the response's own and must not be modified.
	Header http.Header
	// Endpoint is the request's method and URL without credentials, query
	// or fragment, or empty when it is not known.
	Endpoint string
	// Limit is the size limit the body exceeded, in bytes.
	Limit int64
}

ResponseTooLargeError reports a successful response whose body is larger than the client's size limit (16 MiB unless configured otherwise): the body was not read past the limit and nothing was decoded. Retrying the same request cannot help. A failure status with a body over the limit is an *APIError with an empty body instead, which keeps its status and headers.

func (*ResponseTooLargeError) Error

func (e *ResponseTooLargeError) Error() string

Error returns "<Endpoint>: <StatusCode> The response body is larger than the limit of <Limit> bytes. (request_id=<id>)".

func (*ResponseTooLargeError) RequestID

func (e *ResponseTooLargeError) RequestID() (string, bool)

RequestID returns the server's identifier for the request, as APIError.RequestID does.

type ResponseValidationError

type ResponseValidationError struct {
	// StatusCode is the HTTP status code.
	StatusCode int
	// Header is the response header as a new map with each credential's
	// value "***", as [APIError.Header] describes; the values of the other
	// headers are the response's own and must not be modified.
	Header http.Header
	// Body is the response body as it arrived. It is shared, not copied,
	// and must not be modified.
	Body []byte
	// Endpoint is the request's method and URL without credentials, query
	// or fragment, or empty when it is not known.
	Endpoint string
	// FieldPath is the path of the first failure as Error prints it: the
	// Python SDK's field_path, such as "answers.tone.confidence" or
	// "models[1].name", with "." for the body as a whole (Python writes the
	// empty string), and each name the server chose escaped and cut at 128
	// characters.
	FieldPath string
	// contains filtered or unexported fields
}

ResponseValidationError reports a successful response whose body is not the response the SDK expected: not one JSON object, not valid UTF-8, a raw control character in a string, data after the object, nesting past the codec's 4096 levels, or a member that is missing or of the wrong kind. FieldPath names the first failure the Python SDK would report.

Error renders "<Endpoint>: <StatusCode> Invalid response data at '<FieldPath>'. (request_id=<id>)", as the Python SDK's str() of TypeSafeAPIResponseValidationError does. Unwrap returns the decoder's failure, whose type is internal to the SDK. A failure of a typed decode (DecodeAs, Ask) names FieldPath as the Python SDK's response model with one field per answer names it, such as "tone.choice", while the failure it wraps keeps the answer's place in the body, "answers.tone.choice".

func (*ResponseValidationError) Error

func (e *ResponseValidationError) Error() string

Error returns "<Endpoint>: <StatusCode> Invalid response data at '<FieldPath>'. (request_id=<id>)".

func (*ResponseValidationError) RequestID

func (e *ResponseValidationError) RequestID() (string, bool)

RequestID returns the server's identifier for the request, as APIError.RequestID does.

func (*ResponseValidationError) Unwrap

func (e *ResponseValidationError) Unwrap() error

Unwrap returns the decoder's failure, or nil.

type RetryPolicy

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

RetryPolicy decides whether a call makes another attempt after one fails, and how long it waits first, as typesafe-sdk-python's RetryPolicy does. A policy is a value: each setter returns a copy with one setting changed, so a policy can be shared by goroutines and derived from freely. WithRetry sets a client's policy and Retry one call's.

The zero RetryPolicy is DefaultRetry, the Python SDK's RetryPolicy():

After an attempt fails, the call makes another when all of these hold, in the order tenacity, which the Python SDK runs its policy on, checks them:

  • The error is one the policy retries: a *TimeoutError or a *ConnectionError of a kind it retries, an *APIError whose status is in its set, or any error its predicate accepts (RetryPolicy.Predicate). A successful (2xx) status is never retried by status, even when the set holds it: such a response is billed, so a *ResponseValidationError is retried only when the predicate accepts it. A cancellation (context.Canceled) is never retried, whatever the predicate says.
  • Fewer than MaxRetries retries have been made.
  • The budget allows the wait: the time since the call's first attempt started, which is after the call's options were checked and its body encoded, plus the wait is below the budget (tenacity's stop_before_delay).

The wait is the server's when the policy honours it and the failed response carries a Retry-After-Ms (milliseconds) or a Retry-After (seconds, or an HTTP date) that parses, whatever the response's status, as APIError.RetryAfter reads it; however long it is, only the budget refuses it. Otherwise it is the backoff.

A retry sends the same body and headers as the first attempt, plus X-TypeSafe-Retry-Count with the number of attempts before it, and has a deadline of its own (WithTimeout, Timeout). The wait ends early when the call's context is done: a cancellation returns ctx.Err(), context.Canceled, itself, and a passed deadline a *TimeoutError without a timeout. A call that stops returns the error of its last attempt. The transport's own replay of a request inside one attempt (an HTTP/2 request the server refused or never processed) is neither a retry nor counted by Stats, and runs inside that attempt's deadline.

A setting out of range is kept in the value and reported when the policy is used: NewClient with WithRetry, or a call with Retry, fails with a *ConfigError that carries the Python SDK's message, before anything is sent. The messages name the Python SDK's parameters: max_retries is RetryPolicy.MaxRetries; backoff_initial, backoff_max and backoff_jitter are the arguments of RetryPolicy.Backoff; timeout is RetryPolicy.Budget, and timeout=None is RetryPolicy.NoBudget. Its other parameters are http_statuses (RetryPolicy.Statuses), respect_retry_after (RetryPolicy.RespectRetryAfter), api_connection_error (RetryPolicy.ConnectionErrors), api_timeout_error (RetryPolicy.TimeoutErrors) and predicate (RetryPolicy.Predicate); exceptions has no counterpart. A duration cannot be NaN or infinite, where the Python SDK refuses such a number of seconds.

Example
package main

import (
	"context"
	"fmt"
	"io"
	"net/http"
	"strings"

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

// cannedAPI answers every request with one status and body. It stands in
// for the API, so that these examples run without the network.
type cannedAPI struct {
	status int
	body   string
}

// RoundTrip implements http.RoundTripper.
func (c cannedAPI) RoundTrip(req *http.Request) (*http.Response, error) {
	if req.Body != nil {
		_, _ = io.Copy(io.Discard, req.Body)
		_ = req.Body.Close()
	}
	return &http.Response{
		StatusCode: c.status,
		Proto:      "HTTP/2.0",
		ProtoMajor: 2,
		Header: http.Header{
			"Content-Type":          {"application/json"},
			"X-Typesafe-Request-Id": {"req_example"},
		},
		Body:          io.NopCloser(strings.NewReader(c.body)),
		ContentLength: int64(len(c.body)),
		Request:       req,
	}, nil
}

func main() {
	// A policy is a value: each setter returns a changed copy. This one
	// retries twice, at once (a zero backoff), on top of the defaults.
	quick := typesafe.DefaultRetry().MaxRetries(2).Backoff(0, 0, 0)
	client, err := typesafe.NewClient(
		typesafe.WithAPIKey("example-key"),
		typesafe.WithRetry(quick),
		typesafe.WithRoundTripper(cannedAPI{status: http.StatusServiceUnavailable, body: `{"detail":"try again later"}`}),
	)
	if err != nil {
		fmt.Println(err)
		return
	}
	defer client.Close()

	// The client's policy: the first attempt and two retries.
	_, err = client.Models().List(context.Background())
	fmt.Println(err != nil, client.Stats().Attempts)
	// One call's own policy: no retry.
	_, err = client.Models().List(context.Background(), typesafe.Retry(typesafe.NoRetry()))
	fmt.Println(err != nil, client.Stats().Attempts)
}
Output:
true 3
true 4

func DefaultRetry

func DefaultRetry() RetryPolicy

DefaultRetry returns the policy a client uses unless WithRetry sets another, as the Python SDK uses RetryPolicy() when its retry argument is None, with the settings RetryPolicy lists. It is the zero RetryPolicy.

func NoRetry

func NoRetry() RetryPolicy

NoRetry returns the policy that never tries a call again: every call makes exactly one attempt, as the Python SDK's RetryPolicy(max_retries=0) does. It is DefaultRetry().MaxRetries(0).

func (RetryPolicy) Backoff

func (p RetryPolicy) Backoff(initial, maximum time.Duration, jitter float64) RetryPolicy

Backoff returns p with the wait before retry n (from 1) that no Retry-After decides: initial doubled n-1 times, at most maximum, less a random fraction of at most jitter of it, rounded to the millisecond and never above the doubled value (the Python SDK's _backoff). A zero initial or maximum retries at once. initial and maximum must not be negative and jitter must be between 0 and 1, or the policy is refused when it is used.

func (RetryPolicy) Budget

func (p RetryPolicy) Budget(d time.Duration) RetryPolicy

Budget returns p with a budget of d for each call: the call makes no retry whose wait would bring the time since its first attempt started to d or beyond, and returns the error of its last attempt instead (tenacity's stop_before_delay). The clock starts once the call's options are checked and its body is encoded. d must be positive, or the policy is refused when it is used. The default is 30 s; RetryPolicy.NoBudget removes the budget.

func (RetryPolicy) ConnectionErrors

func (p RetryPolicy) ConnectionErrors(retry bool) RetryPolicy

ConnectionErrors returns p retrying, or with false not retrying, an attempt that failed with a *ConnectionError. The default is true.

func (RetryPolicy) MaxRetries

func (p RetryPolicy) MaxRetries(n int) RetryPolicy

MaxRetries returns p with at most n retries after the first attempt of a call; 0 makes every call a single attempt. A negative n is refused when the policy is used.

func (RetryPolicy) NoBudget

func (p RetryPolicy) NoBudget() RetryPolicy

NoBudget returns p without a budget: only MaxRetries and the caller's context bound a call, as the Python SDK's timeout=None does.

func (RetryPolicy) Predicate

func (p RetryPolicy) Predicate(accept func(err error) bool) RetryPolicy

Predicate returns p also retrying every error for which accept returns true, on top of the rules of the other settings, as the Python SDK's predicate does; nil removes a predicate. accept is called with the error of each failed attempt that the other rules do not retry, the last attempt's included, and may be called from several goroutines at once. It is not called for a cancellation (context.Canceled), which is never retried. The Python SDK's exceptions setting has no counterpart: accept can test an error's type with errors.As.

func (RetryPolicy) RespectRetryAfter

func (p RetryPolicy) RespectRetryAfter(respect bool) RetryPolicy

RespectRetryAfter returns p honouring, or with false ignoring, the wait a failed response asks for in Retry-After-Ms or Retry-After; the backoff applies when it is ignored. The default is true.

func (RetryPolicy) Statuses

func (p RetryPolicy) Statuses(codes ...int) RetryPolicy

Statuses returns p retrying the response statuses codes and no others, in place of 408, 429 and 500 to 599; with no codes it retries no status. A successful (2xx) status among them is accepted and never retried.

func (RetryPolicy) TimeoutErrors

func (p RetryPolicy) TimeoutErrors(retry bool) RetryPolicy

TimeoutErrors returns p retrying, or with false not retrying, an attempt that failed with a *TimeoutError. The default is true.

type Score

type Score struct {
	// Instructions is what the model should rate; it is optional.
	Instructions Content
	// Levels describes the scale, lowest first: a level's index is its
	// score. At least one level is required and none may be unset.
	Levels []Content
}

Score is a question that rates the state on an ordered scale. See https://docs.typesafe.ai/primitives/score.

type ScoreAnswer

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

ScoreAnswer is the answer to a score question: the expected score, the question's rubric as the response echoes it, and the probability of every level. Levels count from zero. See the score primitive (https://docs.typesafe.ai/primitives/score).

The zero ScoreAnswer is not an answer a response carried: its Present is false, and MarshalJSON writes it null. DecodeAs leaves one in an optional field whose answer is absent.

func (ScoreAnswer) Confidence

func (a ScoreAnswer) Confidence() float64

Confidence returns how sure the model is of the score, from 0 to 1.

func (ScoreAnswer) Description

func (a ScoreAnswer) Description(level uint32) (Content, bool)

Description returns the description of level from the legend, and whether the legend lists that level. A description sent as JSON keeps the exact bytes it arrived as; they are shared and must not be modified.

func (ScoreAnswer) Legend

func (a ScoreAnswer) Legend() iter.Seq2[uint32, Content]

Legend yields every level of the legend with its description, in the order the response lists them. Levels are unique.

func (ScoreAnswer) MarshalJSON

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

MarshalJSON returns the answer as the Python SDK's model_dump_json writes it, such as {"type":"score","score":0.0,"confidence":1.0,"legend":{"0":"bad"},"probabilities":{"0":1.0}}, the legend and the probabilities in the order of Legend and Probabilities, each level a decimal member name. A structured level is written as the bytes the response carried (ScoreAnswer.Description), where the Python SDK writes the value it parsed: the two differ when those bytes hold an escape, whitespace, a repeated member name (Python keeps the last) or a number spelled otherwise than Python spells it. A float that arrived as -0.0 is written 0.0, where Python writes -0.0: the response reads every zero as 0. An answer that is not Present is written null, as NoulAnswer.MarshalJSON says.

func (ScoreAnswer) Present

func (a ScoreAnswer) Present() bool

Present reports whether the answer is one a response carried, as NoulAnswer.Present does.

func (ScoreAnswer) Probabilities

func (a ScoreAnswer) Probabilities() iter.Seq2[uint32, float64]

Probabilities yields every level with its probability, in the order the response lists them. Levels are unique.

func (ScoreAnswer) Probability

func (a ScoreAnswer) Probability(level uint32) (float64, bool)

Probability returns the probability of level, and whether the answer lists that level.

func (ScoreAnswer) Score

func (a ScoreAnswer) Score() float64

Score returns the expected score: the probability-weighted average of the levels, which may fall between two of them.

type Stats

type Stats struct {
	// Dials counts the connections the SDK's transport (its own, or the
	// clone of WithHTTPTransport) handed to a request for the first time,
	// re-dials and the transport's internal replays included; zero under
	// WithRoundTripper.
	Dials uint64
	// Attempts counts the attempts the client sent: one per request that
	// reached the transport, whatever came of it. The transport's own
	// replay of a request inside one attempt is not counted.
	Attempts uint64
}

Stats counts what a client has done since it was built.

type SystemOneResponse

type SystemOneResponse engine.Response

SystemOneResponse is the response to a System One call: the model that answered, the token usage and the answers, with the HTTP response they came in. See System One (https://docs.typesafe.ai/concepts/system-one).

It is immutable, except that SystemOneResponse.UnmarshalJSON replaces the whole response in place: an Answers view taken before the call shows the new answers, a copy of the response made before the call keeps the old ones, and no other goroutine may read the response during the call.

func (*SystemOneResponse) Answers

func (r *SystemOneResponse) Answers() Answers

Answers returns the answers, keyed by question name. The view reads r: it is valid as long as r is, and after SystemOneResponse.UnmarshalJSON it shows the answers r then holds.

func (SystemOneResponse) MarshalJSON

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

MarshalJSON returns the response's payload, the object {"model":…,"usage":{…},"answers":{…}}, as the Python SDK's model_dump_json writes it: without the HTTP response (Meta) and without any view of the answers. Floats are spelled as Python spells them (0.0, 0.98, 1e-7, 1e+20), a token count the response left out is null, and the answers come in the order of Answers.All. A structured legend level is written as the bytes the response carried (ScoreAnswer.Description), where the Python SDK writes the value it parsed; the two differ only when those bytes hold an escape, whitespace, a repeated member name (Python keeps the last) or a number spelled otherwise than Python spells it. A float member that arrived as -0.0 is written 0.0, where Python writes -0.0: the response reads every zero as 0. Every other byte is the Python SDK's.

A caller's encoding/json escapes <, >, &, U+2028 and U+2029 in this output, as it does in every MarshalJSON's; call MarshalJSON directly for the exact bytes.

The receiver is a value, so a SystemOneResponse marshals the same whether it is held by pointer or by value. SystemOneResponse.UnmarshalJSON reads the payload back.

Example
package main

import (
	"fmt"

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

func main() {
	var stored typesafe.SystemOneResponse
	if err := stored.UnmarshalJSON([]byte(`{"model":"jev-latest","usage":{"input_tokens":12},"answers":{"spam":{"type":"noul","noul":0.02}}}`)); err != nil {
		fmt.Println(err)
		return
	}
	payload, err := stored.MarshalJSON()
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println(string(payload))
}
Output:
{"model":"jev-latest","usage":{"input_tokens":12,"output_tokens":null},"answers":{"spam":{"type":"noul","noul":0.02}}}

func (*SystemOneResponse) Meta

func (r *SystemOneResponse) Meta() ResponseMeta

Meta returns the HTTP response the answers came in.

func (*SystemOneResponse) Model

func (r *SystemOneResponse) Model() string

Model returns the model that answered.

func (*SystemOneResponse) UnmarshalJSON

func (r *SystemOneResponse) UnmarshalJSON(data []byte) error

UnmarshalJSON sets *r to the response whose payload is data, such as one SystemOneResponse.MarshalJSON returned. data is read as the body of a System One response is, with the same checks and the same rules for a repeated member; an answer of a type this version does not model is left out without a log line. The response did not come from a request, so its Meta is empty, and it keeps no reference to data. It overwrites *r in place; SystemOneResponse says what that means to views and copies taken before the call.

A payload the decoder refuses leaves *r unchanged, its Meta included, and fails with a *ResponseValidationError whose FieldPath names the first failure, whose StatusCode is zero, and whose Header, Body and Endpoint are empty. As json.Unmarshaler implementations do by convention, the JSON literal null leaves *r unchanged.

func (*SystemOneResponse) Usage

func (r *SystemOneResponse) Usage() Usage

Usage returns the token usage of the request.

type TimeoutError

type TimeoutError struct {
	// Timeout is the per-attempt timeout the attempt was given, or zero
	// when the deadline came from the caller's context alone.
	Timeout time.Duration
	// contains filtered or unexported fields
}

TimeoutError reports a request attempt that did not complete within its deadline. Retrying may help. It is not a *ConnectionError, which errors.As tells apart; the Python SDK's TypeSafeAPITimeoutError is a subclass of its connection error.

func (*TimeoutError) Error

func (e *TimeoutError) Error() string

Error returns "Request timed out (timeout=<seconds>s).", or "Request timed out." without a timeout, with " on the proxy hop" after "out" when the hop to the proxy timed out. The Python SDK's str() prints the timeout as a float without a unit ("timeout=10.0"); the Go port prints it with an "s" and without a trailing ".0" ("timeout=10s").

func (*TimeoutError) Proxy

func (e *TimeoutError) Proxy() bool

Proxy reports whether the attempt timed out on the hop to the proxy (the connection to it, or its TLS handshake) rather than to the API, as ConnectionError.Proxy reports a proxy failure.

func (*TimeoutError) Unwrap

func (e *TimeoutError) Unwrap() error

Unwrap returns the error that ended the attempt, such as context.DeadlineExceeded, or its stand-in when its chain printed a credential of the request, as ConnectionError describes.

type Usage

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

Usage is the token usage a response reported. The API may leave either count out, which is not the same as a count of zero; a count cannot be negative, and a response that reports one is refused.

func (Usage) InputTokens

func (u Usage) InputTokens() (uint64, bool)

InputTokens returns the number of billable input tokens, and whether the response reported it.

func (Usage) MarshalJSON

func (u Usage) MarshalJSON() ([]byte, error)

MarshalJSON returns the usage as the Python SDK's model_dump_json writes it, such as {"input_tokens":12,"output_tokens":3}, a count the response left out as null.

func (Usage) OutputTokens

func (u Usage) OutputTokens() (uint64, bool)

OutputTokens returns the number of output tokens, and whether the response reported it.

Directories

Path Synopsis
examples
concurrency command
Command concurrency shares one client between goroutines.
Command concurrency shares one client between goroutines.
logging command
Command logging gives the client a log/slog logger.
Command logging gives the client a log/slog logger.
options command
Command options shows a client's settings and a call's: the forms a state may take, a question given as raw fields, and the options that override the client's deadline, retries and headers for one call.
Command options shows a client's settings and a call's: the forms a state may take, a question given as raw fields, and the options that override the client's deadline, retries and headers for one call.
quickstart command
Command quickstart asks the TypeSafe API one question about a support ticket and prints the answer.
Command quickstart asks the TypeSafe API one question about a support ticket and prints the answer.
retries command
Command retries sets a retry policy for a client and for one call, and tells the errors of a call apart once its retries are spent.
Command retries sets a retry policy for a client and for one call, and tells the errors of a call apart once its retries are spent.
transport command
Command transport gives the client a transport of its own: an *http.Transport the SDK clones and configures, or any http.RoundTripper, used as it is.
Command transport gives the client a transport of its own: an *http.Transport the SDK clones and configures, or any http.RoundTripper, used as it is.
typed command
Command typed declares its questions as a struct: each tagged field is a question, and receives its answer.
Command typed declares its questions as a struct: each tagged field is a question, and receives its answer.
internal
benchmark
Package benchmark holds the SDK's benchmarks that need only its exported API and the module's internal packages (docs/perf/benchmarks.md): B5, the whole call against its floor and the naive comparator; B4's Retry-After parse; B6's warm call over loopback; the header template's clone; and an empty loop.
Package benchmark holds the SDK's benchmarks that need only its exported API and the module's internal packages (docs/perf/benchmarks.md): B5, the whole call against its floor and the naive comparator; B4's Retry-After parse; B6's warm call over loopback; the header template's clone; and an empty loop.
codec
Package codec is the SDK's JSON layer.
Package codec is the SDK's JSON layer.
engine
Package engine holds the stages of a call that the root package's public types are built on.
Package engine holds the stages of a call that the root package's public types are built on.
h2gate
Package h2gate is the SDK's transport: the stock net/http.Transport set up for one HTTP/2 connection per API host, with a cold-start gate and a header-write token in front of it.
Package h2gate is the SDK's transport: the stock net/http.Transport set up for one HTTP/2 connection per API host, with a cold-start gate and a header-write token in front of it.
testsupport
Package testsupport holds the test doubles and measurement helpers the SDK's tests share: servers, listeners, dialers and a proxy for transport tests (LoopbackServer, SilentListener, RefusedAddr, GatedDialer, FakeH2CServer, Proxy); request and log doubles (Recorder, FakeAPI, LogRecorder); allocation measurement (Measure, MeasureMin, Spread, FloorCall); response bodies, from the module's testdata directory and generated (Fixture, UnknownAnswerFlood); and the fuzz targets' per-input bound and guard pages (BoundFuzzInput, NewGuard).
Package testsupport holds the test doubles and measurement helpers the SDK's tests share: servers, listeners, dialers and a proxy for transport tests (LoopbackServer, SilentListener, RefusedAddr, GatedDialer, FakeH2CServer, Proxy); request and log doubles (Recorder, FakeAPI, LogRecorder); allocation measurement (Measure, MeasureMin, Spread, FloorCall); response bodies, from the module's testdata directory and generated (Fixture, UnknownAnswerFlood); and the fuzz targets' per-input bound and guard pages (BoundFuzzInput, NewGuard).
testsupport/naive
Package naive is the benchmark comparator of the SDK's performance criteria (docs/perf/frozen-budgets.md): a System One call written the obvious way, with a general-purpose JSON library and no SDK machinery.
Package naive is the benchmark comparator of the SDK's performance criteria (docs/perf/frozen-budgets.md): a System One call written the obvious way, with a general-purpose JSON library and no SDK machinery.
wire
Package wire holds the plain values that a decoded response and a prepared question set are made of: answers, token usage, model cards, response metadata and the prepared question tables.
Package wire holds the plain values that a decoded response and a prepared question set are made of: answers, token usage, model cards, response metadata and the prepared question tables.
Package livetest holds the SDK's tests against the live TypeSafe API, the port of the Python SDK's tests/test_integration.py.
Package livetest holds the SDK's tests against the live TypeSafe API, the port of the Python SDK's tests/test_integration.py.

Jump to

Keyboard shortcuts

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