typesafe

package module
v0.0.0-...-1f9ac50 Latest Latest
Warning

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

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

README

typesafe-go

A Go client for the TypeSafe System One API. Zero dependencies outside the standard library.

System One answers questions about content with a typed judgment and a probability instead of generating text you then have to parse.

go get github.com/2389-research/typesafe-go

Quick start

package main

import (
	"context"
	"fmt"

	"github.com/2389-research/typesafe-go"
)

type department string

const (
	billing   department = "billing"
	technical department = "technical"
	sales     department = "sales"
)

func main() {
	client, err := typesafe.New() // reads TYPESAFE_API_KEY
	if err != nil {
		panic(err)
	}

	ticket := "Help! My payouts have been failing for 3 days."

	urgent := typesafe.Noul("is_urgent", "Does this convey urgency?")
	team := typesafe.Choice[department]("department", "Which team should handle this?",
		typesafe.Opts[department]{
			billing:   "Payments, invoicing, refunds",
			technical: "Bugs, outages, integrations",
			sales:     "Pricing, upgrades, new accounts",
		})

	res, err := client.Ask(context.Background(), ticket, urgent, team)
	if err != nil {
		panic(err)
	}

	p, _ := urgent.From(res)        // 0.92
	assignment, _ := team.From(res) // Value: technical, Confidence: 0.82

	fmt.Println(p, assignment.Value, assignment.Probabilities)
}

Run the full example: go run ./examples/triage

Question handles

A question is a value you keep. It carries its own id and its own answer type, so you read the answer back through the question that asked it:

urgent := typesafe.Noul("is_urgent", "Does this convey urgency?")
res, _ := client.Ask(ctx, ticket, urgent)
p, err := urgent.From(res)

The id appears once. The compiler checks the answer's shape: a Score or Choice handle's From returns its own answer type, so a Noul handle cannot hand you a ChoiceAnswer[T], or vice versa — that part is a compile error, not a zero value at runtime.

The compiler cannot check the id. Two handles can share an id and disagree about the type behind it, and that read compiles; it fails at read time with ErrWrongType rather than decoding a zero value.

From also checks that the answer body carries every documented payload field. A truncated response — one missing noul, or a Choice with no choice — reports ErrIncompleteAnswer instead of reading as a confident zero. A genuine zero ("noul":0, "confidence":0) still decodes.

The three primitives

Primitive Question Answer
Noul yes or no float64 — the probability of yes
Choice one option from a set you define ChoiceAnswer[T] — winner, full distribution, confidence
Score a position on ordered levels ScoreAnswer — weighted value, legend, distribution, confidence

A Noul has no confidence field. Its probability is the answer: 0.5 means the model is genuinely torn.

Instructions and descriptions take a string, or a map or slice when a sentence is not enough:

typesafe.Noul("refund", map[string]any{
	"question": "Is this a refund request?",
	"exclude":  []any{"chargebacks", "partial credits"},
})

Probabilities are the point

switch {
case urgency > 0.8 && frustration.Value > 1.5:
	page(onCall)
case urgency > 0.5:
	queue(sameDay)
default:
	queue(normal)
}

A 0.95 and a 0.51 both become "yes" after a threshold. Route on the number.

Configuration

client, err := typesafe.New(
	typesafe.WithAPIKey(key),
	typesafe.WithModel("jev-1.13.0"),
	typesafe.WithTimeout(20*time.Second),
	typesafe.WithRetry(policy),
)
Variable Default Option
TYPESAFE_API_KEY none — required WithAPIKey
TYPESAFE_BASE_URL https://api.typesafe.ai WithBaseURL
TYPESAFE_DEFAULT_MODEL jev-latest WithModel

Also WithHTTPClient for your own transport.

Transient failures retry without you asking: two retries, 500ms doubling to 5s with 25% jitter, honoring Retry-After, inside a 30-second budget. DefaultRetryPolicy() returns that; WithRetry replaces it.

Errors

res, err := client.Ask(ctx, ticket, urgent)
switch {
case errors.Is(err, typesafe.ErrRateLimited):
	// already retried; you are over your limit
case errors.Is(err, typesafe.ErrUnprocessable):
	// the request was malformed
case err != nil:
	var apiErr *typesafe.Error
	if errors.As(err, &apiErr) {
		log.Printf("status %d: %s", apiErr.StatusCode, apiErr.Body)
	}
}

