Documentation
¶
Overview ¶
Package typesafe provides an SDK for the TypeSafe AI API.
TypeSafe answers typed questions about a piece of state and returns probability distributions, not prose. There is nothing to parse and no format to coax out of a model: a question comes back as a number your code can act on.
To get started, create a Client and call Client.SystemOne with the state to evaluate and the questions to ask:
client, err := typesafe.NewClient(nil) // reads TYPESAFE_API_KEY
if err != nil {
log.Fatal(err)
}
result, err := client.SystemOne(ctx, &typesafe.SystemOneRequest{
State: "I was charged twice. Please fix this ASAP.",
Questions: typesafe.Questions{
"category": &typesafe.ChoiceQuestion{
Instructions: "What is this ticket about?",
Criteria: typesafe.ChoiceCriteria{
"billing": nil,
"technical": nil,
"other": nil,
},
},
},
}, nil)
if err != nil {
log.Fatal(err)
}
category, err := result.Answers.Choice("category")
if err != nil {
log.Fatal(err)
}
fmt.Println(category.Choice, category.Confidence)
Questions and answers ¶
There are three question types, and one call may mix them freely. Each is evaluated in parallel and in isolation against the same state, so a question never sees another's answer.
- A NoulQuestion asks something yes-or-no and is answered with the probability of yes, so that the threshold is yours to choose.
- A ChoiceQuestion selects one of a set of labels and reports a probability for each.
- A ScoreQuestion rates the state against an ordered rubric and reports an expected score, which may fall between two levels.
Keep each question atomic. A question that asks two things at once has no single answer to report a probability for; ask both and combine them in your own code, which is cheaper to understand and to change than a longer prompt.
A question's type decides its answer's, but Go cannot vary a map's value type by key, so the answers arrive as an Answers map of an Answer interface. Name the type you expect with Answers.Noul, Answers.Choice, or Answers.Score, each of which reports a clear error if the service sent something else:
urgency, err := result.Answers.Score("urgency")
An answer owns the readings taken from it, so that the arithmetic a caller would otherwise repeat has one place to be right: ChoiceAnswer.Probability for how likely the selected label was, ScoreAnswer.Level and ScoreAnswer.Description for where an expected score lands on the rubric. NoulAnswer has no such helper on purpose — see its documentation.
Configuration ¶
NewClient takes its settings from ClientOptions, then from the environment, then from the SDK defaults. See EnvAPIKey and the constants beside it for the variables, and DefaultBaseURL and DefaultModel for the defaults. A zero ClientOptions, or a nil one, configures a client entirely from the environment.
Timeouts, retries, and errors ¶
ClientOptions.Timeout bounds each attempt and defaults to DefaultTimeout. Retries have no budget of their own, so bound a whole call with its context.Context:
ctx, cancel := context.WithTimeout(ctx, 30*time.Second) defer cancel()
DefaultRetryPolicy retries 408, 429, and every 5xx, along with connection failures and timeouts, twice each, backing off exponentially with jitter and honoring a Retry-After header within a minute. A RetryPolicy is a complete setting rather than a patch: start from DefaultRetryPolicy and change what you need.
Three kinds of error come back from a call, and errors.Is and errors.As tell them apart:
- An *APIError is a response the service refused to fulfil. Match its class with ErrRateLimit and the sentinels beside it rather than comparing status codes.
- A *ConnectionError is a request that produced no complete response, including one that ran out of time; those also match context.DeadlineExceeded.
- The context's own error is returned when the caller gives up, so context.Canceled means your cancellation and nothing else.
Concurrency ¶
A Client is safe for concurrent use and pools its connections, so create one for the life of the program and share it. Fanning many questions out across goroutines against one client is the intended way to use the API.
Example ¶
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"net/http/httptest"
typesafe "github.com/Tangerg/typesafe-sdk-go"
)
// stubService stands in for api.typesafe.ai so that the examples below run
// offline and print the same thing every time. Point a real client at
// [typesafe.DefaultBaseURL] instead, which is where a client with no BaseURL
// goes.
func stubService(answer func(request map[string]any) any) *httptest.Server {
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var request map[string]any
_ = json.NewDecoder(r.Body).Decode(&request)
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(answer(request))
}))
}
func main() {
service := stubService(func(map[string]any) any {
return map[string]any{
"model": "jev-1.13",
"answers": map[string]any{
"category": map[string]any{
"type": "choice", "choice": "billing", "confidence": 0.94,
"probabilities": map[string]float64{"billing": 0.92, "technical": 0.05, "other": 0.03},
},
},
"usage": map[string]any{"input_tokens": 41, "output_tokens": 3},
}
})
defer service.Close()
client, err := typesafe.NewClient(&typesafe.ClientOptions{
APIKey: "sk-example",
BaseURL: service.URL,
})
if err != nil {
log.Fatal(err)
}
result, err := client.SystemOne(context.Background(), &typesafe.SystemOneRequest{
State: "I was charged twice. Please fix this ASAP.",
Questions: typesafe.Questions{
"category": &typesafe.ChoiceQuestion{
Instructions: "What is this ticket about?",
Criteria: typesafe.ChoiceCriteria{
"billing": nil,
"technical": nil,
"other": nil,
},
},
},
}, nil)
if err != nil {
log.Fatal(err)
}
category, err := result.Answers.Choice("category")
if err != nil {
log.Fatal(err)
}
fmt.Printf("%s (%.2f confidence)\n", category.Choice, category.Confidence)
}
Output: billing (0.94 confidence)
Index ¶
- Constants
- Variables
- func ParseLevel(name string) (slog.Level, error)
- func StatusRange(lo, hi int) []int
- type APIError
- type Answer
- type Answers
- type ChoiceAnswer
- type ChoiceCriteria
- type ChoiceQuestion
- type Client
- func (c *Client) BaseURL() string
- func (c *Client) DefaultModel() string
- func (c *Client) ListModels(ctx context.Context, options *RequestOptions) (*ModelList, error)
- func (c *Client) Retry() RetryPolicy
- func (c *Client) SystemOne(ctx context.Context, request *SystemOneRequest, options *RequestOptions) (*SystemOneResult, error)
- func (c *Client) Timeout() time.Duration
- type ClientOptions
- type ConnectionError
- type Entry
- type Meta
- type ModelCard
- type ModelList
- type NoulAnswer
- type NoulCriteria
- type NoulQuestion
- type Question
- type Questions
- type RequestOptions
- type RetryPolicy
- type ScoreAnswer
- type ScoreCriteria
- type ScoreQuestion
- type SystemOneRequest
- type SystemOneResult
- type UnknownAnswer
- type Usage
Examples ¶
Constants ¶
const ( // DefaultBaseURL is the production API root. DefaultBaseURL = "https://api.typesafe.ai" // DefaultModel is the model used by a request that names none. The // "-latest" suffix tracks the newest release of that model family. DefaultModel = "jev-latest" )
Defaults for the settings ClientOptions leaves unset and the environment does not supply.
const ( // EnvAPIKey holds the API key. It is the only required setting, and // [ClientOptions.APIKey] takes precedence over it. EnvAPIKey = "TYPESAFE_API_KEY" //nolint:gosec // the name of a variable, not a key // EnvBaseURL holds the API root, defaulting to [DefaultBaseURL]. EnvBaseURL = "TYPESAFE_BASE_URL" // EnvDefaultModel holds the model used by requests that omit one, // defaulting to [DefaultModel]. EnvDefaultModel = "TYPESAFE_DEFAULT_MODEL" // EnvLogLevel holds a level name accepted by [ParseLevel]. It installs a // stderr logger only when [ClientOptions.Logger] is nil; a caller who // supplies a logger owns its leveling. EnvLogLevel = "TYPESAFE_LOG_LEVEL" )
Environment variables read by NewClient when the matching ClientOptions field is unset. An empty or whitespace-only value counts as unset, so an exported-but-empty variable does not shadow an SDK default.
const ( ErrBadRequest statusClassError = iota + 1 ErrAuthentication ErrPermissionDenied ErrNotFound ErrUnprocessableEntity ErrRateLimit ErrServerError )
Sentinels for the response classes the API distinguishes. Match them with errors.Is rather than by comparing APIError.Status, which is what most callers actually want to ask:
if errors.Is(err, typesafe.ErrRateLimit) {
// back off and try later
}
ErrServerError matches every status from 500 to 599, so an overloaded 529 is caught alongside a plain 500.
They are constants of an unexported type rather than error variables, because a package-level var is writable by every importer, and one that reassigned it would silently change how every other importer's errors.Is behaved. A constant cannot be made to mean anything else.
const ( TypeNoul = "noul" TypeChoice = "choice" TypeScore = "score" )
The wire discriminants of the three question and answer types. The service calls a yes/no question a noul.
const DefaultTimeout = 10 * time.Second
DefaultTimeout is the per-attempt timeout used when ClientOptions.Timeout is zero.
const LevelOff = slog.Level(math.MaxInt32)
LevelOff is above every level slog defines, so a handler thresholded at it emits nothing. ParseLevel returns it for "off".
const Version = "0.1.0"
Version is the version of this SDK, reported in the User-Agent and X-TypeSafe-SDK request headers. It versions this module, not the API it speaks to.
Variables ¶
var ErrNoAPIKey = errors.New(errPrefix + "no API key")
ErrNoAPIKey reports that NewClient found no API key in either ClientOptions or the environment.
Functions ¶
func ParseLevel ¶
ParseLevel converts a level name, in any capitalization, to the level the SDK logs at. The names are debug, info, warn, error, and off; "warn" maps to slog.LevelWarn and "off" to LevelOff.
func StatusRange ¶
StatusRange returns the status codes from lo to hi inclusive, for building RetryPolicy.HTTPStatuses. It returns nil when hi is below lo.
Types ¶
type APIError ¶
type APIError struct {
// Status is the HTTP response status code.
Status int
// Header holds the HTTP response headers.
Header http.Header
// Body is the parsed JSON body, the response text when the body is not
// JSON, or nil when the body is empty. JSON numbers decode as [json.Number]
// so unmodeled data keeps its original precision and range.
Body any
// RequestID is the x-typesafe-request-id header, or "" when absent. Quote
// it in bug reports: it identifies the request in the service's own logs.
RequestID string
}
An APIError is an unsuccessful HTTP response from the API, returned once the request's retries are exhausted or the status is not one the policy retries.
Example ¶
Match a failure by its class rather than by its status code. The request ID is worth logging: it is what identifies the call in the service's own logs.
package main
import (
"context"
"errors"
"fmt"
"log"
"net/http"
"net/http/httptest"
typesafe "github.com/Tangerg/typesafe-sdk-go"
)
func main() {
service := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("X-Typesafe-Request-Id", "req_01HQ")
w.WriteHeader(http.StatusTooManyRequests)
_, _ = fmt.Fprint(w, `{"error":{"message":"rate limit exceeded"}}`)
}))
defer service.Close()
// Retrying is on by default; it is off here so the example does not wait.
noRetries := typesafe.DefaultRetryPolicy()
noRetries.MaxRetries = 0
client, err := typesafe.NewClient(&typesafe.ClientOptions{
APIKey: "sk-example", BaseURL: service.URL, Retry: &noRetries,
})
if err != nil {
log.Fatal(err)
}
_, err = client.ListModels(context.Background(), nil)
switch {
case errors.Is(err, typesafe.ErrRateLimit):
var apiErr *typesafe.APIError
errors.As(err, &apiErr)
fmt.Printf("rate limited (request %s): %s\n", apiErr.RequestID, err)
case errors.Is(err, typesafe.ErrAuthentication):
fmt.Println("check TYPESAFE_API_KEY")
case err != nil:
fmt.Println("failed:", err)
}
}
Output: rate limited (request req_01HQ): typesafe: 429 rate limit exceeded
func (*APIError) Is ¶
Is matches the class sentinels above, so that a caller can ask about the kind of failure without reaching for the status code.
func (*APIError) RetryAfter ¶
RetryAfter reports the server's requested delay from the Retry-After or retry-after-ms response header, and false when neither carries a valid delay. It is most useful on a 429, which is the status the service documents it for.
type Answer ¶
type Answer interface {
// AnswerType reports the wire discriminant: [TypeNoul], [TypeChoice],
// [TypeScore], or whatever unrecognized type the service sent.
AnswerType() string
// contains filtered or unexported methods
}
An Answer is one of *NoulAnswer, *ChoiceAnswer, *ScoreAnswer, or *UnknownAnswer.
A question's type decides its answer's, but Go cannot give a map values of different types per key, so the type arrives at run time instead: use Answers.Noul, Answers.Choice, and Answers.Score, which name the type you expect and report a clear error when the service disagrees.
type Answers ¶
Answers are the answers to one request, keyed by the names the request's Questions used.
func (Answers) Choice ¶
func (a Answers) Choice(name string) (*ChoiceAnswer, error)
Choice returns the answer to the named choice question, on the same terms as Answers.Noul.
func (Answers) Noul ¶
func (a Answers) Noul(name string) (*NoulAnswer, error)
Noul returns the answer to the named noul question. It reports an error when the response holds no answer under that name, or answered it with a different type than the question asked for.
func (Answers) Score ¶
func (a Answers) Score(name string) (*ScoreAnswer, error)
Score returns the answer to the named score question, on the same terms as Answers.Noul.
func (*Answers) UnmarshalJSON ¶
UnmarshalJSON decodes each answer into the type its discriminant names.
type ChoiceAnswer ¶
type ChoiceAnswer struct {
// Choice is the selected label, one of the keys of the question's
// [ChoiceCriteria].
Choice string `json:"choice"`
// Confidence is the service's reported confidence in Choice, from zero to
// one. It is not the same as the selected label's probability: see
// Probabilities.
Confidence float64 `json:"confidence"`
// Probabilities holds one probability per label of the question.
Probabilities map[string]float64 `json:"probabilities"`
}
A ChoiceAnswer answers a ChoiceQuestion.
func (*ChoiceAnswer) AnswerType ¶
func (a *ChoiceAnswer) AnswerType() string
AnswerType returns TypeChoice.
func (*ChoiceAnswer) MarshalJSON ¶
func (a *ChoiceAnswer) MarshalJSON() ([]byte, error)
func (*ChoiceAnswer) Probability ¶
func (a *ChoiceAnswer) Probability() float64
Probability reports how likely the selected label was, which is the reading of a choice that callers reach for and the one that is easy to confuse with Confidence. It is zero if the service selected a label it reported no probability for, which a well-formed answer never does.
func (*ChoiceAnswer) UnmarshalJSON ¶ added in v0.1.0
func (a *ChoiceAnswer) UnmarshalJSON(data []byte) error
type ChoiceCriteria ¶
ChoiceCriteria maps each label a ChoiceQuestion may select to a description of what it means. A nil description leaves the label undescribed, which is the right choice when the label speaks for itself:
typesafe.ChoiceCriteria{"billing": nil, "technical": nil, "other": nil}
A label with no description is still an alternative; only labels present here can be selected. The service accepts up to 255 of them.
type ChoiceQuestion ¶
type ChoiceQuestion struct {
// Instructions is the question. A nil Instructions is sent as null.
Instructions Entry `json:"instructions"`
// Criteria are the alternatives to select between.
Criteria ChoiceCriteria `json:"criteria"`
}
A ChoiceQuestion selects one of a set of named alternatives. See ChoiceAnswer.
func (*ChoiceQuestion) MarshalJSON ¶
func (q *ChoiceQuestion) MarshalJSON() ([]byte, error)
func (*ChoiceQuestion) QuestionType ¶
func (q *ChoiceQuestion) QuestionType() string
QuestionType returns TypeChoice.
type Client ¶
type Client struct {
// contains filtered or unexported fields
}
A Client is a connection-pooled handle for the TypeSafe API. It is safe for concurrent use, and one client should be shared for the life of a program so that its underlying connections are reused.
func NewClient ¶
func NewClient(options *ClientOptions) (*Client, error)
NewClient returns a client for the TypeSafe API.
Options may be nil, which configures the client entirely from the environment. It returns an error when no API key is available, or when a setting is one this package refuses.
func (*Client) BaseURL ¶
BaseURL reports the API root the client sends to, without a trailing slash.
func (*Client) DefaultModel ¶
DefaultModel reports the model used by requests that name none.
func (*Client) ListModels ¶
ListModels reports the models available to the account.
Example ¶
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"net/http/httptest"
"strings"
typesafe "github.com/Tangerg/typesafe-sdk-go"
)
// stubService stands in for api.typesafe.ai so that the examples below run
// offline and print the same thing every time. Point a real client at
// [typesafe.DefaultBaseURL] instead, which is where a client with no BaseURL
// goes.
func stubService(answer func(request map[string]any) any) *httptest.Server {
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var request map[string]any
_ = json.NewDecoder(r.Body).Decode(&request)
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(answer(request))
}))
}
func main() {
service := stubService(func(map[string]any) any {
return map[string]any{"models": []any{
map[string]any{"name": "jev-1.13", "description": "general purpose", "release_date": "2026-07-01"},
map[string]any{"name": "jev-latest", "description": "alias for the newest jev", "release_date": "2026-07-01"},
}}
})
defer service.Close()
client, err := typesafe.NewClient(&typesafe.ClientOptions{APIKey: "sk-example", BaseURL: service.URL})
if err != nil {
log.Fatal(err)
}
list, err := client.ListModels(context.Background(), nil)
if err != nil {
log.Fatal(err)
}
fmt.Println(strings.Join(list.Names(), ", "))
for _, model := range list.Models {
fmt.Printf("%s: %s\n", model.Name, model.Description)
}
}
Output: jev-1.13, jev-latest jev-1.13: general purpose jev-latest: alias for the newest jev
func (*Client) Retry ¶
func (c *Client) Retry() RetryPolicy
Retry reports a copy of the client's retry policy. Changing it does not affect the client; pass a policy to NewClient or RequestOptions instead.
func (*Client) SystemOne ¶
func (c *Client) SystemOne(ctx context.Context, request *SystemOneRequest, options *RequestOptions) (*SystemOneResult, error)
SystemOne answers named questions about text or structured state.
Each question is evaluated in parallel and in isolation against the same state, and comes back as a probability distribution rather than as prose to parse. Retrieve the answers with Answers.Noul, Answers.Choice, and Answers.Score, which name the type each question should have produced.
It returns an *APIError for a non-2xx response that outlived the retry policy, a *ConnectionError for a request that never completed, and ctx's error when the caller gives up.
Example ¶
A single call may mix all three question types. They are evaluated in parallel and in isolation, so keep each one atomic and combine the answers in your own code rather than in a longer instruction.
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"net/http/httptest"
typesafe "github.com/Tangerg/typesafe-sdk-go"
)
// stubService stands in for api.typesafe.ai so that the examples below run
// offline and print the same thing every time. Point a real client at
// [typesafe.DefaultBaseURL] instead, which is where a client with no BaseURL
// goes.
func stubService(answer func(request map[string]any) any) *httptest.Server {
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var request map[string]any
_ = json.NewDecoder(r.Body).Decode(&request)
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(answer(request))
}))
}
func main() {
service := stubService(func(map[string]any) any {
return map[string]any{
"model": "jev-1.13",
"answers": map[string]any{
"isBilling": map[string]any{"type": "noul", "noul": 0.97},
"tone": map[string]any{
"type": "choice", "choice": "frustrated", "confidence": 0.81,
"probabilities": map[string]float64{"calm": 0.04, "frustrated": 0.79, "angry": 0.17},
},
"urgency": map[string]any{
"type": "score", "score": 2.6, "confidence": 0.7,
"legend": map[string]any{"0": "can wait", "1": "this week", "2": "today", "3": "right now"},
"probabilities": map[string]float64{"0": 0.02, "1": 0.08, "2": 0.18, "3": 0.72},
},
},
"usage": map[string]any{"input_tokens": 88, "output_tokens": 9},
}
})
defer service.Close()
client, err := typesafe.NewClient(&typesafe.ClientOptions{APIKey: "sk-example", BaseURL: service.URL})
if err != nil {
log.Fatal(err)
}
ticket := map[string]any{
"subject": "Charged twice this month",
"body": "I see two charges of $49 on my card for August. Please fix this ASAP.",
}
result, err := client.SystemOne(context.Background(), &typesafe.SystemOneRequest{
State: ticket,
Questions: typesafe.Questions{
"isBilling": &typesafe.NoulQuestion{Instructions: "Is this ticket about billing?"},
"tone": &typesafe.ChoiceQuestion{
Instructions: "What is the customer's tone?",
Criteria: typesafe.ChoiceCriteria{"calm": nil, "frustrated": nil, "angry": nil},
},
"urgency": &typesafe.ScoreQuestion{
Instructions: "How urgent is this ticket?",
Criteria: typesafe.ScoreCriteria{"can wait", "this week", "today", "right now"},
},
},
}, nil)
if err != nil {
log.Fatal(err)
}
isBilling, err := result.Answers.Noul("isBilling")
if err != nil {
log.Fatal(err)
}
tone, err := result.Answers.Choice("tone")
if err != nil {
log.Fatal(err)
}
urgency, err := result.Answers.Score("urgency")
if err != nil {
log.Fatal(err)
}
// A noul is a probability, not a verdict: the threshold is yours.
fmt.Printf("billing: %.2f (route it: %v)\n", isBilling.Noul, isBilling.Noul > 0.8)
fmt.Printf("tone: %s at p=%.2f\n", tone.Choice, tone.Probability())
fmt.Printf("urgency: %.1f of 3, nearest %q\n", urgency.Score, urgency.Description())
fmt.Printf("tokens: %d in, %d out\n", result.Usage.InputTokens, result.Usage.OutputTokens)
}
Output: billing: 0.97 (route it: true) tone: frustrated at p=0.79 urgency: 2.6 of 3, nearest "right now" tokens: 88 in, 9 out
type ClientOptions ¶
type ClientOptions struct {
// APIKey authenticates every request. It falls back to [EnvAPIKey], and
// [NewClient] fails with [ErrNoAPIKey] when neither supplies one.
APIKey string
// BaseURL is an absolute HTTP(S) API root, optionally with a path prefix,
// without credentials, query, or fragment. It falls back to [EnvBaseURL]
// and then [DefaultBaseURL]. Trailing slashes are removed.
BaseURL string
// DefaultModel answers requests that leave [SystemOneRequest.Model] empty.
// It falls back to [EnvDefaultModel] and then [DefaultModel].
DefaultModel string
// Timeout bounds each attempt, not the call as a whole: a retried call may
// take several times as long. Bound the whole call with its
// [context.Context]. Zero means [DefaultTimeout].
Timeout time.Duration
// Retry replaces the SDK's retry policy. Nil means [DefaultRetryPolicy].
Retry *RetryPolicy
// Header is sent with every request. Per-call headers in
// [RequestOptions.Header] override it, and neither can displace the headers
// this package sets to authenticate and identify the request.
Header http.Header
// HTTPClient supplies HTTP settings. Nil uses [http.DefaultClient].
// The client is copied; its Transport and Jar remain shared. Without an
// explicit CheckRedirect, redirects are returned as [*APIError] responses
// so credentials and state stay at the configured endpoint.
//
// Any timeout on it applies to the whole attempt including the body, as
// Timeout already does. Set its Transport to route through a proxy, pin
// TLS, or observe traffic.
HTTPClient *http.Client
// Logger records request and response activity: summaries at info, and
// full headers and bodies at debug. Credential headers are redacted;
// bodies are not, and yours may hold the data you are evaluating.
//
// Nil means no logging, unless [EnvLogLevel] is set, which installs a
// stderr logger at that level.
Logger *slog.Logger
}
ClientOptions configures a Client. The zero value is usable as long as EnvAPIKey is set in the environment.
Every string setting falls back to its environment variable and then to the SDK default, in that order.
type ConnectionError ¶
type ConnectionError struct {
// Timeout is the per-attempt timeout that elapsed, and zero when the
// SDK's deadline did not elapse. A transport timeout has no known duration
// and leaves this field zero; use errors.Is with context.DeadlineExceeded.
Timeout time.Duration
// Err is the underlying transport or context error.
Err error
}
A ConnectionError reports that an attempt produced no complete HTTP response: the connection failed, the server closed it mid-body, or the attempt ran out of time.
A timeout is the same failure as a dropped connection from the caller's point of view — no answer arrived — so both use this type. Ask errors.Is for context.DeadlineExceeded to identify a timeout, including one imposed by the configured HTTP client or transport.
func (*ConnectionError) Error ¶
func (e *ConnectionError) Error() string
func (*ConnectionError) Unwrap ¶
func (e *ConnectionError) Unwrap() error
type Entry ¶
type Entry = any
An Entry is a JSON value the API accepts wherever it takes free-form content: the state under evaluation, a question's instructions, and the description of an outcome.
It is a string for plain text, a map or slice for structured content, and nil for JSON null, which leaves a noul or choice outcome undescribed. State and score levels must be non-null. The alias names this contract in signatures; validation checks the encoded shape. Numbers and booleans are allowed inside objects and arrays, but not as an Entry on their own.
type Meta ¶
type Meta struct {
// RequestID is the x-typesafe-request-id header, or "" when absent. Quote
// it in bug reports: it identifies the request in the service's own logs.
RequestID string
// Status is the HTTP response status code, which for a result is 2xx.
Status int
// Header holds the HTTP response headers.
Header http.Header
// Body is the raw response body. The typed fields of a result drop any
// property this SDK does not model, and this is where to find one: a model
// attribute added after this release, for instance.
Body []byte
}
Meta reports HTTP metadata about a response from the API. It reaches a caller on a result; an unsuccessful response carries the same facts on an *APIError.
type ModelCard ¶
type ModelCard struct {
// Name is the identifier to put in [SystemOneRequest.Model].
Name string `json:"name"`
// Description is a human-readable summary of what the model is for.
Description string `json:"description"`
// ReleaseDate is the model's release date, as the service formats it.
ReleaseDate string `json:"release_date"`
}
A ModelCard describes one model available to the account.
type ModelList ¶
type ModelList struct {
// Models are the available models. An account with none is answered with
// an empty list rather than an error.
Models []ModelCard `json:"models"`
// Meta reports HTTP metadata about the response. Its Body holds any model
// attribute [ModelCard] does not name, such as one added after this
// release or one visible only to the service's own accounts.
Meta Meta `json:"-"`
}
A ModelList holds the models available to the account.
type NoulAnswer ¶
type NoulAnswer struct {
// Noul is the probability that the answer is yes, from zero to one.
//
// It is a probability and not a verdict, which is the point of the
// primitive: pick the threshold your application needs instead of
// accepting one the service picked for you.
Noul float64 `json:"noul"`
}
A NoulAnswer answers a NoulQuestion.
It deliberately offers no Yes or Above helper. A threshold is the one part of a yes/no decision this package does not know: it depends on what a wrong yes and a wrong no each cost you. A helper would make `answer.Yes()` the obvious thing to write and bury that choice at whatever default it shipped with.
func (*NoulAnswer) AnswerType ¶
func (a *NoulAnswer) AnswerType() string
AnswerType returns TypeNoul.
func (*NoulAnswer) MarshalJSON ¶
func (a *NoulAnswer) MarshalJSON() ([]byte, error)
func (*NoulAnswer) UnmarshalJSON ¶ added in v0.1.0
func (a *NoulAnswer) UnmarshalJSON(data []byte) error
type NoulCriteria ¶
type NoulCriteria struct {
// True describes the yes outcome.
True Entry `json:"true,omitzero"`
// False describes the no outcome.
False Entry `json:"false,omitzero"`
}
NoulCriteria describes the two outcomes of a NoulQuestion. Either side may be left nil, which omits it and leaves that outcome undescribed.
type NoulQuestion ¶
type NoulQuestion struct {
// Instructions is the question. A nil Instructions is sent as null, which
// asks the service to judge the state against Criteria alone — so a question
// with neither is refused. Empty text, objects, and arrays also need criteria.
Instructions Entry `json:"instructions"`
// Criteria optionally describes what the two outcomes mean. A nil Criteria
// is omitted.
Criteria *NoulCriteria `json:"criteria,omitzero"`
}
A NoulQuestion asks a yes/no question and is answered with the probability of yes, rather than with a yes or a no. See NoulAnswer.
func (*NoulQuestion) MarshalJSON ¶
func (q *NoulQuestion) MarshalJSON() ([]byte, error)
func (*NoulQuestion) QuestionType ¶
func (q *NoulQuestion) QuestionType() string
QuestionType returns TypeNoul.
type Question ¶
type Question interface {
// QuestionType reports the wire discriminant.
QuestionType() string
// contains filtered or unexported methods
}
A Question is one of *NoulQuestion, *ChoiceQuestion, or *ScoreQuestion.
The set is closed to the three question types in this package.
type Questions ¶
Questions are the questions of one request, keyed by the names their answers are returned under. The names are yours; the service echoes them back in SystemOneResult.Answers.
Each question is evaluated in parallel and in isolation against the same state, so questions do not see one another's answers. Keep each one atomic and combine them in your own code.
func (Questions) Validate ¶
Validate reports whether the question set can be sent.
It checks the encoded values using the same rules as Client.SystemOne. Call it when questions are assembled separately from sending the request.
type RequestOptions ¶
type RequestOptions struct {
// Timeout replaces [ClientOptions.Timeout] for each attempt of this call.
Timeout time.Duration
// Retry replaces the client's retry policy for this call, in full: it is a
// complete policy and not a patch. See [RetryPolicy].
Retry *RetryPolicy
// Header is merged over [ClientOptions.Header] for this call.
Header http.Header
}
RequestOptions overrides client settings for one call. A nil *RequestOptions, and every zero field of a non-nil one, inherits the client's setting.
type RetryPolicy ¶
type RetryPolicy struct {
// MaxRetries is the number of retries after the first attempt. Zero
// disables retrying.
MaxRetries int
// BackoffInitial is the delay before the first retry, doubled before each
// later one up to BackoffMax.
BackoffInitial time.Duration
// BackoffMax caps the doubling.
BackoffMax time.Duration
// BackoffJitter is the fraction of each delay, from 0 to 1, that is
// randomly subtracted so that concurrent clients do not retry in lockstep.
BackoffJitter float64
// HTTPStatuses are the response status codes that are retried. A nil slice
// retries no status; use [StatusRange] to build a contiguous run.
HTTPStatuses []int
// RespectRetryAfter honors a Retry-After or retry-after-ms response header
// in place of the computed backoff.
RespectRetryAfter bool
// MaxRetryAfter is the longest server-requested delay that is honored. A
// longer one falls back to backoff rather than parking the call.
MaxRetryAfter time.Duration
// RetryConnection retries a [ConnectionError] that is not a timeout.
RetryConnection bool
// RetryTimeout retries an attempt or transport timeout.
RetryTimeout bool
}
A RetryPolicy decides which failures are retried and how long to wait first.
A policy is a complete setting rather than a patch: a nil *RetryPolicy in ClientOptions or RequestOptions inherits the level above it, and a non-nil one replaces it outright. Start from DefaultRetryPolicy and change what you need, so that a zero field is never mistaken for an unset one:
policy := typesafe.DefaultRetryPolicy() policy.MaxRetries = 5
Retries have no shared deadline. ClientOptions.Timeout bounds each attempt, and a whole call is bounded by its context.Context.
Example ¶
A RetryPolicy is a complete setting, not a patch: start from the defaults and change what you need, so that a zero field is never mistaken for an unset one.
package main
import (
"fmt"
"log"
"net/http"
typesafe "github.com/Tangerg/typesafe-sdk-go"
)
func main() {
policy := typesafe.DefaultRetryPolicy()
policy.MaxRetries = 5
policy.HTTPStatuses = append(policy.HTTPStatuses, http.StatusConflict)
client, err := typesafe.NewClient(&typesafe.ClientOptions{APIKey: "sk-example", Retry: &policy})
if err != nil {
log.Fatal(err)
}
fmt.Println(client.Retry().MaxRetries, client.Retry().RetriesStatus(http.StatusConflict))
}
Output: 5 true
func DefaultRetryPolicy ¶
func DefaultRetryPolicy() RetryPolicy
DefaultRetryPolicy returns the SDK's default policy: two retries of 408, 429, and every 5xx, and of connection failures and timeouts, with exponential backoff from 500ms to 5s and up to 25% jitter.
It returns a fresh value on each call, so the returned policy can be modified freely.
func (RetryPolicy) Delay ¶
Delay reports how long to wait before a zero-based retry attempt, given the response headers that prompted it, or nil when no response arrived.
A server-requested delay within the policy's ceiling is used exactly, on the grounds that the server knows when it will be ready and jitter would only make the client early. Everything else uses capped exponential backoff, from which jitter is subtracted.
random must return a value in [0, 1); pass math/rand/v2.Float64 outside of a test that needs a fixed delay.
func (RetryPolicy) RetriesStatus ¶
func (p RetryPolicy) RetriesStatus(status int) bool
RetriesStatus reports whether the policy retries a response status code.
type ScoreAnswer ¶
type ScoreAnswer struct {
// Score is the expected score over the rubric, so it may fall between two
// levels: a question split evenly between levels 1 and 3 scores 2, which no
// single level was chosen for.
Score float64 `json:"score"`
// Confidence is the service's reported confidence in Score, from zero to
// one.
Confidence float64 `json:"confidence"`
// Legend is the question's rubric echoed back, keyed by score, so that a
// score can be rendered without holding on to the request.
// Numbers inside structured descriptions decode as [json.Number] to
// preserve their precision and range.
Legend map[int]Entry `json:"legend"`
// Probabilities holds one probability per level of the rubric.
Probabilities map[int]float64 `json:"probabilities"`
}
A ScoreAnswer answers a ScoreQuestion.
func (*ScoreAnswer) AnswerType ¶
func (a *ScoreAnswer) AnswerType() string
AnswerType returns TypeScore.
func (*ScoreAnswer) Description ¶
func (a *ScoreAnswer) Description() Entry
Description reports what the rubric says about ScoreAnswer.Level, and nil when that level was left undescribed.
func (*ScoreAnswer) Level ¶
func (a *ScoreAnswer) Level() int
Level reports the rubric level the expected score lands on, rounding a score that fell between two of them.
Round to render a score, not to decide on one: rounding 1.5 and 2.4 to the same level discards the difference the expected score exists to express.
func (*ScoreAnswer) MarshalJSON ¶
func (a *ScoreAnswer) MarshalJSON() ([]byte, error)
func (*ScoreAnswer) UnmarshalJSON ¶ added in v0.1.0
func (a *ScoreAnswer) UnmarshalJSON(data []byte) error
type ScoreCriteria ¶
type ScoreCriteria []Entry
ScoreCriteria is a rubric: descriptions of each level in ascending order, scored from zero by position. The service accepts one to ten non-null levels. A single level always scores zero. This follows the running service; the documentation and TypeScript SDK currently require at least two levels.
typesafe.ScoreCriteria{"can wait", "this week", "today", "right now"}
scores 0 through 3. The answer's expected score may land between levels; see ScoreAnswer.Score.
type ScoreQuestion ¶
type ScoreQuestion struct {
// Instructions is the question. A nil Instructions is sent as null.
Instructions Entry `json:"instructions"`
// Criteria is the rubric, which must describe at least one level.
Criteria ScoreCriteria `json:"criteria"`
}
A ScoreQuestion rates the state against an ordered rubric. See ScoreAnswer.
func (*ScoreQuestion) MarshalJSON ¶
func (q *ScoreQuestion) MarshalJSON() ([]byte, error)
func (*ScoreQuestion) QuestionType ¶
func (q *ScoreQuestion) QuestionType() string
QuestionType returns TypeScore.
type SystemOneRequest ¶
type SystemOneRequest struct {
// State is what the questions are asked about, and is required: text, a
// JSON object, or a JSON array. The service refuses a null state, and
// refuses one that encodes as a number or a boolean.
//
// An empty string, object, or array is a state; nil is not.
State Entry `json:"state"`
// Questions are the questions to answer, keyed by the names their answers
// come back under. At least one is required.
Questions Questions `json:"questions"`
// Model overrides the client's default model for this request.
Model string `json:"model"`
// Extra carries additional top-level request properties, for using a
// service feature this SDK does not model yet. Its keys are merged into the
// request body and may not collide with the fields above.
Extra map[string]any `json:"-"`
}
A SystemOneRequest asks named questions about a piece of state.
func (*SystemOneRequest) MarshalJSON ¶
func (r *SystemOneRequest) MarshalJSON() ([]byte, error)
MarshalJSON validates the encoded request and merges SystemOneRequest.Extra.
The fields are encoded and decoded again rather than being listed here, so that the struct tags stay the only statement of what this request sends. A second list would be a second owner of that fact, and the collision check below would start passing keys that do collide.
func (*SystemOneRequest) Validate ¶
func (r *SystemOneRequest) Validate() error
Validate reports whether the request can be sent.
It encodes the request and checks the same JSON rules as Client.SystemOne, including Extra collisions. Call it when assembling a request separately from sending it. Custom JSON marshalers must be safe to call again.
type SystemOneResult ¶
type SystemOneResult struct {
// Model is the model that answered, resolved from an alias such as
// "jev-latest" to the release it names.
Model string `json:"model"`
// Answers holds one answer per question, under the same names.
Answers Answers `json:"answers"`
// Usage reports the tokens the request consumed.
Usage Usage `json:"usage"`
// Meta reports HTTP metadata about the response.
Meta Meta `json:"-"`
}
A SystemOneResult holds the answers to a SystemOneRequest.
type UnknownAnswer ¶
type UnknownAnswer struct {
// Raw is the answer's JSON object, including its nonempty type field.
// It is the sole source of both [UnknownAnswer.AnswerType] and the encoded
// answer, so editing it cannot leave a separate type field out of sync.
Raw json.RawMessage
}
An UnknownAnswer carries an answer whose type this SDK does not model, which is what a service newer than the SDK produces.
Decoding keeps it rather than failing, so that one unrecognized answer does not cost you the answers beside it in the same response. The accessors still refuse it: an answer the SDK cannot interpret is never silently treated as one it can.
func (*UnknownAnswer) AnswerType ¶
func (a *UnknownAnswer) AnswerType() string
AnswerType reads the discriminant from Raw. It returns an empty string if Raw is malformed, which UnknownAnswer.MarshalJSON reports as an error.
func (*UnknownAnswer) MarshalJSON ¶
func (a *UnknownAnswer) MarshalJSON() ([]byte, error)
MarshalJSON returns the bytes the service sent. An answer this package could not interpret is one it has no business rewriting.
func (*UnknownAnswer) UnmarshalJSON ¶ added in v0.1.0
func (a *UnknownAnswer) UnmarshalJSON(data []byte) error