systemone

package module
v0.3.0 Latest Latest
Warning

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

Go to latest
Published: Oct 15, 2026 License: MIT Imports: 21 Imported by: 0

README

systemone

CI Go Reference

A Go client for the TypeSafe System One API. Send one state and a set of questions; get one answer per question, each bound to the Go type you declared the question with, so options and score levels are checked by the compiler rather than compared against string literals.

go get github.com/justintout/systemone

This is an unofficial, community-maintained client. It is not built, endorsed, or supported by TypeSafe. The official SDKs are Python and JavaScript.

Usage

A question is a value. Declare it once with the Go type its answer uses, then read the answer back through the same handle.

type Team string

const (
	Billing   Team = "billing"
	Technical Team = "technical"
)

var (
	team = systemone.NewChoice[Team]("team", "Which team should handle this message?",
		systemone.Option(Billing, "Payments, invoicing, refunds"),
		systemone.Option(Technical, "Bugs, outages, integrations"),
	)
	urgent = systemone.NewNoul("urgent", "Does this convey urgency?")
)

client, err := jev.New() // reads TYPESAFE_API_KEY

res, err := client.Ask(ctx, ticket, team, urgent)

t, err := team.From(res)   // systemone.ChoiceAnswer[Team]
u, err := urgent.From(res) // systemone.NoulAnswer

fmt.Println(t.Value, t.Confidence, u.Value)

From is the only way to reach an answer, which is what keeps the typing honest: there is no untyped map to fall back to. t.Value is a Team, so a switch over it is checked against the set the question declared. A typo in an option name, or a level enum borrowed from a different question, is a build failure rather than a branch that silently never fires.

Deployments

The client depends on the deployment. Questions and answers do not: every client takes the same systemone.Question values and returns the same *systemone.Response, and all of them satisfy systemone.Asker.

Package Deployment
jev TypeSafe's hosted Jev models
clef Cloudflare's Clef models on Workers AI
systemone A server that implements the base API itself

jev.New reads TYPESAFE_API_KEY, TYPESAFE_BASE_URL, TYPESAFE_DEFAULT_MODEL and TYPESAFE_LOG_LEVEL. It compresses request bodies of 1 KiB or more with zstd, as TypeSafe's SDKs do; systemone.WithCompression(false) turns that off, and WithCompression(true) turns it on for another deployment that accepts it.

Jev images

An image goes anywhere text can: the state, a question's instructions, or its criteria. A jev.Image placed as a value becomes the image there, and Ref() writes it into a sentence. Every image placed in the request is also listed in Images, which uploads it once:

door, err := jev.NewImage(doorJPG) // PNG or JPEG
badge, err := jev.NewImage(badgeJPG)

var (
	match = systemone.NewNoul("match",
		"Is the person in `door` the person in this badge photo: "+badge.Ref()+"?")
	where = jev.NewBox("where", "the person in `door`")
)

res, err := client.Do(ctx, jev.Request{
	State:     map[string]any{"door": door},
	Questions: []systemone.Question{match},
	Boxes:     []*jev.Box{where},
	Images:    []jev.Image{door, badge},
	Model:     jev.Preview, // Box needs Jev 1.14
})
b, err := where.From(res) // jev.BoxAnswer: Top, Bottom, Left, Right, Confidence
crop := b.Rect(width, height)

A jev.Box finds where something is in an image. Only Jev answers it, so it is not a systemone.Question: it goes in Boxes, and a Box handed to another deployment's client is a build failure.

Images travel as multipart file parts, so they are not inflated by base64. A request that places an image without listing it, or lists one it never places, fails before it is sent. The client sets the TypeSafe-Danger-Zone-Images header TypeSafe requires on any request with an image or a Box question. Images and Box are experimental at TypeSafe, and their limits are enforced there, not here.

Request.LongContext sets TypeSafe-Danger-Zone-Long-Context, which raises the request's context limits to 262k tokens at some cost in speed and accuracy.

Clef

clef.New takes the Workers AI account ID and a Workers AI token:

client, err := clef.New(accountID, systemone.WithAPIKey(os.Getenv("CLOUDFLARE_API_TOKEN")))

That sends clef; set Request.Model to clef.Flash for the faster one. Clef reads images placed before the state rather than referenced from it, and clef.NewImage reads the content type from the bytes:

data, err := os.ReadFile("checkout.png")
img, err := clef.NewImage(data) // PNG, JPEG or WebP

res, err := client.Do(ctx, clef.Request{
	State:     "A customer sent this screenshot of the checkout page.",
	Questions: []systemone.Question{broken, severity},
	Images:    []clef.Image{img},
})

Cloudflare's limits (four images, 4 MiB and 16 megapixels each) are enforced by the deployment, not here. A fine-tuned or self-hosted Clef is clef.New with systemone.WithBaseURL and systemone.WithEvaluatePath.

Your own server

A server that implements only the base API has no default, so name it:

client, err := systemone.New(
	systemone.WithBaseURL("https://decisions.internal"),
	systemone.WithModel("support-ft"),
	systemone.WithAPIKey(key),
)