Error keeps the status code and the raw body. TypeSafe does not document the shape of its error bodies, so the SDK preserves them rather than guessing.

Keep the key on the server

The API key authenticates your account and carries your billing. It belongs in a server-side process, never in a browser bundle, a mobile app, or anything a user can read.

Tests

./scripts/check                              # fmt, vet, lint, race tests
TYPESAFE_API_KEY=sk-... ./scripts/check      # adds live API tests

Live tests are behind the e2e build tag and skip without a key. They cost roughly a thousandth of a cent per call. Even with a key set, ./scripts/check excludes TestLiveChoiceOptionOrderDoesNotMoveTheDistribution: it spends six billed calls checking whether the order of a Choice's options moves the answer distribution. Opts[T] is a Go map and encoding/json sorts map keys, so this SDK sends options alphabetically whatever order you wrote them in. If that turns out to matter, Opts[T] has to become an ordered type. The probe has not been run yet. Run it on its own:

TYPESAFE_API_KEY=sk-... go test -tags=e2e ./... -run TestLiveChoiceOptionOrder -v

License

MIT — see LICENSE. Copyright 2389 Research, Inc.

Documentation

Overview

Package typesafe is a client for the TypeSafe System One API.

System One answers questions about content with a typed judgment and a probability instead of generating text you then have to parse. Ask three questions about a support ticket and you get back a number, an option, and a rating. No JSON mode, no retry loop around malformed output.

Getting started

client, err := typesafe.New() // reads TYPESAFE_API_KEY
if err != nil {
	return err
}

urgent := typesafe.Noul("is_urgent", "Does this convey urgency?")

res, err := client.Ask(ctx, ticket, urgent)
if err != nil {
	return err
}

p, err := urgent.From(res) // 0.92

Question handles

A question is a value you keep. It carries its own id and its own answer type, so you read an answer back through the question that asked it. The id string appears exactly once in your code, and a Score handle cannot read a Noul answer, because From returns a different type for each.

The three primitives

Noul asks yes or no and returns the probability of yes. It has no confidence field: 0.5 means the model is genuinely torn, which is an answer, not a failure.

Choice picks one option from a set you define and returns the whole distribution alongside the winner. Declare the option set as your own string type and the compiler checks every comparison you write against it.

Score rates content against ordered levels and returns a probability-weighted position that can land between them. On a calm/frustrated/angry scale, 1.6 means mostly angry.

Probabilities are the point

Route on the number. A 0.95 and a 0.51 both become "yes" after a threshold, and collapsing them throws away what you paid for.

Configuration

New reads TYPESAFE_API_KEY, TYPESAFE_BASE_URL, and TYPESAFE_DEFAULT_MODEL, and an option overrides any of them. Transient failures retry on their own: two retries, 500ms doubling to 5s, honoring Retry-After, inside a 30-second budget. See RetryPolicy to change that.

Errors

Match failures with errors.Is against ErrUnauthorized, ErrUnprocessable, ErrRateLimited, ErrOverloaded, and ErrServer. For the raw response, use errors.As to reach *Error, which keeps the status code and the body.

Index

Constants

View Source
const (
	// DefaultBaseURL is the TypeSafe API root.
	DefaultBaseURL = "https://api.typesafe.ai"
	// DefaultModel is the alias for the current stable Jev release.
	DefaultModel = "jev-latest"
	// DefaultTimeout bounds one HTTP attempt, not the whole retried call.
	// The retry budget stops the loop before a backoff that would cross
	// it, but the attempt that follows the backoff still runs, so a call
	// can overrun the budget by up to one attempt's timeout.
	DefaultTimeout = 10 * time.Second
)

Documented defaults.

View Source
const (
	EnvAPIKey  = "TYPESAFE_API_KEY"
	EnvBaseURL = "TYPESAFE_BASE_URL"
	EnvModel   = "TYPESAFE_DEFAULT_MODEL"
)

Environment variables New reads when the matching option is absent.

View Source
const Version = "0.1.0"

Version is this SDK's version, reported in the User-Agent header.

