Documentation
¶
Overview ¶
Package typesafe is a community-maintained Go SDK for the TypeSafe System One API and its model, Jev.
It is not affiliated with, endorsed by, or sponsored by TypeSafe AI.
Status ¶
Phase 0 (contract lock). The wire contract is pinned in docs/WIRE_CONTRACT.md and testdata/spec/openapi.json; golden request/response pairs live in testdata/contract. The client, question primitives, and typed answers arrive in Phases 1 and 2. See docs/Dev_Roadmap.md.
The API in one paragraph ¶
You send one state — a string, object, or array — together with a map of named questions, and receive one typed answer per question in a single call. Three question types cover the decision space: Noul (yes/no, answered with the probability of yes), Choice (select one option from a set you define), and Score (rate against ordered levels you define). Answers are constrained to the options you supplied, so a successful response cannot contain a value your code did not ask for.
Design ¶
The core module has no third-party dependencies and never will. Observability, caching, the CLI, the static analyzers, and framework integrations live in separate modules so that importing this one costs nothing.
Example (Documentation) ¶
package main
// The code in docs/MANUAL.md and README.md, verbatim.
//
// It is an Example with no // Output: comment, so the toolchain compiles and
// type-checks it but never runs it — running would need a key and a network.
// That is the whole point: a signature change makes the documentation fail to
// build instead of quietly making it wrong, which is the failure mode prose
// has and code does not.
//
// When you change a snippet in the manual, change it here too. When this stops
// compiling, the manual is already wrong.
import (
"context"
"errors"
"fmt"
"log"
"time"
typesafe "github.com/nibir1/typesafe-go"
"github.com/nibir1/typesafe-go/cassette"
"github.com/nibir1/typesafe-go/decision"
)
type docTopic string
const (
docTopicBilling docTopic = "billing"
docTopicTechnical docTopic = "technical"
)
func docEscalate() {}
func docRoute(string) {}
func docConfirm() {}
func docHandOff() {}
func main() {
ctx := context.Background()
client, err := typesafe.NewClient(
typesafe.WithTimeout(60*time.Second),
typesafe.WithDefaultModel("jev-1.13.0"),
typesafe.WithContextLimitCheck(false),
typesafe.WithRetryPolicy(typesafe.RetryPolicy{
MaxRetries: 2,
BackoffInitial: 500 * time.Millisecond,
BackoffMax: 5 * time.Second,
BackoffJitter: 0.25,
RespectRetryAfter: true,
MaxRetryAfter: 30 * time.Second,
Timeout: 30 * time.Second,
}),
typesafe.WithCircuitBreaker(&typesafe.CircuitBreaker{
Threshold: 5, OpenFor: 30 * time.Second, HalfOpenProbes: 1,
}),
typesafe.WithBudget(typesafe.NewBudget(
typesafe.MaxRequestsPerMinute(600),
typesafe.MaxTotalTokens(5_000_000),
)),
)
if err != nil {
log.Fatal(err)
}
req := &typesafe.SystemOneRequest{
State: "Help! My payouts have been failing for 3 days.",
Questions: typesafe.Questions{
"is_urgent": typesafe.Noul{
Instructions: "Does this message convey urgency?",
Criteria: &typesafe.NoulCriteria{
True: "The sender needs a response today",
False: "The sender can wait",
},
},
"team": typesafe.Choice{
Instructions: "Which team should handle this ticket?",
Criteria: typesafe.Options{
"billing": "Payments, invoicing, refunds",
"technical": "Bugs, outages, integrations",
"other": nil,
},
},
"severity": typesafe.Score{
Instructions: "How severe is the problem described here?",
Criteria: typesafe.Levels{
"No impact on the customer",
"Annoying but there is a workaround",
"One workflow is blocked",
"The product is unusable",
},
},
},
}
est := req.EstimateTokens()
if err := est.Err(); err != nil {
log.Fatal(err)
}
resp, err := client.SystemOne(ctx, req)
if err != nil {
var rl *typesafe.RateLimitError
if errors.As(err, &rl) {
time.Sleep(rl.RetryAfter)
}
if errors.Is(err, typesafe.ErrRateLimit) {
return
}
log.Fatal(err)
}
urgent, err := resp.Noul("is_urgent")
if err != nil {
log.Fatal(err)
}
fmt.Printf("urgency: %.2f\n", urgent.Noul)
if urgent.Bool(0.8) {
docEscalate()
}
team, err := resp.Choice("team")
if err != nil {
log.Fatal(err)
}
severity, err := resp.Score("severity")
if err != nil {
log.Fatal(err)
}
if severity.AtOrAbove(2) > 0.8 {
docEscalate()
}
bands := decision.Bands{ActAbove: 0.90, ConfirmAbove: 0.50}
switch bands.Classify(team.Confidence) {
case decision.Act:
docRoute(team.Choice)
case decision.Confirm:
docRoute(team.Choice)
docConfirm()
case decision.Escalate:
docHandOff()
}
answer, err := resp.Answer("is_urgent")
if err != nil {
log.Fatal(err)
}
switch a := answer.(type) {
case typesafe.NoulAnswer:
_ = a.Noul
case typesafe.ChoiceAnswer:
_, _ = a.Choice, a.Confidence
case typesafe.ScoreAnswer:
_, _ = a.Score, a.Confidence
}
policy := decision.Policy{
Name: "moderation.v3",
Weights: decision.Weights{
"is_solicitation": 3,
"is_unverifiable": 2,
"is_hostile": 2,
},
Normalize: true,
ReviewAbove: 0.55,
BlockAbove: 0.80,
OnMissing: decision.MissingIsError,
}
_ = policy
q := typesafe.TypedChoice[docTopic]("Which team?",
typesafe.OptionOf(docTopicBilling, "Payments, invoicing, refunds"),
typesafe.OptionOf(docTopicTechnical, "Bugs, outages, integrations"),
)
ans, err := q.Answer(resp, "team")
if err == nil {
switch ans.Choice {
case docTopicBilling:
case docTopicTechnical:
}
}
states := []any{"a", "b"}
result := client.SystemOneBatch(ctx, states, req.Questions,
typesafe.WithConcurrency(16))
for i, item := range result.Items {
if item.Err != nil {
continue
}
_ = i
}
replayClient, err := typesafe.NewClient(
typesafe.WithAPIKey("test"),
typesafe.WithHTTPClient(cassette.MustReplay("testdata/cassettes/triage.jsonl")),
)
_, _ = replayClient, err
}
Output:
Index ¶
- Constants
- Variables
- func ContextWithRequestID(ctx context.Context, id string) context.Context
- func Exhaustive[T OptionKey](a ChoiceAnswerOf[T], allowed ...T) error
- func RequestIDFrom(ctx context.Context) string
- func VersionString() string
- type API
- type APIError
- type Answer
- type AttemptInfo
- type AuthenticationError
- type BadRequestError
- type BatchError
- type BatchOption
- type BatchResult
- type Budget
- type BudgetOption
- type BudgetUsage
- type CallInfo
- type Choice
- type ChoiceAnswer
- type ChoiceAnswerOf
- type ChoiceBuilder
- type CircuitBreaker
- type CircuitState
- type Client
- func (c *Client) BaseURL() string
- func (c *Client) DefaultModel() string
- func (c *Client) Models(ctx context.Context) ([]ModelCard, error)
- func (c *Client) SystemOne(ctx context.Context, req *SystemOneRequest) (*SystemOneResponse, error)
- func (c *Client) SystemOneAll(ctx context.Context, requests ...*SystemOneRequest) []Result
- func (c *Client) SystemOneAsync(ctx context.Context, req *SystemOneRequest) <-chan Result
- func (c *Client) SystemOneBatch(ctx context.Context, states []any, qs Questions, opts ...BatchOption) BatchResult
- func (c *Client) SystemOneBatchSeq(ctx context.Context, states []any, qs Questions, opts ...BatchOption) iter.Seq2[int, ItemResult]
- type ConnectionError
- type CostEstimate
- type EntryType
- type ErrorDetail
- type Handler
- type Hooks
- type Interceptor
- type InternalServerError
- type ItemResult
- type LevelKey
- type Levels
- type ModelCard
- type NotFoundError
- type Noul
- type NoulAnswer
- type NoulBuilder
- type NoulCriteria
- type Option
- func WithAPIKey(key string) Option
- func WithBaseURL(raw string) Option
- func WithBudget(b *Budget) Option
- func WithCircuitBreaker(b *CircuitBreaker) Option
- func WithContextLimitCheck(enabled bool) Option
- func WithDefaultModel(model string) Option
- func WithHTTPClient(hc *http.Client) Option
- func WithHeader(key, value string) Option
- func WithHooks(h Hooks) Option
- func WithInterceptor(interceptors ...Interceptor) Option
- func WithLogger(l *slog.Logger) Option
- func WithMaxRetries(n int) Option
- func WithRequestID(fn func() string, header ...string) Option
- func WithRetryObserver(fn func(context.Context, AttemptInfo)) Option
- func WithRetryPolicy(p RetryPolicy) Option
- func WithTimeout(d time.Duration) Option
- func WithUserAgentSuffix(suffix string) Option
- type OptionKey
- type Options
- type OverloadedError
- type PanicError
- type PermissionDeniedError
- type Question
- type Questions
- type RankedOption
- type RankedOptionOf
- type RateLimitError
- type RawQuestion
- type RefWarning
- type ResponseValidationError
- type Result
- type RetryPolicy
- type Score
- type ScoreAnswer
- func (a ScoreAnswer) AtOrAbove(level int) float64
- func (a ScoreAnswer) AtOrBelow(level int) float64
- func (a ScoreAnswer) LevelProbabilities() []float64
- func (a ScoreAnswer) Levels() []EntryType
- func (a ScoreAnswer) MostLikely() (int, float64)
- func (a ScoreAnswer) Nearest() (int, EntryType)
- func (a ScoreAnswer) NumLevels() int
- func (ScoreAnswer) Type() string
- type ScoreAnswerOf
- type ScoreBuilder
- type SystemOneRequest
- type SystemOneResponse
- func (r *SystemOneResponse) All() map[string]Answer
- func (r *SystemOneResponse) Answer(id string) (Answer, error)
- func (r *SystemOneResponse) Choice(id string) (ChoiceAnswer, error)
- func (r *SystemOneResponse) Choices() map[string]ChoiceAnswer
- func (r *SystemOneResponse) Confidence(id string) (float64, bool)
- func (r *SystemOneResponse) Noul(id string) (NoulAnswer, error)
- func (r *SystemOneResponse) Nouls() map[string]NoulAnswer
- func (r *SystemOneResponse) Score(id string) (ScoreAnswer, error)
- func (r *SystemOneResponse) Scores() map[string]ScoreAnswer
- type TimeoutError
- type TokenEstimate
- type TypedChoiceQuestion
- type TypedLevel
- type TypedOption
- type TypedScoreQuestion
- type UnprocessableEntityError
- type Usage
- type ValidationDetail
- type Warning
Examples ¶
Constants ¶
const ( // DefaultBatchConcurrency is how many requests run at once when nothing // else is specified. Deliberately modest: the published rate limits are // generous, but they are shared with everything else on the account, and // a batch that saturates them starves the interactive traffic beside it. DefaultBatchConcurrency = 8 // MinAdaptiveConcurrency is the floor adaptive backoff will not go below. // One worker still makes progress; zero would deadlock. MinAdaptiveConcurrency = 1 )
Batch defaults.
const ( // MaxContextTokens is the ceiling on state plus every question combined. MaxContextTokens = 64_000 // MaxSingleQuestionTokens is the ceiling on state plus the single longest // question. This is a *separate* limit, and the one that is easy to miss: // a request can pass the 64k check and fail this one. MaxSingleQuestionTokens = 32_000 // DefaultRequestsPerMinute is the published request rate limit. DefaultRequestsPerMinute = 1_200 // DefaultTokensPerSecond is the published token rate limit. DefaultTokensPerSecond = 250_000 // DefaultInputCostPerMillionTokens is the published price in US dollars. // Output tokens are free. DefaultInputCostPerMillionTokens = 0.042 )
Jev 1.13 limits, from the published Models page.
Every one of these is a **configurable default, not a constant**. TypeSafe states plainly that rate limits "can change without notice" during early access, so hard-coding them into the client's behavior would mean shipping a new release every time they move. They are starting values for a Budget the caller owns.
const ( // DefaultCircuitThreshold is how many consecutive retryable failures open // the circuit. DefaultCircuitThreshold = 5 // DefaultCircuitOpenFor is how long it stays open before probing. DefaultCircuitOpenFor = 30 * time.Second )
Circuit breaker defaults.
const ( // DefaultBaseURL is the API root. Overridable per client, and by the // TYPESAFE_BASE_URL environment variable. DefaultBaseURL = "https://api.typesafe.ai" // DefaultModel matches the official Python and JavaScript SDKs. DefaultModel = "jev-latest" // SystemOnePath is the evaluation endpoint. SystemOnePath = "/v1/systemone" // ModelsPath lists the model names this account may send. ModelsPath = "/v1/models" )
Wire constants, fixed by the published contract. See docs/WIRE_CONTRACT.md.
const ( EnvAPIKey = "TYPESAFE_API_KEY" EnvBaseURL = "TYPESAFE_BASE_URL" EnvDefaultModel = "TYPESAFE_DEFAULT_MODEL" EnvLogLevel = "TYPESAFE_LOG_LEVEL" )
Environment variables, named to match the official SDKs so that a process configured for Python or JavaScript works unchanged here.
const ( TypeNoul = "noul" TypeChoice = "choice" TypeScore = "score" )
Question type discriminators, used as the "type" field on both questions and their corresponding answers.
const ( // DefaultMaxRetries is 2 — three attempts in total. DefaultMaxRetries = 2 // DefaultBackoffInitial is the first delay, doubled on each attempt. DefaultBackoffInitial = 500 * time.Millisecond // DefaultBackoffMax caps any single delay. DefaultBackoffMax = 5 * time.Second // DefaultBackoffJitter randomizes each delay by ±25%, so that clients // that failed together do not retry together. DefaultBackoffJitter = 0.25 // DefaultRetryTimeout is the budget across all attempts, distinct from // the per-operation timeout in DefaultTimeout. DefaultRetryTimeout = 30 * time.Second // DefaultMaxRetryAfter caps how long a server's retry-after will be // honored. Without it, a single header could park a request for minutes; // see RetryPolicy.MaxRetryAfter. DefaultMaxRetryAfter = 30 * time.Second )
Retry defaults, chosen to match the official Python and JavaScript SDKs exactly rather than to be independently reasonable.
A developer porting a working integration from Python should not have to re-tune anything, and should not discover that this client gives up sooner or hammers harder than the one they came from. Where these numbers look arbitrary, it is because they are the official SDK's numbers.
const ( // MinScoreLevels is the fewest rubric levels the API accepts. // // The prose documentation claims two. It is wrong: a one-level Score // returns 200 with score 0 and confidence 1. Degenerate, but legal, and // this SDK does not reject requests the server would accept. MinScoreLevels = 1 // MaxScoreLevels is the most rubric levels the API accepts. Exceeding it // returns 400 "Too many score levels. Must have at most 10 levels." // // Documented in neither the OpenAPI schema nor the prose docs. Validate // enforces it client-side so the failure costs no round trip. MaxScoreLevels = 10 )
Score rating bounds, both established against the live API rather than from documentation.
const DefaultTimeout = 10 * time.Second
DefaultTimeout bounds each HTTP operation, matching the official SDKs.
const RequestIDHeader = "x-typesafe-request-id"
RequestIDHeader carries a per-call identifier worth quoting in a support ticket. Present on both successful and failed responses.
const StatusOverloaded = 529
StatusOverloaded is 529, which net/http does not name. TypeSafe returns it when the service is temporarily saturated.
Variables ¶
var ( // ErrNoSuchAnswer means the response carried no answer under that id. ErrNoSuchAnswer = errors.New("typesafe: no answer with that id") // ErrWrongAnswerType means the answer exists but is a different primitive // than the accessor asked for. ErrWrongAnswerType = errors.New("typesafe: wrong answer type") )
Answer errors.
var ( // ErrNoAPIKey means no key was supplied and TYPESAFE_API_KEY is unset. ErrNoAPIKey = errors.New("typesafe: no API key") ErrBadRequest = errors.New("typesafe: bad request") ErrAuthentication = errors.New("typesafe: authentication failed") ErrPermissionDenied = errors.New("typesafe: permission denied") ErrNotFound = errors.New("typesafe: not found") ErrInvalidRequest = errors.New("typesafe: request failed validation") ErrRateLimit = errors.New("typesafe: rate limited") ErrOverloaded = errors.New("typesafe: service overloaded") ErrInternalServer = errors.New("typesafe: server error") ErrConnection = errors.New("typesafe: connection failed") ErrTimeout = errors.New("typesafe: request timed out") ErrInvalidResponse = errors.New("typesafe: malformed response") ErrInvalidConfig = errors.New("typesafe: invalid client configuration") )
Sentinel errors, for callers who prefer errors.Is over errors.As. Every typed error below matches exactly one of these.
if errors.Is(err, typesafe.ErrRateLimit) { ... }
The typed forms carry more: status, request id, retry-after, and for a 422 the exact field the server rejected. Prefer errors.As when you need those.
var ErrBatchPartialFailure = errors.New("typesafe: some batch items failed")
ErrBatchPartialFailure matches any BatchError through errors.Is.
var ErrBudgetExceeded = errors.New("typesafe: budget exceeded")
ErrBudgetExceeded means a Budget refused the request. Nothing was sent.
var ErrCircuitOpen = errors.New("typesafe: circuit breaker is open")
ErrCircuitOpen means the breaker is open and the request was not attempted.
It is not an API failure: nothing was sent. Treat it as a signal to shed load or serve a fallback, not as evidence about this particular request.
var ErrRetriesExhausted = errors.New("typesafe: retries exhausted")
ErrRetriesExhausted wraps the last failure when every attempt was used.
The underlying error remains reachable, so errors.As still finds the terminal *RateLimitError or *InternalServerError and errors.Is still matches its sentinel. Code that already handles those does not need changing.
var ErrUndeclaredOption = fmt.Errorf("typesafe: answer contains an option the question did not declare")
ErrUndeclaredOption means an answer named something outside the declared set.
var Version = "0.0.0-dev"
Version is the SDK version, reported in the User-Agent header.
A var rather than a const so a release build can stamp it with -ldflags "-X github.com/nibir1/typesafe-go.Version=v1.0.0". The linker's -X flag only writes to variables; as a const this was unsettable, and a released binary would have reported 0.0.0-dev forever.
A module installed with `go install ...@v1.0.0` gets its real version from the build info instead, which is why this is a fallback rather than the source of truth. See VersionString.
Functions ¶
func ContextWithRequestID ¶
ContextWithRequestID puts an existing correlation id on ctx, so a call made with that context reuses it instead of generating a new one.
ctx = typesafe.ContextWithRequestID(ctx, r.Header.Get("X-Request-Id"))
resp, err := client.SystemOne(ctx, req)
For propagating an id that already exists — the one a load balancer put on an inbound request, or a job id from a queue. Correlating an LLM response with the request that caused it is the first thing anyone wants during an incident, and it is impossible if every call invents its own id.
An empty id is ignored, so a missing inbound header falls through to WithRequestID's generator rather than blanking the id out.
func Exhaustive ¶
func Exhaustive[T OptionKey](a ChoiceAnswerOf[T], allowed ...T) error
Exhaustive checks that every option in the answer is one of allowed.
if err := typesafe.Exhaustive(ans, AllTopics...); err != nil {
return err
}
It returns an error rather than panicking, and it is worth calling: the only way an answer can carry an undeclared option is that the request and the type have drifted apart — a question built somewhere else, or an enum that gained a value the question was never updated with. A type switch handles that by falling through to no branch at all, silently.
Passing no allowed values checks nothing and returns nil, so a caller that has not enumerated its set is not forced to.
func RequestIDFrom ¶
RequestIDFrom returns the correlation id this SDK generated for the call, or "" when none was configured.
Useful inside a hook or an interceptor to tie SDK activity to the rest of a trace. Distinct from APIError.RequestID, which is the id *TypeSafe* assigned and is the one to quote in a support ticket.
func VersionString ¶
func VersionString() string
VersionString returns the version this binary or module was built as.
Prefers the version the Go toolchain recorded — which `go install module@version` sets automatically and correctly — and falls back to Version, which a release build stamps with -ldflags. Preferring build info means a user who installed with `go install` sees the version they asked for, not whatever the last person to edit this file typed.
Types ¶
type API ¶
type API interface {
// SystemOne evaluates one state against a map of named questions.
SystemOne(ctx context.Context, req *SystemOneRequest) (*SystemOneResponse, error)
// Models lists the model names this account may use.
Models(ctx context.Context) ([]ModelCard, error)
}
API is the behavior *Client provides.
Go convention says consumers declare the interfaces they need, and for most code that remains the better habit — depend on a one-method interface you define at the point of use, not on everything a client can do.
This one is provided because writing it out is otherwise the first thing every caller does, and because typesafetest.Mock needs a shared shape to satisfy. It is deliberately small and will not grow: methods added to *Client in later phases stay off this interface unless they are part of the core request path, so that implementing it never becomes a burden.
func triage(ctx context.Context, api typesafe.API, ticket Ticket) (Queue, error) {
resp, err := api.SystemOne(ctx, &typesafe.SystemOneRequest{ ... })
...
}
Both *Client and typesafetest.Mock satisfy it, so the same function can be exercised against a mock, a test server, a cassette, or the live API without changing its signature.
type APIError ¶
type APIError struct {
// Status is the HTTP status code.
Status int
// Body is the raw response body, for statuses whose shape we do not model.
Body []byte
// Header is the response header. Never contains request credentials.
Header http.Header
// Endpoint is the method and path, without credentials or query.
Endpoint string
// RequestID is the x-typesafe-request-id header, or "" if absent. Worth
// quoting in a support ticket.
RequestID string
// Detail is the parsed field-level validation failures from a 422 body.
// Empty for every other status. See ValidationDetail.
Detail []ValidationDetail
// Reason is the server's typed error message, used by 400 and 401.
//
// The API returns "detail" in two different shapes: an array of
// ValidationDetail for schema failures (422), and a single object with
// error_type and message for everything else. Neither shape is published;
// both were observed against the live API. A client that assumes one shape
// silently loses the other, which is why both are modeled here.
Reason *ErrorDetail
// contains filtered or unexported fields
}
APIError is any non-2xx response. Every status-specific error below wraps one, so errors.As(err, &apiErr) succeeds for all of them.
type Answer ¶
type Answer interface {
// Type reports the wire discriminator: "noul", "choice", or "score".
Type() string
// contains filtered or unexported methods
}
Answer is a decoded answer: NoulAnswer, ChoiceAnswer, or ScoreAnswer.
The interface is sealed, so a type switch over it is exhaustive:
switch a := ans.(type) {
case typesafe.NoulAnswer: // a.Noul
case typesafe.ChoiceAnswer: // a.Choice, a.Confidence
case typesafe.ScoreAnswer: // a.Score, a.Confidence
}
type AttemptInfo ¶
type AttemptInfo struct {
// Attempt is 1 for the first try, 2 for the first retry, and so on.
Attempt int
// Err is what the attempt failed with.
Err error
// Status is the HTTP status, or 0 when the attempt produced no response.
Status int
// Delay is how long the client will wait before the next attempt.
Delay time.Duration
// RetryAfterHonored reports whether Delay came from the server's
// retry-after header rather than from computed backoff.
RetryAfterHonored bool
// Elapsed is the time spent since the first attempt began.
Elapsed time.Duration
}
AttemptInfo describes one completed attempt, passed to a retry observer.
type AuthenticationError ¶
type AuthenticationError struct{ *APIError }
AuthenticationError is a 401: the key is missing or invalid.
func (*AuthenticationError) Is ¶
func (e *AuthenticationError) Is(target error) bool
func (*AuthenticationError) Unwrap ¶
func (e *AuthenticationError) Unwrap() error
type BadRequestError ¶
type BadRequestError struct{ *APIError }
BadRequestError is a 400: the request was well-formed against the schema but the server rejected its content.
Undocumented in both the OpenAPI spec (which declares only 200 and 422) and the prose docs. Observed for an unknown model name and for a Score with more than ten levels. Not retryable.
func (*BadRequestError) Is ¶
func (e *BadRequestError) Is(target error) bool
func (*BadRequestError) Unwrap ¶
func (e *BadRequestError) Unwrap() error
type BatchError ¶
type BatchError struct {
// Summary is the human-readable grouping.
Summary string
// Failed and Total count the outcome.
Failed, Total int
}
BatchError summarizes partial failure.
It wraps nothing: a batch failure is not one error, and pretending it is would let errors.As pick an arbitrary item's cause and look authoritative. Use BatchResult.Errors to inspect the individual failures.
func (*BatchError) Error ¶
func (e *BatchError) Error() string
func (*BatchError) Is ¶
func (e *BatchError) Is(target error) bool
Is reports whether target is ErrBatchPartialFailure.
type BatchOption ¶
type BatchOption func(*batchConfig)
BatchOption configures a batch.
func WithAdaptiveConcurrency ¶
func WithAdaptiveConcurrency(enabled bool) BatchOption
WithAdaptiveConcurrency turns global rate-limit backoff on or off. On by default.
When a worker meets a 429 or a 529, the limit for the *whole batch* halves, and it recovers by one after a run of successes. Without this, every worker independently rediscovers the same limit, and the batch spends its time in per-request backoff while continuing to push at the rate that caused the problem.
func WithBatchModel ¶
func WithBatchModel(model string) BatchOption
WithBatchModel selects the model for every request in the batch.
SystemOneBatch builds each request itself, so SystemOneRequest.Model is not reachable from the call site; without this the client default is the only option. Leave it unset to use that default.
func WithConcurrency ¶
func WithConcurrency(n int) BatchOption
WithConcurrency bounds how many requests run at once.
Values below 1 are treated as 1. There is no unbounded mode: a batch that launches a goroutine per input is a way to convert a large slice into a rate limit error, and the useful ceiling is set by the account's quota rather than by the size of the input.
func WithItemCallback ¶
func WithItemCallback(fn func(ItemResult)) BatchOption
WithItemCallback registers a function called as each item completes, in completion order.
For progress reporting on a long batch. It runs on the worker's goroutine, so keep it quick and make it safe for concurrent use. For processing results as they land, prefer SystemOneBatchSeq, which does not require that care.
type BatchResult ¶
type BatchResult struct {
// Items holds one result per input state, **in input order**.
Items []ItemResult
// Usage is the summed token usage across successful items.
Usage Usage
// Succeeded and Failed count the outcomes.
Succeeded int
Failed int
// Duration is the wall-clock time for the whole batch.
Duration time.Duration
// PeakConcurrency is the highest number of requests in flight at once,
// and MinConcurrency the lowest the limit fell to. When adaptive
// concurrency is on, a gap between them means the batch was throttled.
PeakConcurrency int
MinConcurrency int
}
BatchResult is the outcome of a whole batch.
func (BatchResult) Err ¶
func (r BatchResult) Err() error
Err returns a summary error when any item failed, or nil.
Deliberately *not* the first failure. A batch of a thousand where three items failed for two different reasons is badly served by surfacing one of them; the summary names how many failed and why, and Errors gives the rest.
func (BatchResult) Errors ¶
func (r BatchResult) Errors() []error
Errors returns every item failure, in input order.
func (BatchResult) Responses ¶
func (r BatchResult) Responses() []*SystemOneResponse
Responses returns the successful responses, in input order.
type Budget ¶
type Budget struct {
// contains filtered or unexported fields
}
Budget caps how much a process may spend against the API.
What this is for ¶
A retry loop with a bug, a batch job over the wrong input file, a test that escapes into CI with a live key — each of these can burn a quota in minutes, and the API will cheerfully serve every request until the money or the rate limit runs out. A Budget is the thing that says no first.
It fails **before any network I/O**, so an over-limit call costs nothing and the error arrives immediately rather than after a rate-limit round trip.
budget := typesafe.NewBudget(
typesafe.MaxRequestsPerMinute(1200),
typesafe.MaxTokensPerSecond(250_000),
typesafe.MaxTotalRequests(10_000), // this process, ever
)
client, err := typesafe.NewClient(typesafe.WithBudget(budget))
The published limits are defaults, not truth ¶
The rate constants this package exposes come from TypeSafe's Models page, which states that they can change without notice during early access. They are starting values. A Budget is configured by you, and the SDK never assumes a limit it was not given.
Budget is safe for concurrent use and is meant to be shared across every client in a process.
func DefaultBudget ¶
func DefaultBudget() *Budget
DefaultBudget returns a Budget set to the published Jev 1.13 rate limits.
A convenience, not a guarantee: the limits are documented as changeable, and a client at exactly the published rate will still meet 429s under contention with other traffic on the same account.
func NewBudget ¶
func NewBudget(opts ...BudgetOption) *Budget
NewBudget builds a Budget. With no options it enforces nothing.
type BudgetOption ¶
type BudgetOption func(*Budget)
BudgetOption configures a Budget.
func MaxRequestsPerMinute ¶
func MaxRequestsPerMinute(n int) BudgetOption
MaxRequestsPerMinute caps the request rate. Zero disables the check.
func MaxTokensPerSecond ¶
func MaxTokensPerSecond(n int) BudgetOption
MaxTokensPerSecond caps the estimated token rate. Zero disables the check.
func MaxTotalRequests ¶
func MaxTotalRequests(n int) BudgetOption
MaxTotalRequests caps requests for the lifetime of this Budget. Zero disables the check.
The blunt instrument, and the one that actually stops a runaway loop: a rate limit lets a bug spend all day at exactly the permitted speed.
func MaxTotalTokens ¶
func MaxTotalTokens(n int) BudgetOption
MaxTotalTokens caps estimated tokens for the lifetime of this Budget. Zero disables the check.
type BudgetUsage ¶
type BudgetUsage struct {
RequestsLastMinute int
TokensLastSecond int
TotalRequests int
TotalTokens int
}
BudgetUsage is a snapshot of consumption.
type CallInfo ¶
type CallInfo struct {
// RequestID is the client-generated correlation id, when one is
// configured. See WithRequestID.
RequestID string
// Duration is how long the whole call took, retries included.
Duration time.Duration
// Questions is how many questions were asked.
Questions int
// Model is the versioned model that answered, empty on failure.
Model string
// InputTokens and OutputTokens come from the response, zero on failure.
InputTokens int
OutputTokens int
// Err is the failure, nil on success.
Err error
}
CallInfo describes a completed call.
type Choice ¶
type Choice struct {
// Instructions is what the model should decide. Optional per the schema.
Instructions EntryType
// Criteria maps each option to a description of when it applies.
// Required, and must be non-empty.
//
// A nil value means "interpret this option by its name alone" and is sent
// as JSON null.
Criteria Options
}
Choice selects exactly one option from a set you define.
typesafe.Choice{
Instructions: "Which team should handle this?",
Criteria: typesafe.Options{
"billing": "Payments, invoicing, refunds",
"technical": "Bugs, outages, integrations",
"sales": nil, // interpreted by its name alone
},
}
The answer names the winning option and gives a probability for every one, so the result is always a member of the set you supplied. Include a catch-all option when your set may not cover every input — without one, the model must pick from what it was given.
func (Choice) MarshalJSON ¶
MarshalJSON emits the wire form, adding the required type discriminator.
Criteria is always emitted, even when empty, because the API requires the field to be present; Validate rejects an empty one before it is sent.
type ChoiceAnswer ¶
type ChoiceAnswer struct {
// Choice is the highest-probability option.
Choice string `json:"choice"`
// Probabilities gives every option's probability. They sum to 1.
Probabilities map[string]float64 `json:"probabilities"`
// Confidence in [0,1], derived from the shape of the distribution. A flat
// distribution means no option clearly won, which usually means the
// options overlap or the state does not contain enough to decide.
Confidence float64 `json:"confidence"`
}
ChoiceAnswer is the answer to a Choice.
func (ChoiceAnswer) Margin ¶
func (a ChoiceAnswer) Margin() float64
Margin is the gap between the top two options.
It answers a different question than Confidence: a wide margin means the winner beat its nearest rival clearly, even if probability is spread across the remaining options. Returns the winner's probability when there is only one option.
func (ChoiceAnswer) ProbabilityOf ¶
func (a ChoiceAnswer) ProbabilityOf(option string) (float64, bool)
ProbabilityOf returns the probability assigned to an option, and whether the option was part of the answer at all.
func (ChoiceAnswer) Ranked ¶
func (a ChoiceAnswer) Ranked() []RankedOption
Ranked returns every option ordered by descending probability.
Ties break by option name so the order is deterministic across runs and machines — Go map iteration is not, and a non-deterministic ranking would make snapshot tests and audit logs unstable.
func (ChoiceAnswer) Type ¶
func (ChoiceAnswer) Type() string
type ChoiceAnswerOf ¶
type ChoiceAnswerOf[T OptionKey] struct { // Choice is the highest-probability option, as a T. Choice T // Probabilities gives every option's probability, keyed by T. Probabilities map[T]float64 // Confidence in [0,1], derived from the shape of the distribution. Confidence float64 }
ChoiceAnswerOf is a ChoiceAnswer whose option keys are values of T.
func TypedChoiceAnswer ¶
func TypedChoiceAnswer[T OptionKey](r *SystemOneResponse, id string) (ChoiceAnswerOf[T], error)
TypedChoiceAnswer decodes the answer under id with T as the option type.
ans, err := typesafe.TypedChoiceAnswer[Topic](resp, "department")
This is the free-standing form, for a response you did not build the question for. It converts whatever came back into T without checking it against a declared set — nothing here knows what that set was. When you have the question, prefer its Answer method, which checks; otherwise pass your enum's values to Exhaustive.
func (ChoiceAnswerOf[T]) Margin ¶
func (a ChoiceAnswerOf[T]) Margin() float64
Margin is the gap between the top two options.
func (ChoiceAnswerOf[T]) ProbabilityOf ¶
func (a ChoiceAnswerOf[T]) ProbabilityOf(option T) (float64, bool)
ProbabilityOf returns an option's probability, and whether it was part of the answer at all.
func (ChoiceAnswerOf[T]) Ranked ¶
func (a ChoiceAnswerOf[T]) Ranked() []RankedOptionOf[T]
Ranked returns every option ordered by descending probability, ties broken by name so the order is stable across runs.
func (ChoiceAnswerOf[T]) Untyped ¶
func (a ChoiceAnswerOf[T]) Untyped() ChoiceAnswer
Untyped returns the plain ChoiceAnswer, for the accessors that do not need the type parameter and for code that has not been converted yet.
type ChoiceBuilder ¶
type ChoiceBuilder struct {
// contains filtered or unexported fields
}
ChoiceBuilder builds a Choice.
func NewChoice ¶
func NewChoice(instructions EntryType) ChoiceBuilder
NewChoice starts a Choice.
typesafe.NewChoice("Which team should handle this?").
Option("billing", "Payments, invoicing, refunds").
Option("technical", "Bugs, outages, integrations").
Option("other", nil)
func (ChoiceBuilder) MarshalJSON ¶
func (b ChoiceBuilder) MarshalJSON() ([]byte, error)
MarshalJSON delegates to the built question.
func (ChoiceBuilder) Option ¶
func (b ChoiceBuilder) Option(name string, description EntryType) ChoiceBuilder
Option adds an option and its description.
Pass nil for the description when the option's name says everything; it is sent as JSON null, which the API reads as "interpret this by its name alone".
func (ChoiceBuilder) Options ¶
func (b ChoiceBuilder) Options(names ...string) ChoiceBuilder
Options adds several options that need no description, for the common case where the names are self-explanatory.
typesafe.NewChoice("What is the tone?").Options("calm", "angry", "excited")
type CircuitBreaker ¶
type CircuitBreaker struct {
// Threshold is the number of consecutive retryable failures that open the
// circuit. Zero uses DefaultCircuitThreshold.
Threshold int
// OpenFor is how long the circuit stays open before allowing a probe.
// Zero uses DefaultCircuitOpenFor.
OpenFor time.Duration
// HalfOpenProbes is how many requests may pass while half-open. Zero
// means one.
HalfOpenProbes int
// OnStateChange, if set, is called whenever the state changes. It runs on
// the calling goroutine, so keep it quick.
OnStateChange func(from, to CircuitState)
// contains filtered or unexported fields
}
CircuitBreaker stops a client from hammering a service that is already failing.
Retries help with a blip and hurt during an outage: every caller politely backing off and trying again still multiplies load on something that cannot serve it. The breaker converts sustained failure into immediate, cheap rejection, which both protects the service and lets a caller fail over quickly instead of waiting out a full retry budget per request.
It is off by default. Enable it with WithCircuitBreaker, and share one breaker per upstream — a breaker per goroutine observes nothing useful.
Only failures the retry policy considers retryable count toward opening it. A 422 means the request was wrong, not that the service is unwell, and tripping on those would open the circuit for one caller's bad input.
func NewCircuitBreaker ¶
func NewCircuitBreaker() *CircuitBreaker
NewCircuitBreaker returns a breaker with the default settings.
func (*CircuitBreaker) Reset ¶
func (b *CircuitBreaker) Reset()
Reset returns the breaker to closed and clears its counters.
func (*CircuitBreaker) State ¶
func (b *CircuitBreaker) State() CircuitState
State reports the current state, moving an expired open circuit to half-open as a side effect.
type CircuitState ¶
type CircuitState int
CircuitState is a breaker's current state.
const ( // CircuitClosed passes every request through. The normal state. CircuitClosed CircuitState = iota // CircuitOpen rejects immediately, without attempting the request. CircuitOpen // CircuitHalfOpen lets a limited number of probes through to discover // whether the service has recovered. CircuitHalfOpen )
Breaker states.
func (CircuitState) String ¶
func (s CircuitState) String() string
type Client ¶
type Client struct {
// contains filtered or unexported fields
}
Client calls the TypeSafe System One API. It is safe for concurrent use and is meant to be created once and shared.
func NewClient ¶
NewClient builds a client.
Configuration resolves in this order, each falling back to the next:
API key WithAPIKey -> TYPESAFE_API_KEY -> error Base URL WithBaseURL -> TYPESAFE_BASE_URL -> https://api.typesafe.ai Model WithDefaultModel -> TYPESAFE_DEFAULT_MODEL -> jev-latest Timeout WithTimeout -> 10s
This mirrors the official Python and JavaScript SDKs, so a process already configured for either works here unchanged.
client, err := typesafe.NewClient()
if err != nil {
return err
}
func (*Client) DefaultModel ¶
DefaultModel reports the model used when a request leaves Model empty.
func (*Client) Models ¶
Models lists the model names and aliases this account may send in SystemOneRequest.Model.
Versioned ids such as jev-1.13.0 are accepted by the API whether or not they appear here, so this is a discovery aid rather than an allow-list.
func (*Client) SystemOne ¶
func (c *Client) SystemOne(ctx context.Context, req *SystemOneRequest) (*SystemOneResponse, error)
SystemOne evaluates one state against a map of named questions and returns one answer per question.
Every question sees the same state and is evaluated independently and in parallel, so asking several costs little more than asking one. Pack every question about a given state into a single call.
Returns a typed error for every documented failure: AuthenticationError, UnprocessableEntityError, RateLimitError, OverloadedError, and so on. Use errors.As to reach the detail, or errors.Is against the sentinels.
func (*Client) SystemOneAll ¶
func (c *Client) SystemOneAll(ctx context.Context, requests ...*SystemOneRequest) []Result
SystemOneAll runs several requests concurrently and returns their results in input order.
results := client.SystemOneAll(ctx, reqA, reqB, reqC)
for i, r := range results {
if r.Err != nil { ... }
}
Results are positional: results[i] belongs to requests[i], regardless of which finished first. A failure in one does not affect the others — each carries its own error — because a batch where one bad input discards the other ninety-nine results is not useful.
This is the unbounded form, appropriate for a handful of requests. For thousands, with a worker pool and rate-limit awareness, use the batch API.
func (*Client) SystemOneAsync ¶
func (c *Client) SystemOneAsync(ctx context.Context, req *SystemOneRequest) <-chan Result
SystemOneAsync starts a call and returns a channel that will carry its one result.
a := client.SystemOneAsync(ctx, reqA) b := client.SystemOneAsync(ctx, reqB) ra, rb := <-a, <-b
For fanning out over several states without writing the goroutine and channel plumbing each time. Every question about *one* state belongs in a single request — they are evaluated in parallel server-side and cost only the extra tokens — so this is for fanning out over different states, not different questions.
It cannot leak ¶
The channel is buffered with room for the single result, so the goroutine always completes its send and exits even if nobody ever reads. That matters more than it sounds: the obvious unbuffered implementation leaks a goroutine for every abandoned call, and abandoning calls is exactly what happens when a caller takes the first of several results and returns.
The channel is closed after the send, so a range over it terminates and a second receive yields the zero Result rather than blocking.
Canceling ctx does not close the channel early — the in-flight request is canceled, and the resulting error arrives on the channel as a normal result. A caller waiting on the channel is therefore always woken exactly once, whether the call succeeded, failed, or was canceled.
func (*Client) SystemOneBatch ¶
func (c *Client) SystemOneBatch(ctx context.Context, states []any, qs Questions, opts ...BatchOption) BatchResult
SystemOneBatch evaluates the same questions against many states.
result := client.SystemOneBatch(ctx, states, typesafe.Questions{
"is_spam": typesafe.Noul{Instructions: "Is this spam?"},
"sentiment": typesafe.Choice{ ... },
}, typesafe.WithConcurrency(16))
if err := result.Err(); err != nil {
log.Printf("%v", err) // a summary, not one arbitrary failure
}
for _, item := range result.Items { // input order
...
}
It batches states, never questions ¶
Jev ingests the state once and evaluates every question against it in parallel, so a second question costs only its own tokens while a second state costs a whole request. Pack every question about one state into a single call and use this to fan out across states — batching questions would be strictly more expensive and slower.
Per-item error isolation ¶
One failure never aborts the batch. Each item carries its own error, and the rest continue at full speed. A batch where one bad input discards ninety-nine good results is not useful, and retrying the whole thing to recover them is worse.
Canceling ctx stops the batch: items already running finish or fail, and items not yet started are returned with the context error. Every result slot is filled either way, so Items always has one entry per input.
func (*Client) SystemOneBatchSeq ¶
func (c *Client) SystemOneBatchSeq(ctx context.Context, states []any, qs Questions, opts ...BatchOption) iter.Seq2[int, ItemResult]
SystemOneBatchSeq is SystemOneBatch as a stream, yielding results as they complete rather than when the whole batch finishes.
for i, item := range client.SystemOneBatchSeq(ctx, states, qs) {
if item.Err != nil { ... }
}
Use it when the batch is large enough that holding every response in memory matters, or when downstream work can start on the first result. Results arrive in **completion** order; ItemResult.Index gives the input position.
Breaking out of the range stops the batch: the remaining work is canceled and every goroutine exits before the loop returns.
type ConnectionError ¶
ConnectionError is a transport failure: the request never produced a response. Safe to retry.
func (*ConnectionError) Error ¶
func (e *ConnectionError) Error() string
func (*ConnectionError) Is ¶
func (e *ConnectionError) Is(target error) bool
func (*ConnectionError) Unwrap ¶
func (e *ConnectionError) Unwrap() error
type CostEstimate ¶
type CostEstimate struct {
// Tokens is the underlying token estimate.
Tokens TokenEstimate
// Questions is how many questions the request asks.
Questions int
// InputCostUSD is the estimated charge. Output tokens are free.
//
// Derived from a conservative token estimate, so it over-reports. Use it
// to size a workload, never to reconcile a bill.
InputCostUSD float64
// RatePerMillionUSD is the price used.
RatePerMillionUSD float64
}
CostEstimate is a pre-flight guess at what a request will cost in money.
func (CostEstimate) String ¶
func (c CostEstimate) String() string
String renders the cost estimate, always flagged as approximate.
type EntryType ¶
type EntryType = any
EntryType is the union the API uses for every human-readable field:
string | object | array | null
It appears as a question's Instructions, as a Choice option's description, as a Score level's description, and as a Noul's true/false criteria.
Structured values are a supported feature, not a quirk. A schema, a taxonomy, or a database row is already JSON; passing it directly is clearer than flattening it into a sentence, and the model is trained to read structure.
Instructions: "Does this convey urgency?"
Instructions: map[string]any{
"field": map[string]any{"name": "amount_due", "unit": "USD"},
"question": "How large is the `field` value in `source_text`?",
}
A nil EntryType is omitted from the request. The API treats an absent field and an explicit null identically.
type ErrorDetail ¶
ErrorDetail is the object form of the "detail" field, returned for errors that are not per-field schema violations.
{"detail": {"error_type": "api_usage_error", "message": "Unknown model: x"}}
Observed error_type values include "authentication_error" and "api_usage_error". The set is not published, so treat it as open.
func (ErrorDetail) String ¶
func (d ErrorDetail) String() string
type Handler ¶
type Handler func(ctx context.Context, req *SystemOneRequest) (*SystemOneResponse, error)
Handler performs one logical SystemOne call.
Interceptors wrap a Handler to observe or alter a call. The signature deliberately matches Client.SystemOne, so the client itself is a Handler and an interceptor chain composes onto it without adaptation.
type Hooks ¶
type Hooks struct {
// OnRequest runs before a request is sent.
OnRequest func(ctx context.Context, req *SystemOneRequest)
// OnResponse runs after a successful call.
OnResponse func(ctx context.Context, info CallInfo)
// OnError runs after a failed call.
OnError func(ctx context.Context, info CallInfo)
}
Hooks are callbacks at fixed points in a call, for code that wants to observe without composing an interceptor.
Every hook is optional. They run synchronously on the calling goroutine, so keep them quick; a slow hook is latency the caller pays. A panic in one is recovered and returned as a *PanicError.
typesafe.WithHooks(typesafe.Hooks{
OnResponse: func(ctx context.Context, i typesafe.CallInfo) {
log.Printf("%s took %s, %d tokens", i.RequestID, i.Duration, i.InputTokens)
},
})
type Interceptor ¶
Interceptor wraps a Handler, in the style of a gRPC unary interceptor.
func timing(next typesafe.Handler) typesafe.Handler {
return func(ctx context.Context, req *typesafe.SystemOneRequest) (*typesafe.SystemOneResponse, error) {
start := time.Now()
resp, err := next(ctx, req)
metrics.Observe(time.Since(start), err)
return resp, err
}
}
What an interceptor sees ¶
One *logical* call. Retries, backoff and the circuit breaker all happen beneath the innermost handler, so an interceptor observes a single attempt from the caller's point of view no matter how many HTTP requests it took. That is almost always what instrumentation wants: a latency histogram should record what the caller waited for, not one bar per retry. When you do want per-attempt visibility, use WithRetryObserver, which is the layer below.
func WithLogging ¶
func WithLogging(l *slog.Logger) Interceptor
WithLogging returns an interceptor that logs each call through slog.
A ready-made alternative to WithLogger for callers who want request-level logging composed with their other interceptors rather than emitted from inside the client.
It logs the request id, question count, duration, model and token usage. It does **not** log the state, the questions, or anything derived from them: state is the caller's data and routinely contains personal information, and a logging helper that leaks it by default is worse than none.
type InternalServerError ¶
type InternalServerError struct{ *APIError }
InternalServerError is a 5xx other than 529.
func (*InternalServerError) Is ¶
func (e *InternalServerError) Is(target error) bool
func (*InternalServerError) Unwrap ¶
func (e *InternalServerError) Unwrap() error
type ItemResult ¶
type ItemResult struct {
// Index is the position in the input slice. Results are returned in input
// order, so this is redundant there — it matters in the streaming view,
// where results arrive as they finish.
Index int
// State is the input this result belongs to, carried through so a caller
// handling a failure does not have to index back into the original slice.
State any
// Response is the answer set, nil on failure.
Response *SystemOneResponse
// Err is the failure, nil on success.
Err error
// Duration is how long this item took, retries included.
Duration time.Duration
}
ItemResult is the outcome for one state in a batch.
type LevelKey ¶
LevelKey constrains a Score's level type: any named integer type.
A Score level's position is its score, so the enum's values must be its indices — the usual iota declaration, lowest level first. TypedScore checks that and reports a mismatch through Validate rather than silently mapping answers to the wrong label.
type ModelCard ¶
type ModelCard struct {
// Name is the id or alias accepted by SystemOneRequest.Model.
Name string `json:"name"`
// Description says what the model is for.
Description string `json:"description"`
// ReleaseDate is when the model or alias was released.
ReleaseDate string `json:"release_date"`
}
ModelCard describes one model or alias the account may use.
type NotFoundError ¶
type NotFoundError struct{ *APIError }
NotFoundError is a 404.
func (*NotFoundError) Is ¶
func (e *NotFoundError) Is(target error) bool
func (*NotFoundError) Unwrap ¶
func (e *NotFoundError) Unwrap() error
type Noul ¶
type Noul struct {
// Instructions is the yes/no question or statement to evaluate.
//
// Phrase it positively. The model reads instructions literally, and a
// double negative costs accuracy — see the "Indirection" entry in
// TypeSafe's published model-jaggedness notes.
Instructions EntryType
// Criteria optionally describes what a yes and a no mean. Nil is omitted.
//
// Keep it aligned with Instructions. A Noul whose true describes a "no"
// performs measurably worse than one phrased consistently.
//
// A nil True or False is omitted from the request rather than sent as an
// explicit null. The schema marks both optional and nullable, so the two
// encodings are equivalent to the server and this SDK emits the shorter
// one. Contrast Choice, where a nil option description must be sent as
// null — there the key itself carries the option name, so omitting it
// would remove the option.
Criteria *NoulCriteria
}
Noul is a yes/no question, answered with the probability that the answer is yes.
typesafe.Noul{Instructions: "Does this convey urgency?"}
The answer is a single number in [0,1]. Near 1 is a strong yes, near 0 a strong no, and near 0.5 is the model saying it does not know — which is why a Noul answer carries no separate confidence field. The probability is the uncertainty.
Both fields are optional per the API schema, though a Noul with neither says nothing about what to judge; Validate warns about that.
func (Noul) MarshalJSON ¶
MarshalJSON emits the wire form, adding the required type discriminator.
type NoulAnswer ¶
type NoulAnswer struct {
// Noul is the probability that the answer is yes, in [0,1].
Noul float64 `json:"noul"`
}
NoulAnswer is the answer to a Noul.
It has no Confidence field, and that is deliberate on the API's part rather than an omission here: the probability already expresses the uncertainty. Code that reaches for a confidence on a Noul is reading a zero that means nothing.
func (NoulAnswer) Bool ¶
func (a NoulAnswer) Bool(threshold float64) bool
Bool collapses the probability to a decision at the given threshold.
The threshold is your policy, not the model's: there is no universally correct value, and the right one depends on the cost of each kind of mistake in your domain. Name the constant you pass rather than inlining a literal.
if ans.Bool(spamThreshold) { ... }
func (NoulAnswer) Type ¶
func (NoulAnswer) Type() string
func (NoulAnswer) Uncertain ¶
func (a NoulAnswer) Uncertain(delta float64) bool
Uncertain reports whether the probability sits within delta of 0.5, the region where the model is expressing genuine ignorance rather than a weak opinion.
if ans.Uncertain(0.1) { routeToHuman() }
type NoulBuilder ¶
type NoulBuilder struct {
// contains filtered or unexported fields
}
NoulBuilder builds a Noul.
The methods return a copy rather than mutating, so a partially built question can be shared as a base without one caller's additions leaking into another's.
func NewNoul ¶
func NewNoul(instructions EntryType) NoulBuilder
NewNoul starts a Noul.
typesafe.NewNoul("Does this convey urgency?").
Means("Explicitly time-sensitive", "No urgency expressed")
func (NoulBuilder) MarshalJSON ¶
func (b NoulBuilder) MarshalJSON() ([]byte, error)
MarshalJSON delegates to the built question, so a builder used in place of a question produces byte-identical output.
func (NoulBuilder) Means ¶
func (b NoulBuilder) Means(yes, no EntryType) NoulBuilder
Means describes what a yes and a no mean.
Keep the two aligned with the instructions. TypeSafe documents that a Noul whose true describes a "no" performs measurably worse, and nothing in the resulting probability reveals the mistake.
type NoulCriteria ¶
type NoulCriteria struct {
// True is what a value near 1 means.
True EntryType `json:"true,omitempty"`
// False is what a value near 0 means.
False EntryType `json:"false,omitempty"`
}
NoulCriteria describes what each end of the 0-to-1 range means.
type Option ¶
type Option func(*config) error
Option configures a Client. Options are applied in order, so a later one overrides an earlier one.
func WithAPIKey ¶
WithAPIKey sets the key explicitly, taking precedence over TYPESAFE_API_KEY.
Prefer the environment variable in deployed code: a key in a source file is a key in your git history.
func WithBaseURL ¶
WithBaseURL overrides the API root, for a proxy, a gateway, or a test server. Takes precedence over TYPESAFE_BASE_URL.
func WithBudget ¶
WithBudget attaches a Budget to a client.
Share one Budget across every client in a process that talks to the same account: a per-client budget enforces nothing, since the account is what has the quota.
func WithCircuitBreaker ¶
func WithCircuitBreaker(b *CircuitBreaker) Option
WithCircuitBreaker attaches a breaker, which is off by default.
Share one breaker across every client that talks to the same upstream. A breaker observes consecutive failures, and one that sees only a fraction of the traffic will not trip when it should.
breaker := typesafe.NewCircuitBreaker()
breaker.OnStateChange = func(from, to typesafe.CircuitState) {
log.Warn("typesafe circuit", "from", from, "to", to)
}
client, err := typesafe.NewClient(typesafe.WithCircuitBreaker(breaker))
When open, SystemOne returns ErrCircuitOpen without sending anything.
func WithContextLimitCheck ¶
WithContextLimitCheck controls whether the client refuses requests whose estimated size exceeds a documented ceiling.
On by default. The estimate is conservative and over-reports by roughly 25%, so it will occasionally refuse a request the server would have accepted. That trade is deliberate — the alternative is a wasted round trip and a 400 whose message does not say which of the two limits was hit — but turn it off if you would rather let the server decide.
func WithDefaultModel ¶
WithDefaultModel sets the model used when a request leaves Model empty. Takes precedence over TYPESAFE_DEFAULT_MODEL.
func WithHTTPClient ¶
WithHTTPClient supplies your own *http.Client — for a custom transport, connection pool, proxy, or instrumentation.
The client's Timeout is respected as given. If it is zero, WithTimeout (or the 10s default) is applied to a shallow copy, so that supplying a client never silently removes the timeout.
func WithHeader ¶
WithHeader sets a header sent on every request.
Authorization cannot be set this way — it is derived from the API key, and allowing an override here would make credential handling ambiguous.
func WithHooks ¶
WithHooks installs callbacks at fixed points in a call.
Hooks are implemented as an interceptor, so they nest with any other interceptors in the order everything was declared. Use Hooks for the common case of observing a call; use WithInterceptor when you need to alter one.
func WithInterceptor ¶
func WithInterceptor(interceptors ...Interceptor) Option
WithInterceptor adds interceptors to a client.
They run outermost-first, in the order given, so the first interceptor listed is the first to see a request and the last to see its response — the same nesting as gRPC and as net/http middleware.
client, err := typesafe.NewClient(
typesafe.WithInterceptor(tracing, metrics, logging),
)
// tracing -> metrics -> logging -> the API
Why this is an option and not a Use method ¶
The roadmap sketched `client.Use(...)`. A method mutating a live client is a data race waiting to happen: Client is documented as safe for concurrent use and is meant to be built once and shared, so a Use call from one goroutine while another is mid-request would be exactly the bug this SDK should not ship. Composing at construction makes the chain immutable, which costs nothing — the set of interceptors is a deployment decision, not a per-call one.
func WithLogger ¶
WithLogger attaches a structured logger.
The SDK never logs the API key, the Authorization header, or the request state at any level: state is the caller's data and frequently contains personal information. Request ids, statuses, and timings are logged.
func WithMaxRetries ¶
WithMaxRetries adjusts only the retry count, leaving the rest of the default policy alone. Zero disables retrying.
func WithRequestID ¶
WithRequestID generates a correlation id for every call.
The id is placed on the context, where hooks and interceptors can read it with RequestIDFrom, and is logged alongside every message about the call.
It is **not** sent to the API by default. TypeSafe documents no request header for a client-supplied id, and inventing one risks colliding with a meaning the server later assigns. Pass a header name to send it anyway:
typesafe.WithRequestID(uuid.NewString) // local only typesafe.WithRequestID(uuid.NewString, "x-correlation-id") // also sent
func WithRetryObserver ¶
func WithRetryObserver(fn func(context.Context, AttemptInfo)) Option
WithRetryObserver registers a callback invoked after each failed attempt that will be retried, before the backoff begins.
Useful for metrics and for surfacing retry behavior in traces. It is called synchronously on the calling goroutine, so keep it quick and do not block.
typesafe.WithRetryObserver(func(ctx context.Context, a typesafe.AttemptInfo) {
metrics.Retries.WithLabelValues(strconv.Itoa(a.Status)).Inc()
})
The context is the one the call was made with. Tracing integrations need it: without it an attempt cannot be attached to the span of the call it belongs to, and a per-attempt span would be an orphan.
func WithRetryPolicy ¶
func WithRetryPolicy(p RetryPolicy) Option
WithRetryPolicy replaces the whole retry policy.
The default matches the official Python and JavaScript SDKs: two retries, 500ms initial backoff doubling to a 5s ceiling, ±25% jitter, retrying 408, 429 and 5xx, honoring retry-after, within a 30s overall budget.
typesafe.WithRetryPolicy(typesafe.NoRetry()) p := typesafe.DefaultRetryPolicy() p.MaxRetries = 5 typesafe.WithRetryPolicy(p)
func WithTimeout ¶
WithTimeout bounds each HTTP operation. The default is 10s, matching the official SDKs.
This is per operation, not per call: a single SystemOne may span several operations when retrying, bounded separately by RetryPolicy.Timeout. Whichever fires first wins.
func WithUserAgentSuffix ¶
WithUserAgentSuffix appends an identifier to the SDK's User-Agent, for applications that want their own name in TypeSafe's logs. The SDK's own identity is always sent first and cannot be replaced.
type OptionKey ¶
type OptionKey interface {
~string
}
OptionKey constrains a Choice's option type: any named string type.
type Topic string
type Options ¶
Options maps an option name to its description. A nil value is sent as JSON null, meaning the option is interpreted by its name alone.
type OverloadedError ¶
OverloadedError is a 529: TypeSafe is temporarily overloaded. Distinct from InternalServerError because it is explicitly transient and expected under load, not a defect.
func (*OverloadedError) Is ¶
func (e *OverloadedError) Is(target error) bool
func (*OverloadedError) Unwrap ¶
func (e *OverloadedError) Unwrap() error
type PanicError ¶
type PanicError struct {
// Value is whatever was passed to panic.
Value any
// Stack is the stack trace captured at the moment of recovery.
Stack []byte
}
PanicError is a panic recovered from an interceptor or a hook.
A panic in instrumentation should not take down a request path. Recovering it and returning it as an error keeps the caller's error handling in charge, and the captured stack points at the interceptor rather than at the recovery site — which is the difference between a two-minute fix and an afternoon.
func (*PanicError) Error ¶
func (e *PanicError) Error() string
func (*PanicError) Unwrap ¶
func (e *PanicError) Unwrap() error
Unwrap returns the panic value when it was itself an error.
type PermissionDeniedError ¶
type PermissionDeniedError struct{ *APIError }
PermissionDeniedError is a 403.
func (*PermissionDeniedError) Is ¶
func (e *PermissionDeniedError) Is(target error) bool
func (*PermissionDeniedError) Unwrap ¶
func (e *PermissionDeniedError) Unwrap() error
type Question ¶
type Question interface {
// contains filtered or unexported methods
}
Question is one of the three System One primitives: Noul, Choice, or Score.
The interface is sealed — only this package can implement it — so the set of question types stays closed and a type switch over them is exhaustive. RawQuestion is the escape hatch for anything this SDK does not yet model.
type Questions ¶
Questions is a named set of questions, for readability at a call site where the map type would otherwise dominate the line.
type RankedOption ¶
RankedOption is one option and its probability.
type RankedOptionOf ¶
RankedOptionOf is one typed option and its probability.
type RateLimitError ¶
type RateLimitError struct {
*APIError
// RetryAfter is the delay the server asked for, or 0 if it did not send a
// retry-after header. Zero does not mean "retry immediately" — it means
// the server expressed no preference, so use your own backoff.
RetryAfter time.Duration
}
RateLimitError is a 429.
func (*RateLimitError) Is ¶
func (e *RateLimitError) Is(target error) bool
func (*RateLimitError) Unwrap ¶
func (e *RateLimitError) Unwrap() error
type RawQuestion ¶
RawQuestion is an arbitrary JSON-encodable question body, sent exactly as given.
It exists so a caller is never blocked by this SDK lagging the API: a question shape added after this release can be sent today. Nothing is validated and nothing is defaulted — including the required "type" field, which you must set yourself.
typesafe.RawQuestion{"type": "noul", "instructions": "Is this spam?"}
Prefer the typed primitives. They validate before the request leaves the process, and their answers decode into typed accessors.
type RefWarning ¶
type RefWarning struct {
// QuestionID is the question containing the reference.
QuestionID string
// Path is the reference, without its backticks.
Path string
// Reason says which segment failed and why.
Reason string
}
RefWarning is a backticked state reference in a question that does not resolve against the state being sent.
func (RefWarning) String ¶
func (w RefWarning) String() string
type ResponseValidationError ¶
ResponseValidationError is a 2xx whose body does not match the contract. Getting one means either the API changed or this SDK's reading of it is wrong; both are worth reporting.
func (*ResponseValidationError) Error ¶
func (e *ResponseValidationError) Error() string
func (*ResponseValidationError) Is ¶
func (e *ResponseValidationError) Is(target error) bool
func (*ResponseValidationError) Unwrap ¶
func (e *ResponseValidationError) Unwrap() error
type Result ¶
type Result struct {
// Response is the answer set, nil on failure.
Response *SystemOneResponse
// Err is the failure, nil on success.
Err error
}
Result is the outcome of an asynchronous call: exactly one of Response and Err is non-nil.
type RetryPolicy ¶
type RetryPolicy struct {
// MaxRetries is the number of retries *after* the first attempt. 0
// disables retrying entirely.
MaxRetries int
// BackoffInitial is the first delay. Each subsequent delay doubles.
BackoffInitial time.Duration
// BackoffMax caps any single delay.
BackoffMax time.Duration
// BackoffJitter randomizes each delay by this fraction, in [0,1].
// 0.25 means the delay lands uniformly within ±25% of its nominal value.
BackoffJitter float64
// HTTPStatuses are the response codes worth retrying. The default is
// {408, 429, 500–599}, which includes 529.
//
// 422 is deliberately absent: a request that failed validation will fail
// it again, identically, and retrying only delays the error.
HTTPStatuses map[int]bool
// RespectRetryAfter honors a retry-after header in place of the computed
// backoff, subject to MaxRetryAfter and the remaining budget.
RespectRetryAfter bool
// MaxRetryAfter caps a server-supplied delay. A retry-after longer than
// this is treated as "too long to wait" and the error is returned
// immediately, rather than blocking the caller for an unbounded time on
// the server's say-so.
MaxRetryAfter time.Duration
// RetryOnConnection retries when the request produced no response.
RetryOnConnection bool
// RetryOnTimeout retries when an attempt exceeded the per-operation
// timeout. A caller's own canceled context is never retried.
RetryOnTimeout bool
// Predicate, when set, overrides every rule above. Return true to retry.
//
// It sees the error from the attempt, including the typed API errors, so
// a caller can express policies this struct does not cover.
Predicate func(error) bool
// Timeout bounds the whole operation across every attempt and every
// backoff. Zero means no overall budget, and only MaxRetries limits the
// work.
Timeout time.Duration
// contains filtered or unexported fields
}
RetryPolicy configures retry behavior.
The zero value is not usable — use DefaultRetryPolicy and adjust, or WithMaxRetries for the common case.
func DefaultRetryPolicy ¶
func DefaultRetryPolicy() RetryPolicy
DefaultRetryPolicy returns the policy the client uses when none is set.
type Score ¶
type Score struct {
// Instructions is what the model should rate. Optional per the schema.
Instructions EntryType
// Criteria is the ordered level descriptions, lowest first. Required.
// Must hold between MinScoreLevels and MaxScoreLevels entries.
Criteria Levels
}
Score rates the state against ordered levels you define.
typesafe.Score{
Instructions: "How frustrated is the customer?",
Criteria: typesafe.Levels{"Calm", "Frustrated", "Very angry"},
}
Order is meaning: a level's position is its score, starting at zero. The answer is a probability-weighted value that may land between levels.
Do not read a magnitude out of a score ¶
TypeSafe documents that Jev's score levels are weakly calibrated numerically. Thresholding is sound — "is this at least Frustrated?" — but interpolating a real-world quantity between two levels is not. If you need a number, extract it as a Choice over enumerated parts and compute in code.
func (Score) MarshalJSON ¶
MarshalJSON emits the wire form, adding the required type discriminator.
type ScoreAnswer ¶
type ScoreAnswer struct {
// Score is the probability-weighted position across the levels. It may
// fall between two of them. Treat it as ordinal, not as a magnitude.
Score float64 `json:"score"`
// Legend maps each level index to its description. Values carry whatever
// shape you supplied as criteria — a structured rubric returns structured
// legend entries, so this is EntryType and not string.
Legend map[string]EntryType `json:"legend"`
// Probabilities maps each level index to its probability. They sum to 1.
Probabilities map[string]float64 `json:"probabilities"`
// Confidence in [0,1], derived from the distribution's shape.
Confidence float64 `json:"confidence"`
}
ScoreAnswer is the answer to a Score.
Legend and Probabilities are keyed by the level index as a string — "0", "1", and so on. Use the ordered accessors rather than ranging over the maps: Go map order is random, and these keys are integers wearing string clothes.
func (ScoreAnswer) AtOrAbove ¶
func (a ScoreAnswer) AtOrAbove(level int) float64
AtOrAbove returns the total probability of landing at level or higher.
This is the safe way to threshold a Score: it works on the distribution the model actually produced, rather than on an interpolated magnitude the levels do not support.
if ans.AtOrAbove(2) > 0.8 { escalate() }
func (ScoreAnswer) AtOrBelow ¶
func (a ScoreAnswer) AtOrBelow(level int) float64
AtOrBelow returns the total probability of landing at level or lower.
func (ScoreAnswer) LevelProbabilities ¶
func (a ScoreAnswer) LevelProbabilities() []float64
LevelProbabilities returns the distribution in index order.
func (ScoreAnswer) Levels ¶
func (a ScoreAnswer) Levels() []EntryType
Levels returns the legend in index order.
Keys are parsed as integers rather than sorted as strings. With the current ten-level ceiling the two orderings coincide, so this is insurance rather than a fix — but it is the ordering the data actually means, and it stays correct if the ceiling is ever raised.
func (ScoreAnswer) MostLikely ¶
func (a ScoreAnswer) MostLikely() (int, float64)
MostLikely returns the single highest-probability level and its probability.
This is not always Nearest: a bimodal distribution — heavy at both ends, light in the middle — has a weighted mean that sits in a level the model considers unlikely. When the two disagree, the distribution is telling you the question has more than one reading.
func (ScoreAnswer) Nearest ¶
func (a ScoreAnswer) Nearest() (int, EntryType)
Nearest returns the level index closest to Score, with its description.
Rounds half away from zero. Returns -1 and nil for an empty legend.
func (ScoreAnswer) NumLevels ¶
func (a ScoreAnswer) NumLevels() int
NumLevels is the size of the rubric this answer was scored against.
func (ScoreAnswer) Type ¶
func (ScoreAnswer) Type() string
type ScoreAnswerOf ¶
type ScoreAnswerOf[L LevelKey] struct { // Score is the probability-weighted position across the levels. It is // ordinal, and may fall between two levels — which is why it stays a // float64 rather than becoming an L. Score float64 // Level is the nearest declared level to Score. Level L // Legend maps each level to its description. Legend map[L]EntryType // Probabilities maps each level to its probability. They sum to 1. Probabilities map[L]float64 // Confidence in [0,1], derived from the distribution's shape. Confidence float64 }
ScoreAnswerOf is a ScoreAnswer whose level indices are values of L.
func TypedScoreAnswer ¶
func TypedScoreAnswer[L LevelKey](r *SystemOneResponse, id string) (ScoreAnswerOf[L], error)
TypedScoreAnswer decodes the answer under id with L as the level type.
Level indices come back as strings on the wire — "0", "1" — and are parsed as integers, not sorted as strings. An index that does not parse is an error rather than a silently dropped level.
func (ScoreAnswerOf[L]) AtOrAbove ¶
func (a ScoreAnswerOf[L]) AtOrAbove(level L) float64
AtOrAbove returns the total probability of landing at level or higher.
This is the safe way to threshold a Score: it works on the distribution the model produced rather than on an interpolated magnitude the levels do not support.
if ans.AtOrAbove(Angry) > 0.8 { escalate() }
func (ScoreAnswerOf[L]) AtOrBelow ¶
func (a ScoreAnswerOf[L]) AtOrBelow(level L) float64
AtOrBelow returns the total probability of landing at level or lower.
func (ScoreAnswerOf[L]) MostLikely ¶
func (a ScoreAnswerOf[L]) MostLikely() (L, float64)
MostLikely returns the single highest-probability level and its probability.
This is not always Level: a bimodal distribution has a weighted mean sitting in a level the model considers unlikely. When the two disagree, the question has more than one reading.
func (ScoreAnswerOf[L]) Untyped ¶
func (a ScoreAnswerOf[L]) Untyped() ScoreAnswer
Untyped returns the plain ScoreAnswer.
type ScoreBuilder ¶
type ScoreBuilder struct {
// contains filtered or unexported fields
}
ScoreBuilder builds a Score.
func NewScore ¶
func NewScore(instructions EntryType) ScoreBuilder
NewScore starts a Score.
typesafe.NewScore("How frustrated is the customer?").
Levels("Calm", "Frustrated", "Very angry")
Order is meaning: a level's position is its score, lowest first.
func (ScoreBuilder) Level ¶
func (b ScoreBuilder) Level(description EntryType) ScoreBuilder
Level appends one level. Call it in order, lowest first.
func (ScoreBuilder) Levels ¶
func (b ScoreBuilder) Levels(descriptions ...string) ScoreBuilder
Levels appends several string levels in order, lowest first.
func (ScoreBuilder) MarshalJSON ¶
func (b ScoreBuilder) MarshalJSON() ([]byte, error)
MarshalJSON delegates to the built question.
type SystemOneRequest ¶
type SystemOneRequest struct {
// State is the content every question refers to: a string, or any value
// that marshals to a JSON object or array.
//
// Prefer an object for anything beyond a single passage, so each part of
// the state has a descriptive name. Jev accepts text only — pre-process
// images, audio, and binaries into text or structured fields first.
State any `json:"state"`
// Model selects which model handles the request. Leave empty to use the
// client's default (see WithDefaultModel and TYPESAFE_DEFAULT_MODEL).
//
// Aliases such as jev-latest move without notice. The response reports
// the versioned id that actually answered.
Model string `json:"model"`
// Questions must contain at least one entry.
Questions map[string]Question `json:"questions"`
}
SystemOneRequest is one evaluation: a single state, and a map of named questions asked about it.
All three fields are required by the API. Question ids are yours to choose; answers come back under the same ids. The documentation notes that ids are not sent to the model and play no part in inference, so name them for your own code's benefit and put nothing load-bearing in them.
func (*SystemOneRequest) CheckReferences ¶
func (r *SystemOneRequest) CheckReferences() []RefWarning
CheckReferences resolves every backticked state reference in every question against the request's state, and reports those that name nothing.
TypeSafe's documentation recommends pointing a question at the relevant part of a structured state by path:
"Does `ticket.messages[0].text` request a refund?"
The server does not resolve these. The model is trained to read them, which means a typo is silent: `ticket.mesage` names nothing, the model answers anyway, and the answer is quietly worse. Nothing in the response indicates that it happened.
This catches it before the request is sent. It is advisory — a reference may legitimately point outside the state, and backticks are also used for ordinary emphasis — so the result is warnings, never an error.
for _, w := range req.CheckReferences() {
log.Warn(w.String())
}
func (*SystemOneRequest) EstimateCost ¶
func (r *SystemOneRequest) EstimateCost(ratePerMillionUSD float64) CostEstimate
EstimateCost estimates the input charge for a request.
ratePerMillionUSD is the price per million input tokens. Pass 0 to use DefaultInputCostPerMillionTokens, which is the published Jev 1.13 rate — but pass your own if you have negotiated terms, because a hard-coded price is wrong the moment anyone's contract differs.
cost := req.EstimateCost(0)
log.Printf("%s", cost) // never present this as authoritative
func (*SystemOneRequest) EstimateTokens ¶
func (r *SystemOneRequest) EstimateTokens() TokenEstimate
EstimateTokens returns a conservative estimate of the request's input cost.
est := req.EstimateTokens()
if err := est.Err(); err != nil {
return err // caught before a round trip
}
Both documented ceilings are checked. The single-question one is the trap: a request comfortably inside the 64k whole-request budget can still be rejected for a state plus one long question exceeding 32k, and nothing in the error the server returns explains which limit was hit.
func (*SystemOneRequest) Validate ¶
func (r *SystemOneRequest) Validate() ([]Warning, error)
Validate checks a request before it is sent.
The error is returned for anything the server is known to reject; every such rule below was confirmed against the live API, not inferred from the docs. Warnings are for the legal-but-suspicious, and are advisory.
warnings, err := req.Validate()
if err != nil {
return err
}
for _, w := range warnings {
log.Warn(w.String())
}
SystemOne applies the error half automatically. Call this yourself when you want the warnings, or to check a request you build ahead of time.
type SystemOneResponse ¶
type SystemOneResponse struct {
// Model is the versioned model id that answered, which may differ from
// the alias you requested. Log it: aliases move, and a confidence
// threshold tuned against one version is not calibrated for another.
Model string `json:"model"`
// Answers holds one answer per question id.
//
// Phase 1 leaves these undecoded. The typed accessors — Noul, Choice,
// Score — arrive in Phase 2, at which point the raw form remains
// available for anything this SDK does not model.
Answers map[string]json.RawMessage `json:"answers"`
// Usage is the token accounting for this request.
Usage Usage `json:"usage"`
}
SystemOneResponse is one answer per question, keyed by the ids you sent.
func (*SystemOneResponse) All ¶
func (r *SystemOneResponse) All() map[string]Answer
All decodes every answer, keyed by question id.
An answer whose type this SDK does not recognize is skipped rather than failing the whole call, so a new primitive added server-side degrades to "not visible here" instead of breaking existing code. Reach it through SystemOneResponse.Answers, which always holds the raw JSON.
func (*SystemOneResponse) Answer ¶
func (r *SystemOneResponse) Answer(id string) (Answer, error)
Answer decodes the answer under id into its concrete type, discovered from the wire discriminator.
Use this when the question type is not known statically; use Noul, Choice, or Score when it is.
func (*SystemOneResponse) Choice ¶
func (r *SystemOneResponse) Choice(id string) (ChoiceAnswer, error)
Choice decodes the answer under id as a ChoiceAnswer.
func (*SystemOneResponse) Choices ¶
func (r *SystemOneResponse) Choices() map[string]ChoiceAnswer
Choices decodes every Choice answer. Answers of other types are omitted.
func (*SystemOneResponse) Confidence ¶
func (r *SystemOneResponse) Confidence(id string) (float64, bool)
Confidence returns the confidence of the answer under id.
The second result is false for a Noul, which carries no confidence — the probability itself expresses the uncertainty. Callers that treat a missing confidence as zero would read every Noul as maximally uncertain, so this reports absence rather than substituting a number.
func (*SystemOneResponse) Noul ¶
func (r *SystemOneResponse) Noul(id string) (NoulAnswer, error)
Noul decodes the answer under id as a NoulAnswer.
Returns ErrNoSuchAnswer if no such answer exists, or ErrWrongAnswerType if it is a different primitive. Both are returned, never panicked: a wrong accessor is a coding mistake worth an error, not a crashed process.
func (*SystemOneResponse) Nouls ¶
func (r *SystemOneResponse) Nouls() map[string]NoulAnswer
Nouls decodes every Noul answer, mirroring the Python SDK's grouped accessors. Answers of other types are omitted.
func (*SystemOneResponse) Score ¶
func (r *SystemOneResponse) Score(id string) (ScoreAnswer, error)
Score decodes the answer under id as a ScoreAnswer.
func (*SystemOneResponse) Scores ¶
func (r *SystemOneResponse) Scores() map[string]ScoreAnswer
Scores decodes every Score answer. Answers of other types are omitted.
type TimeoutError ¶
TimeoutError is a deadline exceeded while waiting for a response.
A caller's own canceled context surfaces as context.Canceled, not as this: deliberate cancellation is not a timeout, and conflating them makes shutdown paths log spurious failures.
func (*TimeoutError) Error ¶
func (e *TimeoutError) Error() string
func (*TimeoutError) Is ¶
func (e *TimeoutError) Is(target error) bool
func (*TimeoutError) Unwrap ¶
func (e *TimeoutError) Unwrap() error
type TokenEstimate ¶
type TokenEstimate struct {
// State is the estimated cost of the state alone.
State int
// PerQuestion is the estimated cost of each question, by id.
PerQuestion map[string]int
// Overhead is the fixed per-request cost, independent of content.
Overhead int
// Total is the whole request: compare against MaxContextTokens.
Total int
// LongestSingle is state plus the most expensive single question:
// compare against MaxSingleQuestionTokens.
LongestSingle int
// LongestQuestionID names the question driving LongestSingle.
LongestQuestionID string
// ExceedsTotal reports that Total is over MaxContextTokens.
ExceedsTotal bool
// ExceedsSingle reports that LongestSingle is over
// MaxSingleQuestionTokens. A request can pass ExceedsTotal and fail this.
ExceedsSingle bool
// Approximate is always true.
Approximate bool
}
TokenEstimate is a conservative guess at what a request will cost.
It is an estimate, and it says so ¶
TypeSafe publishes no tokenizer. This is fitted from measurements against the live API: token count tracks serialized JSON size closely and linearly, with a large fixed overhead of roughly 250 tokens per call regardless of content. The model carries 25% headroom on the marginal rate so that it errs high rather than low — an estimate that is sometimes under is worse than none, because it fails exactly when a request is near the limit.
Expect it to over-report by roughly a quarter. Do not use it for billing.
func (TokenEstimate) Err ¶
func (e TokenEstimate) Err() error
Err returns a descriptive error when a limit is crossed, or nil.
The message names which ceiling, by how much, and which question is responsible — because "request too large" sends the reader back to count bytes by hand.
func (TokenEstimate) String ¶
func (e TokenEstimate) String() string
String renders the estimate for a terminal.
func (TokenEstimate) WouldExceedLimits ¶
func (e TokenEstimate) WouldExceedLimits() bool
WouldExceedLimits reports whether either documented ceiling is crossed.
type TypedChoiceQuestion ¶
TypedChoiceQuestion is a Choice whose options are values of T.
It embeds Choice, so it is a Question, it marshals identically, and every plain-Choice field remains reachable.
func TypedChoice ¶
func TypedChoice[T OptionKey](instructions EntryType, opts ...TypedOption[T]) TypedChoiceQuestion[T]
TypedChoice builds a Choice whose options are values of T.
type Topic string
const (
TopicBilling Topic = "billing"
TopicTechnical Topic = "technical"
TopicOther Topic = "other"
)
q := typesafe.TypedChoice[Topic]("Which team should handle this?",
typesafe.OptionOf(TopicBilling, "Invoices, charges, refunds"),
typesafe.OptionOf(TopicTechnical, "Bugs, outages, API errors"),
typesafe.OptionOf(TopicOther, nil),
)
A duplicate option name is a programming error that would silently drop an option from the request; Validate reports it rather than sending a question with fewer options than the code appears to declare.
func (TypedChoiceQuestion[T]) Answer ¶
func (q TypedChoiceQuestion[T]) Answer(r *SystemOneResponse, id string) (ChoiceAnswerOf[T], error)
Answer decodes the answer under id as a ChoiceAnswerOf[T], and checks it against this question's declared option set.
ans, err := q.Answer(resp, "department")
switch ans.Choice { // Topic, not string
case TopicBilling: ...
case TopicTechnical: ...
}
An answer naming an option the question did not declare is an error. That can only happen if the request and the type have drifted apart, and it is exactly the case a switch would handle by silently falling through.
func (TypedChoiceQuestion[T]) Options ¶
func (q TypedChoiceQuestion[T]) Options() []T
Options returns the declared option set, in declaration order.
type TypedLevel ¶
type TypedLevel[L LevelKey] struct { // Level is the rubric level, as a value of your named type. Its numeric // value must equal its position, lowest first. Level L // Description says what this level means. Description EntryType }
TypedLevel is one level of a TypedScore: a typed level and its description.
func LevelOf ¶
func LevelOf[L LevelKey](level L, description EntryType) TypedLevel[L]
LevelOf builds one typed level.
typesafe.LevelOf(FrustrationCalm, "No sign of irritation")
type TypedOption ¶
type TypedOption[T OptionKey] struct { // Name is the option, as a value of your named type. Name T // Description says when the option applies. nil is sent as JSON null, // meaning the option is read by its name alone. Description EntryType }
TypedOption is one option of a TypedChoice: a typed name and its description.
func OptionOf ¶
func OptionOf[T OptionKey](name T, description EntryType) TypedOption[T]
OptionOf builds one typed option.
typesafe.OptionOf(TopicBilling, "Invoices, charges, refunds") typesafe.OptionOf(TopicOther, nil) // read by its name alone
Named OptionOf rather than Option because Option is already this package's client-configuration type (WithAPIKey and the rest return one).
type TypedScoreQuestion ¶
TypedScoreQuestion is a Score whose levels are values of L.
func TypedScore ¶
func TypedScore[L LevelKey](instructions EntryType, levels ...TypedLevel[L]) TypedScoreQuestion[L]
TypedScore builds a Score whose levels are values of L.
type Frustration int
const (
Calm Frustration = iota
Annoyed
Angry
)
q := typesafe.TypedScore[Frustration]("How frustrated is the customer?",
typesafe.LevelOf(Calm, "No sign of irritation"),
typesafe.LevelOf(Annoyed, "Clearly unhappy, still civil"),
typesafe.LevelOf(Angry, "Hostile, threatening to leave"),
)
Position is meaning: the first level scores 0, the second 1, and so on. The enum's values must therefore match their positions, which an ordinary iota declaration gives you. Validate reports a mismatch — passing levels out of order would map every answer to the wrong label, and nothing in the resulting score would reveal it.
func (TypedScoreQuestion[L]) Answer ¶
func (q TypedScoreQuestion[L]) Answer(r *SystemOneResponse, id string) (ScoreAnswerOf[L], error)
Answer decodes the answer under id as a ScoreAnswerOf[L].
func (TypedScoreQuestion[L]) Levels ¶
func (q TypedScoreQuestion[L]) Levels() []L
Levels returns the declared levels, in rubric order.
type UnprocessableEntityError ¶
type UnprocessableEntityError struct{ *APIError }
UnprocessableEntityError is a 422: the request body failed validation.
This is never retried — the same bytes will fail the same way. Read Detail to find out which field the server rejected.
func (*UnprocessableEntityError) Is ¶
func (e *UnprocessableEntityError) Is(target error) bool
func (*UnprocessableEntityError) Unwrap ¶
func (e *UnprocessableEntityError) Unwrap() error
type Usage ¶
Usage reports token accounting for a request. Only input tokens are billed; TypeSafe does not charge for output.
type ValidationDetail ¶
type ValidationDetail struct {
// Loc is the path to the offending field, e.g.
// ["body", "questions", "frustration", "criteria"]. Elements are strings
// or integers.
Loc []any `json:"loc"`
// Msg is the server's human-readable explanation.
Msg string `json:"msg"`
// Type is the machine-readable error kind, e.g. "missing".
Type string `json:"type"`
// Input is the value that failed validation, when the server includes it.
Input any `json:"input,omitempty"`
// Ctx carries kind-specific context, when the server includes it.
Ctx map[string]any `json:"ctx,omitempty"`
}
ValidationDetail is one entry from a 422 response body. The server reports a path to the offending field, so a caller can see precisely which question and which field were rejected rather than re-reading their whole request.
func (ValidationDetail) Path ¶
func (d ValidationDetail) Path() string
Path renders Loc as a dotted path with bracketed indices, e.g. "body.questions.frustration.criteria" or "body.questions.q.criteria[0]".
func (ValidationDetail) String ¶
func (d ValidationDetail) String() string
type Warning ¶
type Warning struct {
// QuestionID is the question this concerns, or "" for the request itself.
QuestionID string
// Message describes what looks wrong.
Message string
}
Warning is something legal that is probably not what you meant.
Warnings never block a request. The rule this SDK follows: if the server would accept it, we send it. Rejecting a request the API would have answered is a worse failure than passing through something odd, because the caller can see an odd answer and cannot see a request we refused to make.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package cassette records real API interactions to a file and replays them offline, so that an integration test needs a live key exactly once.
|
Package cassette records real API interactions to a file and replays them offline, so that an integration test needs a live key exactly once. |
|
cmd
|
|
|
typesafe
command
Command typesafe is a command-line client for the TypeSafe System One API.
|
Command typesafe is a command-line client for the TypeSafe System One API. |
|
typesafe-gen
command
Command typesafe-gen generates typed TypeSafe questions from Go enums.
|
Command typesafe-gen generates typed TypeSafe questions from Go enums. |
|
Package decision composes System One answers into decisions.
|
Package decision composes System One answers into decisions. |
|
integrations
|
|
|
echo
module
|
|
|
fiber
module
|
|
|
gin
module
|
|
|
langchaingo
module
|
|
|
mcp
module
|
|
|
nethttp
module
|
|
|
temporal
module
|
|
|
internal
|
|
|
canonical
Package canonical produces a deterministic byte form of a JSON value.
|
Package canonical produces a deterministic byte form of a JSON value. |
|
fixtures
Package fixtures locates and loads the golden contract fixtures.
|
Package fixtures locates and loads the golden contract fixtures. |
|
genexample
Package genexample is the worked example typesafe-gen generates from.
|
Package genexample is the worked example typesafe-gen generates from. |
|
statepath
Package statepath resolves the backticked state references that TypeSafe's documentation recommends writing inside question instructions.
|
Package statepath resolves the backticked state references that TypeSafe's documentation recommends writing inside question instructions. |
|
tokens
Package tokens estimates how many input tokens a request will cost.
|
Package tokens estimates how many input tokens a request will cost. |
|
lint
module
|
|
|
typesafecache
module
|
|
|
typesafeotel
module
|
|
|
typesafeprom
module
|
|
|
Package typesafetest provides doubles for testing code that calls the TypeSafe API: a programmable HTTP server, canned responses for every documented failure, and assertion helpers.
|
Package typesafetest provides doubles for testing code that calls the TypeSafe API: a programmable HTTP server, canned responses for every documented failure, and assertion helpers. |