WithEvaluatePath, WithModelsPath, WithResultKey and WithRequestIDHeader cover a deployment whose paths, envelope or request ID header differ. A {model} in the evaluate path is replaced with the model of the request.

See examples/ for more, including the support-ticket walkthrough from the quick start written with this SDK.

Primitives

Question Answer Use it for
NewChoice[T ~string] ChoiceAnswer[T]: Value, Probabilities, Confidence One of a defined set
NewNoul NoulAnswer: Value, the probability of yes Whether a condition holds
NewScore[T ~int] ScoreAnswer[T]: Value, Level, Legend, Probabilities, Confidence Degree along ordered levels

Instructions and every criterion take a string or any value that encodes to a JSON object or array, so a question can carry named data it refers to by backticked path. Your own types work; there is no reason to reach for a map:

type Candidate struct {
	Name         string `json:"name"`
	LastEmployer string `json:"last_employer"`
}

type match struct {
	Candidate Candidate `json:"candidate"`
	Question  string    `json:"question"`
}

var duplicate = systemone.NewNoul("duplicate", match{
	Candidate: Candidate{Name: "John Smith", LastEmployer: "Google"},
	Question:  "Is the resume for the same person as `candidate`?",
})

Build errors, such as a duplicate option or a level declared out of order, are reported by the call that sends the question rather than at construction, so questions can be package-level variables.

A ScoreAnswer's Legend holds each level's description as it was sent, so structured levels come back structured. Describe() renders the current level as text, JSON-encoding a structured one.

Client

client, err := jev.New(
	systemone.WithAPIKey(key),          // default: $TYPESAFE_API_KEY
	systemone.WithBaseURL(url),         // default: $TYPESAFE_BASE_URL, then api.typesafe.ai
	systemone.WithModel("jev-1.13.0"),  // default: $TYPESAFE_DEFAULT_MODEL, then jev-latest
	systemone.WithHTTPClient(hc),
	systemone.WithTimeout(30*time.Second), // per attempt
	systemone.WithRetry(systemone.DefaultRetry()),
	systemone.WithHeader("X-Trace-Id", id),
	systemone.WithLogger(slog.Default()),
)

The same options configure clef.New and systemone.New. Models lists the models the account can send. Do takes a Request when one call needs to differ from the client's defaults:

res, err := client.Do(ctx, jev.Request{
	State:     ticket,
	Questions: []systemone.Question{team, urgent},
	Model:     "jev-1.13.0",
	Header:    http.Header{"X-Trace-Id": {id}},
	Retry:     &policy,              // nil inherits the client's
	Timeout:   45 * time.Second,     // per attempt; zero inherits, NoTimeout removes the bound
})

Give ctx a deadline to bound the whole call including retries; Timeout bounds one attempt.

Retries

The client does not retry. WithRetry(systemone.DefaultRetry()) turns on two retries with exponential backoff, jitter, and retry-after support; adjust the RetryPolicy fields or supply your own Retry predicate. Request.Retry sets a policy for one call, which is worth reaching for when a cheap three-question triage and an expensive two-hundred-question fan-out share a client. A retried request may be evaluated more than once by the service.

Errors

An unsuccessful response is a *systemone.Error carrying the status, the raw body, the response headers, and the request ID, read from whichever header the deployment names. Classify it with errors.Is:

switch {
case errors.Is(err, systemone.ErrRateLimit):     // 429
case errors.Is(err, systemone.ErrUnprocessable): // 422, body names the field
case errors.Is(err, systemone.ErrOverloaded):    // 529
case errors.Is(err, systemone.ErrServer):        // any 5xx
}
Logging

The client is silent until you give it a logger. WithLogger(log) writes one log/slog record per HTTP attempt at info, or the level WithLogLevel sets. jev.New reads it from $TYPESAFE_LOG_LEVEL (debug, info, warn, error, off).

At info each record is a summary: method, URL, attempt, duration, status, and request ID. At debug it also carries headers and bodies. Credential headers are redacted; bodies are not, and the request body holds your state.

Patterns

pattern composes answers the way the TypeSafe docs describe. It is ordinary code over answers this package returns; the thresholds and weights are yours.

  • Thresholds.Band splits confidence into low, medium, and high so riskier actions can sit behind a higher bar.
  • Composite takes the weighted mean of normalized Weigh/WeighNoul terms. Changing a weight reranks existing answers without a new request.
  • Walk classifies down a Tree one Choice per level, showing each option's subtree so the model can see what lives under a branch before committing. Every step carries the full sibling distribution, so a beam search over close branches is a few lines on top.

Contributing

See CONTRIBUTING.md for the checks CI runs, the conventions the code follows, and the versioning and release policy.

License

MIT. See LICENSE.

Documentation

Overview

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

A request sends one state and a set of typed questions; the response carries one answer per question. Questions are values you declare once and reuse:

type Dept string

const (
	Billing   Dept = "billing"
	Technical Dept = "technical"
)

var (
	dept = systemone.NewChoice[Dept]("department", "Which team should handle this?",
		systemone.Option(Billing, "Payments, invoicing, refunds"),
		systemone.Option(Technical, "Bugs, outages, integrations"),
	)
	urgent = systemone.NewNoul("is_urgent", "Does this convey urgency?")
)