Variables

View Source
var (
	ErrUnauthorized  = errors.New("typesafe: unauthorized")
	ErrUnprocessable = errors.New("typesafe: unprocessable entity")
	ErrRateLimited   = errors.New("typesafe: rate limited")
	ErrOverloaded    = errors.New("typesafe: overloaded")
	ErrServer        = errors.New("typesafe: server error")

	ErrNoAnswer         = errors.New("typesafe: no answer for question id")
	ErrWrongType        = errors.New("typesafe: answer type does not match question type")
	ErrIncompleteAnswer = errors.New("typesafe: answer is missing a required field")

	ErrUnexpectedOption = errors.New("typesafe: answer holds an option the question did not declare")

	ErrNoAPIKey     = errors.New("typesafe: no API key")
	ErrNoQuestions  = errors.New("typesafe: no questions")
	ErrNilQuestion  = errors.New("typesafe: nil question")
	ErrEmptyID      = errors.New("typesafe: empty question id")
	ErrDuplicateID  = errors.New("typesafe: duplicate question id")
	ErrNoOptions    = errors.New("typesafe: choice has no options")
	ErrTooFewLevels = errors.New("typesafe: score needs at least two levels")
)

Sentinel errors. Match these with errors.Is rather than comparing status codes at the call site.

Functions

This section is empty.

Types

type ChoiceAnswer

type ChoiceAnswer[T ~string] struct {
	// Value is the highest-probability option.
	Value T
	// Probabilities maps every option to its probability. They sum to 1.
	Probabilities map[T]float64
	// Confidence is how certain the model is, derived from Probabilities.
	Confidence float64
}

ChoiceAnswer is one Choice result, in the caller's own option type.

type ChoiceQuestion

type ChoiceQuestion[T ~string] struct {
	// contains filtered or unexported fields
}

ChoiceQuestion picks one option from a set you define. Build one with Choice.

func Choice

func Choice[T ~string](id string, instructions Entry, options Opts[T]) ChoiceQuestion[T]

Choice picks one option from a set you define. Declare T as your own string type and the compiler will reject any option you write outside it; what the server sends back is checked at read time, where From fails with an error wrapping ErrUnexpectedOption if the answer holds an option the question never declared.

func (ChoiceQuestion[T]) From

func (q ChoiceQuestion[T]) From(r *Result) (ChoiceAnswer[T], error)

From reads this question's answer, typed as the option type you declared. An answer body missing choice, probabilities, or confidence reports ErrIncompleteAnswer; an empty probabilities map counts as missing, since the API documents it as summing to 1.

Both the winning choice and every key in Probabilities are checked against the options the question declared: the compiler checks what you write, only this method can check what the server sends back. An option that was never declared fails with an error wrapping ErrUnexpectedOption rather than decoding into a value that equals none of your constants.

func (ChoiceQuestion[T]) ID

func (q ChoiceQuestion[T]) ID() string

ID is the key this question's answer comes back under.

type Client

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

Client talks to the TypeSafe API. It is safe for concurrent use.

func New

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

New builds a client.

Configuration resolves in three layers, later winning over earlier: documented defaults, then TYPESAFE_API_KEY, TYPESAFE_BASE_URL, and TYPESAFE_DEFAULT_MODEL, then the options you pass.

An API key is the one hard requirement; without one New returns ErrNoAPIKey rather than failing later on the first call.

func (*Client) Ask

func (c *Client) Ask(ctx context.Context, state any, questions ...Question) (*Result, error)

Ask evaluates state against every question in one request.

State is any JSON-encodable value: a string for plain text, or a struct, map, or slice for a chat log or a record. Questions are answered independently and in parallel; none of them can see another's answer, so chain a second Ask when one judgment depends on another.

Ask retries transient failures, and a retry can bill twice for one evaluation: the client cannot tell a request that never reached the server from one whose response was lost after the server already processed it, so it retries either one. If you need at-most-once behavior, set WithRetry to a policy with MaxRetries: 0.

Read each answer through the question value you passed in:

urgent := typesafe.Noul("is_urgent", "Does this convey urgency?")
res, err := client.Ask(ctx, ticket, urgent)
p, err := urgent.From(res)