client, err := jev.New()
res, err := client.Ask(ctx, "Help! My payouts have been failing for 3 days.", dept, urgent)
if err != nil {
	return err
}
d, err := dept.From(res)   // ChoiceAnswer[Dept]
u, err := urgent.From(res) // NoulAnswer

Reading an answer through its question handle is the only way to reach it, so the option type, the level type, and the answer type are all checked at compile time.

See https://docs.typesafe.ai for the concepts behind state, questions, probabilities, and confidence.

The client depends on the deployment. Package jev reaches TypeSafe's hosted Jev models and package clef reaches Cloudflare's Clef models; each adds its deployment's extensions to the API. Client in this package speaks the base API alone, for a server that implements it itself. Each deployment sends images its own way, so they are types of package jev and package clef. All three take the same questions, return the same Response, and satisfy Asker.

This is an unofficial, community-maintained client, not built, endorsed, or supported by TypeSafe.

Example

Independent questions about the same state go in one request. They are answered in parallel and cannot see one another's answers.

package main

import (
	"context"
	"fmt"
	"log"

	"github.com/justintout/systemone"
	"github.com/justintout/systemone/jev"
)

// Questions are values. Declare them once and reuse them across requests.
type Team string

const (
	TeamBilling   Team = "billing"
	TeamTechnical Team = "technical"
	TeamSales     Team = "sales"
)

type Anger int

var (
	team = systemone.NewChoice[Team]("team", "Which team should handle this message?",
		systemone.Option(TeamBilling, "Payments, invoicing, refunds"),
		systemone.Option(TeamTechnical, "Bugs, outages, integrations"),
		systemone.Option(TeamSales, "Pricing, upgrades, new accounts"),
	)
	timeSensitive = systemone.NewNoul("time_sensitive", "Does this convey urgency?",
		systemone.Yes("Explicitly time-sensitive"),
		systemone.No("No urgency expressed"),
	)
	anger = systemone.NewScore[Anger]("anger", "How frustrated is the customer?",
		systemone.Levels[Anger]("Calm and matter-of-fact", "Visibly frustrated", "Angry, threatening to leave"),
	)
)

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

	ticket := map[string]any{
		"subject": "Payouts failing",
		"body":    "Help! My payouts have been failing for 3 days and nobody has replied.",
		"plan":    "enterprise",
	}

	res, err := client.Ask(context.Background(), ticket, team, timeSensitive, anger)
	if err != nil {
		log.Fatal(err)
	}

	t, err := team.From(res)
	if err != nil {
		log.Fatal(err)
	}
	u, err := timeSensitive.From(res)
	if err != nil {
		log.Fatal(err)
	}
	a, err := anger.From(res)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(t.Value, t.Confidence, u.Yes(0.8), a.Level, a.Describe())
}
Example (CompositeScoring)

Score each dimension on its own, then combine with weights your code owns. Changing a weight reranks existing answers without a new request.

package main

import (
	"context"
	"fmt"
	"log"

	"github.com/justintout/systemone"
	"github.com/justintout/systemone/jev"
	"github.com/justintout/systemone/pattern"
)

// Questions are values. Declare them once and reuse them across requests.
type Team string

const (
	TeamBilling   Team = "billing"
	TeamTechnical Team = "technical"
	TeamSales     Team = "sales"
)

type Anger int

var (
	timeSensitive = systemone.NewNoul("time_sensitive", "Does this convey urgency?",
		systemone.Yes("Explicitly time-sensitive"),
		systemone.No("No urgency expressed"),
	)
	anger = systemone.NewScore[Anger]("anger", "How frustrated is the customer?",
		systemone.Levels[Anger]("Calm and matter-of-fact", "Visibly frustrated", "Angry, threatening to leave"),
	)
)

func main() {
	client, err := jev.New()
	if err != nil {
		log.Fatal(err)
	}
	res, err := client.Ask(context.Background(), "resume text", anger, timeSensitive)
	if err != nil {
		log.Fatal(err)
	}
	a, _ := anger.From(res)
	u, _ := timeSensitive.From(res)

	priority := pattern.Composite(
		pattern.Weigh(a, 0.7),
		pattern.WeighNoul(u, 0.3),
	)
	fmt.Printf("priority %.2f\n", priority)
}
Example (ConfidenceGatedRouting)

Confidence says whether to act on an answer, not whether the answer is right. Gate riskier actions at a higher bar.

package main

import (
	"context"
	"fmt"
	"log"

	"github.com/justintout/systemone"
	"github.com/justintout/systemone/jev"
	"github.com/justintout/systemone/pattern"
)

// Questions are values. Declare them once and reuse them across requests.
type Team string

const (
	TeamBilling   Team = "billing"
	TeamTechnical Team = "technical"
	TeamSales     Team = "sales"
)

type Anger int

var team = systemone.NewChoice[Team]("team", "Which team should handle this message?",
	systemone.Option(TeamBilling, "Payments, invoicing, refunds"),
	systemone.Option(TeamTechnical, "Bugs, outages, integrations"),
	systemone.Option(TeamSales, "Pricing, upgrades, new accounts"),
)

func main() {
	client, err := jev.New()
	if err != nil {
		log.Fatal(err)
	}
	res, err := client.Ask(context.Background(), "Move everything into savings.", team)
	if err != nil {
		log.Fatal(err)
	}
	t, err := team.From(res)
	if err != nil {
		log.Fatal(err)
	}

	switch (pattern.Thresholds{Medium: 0.6, High: 0.85}).Band(t.Confidence) {
	case pattern.High:
		fmt.Println("route to", t.Value)
	case pattern.Medium:
		fmt.Println("confirm with the customer, then route to", t.Value)
	case pattern.Low:
		fmt.Println("hand to a support agent")
	}
}

Index

Examples

Constants

View Source
const (
	MinScoreLevels = 2
	MaxScoreLevels = 10
)

Score level count limits the API enforces.

View Source
const DefaultTimeout = 10 * time.Second

DefaultTimeout bounds one attempt unless WithTimeout says otherwise.

View Source
const LevelOff = slog.Level(math.MaxInt32)

LevelOff disables logging. It sits above every level slog defines, so no record is ever enabled.

View Source
const MaxChoiceOptions = 255

MaxChoiceOptions is the largest number of options the API accepts on one Choice question.

View Source
const ModelPlaceholder = "{model}"

ModelPlaceholder is replaced with the model of the request in the path set by WithEvaluatePath, for deployments that name the model in the URL.

View Source
const NoTimeout = -1 * time.Nanosecond

NoTimeout removes the per-attempt bound from a call when given as a Request.Timeout. Any negative duration does the same.

Variables

View Source
var (
	ErrInvalidRequest   = errors.New("systemone: invalid request")
	ErrAuthentication   = errors.New("systemone: authentication failed")
	ErrPermissionDenied = errors.New("systemone: permission denied")
	ErrNotFound         = errors.New("systemone: not found")
	ErrUnprocessable    = errors.New("systemone: request failed validation")
	ErrRateLimit        = errors.New("systemone: rate limit exceeded")
	ErrOverloaded       = errors.New("systemone: service overloaded")
	ErrServer           = errors.New("systemone: server error")
)

Sentinels for classifying an *Error with errors.Is:

if errors.Is(err, systemone.ErrRateLimit) { ... }

ErrServer also matches ErrOverloaded, since 529 is a server status.

View Source
var (
	// ErrNoAnswer means the response carries no answer under the question's ID.
	ErrNoAnswer = errors.New("no answer for question")
	// ErrAnswerType means the answer under that ID is of a different primitive.
	ErrAnswerType = errors.New("answer is of a different question type")
	// ErrIncompleteAnswer means the answer arrived without a field the API
	// documents as required, so it cannot be read without inventing a value.
	ErrIncompleteAnswer = errors.New("answer is missing a required field")
)

Errors returned when reading an answer out of a Response.

View Source
var ErrImage = errors.New("systemone: image")

ErrImage is returned by packages jev and clef for an image they will not send: one with no data, or one in a format the deployment does not read.

View Source
var ErrNoAPIKey = errors.New("systemone: no API key")

ErrNoAPIKey is returned by New when no API key is given.

View Source
var ErrNoModelList = errors.New("systemone: deployment has no model listing")

ErrNoModelList is returned by Client.Models when the deployment has no model listing.

Functions

This section is empty.

Types

type Asker added in v0.3.0

type Asker interface {
	Ask(ctx context.Context, state any, questions ...Question) (*Response, error)
}

Asker asks questions about a state. Client and the clients in packages jev and clef all satisfy it, for code that chooses a deployment at run time.

type Choice

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

Choice asks the model to pick one option from a set you define. T is the Go type of the options, so both the declared set and the answer are checked at compile time:

type Dept string

const (
	Billing   Dept = "billing"
	Technical Dept = "technical"
)

var dept = systemone.NewChoice[Dept]("department", "Which team should handle this?",
	systemone.Option(Billing, "Payments, invoicing, refunds"),
	systemone.Option(Technical, "Bugs, outages, integrations"),
)

See https://docs.typesafe.ai/primitives/choice.

func NewChoice

func NewChoice[T ~string](id string, instructions Content, opts ...ChoiceOption[T]) *Choice[T]

NewChoice builds a Choice question. A build error, such as a duplicate option, is reported by the Client call that sends the question rather than here, so questions can be declared as package-level variables.

func (*Choice[T]) From

func (q *Choice[T]) From(r *Response) (ChoiceAnswer[T], error)

From reads this question's answer out of a response. It returns a *QuestionError wrapping ErrNoAnswer when the response has no answer under the question's ID, or ErrAnswerType when that answer is a different primitive.

func (*Choice) ID

func (q *Choice) ID() string

func (*Choice[T]) Options

func (q *Choice[T]) Options() []T

Options returns the declared options in declaration order.

type ChoiceAnswer