func (*Client) Models

func (c *Client) Models(ctx context.Context) ([]Model, error)

Models lists the models available to the account, newest aliases included.

type Entry

type Entry = any

Entry is any JSON-encodable value the API accepts where structure is allowed: a string, an object, or an array.

Instructions, Choice option descriptions, Score level descriptions, and Noul true/false criteria all take this shape. Structured entries let you hand the model a rubric instead of a sentence.

Upstream documents null as valid only for a Choice option's description (criteria is map<string, string | null>: null means the option needs no extra detail). Nothing in the docs says null is valid for Instructions, Score levels, or Noul true/false criteria.

type Error

type Error struct {
	StatusCode int
	Body       []byte
	Message    string
}

Error is an HTTP-level failure from the TypeSafe API.

TypeSafe documents its status codes but not the shape of its error bodies, so Body carries the response verbatim and Message is a best-effort guess. Read Body when you need certainty.

func (*Error) Error

func (e *Error) Error() string

func (*Error) Is

func (e *Error) Is(target error) bool

Is maps status codes onto the package sentinels so callers can write errors.Is(err, typesafe.ErrRateLimited) instead of unpacking the status.

type Model

type Model struct {
	// Name is what you pass to WithModel, such as jev-latest.
	Name        string `json:"name"`
	Description string `json:"description"`
	// ReleaseDate is the API's value, unparsed. Its format is not
	// documented, so turning it into a time.Time would be a guess.
	ReleaseDate string `json:"release_date"`
}

Model is one model or alias the account can use.

type NoulQuestion

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

NoulQuestion asks a yes/no question. Build one with Noul.

func Noul

func Noul(id string, instructions Entry) NoulQuestion

Noul asks a yes/no question. The answer is the probability of yes, from 0 to 1. A Noul carries no confidence: the probability is the whole answer.

func (NoulQuestion) From

func (q NoulQuestion) From(r *Result) (float64, error)

From reads this question's answer: the probability of yes, from 0 to 1. An answer body that omits or nulls the noul field reports ErrIncompleteAnswer instead of decoding to 0.

A Noul carries no confidence field. The probability is the whole answer: 0.5 means the model is genuinely torn, not that it is unsure.

func (NoulQuestion) ID

func (q NoulQuestion) ID() string

ID is the key this question's answer comes back under.

func (NoulQuestion) WithCriteria

func (q NoulQuestion) WithCriteria(yes, no Entry) NoulQuestion

WithCriteria describes what a yes and a no mean, which sharpens the probability. It returns a copy; question values never change in place.

type Option

type Option func(*Client) error

Option configures a Client. Pass options to New.

func WithAPIKey

func WithAPIKey(key string) Option

WithAPIKey sets the key, overriding TYPESAFE_API_KEY.

func WithBaseURL

func WithBaseURL(raw string) Option

WithBaseURL points the client somewhere other than api.typesafe.ai, which is what you want for a proxy or a record-and-replay test server.

func WithHTTPClient

func WithHTTPClient(h *http.Client) Option

WithHTTPClient supplies your own transport, for proxies, custom TLS, or instrumentation. The client is used as given and never modified.

func WithModel

func WithModel(model string) Option

WithModel sets the model sent with every request, overriding TYPESAFE_DEFAULT_MODEL. Call Models to see what your account can use.

func WithRetry

func WithRetry(p RetryPolicy) Option

WithRetry replaces the retry policy. Start from DefaultRetryPolicy and adjust; the zero RetryPolicy retries nothing.

Statuses is copied, so the caller may keep using the slice it passed: editing it or appending into its spare capacity does not retune the client.

func WithTimeout

func WithTimeout(d time.Duration) Option

WithTimeout bounds each HTTP attempt. It applies to a copy of the HTTP client, so it works alongside WithHTTPClient in either order.

type Opts

type Opts[T ~string] map[T]Entry

Opts maps each Choice option to its description. A nil description means the option needs no extra detail.

type Question

type Question interface {
	// ID is the key this question's answer comes back under.
	ID() string
	// contains filtered or unexported methods
}

Question is one typed judgment in a request.

The interface is sealed by its unexported methods: only Noul, Choice, and Score satisfy it, because the API accepts nothing else.