type ChoiceAnswer[T ~string] struct {
	// Value is the highest-probability option.
	Value T
	// Probabilities maps every declared option to its probability. They sum to 1.
	Probabilities map[T]float64
	// Confidence is how concentrated Probabilities is, from 0 to 1. It says
	// whether to act on Value, not whether Value is correct. See
	// https://docs.typesafe.ai/confidence.
	Confidence float64
}

ChoiceAnswer is the answer to a Choice question.

func (ChoiceAnswer[T]) Probability

func (a ChoiceAnswer[T]) Probability(option T) float64

Probability returns the probability of one option, or 0 if the answer does not carry it.

func (ChoiceAnswer[T]) Ranked

func (a ChoiceAnswer[T]) Ranked() []T

Ranked returns the options ordered by descending probability. Equal probabilities are ordered by option name so the result is stable.

type ChoiceOption

type ChoiceOption[T ~string] interface {
	// contains filtered or unexported methods
}

ChoiceOption configures a Choice during construction.

func Option

func Option[T ~string](value T, description Content) ChoiceOption[T]

Option declares one selectable option and the rubric that describes it. A nil description sends the option with no extra detail. The description may be a string or any JSON-encodable structure; see https://docs.typesafe.ai/primitives/advanced.

func Options

func Options[T ~string](values ...T) ChoiceOption[T]

Options declares several options at once, none of them described. Use it when the option names carry the whole meaning.

type Client

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

Client sends requests to a deployment of the base System One API: text state, and Choice, Noul and Score questions. It is safe for concurrent use.

Use it for a server that implements the API itself. TypeSafe's Jev and Cloudflare's Clef extend the API, and their clients, in packages jev and clef, configure this one and add those extensions.

func New

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

New builds a client. A deployment has no default, so WithAPIKey, WithBaseURL and WithModel are required:

c, err := systemone.New(
	systemone.WithBaseURL("https://decisions.internal"),
	systemone.WithModel("support-ft"),
	systemone.WithAPIKey(key),
)

Evaluations are posted to /v1/systemone and models are listed from /v1/models, the paths the API defines, unless WithEvaluatePath and WithModelsPath say otherwise.

Example

A server that implements the base API itself has no default deployment, so name its URL and model.

package main

import (
	"log"
	"os"

	"github.com/justintout/systemone"
)

func main() {
	client, err := systemone.New(
		systemone.WithBaseURL("https://decisions.internal"),
		systemone.WithModel("support-ft"),
		systemone.WithAPIKey(os.Getenv("DECISIONS_API_KEY")),
	)
	if err != nil {
		log.Fatal(err)
	}
	_ = client
}

func (*Client) Ask

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

Ask evaluates state against questions using the client's default model. It is Client.Do for the common case.

func (*Client) Do

func (c *Client) Do(ctx context.Context, req Request) (*Response, error)

Do sends one evaluation request.

func (*Client) Model

func (c *Client) Model() string

Model returns the model used for requests that do not name one.

func (*Client) Models

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

Models lists the models the account can use. See https://docs.typesafe.ai/models.

type ClientOption

type ClientOption interface {
	// contains filtered or unexported methods
}

ClientOption configures a Client. The clients in packages jev and clef take the same options, applied after their own defaults, so an option given there overrides the deployment's setting.

func WithAPIKey

func WithAPIKey(key string) ClientOption

WithAPIKey sets the API key.

func WithBaseURL

func WithBaseURL(rawURL string) ClientOption

WithBaseURL sets the API root. Endpoint paths are appended to it.

func WithCompression added in v0.3.0

func WithCompression(on bool) ClientOption

WithCompression compresses request bodies of 1 KiB or more with zstd, sent as Content-Encoding: zstd. The client in package jev turns it on, since TypeSafe accepts it; turn it on elsewhere only for a server that does.

func WithEvaluatePath added in v0.3.0

func WithEvaluatePath(path string) ClientOption

WithEvaluatePath sets the path an evaluation is posted to, appended to the base URL. A ModelPlaceholder in it is replaced with the model of the request, for deployments that name the model in the URL as well as the body.

func WithHTTPClient

func WithHTTPClient(hc *http.Client) ClientOption

WithHTTPClient sets the HTTP client used for every request. Use it to configure transport, proxies, or connection pooling. The client's own Timeout field, if set, bounds the whole call including retries, which is separate from WithTimeout.

func WithHeader

func WithHeader(key, value string) ClientOption

WithHeader adds a header sent on every request. A Request header of the same name replaces it for that request, except for Authorization, Accept, Content-Type, Content-Encoding and User-Agent, which the client sets itself. WithUserAgent changes the last of those.

func WithLogLevel

func WithLogLevel(level slog.Level) ClientOption

WithLogLevel sets the level WithLogger logs at. Pass LevelOff to disable logging without removing the logger.

func WithLogger

func WithLogger(log *slog.Logger) ClientOption

WithLogger sends one record per HTTP attempt to log. The client logs nothing until this option is given, so it stays quiet inside a library by default.

The level defaults to info; WithLogLevel changes it. At info each record is a summary: method, URL, attempt, duration, status, and request ID. At debug it also carries headers and bodies.

Credential headers are redacted. Bodies are not, and the request body holds the state, so debug logging writes whatever you are evaluating to the log.

func WithModel

func WithModel(model string) ClientOption

WithModel sets the model used when a request does not name one.

func WithModelsPath added in v0.3.0

func WithModelsPath(path string) ClientOption

WithModelsPath sets the path Client.Models reads. An empty path says the deployment publishes no listing, and Models then returns ErrNoModelList.

func WithRequestIDHeader added in v0.3.0

func WithRequestIDHeader(name string) ClientOption

WithRequestIDHeader sets the response header read into Response.RequestID and Error.RequestID. An empty name says the deployment sends none.

func WithResultKey added in v0.3.0

func WithResultKey(key string) ClientOption

WithResultKey sets the response field the payload is wrapped in, for a deployment that wraps it in an envelope. An empty key says the body is the payload.

func WithRetry

func WithRetry(p RetryPolicy) ClientOption

WithRetry turns on retries. The client does not retry by default; pass DefaultRetry for a sensible policy.

Example

Retries are off by default. Turn them on with a policy you can tune.

package main

import (
	"log"
	"time"

	"github.com/justintout/systemone"
	"github.com/justintout/systemone/jev"
)

func main() {
	policy := systemone.DefaultRetry()
	policy.MaxRetries = 4
	policy.MaxBackoff = 10 * time.Second

	client, err := jev.New(
		systemone.WithRetry(policy),
		systemone.WithTimeout(30*time.Second),
	)
	if err != nil {
		log.Fatal(err)
	}
	_ = client
}

func WithTimeout

func WithTimeout(d time.Duration) ClientOption

WithTimeout bounds one attempt, not the whole call. A zero duration removes the bound. The default is DefaultTimeout.

func WithUserAgent

func WithUserAgent(ua string) ClientOption

WithUserAgent replaces the default User-Agent header.

type Content

type Content = any

Content is the body of a question's instructions or of a single criterion. A string covers most questions. Any value that encodes to a JSON object or array is also accepted, which lets a question carry named data it refers to by backticked path. See https://docs.typesafe.ai/primitives/advanced.

type Error

type Error struct {
	// StatusCode is the HTTP status code.
	StatusCode int
	// RequestID is the request ID header the provider names, empty if absent.
	RequestID string
	// Body is the raw response body, which describes the offending field on a
	// 422. It is not parsed, because the API does not contract its shape.
	Body []byte
	// Header is the response header.
	Header http.Header
	// RetryAfter is the wait the server asked for, or 0 if it asked for none.
	RetryAfter time.Duration
}

Error is an unsuccessful HTTP response from the API.

Example

Classify an HTTP failure by its status.

package main

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

	"github.com/justintout/systemone"
	"github.com/justintout/systemone/jev"
)

// Questions are values. Declare them once and reuse them across requests.
type Team string

const (
	TeamBilling   Team = "billing"
	TeamTechnical Team = "technical"
	TeamSales     Team = "sales"
)

type Anger int

var team = systemone.NewChoice[Team]("team", "Which team should handle this message?",
	systemone.Option(TeamBilling, "Payments, invoicing, refunds"),
	systemone.Option(TeamTechnical, "Bugs, outages, integrations"),
	systemone.Option(TeamSales, "Pricing, upgrades, new accounts"),
)

func main() {
	client, err := jev.New()
	if err != nil {
		log.Fatal(err)
	}
	_, err = client.Ask(context.Background(), "state", team)

	var apiErr *systemone.Error
	switch {
	case err == nil:
	case errors.Is(err, systemone.ErrRateLimit):
		fmt.Println("back off and retry")
	case errors.Is(err, systemone.ErrUnprocessable):
		// The body names the offending field.
		errors.As(err, &apiErr)
		fmt.Println("bad question:", string(apiErr.Body))
	default:
		fmt.Println(err)
	}
}

func (*Error) Error

func (e *Error) Error() string

func (*Error) Is

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

Is reports whether the status this error carries is the class target names.

type Model

type Model struct {
	Name        string
	Description string
	// ReleaseDate is when the model or alias was released. It is the zero time
	// when the API omits it.
	//
	// The API sends RFC 3339, for example 2026-09-10T18:38:01.391457+00:00,
	// despite the SDK documentation showing a bare YYYY-MM-DD date.
	ReleaseDate time.Time
}

Model describes a model or alias the account may send in a request.

func (*Model) UnmarshalJSON

func (m *Model) UnmarshalJSON(data []byte) error

UnmarshalJSON decodes a model entry, parsing its release date, which the API sends as an RFC 3339 timestamp.

type Noul

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

Noul asks a yes/no question and returns the probability that the answer is yes. A value near 0.5 means yes and no are close to equally likely, not that the condition half holds; see https://docs.typesafe.ai/primitives/noul.

var urgent = systemone.NewNoul("is_urgent", "Does this convey urgency?",
	systemone.Yes("Explicitly time-sensitive"),
	systemone.No("No urgency expressed"),
)

Use one Noul per label when several labels may apply at once.

func NewNoul