type Result

type Result struct {
	// Model is the model that performed the evaluation. When you send an
	// alias like jev-latest, this is the concrete version it resolved to.
	Model string
	Usage Usage
	// contains filtered or unexported fields
}

Result holds the answers to one Ask.

Read an answer through the question value that asked it — urgent.From(res) — rather than by key. Each handle's From fixes the answer's shape at compile time: NoulQuestion.From returns float64 and nothing else, and ChoiceQuestion[T].From returns ChoiceAnswer[T].

The compiler cannot check the id, though. Two handles may share an id and disagree about its type, and that read compiles; it fails at read time with ErrWrongType rather than decoding a zero value.

func (*Result) RawAnswer

func (r *Result) RawAnswer(id string) (json.RawMessage, error)

RawAnswer returns the exact wire bytes the API sent for one answer, keyed by question id, with no decoding or type checking applied. The caller owns the returned copy: mutating it cannot affect the Result, so raw answer evidence can be retained and logged without sharing mutable state with the typed readers.

This is the supported way to keep exact raw answer evidence — the bytes the API returned, not a re-encoding of them. A question id the response did not carry reports ErrNoAnswer.

type RetryPolicy

type RetryPolicy struct {
	// MaxRetries is how many attempts follow the first one.
	MaxRetries int
	// BackoffInitial is the delay before the first retry. It doubles each
	// time, up to BackoffMax.
	BackoffInitial time.Duration
	// BackoffMax caps the computed delay. Retry-After can still exceed it.
	// Zero means no cap: the delay doubles until it saturates at the
	// largest representable duration.
	BackoffMax time.Duration
	// BackoffJitter is the fraction of the delay that may be subtracted at
	// random, spreading out a thundering herd. 0.25 means up to 25% off.
	BackoffJitter float64
	// Statuses lists the response codes worth retrying.
	Statuses []int
	// RespectRetryAfter honors a Retry-After or retry-after-ms header in
	// preference to the computed delay.
	RespectRetryAfter bool
	// Budget caps the total wall time of a call, delays included. Zero
	// means no cap.
	Budget time.Duration
}

RetryPolicy controls how transient failures are retried.

The zero value retries nothing. Start from DefaultRetryPolicy and adjust.

func DefaultRetryPolicy

func DefaultRetryPolicy() RetryPolicy

DefaultRetryPolicy mirrors the documented defaults of TypeSafe's Python SDK: two retries, 500ms doubling to 5s, 25% jitter, 30s budget, retrying 408, 429, and every 5xx.

func (RetryPolicy) Retryable

func (p RetryPolicy) Retryable(status int) bool

Retryable reports whether a response code is worth another attempt.

type ScoreAnswer

type ScoreAnswer struct {
	// Value is probability-weighted across the levels and can land between
	// two of them: 1.6 sits closer to level 2 than to level 1.
	Value float64
	// Legend maps each level index back to the description you supplied.
	Legend map[int]string
	// Probabilities maps each level index to its probability. They sum to 1.
	Probabilities map[int]float64
	// Confidence is how certain the model is, derived from Probabilities.
	Confidence float64
}

ScoreAnswer is one Score result.

type ScoreQuestion

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

ScoreQuestion rates the state against ordered levels. Build one with Score.

func Score

func Score(id string, instructions Entry, levels ...Entry) ScoreQuestion

Score rates the state against levels given lowest first. The answer is probability-weighted across them and can land between two levels. The API requires at least two.

func (ScoreQuestion) From

func (q ScoreQuestion) From(r *Result) (ScoreAnswer, error)

From reads this question's answer, with the wire format's string level keys converted back to the integer indexes you passed levels in. An answer body missing score, legend, probabilities, or confidence reports ErrIncompleteAnswer; empty legend or probabilities maps count as missing.

func (ScoreQuestion) ID

func (q ScoreQuestion) ID() string

ID is the key this question's answer comes back under.

type Usage

type Usage struct {
	InputTokens  int `json:"input_tokens"`
	OutputTokens int `json:"output_tokens"`
}

Usage reports token consumption for one request. TypeSafe bills input tokens only.

Directories

Path Synopsis
examples
triage command

Jump to

Keyboard shortcuts

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