func NewNoul(id string, instructions Content, opts ...NoulOption) *Noul

NewNoul builds a Noul question. Criteria are optional; describe both sides when the boundary between yes and no is subtle.

func (*Noul) From

func (q *Noul) From(r *Response) (NoulAnswer, error)

From reads this question's answer out of a response. It returns a *QuestionError wrapping ErrNoAnswer when the response has no answer under the question's ID, or ErrAnswerType when that answer is a different primitive.

func (*Noul) ID

func (q *Noul) ID() string

type NoulAnswer

type NoulAnswer struct {
	// Value is the probability that the answer is yes, from 0 to 1.
	Value float64
}

NoulAnswer is the answer to a Noul question. Noul answers carry no confidence: the probability is the whole answer.

func (NoulAnswer) Yes

func (a NoulAnswer) Yes(threshold float64) bool

Yes reports whether the probability of yes is at least threshold. Pick the threshold from your own data and the cost of being wrong.

type NoulOption

type NoulOption interface {
	// contains filtered or unexported methods
}

NoulOption configures a Noul during construction.

func No

func No(description Content) NoulOption

No describes what a no, a value near 0, means.

func Yes

func Yes(description Content) NoulOption

Yes describes what a yes, a value near 1, means.

type Question

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

Question is one typed question in a request. The three implementations are Choice, Score and Noul; the interface is closed.

type QuestionError

type QuestionError struct {
	QuestionID string
	Err        error
}

QuestionError reports a question that cannot be sent as built.

func (*QuestionError) Error

func (e *QuestionError) Error() string

func (*QuestionError) Unwrap

func (e *QuestionError) Unwrap() error

type Request

type Request struct {
	// State is the content to evaluate: a string, or any value that encodes to
	// a JSON object or array. See https://docs.typesafe.ai/concepts/state.
	State any
	// Questions are asked in one call and answered in parallel. They cannot see
	// one another's answers.
	Questions []Question
	// Model overrides the client default for this request.
	Model string
	// Header adds or replaces headers for this request. Authorization, Accept,
	// Content-Type, Content-Encoding and User-Agent are set by the client after
	// this is applied, so they cannot be overridden here; see [WithUserAgent].
	Header http.Header
	// Retry overrides the client's retry policy for this request. A nil Retry
	// inherits it; a zero RetryPolicy turns retries off for this call alone.
	Retry *RetryPolicy
	// Timeout overrides the client's per-attempt timeout for this request. Zero
	// inherits it, and [NoTimeout] removes the bound. To bound the whole call
	// including retries, give ctx a deadline.
	Timeout time.Duration
}

Request is one evaluation: a state, the questions to ask about it, and how to send them.

type Response

type Response struct {
	// Model is the versioned model that answered, even when the request named
	// an alias such as jev-latest.
	Model string
	// Usage is the token count for the request.
	Usage Usage
	// RequestID is the request ID header the provider names, empty if absent.
	RequestID string
	// contains filtered or unexported fields
}

Response is the result of one evaluation. Answers are read through the question handles that produced them: Choice.From, Score.From, Noul.From.

type RetryPolicy

type RetryPolicy struct {
	// MaxRetries is how many attempts follow the first one.
	MaxRetries int
	// InitialBackoff is the wait before the first retry. It doubles each
	// attempt up to MaxBackoff.
	InitialBackoff time.Duration
	// MaxBackoff caps the wait between attempts.
	MaxBackoff time.Duration
	// Jitter spreads each wait by up to this fraction in either direction, so
	// concurrent clients do not retry in lockstep. 0.25 means plus or minus a
	// quarter of the computed wait. It must be between 0 and 1: above 1 the
	// spread exceeds the delay and produces a negative wait.
	Jitter float64
	// Retry reports whether a response with this status should be retried. A
	// nil Retry retries 408, 429, and every 5xx.
	Retry func(status int) bool
	// RetryTransportError retries a request that failed without a response,
	// including a per-attempt timeout.
	RetryTransportError bool
	// RespectRetryAfter uses the server's retry-after header in place of the
	// computed backoff when the response carries one.
	RespectRetryAfter bool
}

RetryPolicy controls whether the client retries a failed attempt. The zero value performs no retries, which is the client default; pass DefaultRetry to WithRetry to turn retries on.

Retries apply to the whole request, including transport failures, so a retried request may be evaluated more than once by the service.

func DefaultRetry

func DefaultRetry() RetryPolicy

DefaultRetry is the policy to start from: two retries with exponential backoff from 500ms, capped at 5s, honoring retry-after.

type Score

type Score[T ~int] struct {
	// contains filtered or unexported fields
}

Score rates the state along ordered levels you define and returns a probability-weighted position across them. T is an integer type whose constants are the levels, declared from lowest to highest starting at zero:

type Frustration int

const (
	Calm Frustration = iota
	Frustrated
	VeryAngry
)

var frustration = systemone.NewScore[Frustration]("frustration", "How frustrated is the customer?",
	systemone.Level(Calm, "Calm"),
	systemone.Level(Frustrated, "Frustrated"),
	systemone.Level(VeryAngry, "Very angry"),
)

Each level description must describe a concrete situation and stand on its own. See https://docs.typesafe.ai/primitives/score.

func NewScore

func NewScore[T ~int](id string, instructions Content, opts ...ScoreOption[T]) *Score[T]

NewScore builds a Score question.

func (*Score[T]) From

func (q *Score[T]) From(r *Response) (ScoreAnswer[T], error)

From reads this question's answer out of a response. It returns a *QuestionError wrapping ErrNoAnswer when the response has no answer under the question's ID, or ErrAnswerType when that answer is a different primitive.

func (*Score) ID

func (q *Score) ID() string

func (*Score[T]) Levels

func (q *Score[T]) Levels() int

Levels returns the number of declared levels.

type ScoreAnswer

type ScoreAnswer[T ~int] struct {
	// Value is the probability-weighted position across the levels. It can land
	// between two levels, so it is a float rather than a T.
	Value float64
	// Level is the single most probable level. It can differ from rounding
	// Value when the distribution is lopsided.
	Level T
	// Probabilities maps every level to its probability. They sum to 1.
	Probabilities map[T]float64
	// Legend maps every level back to the description that was sent for it.
	// The API echoes the description as it was sent, so a level declared with
	// a structured description comes back structured. A response may carry
	// fewer entries than the question declared, so it does not give the scale.
	Legend map[T]Content
	// LevelCount is how many levels the question declared. It comes from the
	// question, not the response, and is what [ScoreAnswer.Normalized] divides
	// by. A ScoreAnswer built by hand must set it.
	LevelCount int
	// Confidence is how concentrated Probabilities is, from 0 to 1. See
	// https://docs.typesafe.ai/confidence.
	Confidence float64
}

ScoreAnswer is the answer to a Score question.

func (ScoreAnswer[T]) Describe

func (a ScoreAnswer[T]) Describe() string

Describe returns the description ScoreAnswer.Level was sent with, as text. A level declared with a structured description is rendered as JSON; reach into ScoreAnswer.Legend to read its fields.

func (ScoreAnswer[T]) Normalized

func (a ScoreAnswer[T]) Normalized() float64

Normalized maps Value onto 0 to 1, dividing by the highest declared level. Use it to put scores with different level counts on one scale before combining them. It returns 0 when LevelCount is unset, which only happens on a ScoreAnswer that did not come from Score.From.

func (ScoreAnswer[T]) Probability

func (a ScoreAnswer[T]) Probability(level T) float64

Probability returns the probability of one level, or 0 if the answer does not carry it.

type ScoreOption

type ScoreOption[T ~int] interface {
	// contains filtered or unexported methods
}

ScoreOption configures a Score during construction.

func Level

func Level[T ~int](level T, description Content) ScoreOption[T]

Level declares one level and its description. Levels are sent as an ordered array, so level must equal the position it is declared in: the first Level must be 0, the second 1, and so on. A mismatch fails the request rather than silently shifting the scale.

func Levels

func Levels[T ~int](descriptions ...Content) ScoreOption[T]

Levels declares every level at once, in order from lowest to highest. The level values are the positions in the list.

type Usage

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

Usage is the token count the request was billed on. Only input tokens are charged; see https://docs.typesafe.ai/models.

Directories

Path Synopsis
Package clef is a client for Cloudflare's Clef models on Workers AI.
Package clef is a client for Cloudflare's Clef models on Workers AI.
examples
locate command
Command locate finds something in a photo with a Box question, following https://docs.typesafe.ai/primitives/box, and saves a crop of it.
Command locate finds something in a photo with a Box question, following https://docs.typesafe.ai/primitives/box, and saves a crop of it.
screenshot command
Command screenshot triages a support ticket that came with a screenshot, using Cloudflare's Clef models on Workers AI.
Command screenshot triages a support ticket that came with a screenshot, using Cloudflare's Clef models on Workers AI.
triage command
Command triage is the support-ticket example from the TypeSafe quick start, written with this SDK.
Command triage is the support-ticket example from the TypeSafe quick start, written with this SDK.
internal
bridge
Package bridge lets the jev and clef packages send requests through package systemone without the hooks they need becoming part of its public API.
Package bridge lets the jev and clef packages send requests through package systemone without the hooks they need becoming part of its public API.
livetest
Package livetest holds what the live API tests in packages jev and clef share.
Package livetest holds what the live API tests in packages jev and clef share.
releasebot command
Command releasebot works out whether the changes since the last release are worth publishing, and whether they may break existing callers.
Command releasebot works out whether the changes since the last release are worth publishing, and whether they may break existing callers.
Package jev is a client for TypeSafe's hosted System One API and its Jev models.
Package jev is a client for TypeSafe's hosted System One API and its Jev models.
Package pattern composes System One answers into the shapes the TypeSafe docs describe: confidence-gated routing, composite scoring, and walking a taxonomy one Choice at a time.
Package pattern composes System One answers into the shapes the TypeSafe docs describe: confidence-gated routing, composite scoring, and walking a taxonomy one Choice at a time.

Jump to

Keyboard shortcuts

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