typesafe

package module
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Sep 17, 2026 License: MIT Imports: 20 Imported by: 0

README

typesafego

A Go client for the TypeSafe AI API.

TypeSafe's System One model, Jev, evaluates typed questions against a state and returns structured, probabilistic answers instead of generated text:

  • Noul (yes/no) → a probability that the answer is yes
  • Choice → the selected option, the probability of every option, and a confidence
  • Score → a probability-weighted position on an ordered rubric, the probability of every level, and a confidence

Every question in a request is evaluated independently against the same state, so you can ask all the questions your code needs in one call.

The library depends only on the Go standard library.

Install

go get github.com/adibhanna/typesafego

Requires Go 1.24 or later. The package name is typesafe:

import typesafe "github.com/adibhanna/typesafego"

Quick start

package main

import (
	"context"
	"fmt"
	"log"

	typesafe "github.com/adibhanna/typesafego"
)

func main() {
	client, err := typesafe.NewClient() // reads TYPESAFE_API_KEY
	if err != nil {
		log.Fatal(err)
	}

	resp, err := client.SystemOne(context.Background(), typesafe.SystemOneRequest{
		State: "My payment failed twice and I still got charged. Fix this now!",
		Questions: typesafe.Questions{
			"is_urgent": typesafe.Noul("Does the customer convey urgency?"),
			"department": typesafe.Choice("Which team should handle this?", map[string]any{
				"billing":   "Payments, invoices, refunds",
				"technical": "Bugs, outages, integrations",
				"sales":     "Pricing, upgrades, new accounts",
			}),
			"frustration": typesafe.Score("How frustrated is the customer?",
				"Calm", "Frustrated", "Very angry"),
		},
	})
	if err != nil {
		log.Fatal(err)
	}

	urgent, _ := resp.Noul("is_urgent")
	department, _ := resp.Choice("department")
	frustration, _ := resp.Score("frustration")

	fmt.Println(urgent.IsTrue(0.5), urgent.Noul)                 // true 0.93
	fmt.Println(department.Choice, department.Confidence)        // billing 0.81
	fmt.Println(frustration.Score, frustration.Label())          // 1.7 Very angry
	fmt.Println(resp.Model, resp.Usage.InputTokens)              // jev-1.13.0 312
}

A complete, runnable version lives in examples/triage:

export TYPESAFE_API_KEY=sk-...
go run ./examples/triage "My card was charged twice and support is ignoring me!"

Questions

State can be any JSON-encodable value: a string, a struct, a map, or a slice. Question instructions and criteria can be strings or structured JSON values (objects, arrays), which the model reads as-is.

Noul (yes/no)
typesafe.Noul("Is the customer asking for a refund?")

// Optionally spell out what a yes and a no mean.
typesafe.Noul("Is the customer asking for a refund?").WithCriteria(
	"The customer explicitly asks for money back or a chargeback.",
	"Anything else, including billing questions without a refund request.",
)

Answer: *NoulAnswer with Noul (probability of yes) and IsTrue(threshold), which is Noul >= threshold.

Choice
// Options with descriptions. A nil description sends null.
typesafe.Choice("Which team should handle this?", map[string]any{
	"billing":   "Payments, invoices, refunds",
	"technical": map[string]any{"handles": []string{"bugs", "outages"}},
	"sales":     nil,
})

// Options without descriptions.
typesafe.ChoiceOf("What is the sentiment?", "positive", "neutral", "negative")

Answer: *ChoiceAnswer with Choice, Probabilities (option → probability), Confidence, plus ProbabilityOf(option) and Ranked() (options sorted by probability, highest first).

Score
typesafe.Score("How frustrated is the customer?",
	"Calm",         // level 0
	"Frustrated",   // level 1
	"Very angry",   // level 2
)

Levels are ordered from lowest to highest and each may be a string or a structured value. At least two are required.

Answer: *ScoreAnswer with Score (probability-weighted, may fall between levels), Legend (level → description), Probabilities (level → probability), and Confidence, plus:

Method Returns
Level() the most probable level (lowest index on a tie)
NearestLevel() Score rounded to the nearest level, clamped to the rubric
Label() / LabelOf(level) the legend entry for Level() / a given level
ProbabilityOf(level) the probability of a level
Normalized() Score / MaxLevel(), a 0..1 value comparable across rubrics of different sizes
MaxLevel() the highest level index

Level() and NearestLevel() differ when the distribution is split: with probabilities [0.5, 0, 0.5] the score is 1.0, so NearestLevel() is 1 while Level() is 0.

Raw questions

RawQuestion sends an arbitrary JSON object for fields or question types this package does not model. It must contain a string "type".

typesafe.RawQuestion{"type": "noul", "instructions": "Is this spam?"}

Answers of a type this package does not know decode to *UnknownAnswer carrying the raw JSON, so new answer types never break existing code.

Reading answers

SystemOneResponse.Answers is a map[string]Answer. The typed getters return an error wrapping ErrAnswerNotFound if the id is missing and ErrAnswerType if the answer has another type:

department, err := resp.Choice("department")
if err != nil {
	return err // typesafe: answer not found: "department"
}
Confidence-gated routing
department, _ := resp.Choice("department")
if department.Confidence < 0.7 {
	return routeToHuman(ticket, department.Ranked())
}
return routeTo(department.Choice)
Composite scores

Normalized() puts rubrics with different numbers of levels on the same 0..1 scale so they can be weighted together with Noul probabilities:

priority := 0.4*frustration.Normalized() + 0.35*urgent.Noul + 0.25*churn.Noul

Configuration

NewClient reads its defaults from the environment, and explicit options take precedence:

Option Environment variable Default
WithAPIKey(key) TYPESAFE_API_KEY required
WithBaseURL(url) TYPESAFE_BASE_URL https://api.typesafe.ai
WithModel(model) TYPESAFE_DEFAULT_MODEL jev-latest
WithTimeout(d) 10s per attempt (0 disables)
WithRetryPolicy(p) / WithMaxRetries(n) DefaultRetryPolicy()
WithHTTPClient(c) http.DefaultClient
WithHeader(key, value)
WithLogger(l) discard
policy := typesafe.DefaultRetryPolicy()
policy.MaxRetries = 4

client, err := typesafe.NewClient(
	typesafe.WithAPIKey(os.Getenv("MY_TYPESAFE_KEY")),
	typesafe.WithModel(typesafe.ModelJevPreview),
	typesafe.WithTimeout(20*time.Second),
	typesafe.WithRetryPolicy(policy),
	typesafe.WithLogger(slog.Default()),
)

The same options can be applied to a single call without changing the client:

resp, err := client.SystemOne(ctx, req, typesafe.WithModel("jev-1.13.0"), typesafe.WithMaxRetries(0))

The model used for a request is, in order of precedence: SystemOneRequest.Model, a per-call WithModel, the client's WithModel, TYPESAFE_DEFAULT_MODEL, then jev-latest.

Models

ModelJevLatest (jev-latest) follows the most recent stable release and ModelJevPreview (jev-preview) the most recent release of any kind. Pin a versioned ID such as jev-1.13.0 if you have tuned thresholds against a specific version; the response's Model field always reports the versioned ID that answered. client.ListModels(ctx) returns the available models.

Timeouts, retries, and errors

WithTimeout bounds a single HTTP attempt (default 10s). Each retry gets a fresh timeout; use the context deadline to bound the whole call. When a retry's delay would run past that deadline, the client returns the last error (for example the 429 *APIError) right away instead of waiting only to fail with context.DeadlineExceeded.

DefaultRetryPolicy() retries up to 2 times with exponential backoff from 500ms doubling to 5s with 25% jitter. It retries HTTP 408, 429, and all 5xx responses (including TypeSafe's 529 Overloaded), connection errors, and timeouts, and it honors retry-after-ms and Retry-After headers of up to 60s. Retried attempts carry an X-Typesafe-Retry-Count header. Every field of RetryPolicy can be changed; WithMaxRetries(0) disables retries.

Errors fall into three groups:

var apiErr *typesafe.APIError
switch {
case errors.As(err, &apiErr):
	// Non-2xx response after retries. StatusCode, Message, Body, Header,
	// and RequestID are available; RetryAfter() reads the retry headers.
	log.Printf("%d %s (request id %s)", apiErr.StatusCode, apiErr.Message, apiErr.RequestID)
case errors.Is(err, typesafe.ErrInvalidRequest):
	// Rejected locally before sending: nil state, no questions, a choice
	// without options, a score with fewer than two levels, ...
case errors.Is(err, typesafe.ErrInvalidResponse):
	// The body could not be decoded.
default:
	// Transport failure or context cancellation; errors.Is works on the
	// underlying error, e.g. errors.Is(err, context.DeadlineExceeded).
}

APIError.Error() reads like typesafe: POST https://api.typesafe.ai/v1/systemone: 401 Unauthorized: Invalid API key (request id req_123). FastAPI-style validation errors are flattened to questions.q.criteria: Field required.

Testing your code

The client is an ordinary HTTP client, so point it at an httptest.Server:

server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json")
	io.WriteString(w, `{"model":"jev-1.13.0","answers":{"is_urgent":{"type":"noul","noul":0.9}},"usage":{"input_tokens":10,"output_tokens":1}}`)
}))
defer server.Close()

client, err := typesafe.NewClient(
	typesafe.WithAPIKey("test"),
	typesafe.WithBaseURL(server.URL),
	typesafe.WithMaxRetries(0),
)

Answer types are plain structs, so code that consumes them can be tested without a server:

answer := &typesafe.ScoreAnswer{Score: 1.6, Legend: []any{"Calm", "Frustrated", "Very angry"}}

Development

gofmt -l .
go vet ./...
go test -race ./...

License

MIT

Documentation

Overview

Package typesafe is a Go client for the TypeSafe AI API.

TypeSafe's System One model, Jev, evaluates typed questions against a state and returns structured answers instead of generated text: a probability for a yes/no (Noul) question, a selected option with a full probability distribution for a Choice question, and a probability-weighted position on an ordered rubric for a Score question. Every question in a request is evaluated independently against the same state, so ask all the questions your code might need in one call.

Create a client, then send a state and a map of named questions:

client, err := typesafe.NewClient() // reads TYPESAFE_API_KEY
if err != nil {
	log.Fatal(err)
}

resp, err := client.SystemOne(ctx, typesafe.SystemOneRequest{
	State: "Help! My payouts have been failing for 3 days.",
	Questions: typesafe.Questions{
		"department": typesafe.Choice("Which team should handle this?", map[string]any{
			"billing":   "Payments, invoicing, refunds",
			"technical": "Bugs, outages, integrations",
			"sales":     "Pricing, upgrades, new accounts",
		}),
		"frustration": typesafe.Score("How frustrated is the customer?",
			"Calm", "Frustrated", "Very angry"),
		"is_urgent": typesafe.Noul("Does this convey urgency?"),
	},
})
if err != nil {
	log.Fatal(err)
}

department, _ := resp.Choice("department")
frustration, _ := resp.Score("frustration")
urgent, _ := resp.Noul("is_urgent")

fmt.Println(department.Choice, department.Confidence)
fmt.Println(frustration.Score, frustration.Label())
fmt.Println(urgent.IsTrue(0.8))

The client retries rate-limited (429), overloaded (529), and other transient failures with exponential backoff, honoring Retry-After headers. Non-2xx responses that survive retries are returned as an *APIError.

See https://docs.typesafe.ai for the API documentation.

Example
package main

import (
	"context"
	"fmt"
	"log"

	typesafe "github.com/adibhanna/typesafego"
)

func main() {
	client, err := typesafe.NewClient() // reads TYPESAFE_API_KEY
	if err != nil {
		log.Fatal(err)
	}

	resp, err := client.SystemOne(context.Background(), typesafe.SystemOneRequest{
		State: "My payment failed twice and I still got charged. Fix this now!",
		Questions: typesafe.Questions{
			"is_urgent":   typesafe.Noul("Does the customer convey urgency?"),
			"department":  typesafe.ChoiceOf("Which team should handle this?", "billing", "technical", "sales"),
			"frustration": typesafe.Score("How frustrated is the customer?", "Calm", "Frustrated", "Very angry"),
		},
	})
	if err != nil {
		log.Fatal(err)
	}

	urgent, _ := resp.Noul("is_urgent")
	department, _ := resp.Choice("department")
	frustration, _ := resp.Score("frustration")

	fmt.Printf("urgent: %v (p=%.2f)\n", urgent.IsTrue(0.5), urgent.Noul)
	fmt.Printf("department: %s (confidence %.2f)\n", department.Choice, department.Confidence)
	fmt.Printf("frustration: %.2f → %v\n", frustration.Score, frustration.Label())
}

Index

Examples

Constants

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

	// DefaultModel is the model used when a request does not name one.
	// It is an alias for the most recent stable Jev release.
	DefaultModel = ModelJevLatest

	// DefaultTimeout is the default timeout for a single HTTP attempt.
	// Retries each get their own timeout; use the context deadline to bound
	// the whole call.
	DefaultTimeout = 10 * time.Second
)

Defaults used when neither an Option nor an environment variable sets a value.

View Source
const (
	// EnvAPIKey holds the API key.
	EnvAPIKey = "TYPESAFE_API_KEY"

	// EnvBaseURL overrides the API root.
	EnvBaseURL = "TYPESAFE_BASE_URL"

	// EnvDefaultModel overrides the default model.
	EnvDefaultModel = "TYPESAFE_DEFAULT_MODEL"
)

Environment variables read by NewClient. Explicit options take precedence over environment variables; empty or whitespace-only values are ignored.

View Source
const (
	// ModelJevLatest is the most recent stable, official Jev release.
	ModelJevLatest = "jev-latest"

	// ModelJevPreview is the most recent release, whether or not it is an
	// official one.
	ModelJevPreview = "jev-preview"
)

Model aliases accepted by the API's model field. An alias moves when a new release ships; pin a versioned ID such as "jev-1.13.0" (see https://docs.typesafe.ai/models) if you have tuned thresholds against a specific version. The response's Model field reports the versioned ID that answered.

View Source
const StatusOverloaded = 529

StatusOverloaded is the non-standard status the API returns when it is temporarily overloaded.

View Source
const Version = "0.1.0"

Version is the version of this library, sent in the User-Agent header.

Variables

View Source
var (
	// ErrMissingAPIKey is returned by NewClient when no API key is configured.
	ErrMissingAPIKey = errors.New("typesafe: missing API key: set " + EnvAPIKey + " or use WithAPIKey")

	// ErrInvalidRequest is returned before a request is sent when it fails
	// local validation, such as a missing state or an empty question set.
	ErrInvalidRequest = errors.New("typesafe: invalid request")

	// ErrInvalidResponse is returned when a 2xx response body cannot be
	// decoded.
	ErrInvalidResponse = errors.New("typesafe: invalid response")

	// ErrAnswerNotFound is returned by the typed answer getters when no answer
	// has the requested id.
	ErrAnswerNotFound = errors.New("typesafe: answer not found")

	// ErrAnswerType is returned by the typed answer getters when the answer
	// has a different type than requested.
	ErrAnswerType = errors.New("typesafe: unexpected answer type")
)

Sentinel errors. Match them with errors.Is; they are wrapped with detail about what went wrong.

Functions

func DefaultShouldRetryStatus

func DefaultShouldRetryStatus(status int) bool

DefaultShouldRetryStatus reports whether a status is retried by default: 408 Request Timeout, 429 Too Many Requests, and every 5xx status, including 529 Overloaded.

Types

type APIError

type APIError struct {
	// StatusCode is the HTTP status code.
	StatusCode int

	// Method and URL identify the request. The URL never includes credentials.
	Method string
	URL    string

	// Message is the human-readable error extracted from the response body,
	// or "" if none was found.
	Message string

	// Body is the raw response body.
	Body []byte

	// Header holds the HTTP response headers.
	Header http.Header

	// RequestID is the x-typesafe-request-id response header, useful when
	// contacting support. Empty if the server did not send one.
	RequestID string
}

APIError is returned when the API responds with a non-2xx status after any retries. Match it with errors.As:

var apiErr *typesafe.APIError
if errors.As(err, &apiErr) && apiErr.StatusCode == http.StatusTooManyRequests {
	// rate limited
}

Status codes documented by the API: 401 (bad API key), 422 (request failed validation; Message names the field), 429 (rate limited) and 529 (overloaded). The client retries 429, 529 and other transient statuses by default; see RetryPolicy.

Example
package main

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

	typesafe "github.com/adibhanna/typesafego"
)

func main() {
	client, err := typesafe.NewClient(typesafe.WithAPIKey("sk-invalid"))
	if err != nil {
		log.Fatal(err)
	}
	_, err = client.SystemOne(context.Background(), typesafe.SystemOneRequest{
		State:     "hello",
		Questions: typesafe.Questions{"q": typesafe.Noul("Is this a greeting?")},
	})

	var apiErr *typesafe.APIError
	switch {
	case errors.As(err, &apiErr) && apiErr.StatusCode == 401:
		fmt.Println("bad API key; request id", apiErr.RequestID)
	case errors.As(err, &apiErr) && apiErr.StatusCode == 429:
		delay, _ := apiErr.RetryAfter()
		fmt.Println("rate limited; retry after", delay)
	case errors.Is(err, typesafe.ErrInvalidRequest):
		fmt.Println("fix the request:", err)
	case err != nil:
		fmt.Println("transport or decoding failure:", err)
	}
}

func (*APIError) Error

func (e *APIError) Error() string

Error formats the error as "typesafe: POST https://api.typesafe.ai/v1/systemone: 429 Too Many Requests: <message> (request id <id>)".

func (*APIError) RetryAfter

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

RetryAfter returns the delay requested by the response's retry-after-ms or Retry-After header. ok is false if neither header holds a valid delay.

type Answer

type Answer interface {
	json.Marshaler

	// Type returns the answer's wire type, which matches its question's.
	Type() QuestionType
	// contains filtered or unexported methods
}

Answer is the typed result of one question.

The concrete types are *NoulAnswer, *ChoiceAnswer and *ScoreAnswer, plus *UnknownAnswer for answer types this package does not know about. Use a type switch, or the typed getters on Answers and SystemOneResponse. The interface is sealed: only this package's types implement it.

type Answers

type Answers map[string]Answer

Answers maps question ids to answers, under the same ids used in the request. Values are *NoulAnswer, *ChoiceAnswer, *ScoreAnswer or *UnknownAnswer, never nil.

func (Answers) Choice

func (a Answers) Choice(id string) (*ChoiceAnswer, error)

Choice returns the choice answer with the given id. The error wraps ErrAnswerNotFound if the id is missing and ErrAnswerType if the answer is of another type.

func (Answers) Noul

func (a Answers) Noul(id string) (*NoulAnswer, error)

Noul returns the yes/no answer with the given id. The error wraps ErrAnswerNotFound if the id is missing and ErrAnswerType if the answer is of another type.

func (Answers) Score

func (a Answers) Score(id string) (*ScoreAnswer, error)

Score returns the score answer with the given id. The error wraps ErrAnswerNotFound if the id is missing and ErrAnswerType if the answer is of another type.

func (*Answers) UnmarshalJSON

func (a *Answers) UnmarshalJSON(data []byte) error

UnmarshalJSON decodes a map of answers, dispatching on each answer's "type". Unknown types decode to *UnknownAnswer so that new answer types do not break existing code, and a null answer is left out of the map.

type ChoiceAnswer

type ChoiceAnswer struct {
	// Choice is the option with the highest probability.
	Choice string `json:"choice"`

	// Probabilities maps every option to its probability. They sum to 1.
	Probabilities map[string]float64 `json:"probabilities"`

	// Confidence, from 0 to 1, is how certain the model is about Choice. It
	// is derived from the probability distribution; see
	// https://docs.typesafe.ai/confidence.
	Confidence float64 `json:"confidence"`
}

ChoiceAnswer answers a ChoiceQuestion.

func (*ChoiceAnswer) MarshalJSON

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

MarshalJSON encodes the answer in the API's wire format.

func (*ChoiceAnswer) ProbabilityOf

func (a *ChoiceAnswer) ProbabilityOf(option string) float64

ProbabilityOf returns the probability of the given option, or 0 if the option is not in the answer.

func (*ChoiceAnswer) Ranked

func (a *ChoiceAnswer) Ranked() []RankedOption

Ranked returns every option ordered from most to least probable. Options with equal probability are ordered by name.

func (*ChoiceAnswer) Type

func (*ChoiceAnswer) Type() QuestionType

Type returns TypeChoice.

type ChoiceQuestion

type ChoiceQuestion struct {
	// Instructions says what the model should decide. It may be a string or
	// JSON structure.
	Instructions any

	// Options maps each option name to its description. A description may be
	// a string, JSON structure, or nil when the name speaks for itself. It is
	// sent as the question's "criteria".
	Options map[string]any
}

ChoiceQuestion picks one option from a set you define. The answer is a *ChoiceAnswer with the selected option, the probability of every option, and a confidence.

func Choice

func Choice(instructions any, options map[string]any) ChoiceQuestion

Choice builds a question that selects one of the given options. Each option maps to a description; use nil for options that need none.

typesafe.Choice("Which team should handle this?", map[string]any{
	"billing":   "Payments, invoicing, refunds",
	"technical": "Bugs, outages, integrations",
	"sales":     nil,
})
Example (Structured)
package main

import (
	typesafe "github.com/adibhanna/typesafego"
)

func main() {
	// Instructions and option descriptions may be any JSON value, not only
	// strings, so a rubric can be given as structured data.
	question := typesafe.Choice(
		map[string]any{
			"task":    "Route the ticket to the right team.",
			"context": "The company sells developer tools.",
		},
		map[string]any{
			"billing":   "Payments, invoices, refunds and subscription changes.",
			"technical": map[string]any{"handles": []string{"bugs", "outages", "API errors"}},
			"sales":     nil, // no description
		},
	)
	_ = typesafe.Questions{"department": question}
}

func ChoiceOf

func ChoiceOf(instructions any, options ...string) ChoiceQuestion

ChoiceOf builds a question that selects one of the named options, none of which carries a description.

typesafe.ChoiceOf("What is the customer's sentiment?", "positive", "neutral", "negative")

func (ChoiceQuestion) MarshalJSON

func (q ChoiceQuestion) MarshalJSON() ([]byte, error)

MarshalJSON encodes the question in the API's wire format.

func (ChoiceQuestion) Type

Type returns TypeChoice.

type Client

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

Client calls the TypeSafe API. Create one with NewClient and share it; it is safe for concurrent use.

func NewClient

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

NewClient returns a client configured from the environment and opts.

The API key comes from WithAPIKey or the TYPESAFE_API_KEY environment variable; NewClient returns ErrMissingAPIKey if neither is set. The base URL and default model come from WithBaseURL and WithModel, then from TYPESAFE_BASE_URL and TYPESAFE_DEFAULT_MODEL, then from DefaultBaseURL and DefaultModel. Retries use DefaultRetryPolicy unless WithRetryPolicy or WithMaxRetries says otherwise.

Example (Options)
package main

import (
	"context"
	"log"
	"time"

	typesafe "github.com/adibhanna/typesafego"
)

func main() {
	policy := typesafe.DefaultRetryPolicy()
	policy.MaxRetries = 4

	client, err := typesafe.NewClient(
		typesafe.WithAPIKey("sk-..."),
		typesafe.WithModel(typesafe.ModelJevPreview),
		typesafe.WithTimeout(20*time.Second),
		typesafe.WithRetryPolicy(policy),
		typesafe.WithHeader("X-Request-Source", "billing-worker"),
	)
	if err != nil {
		log.Fatal(err)
	}

	// Options can also be applied to a single call without changing the client.
	_, _ = client.SystemOne(context.Background(), typesafe.SystemOneRequest{
		State:     "hello",
		Questions: typesafe.Questions{"q": typesafe.Noul("Is this a greeting?")},
	}, typesafe.WithModel("jev-1.13.0"), typesafe.WithMaxRetries(0))
}

func (*Client) BaseURL

func (c *Client) BaseURL() string

BaseURL returns the API root requests are sent to, without a trailing slash.

func (*Client) ListModels

func (c *Client) ListModels(ctx context.Context, opts ...Option) (*ListModelsResponse, error)

ListModels returns the models and aliases your account can name in a request. opts override the client's settings for this call only.

func (*Client) Model

func (c *Client) Model() string

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

func (*Client) SystemOne

func (c *Client) SystemOne(ctx context.Context, req SystemOneRequest, opts ...Option) (*SystemOneResponse, error)

SystemOne evaluates req.State against req.Questions and returns one answer per question. Every question is evaluated independently against the same state, so ask everything you might need in one call.

The request is validated locally first (see Questions.Validate); failures wrap ErrInvalidRequest. Transient failures are retried according to the retry policy; a retry whose delay would run past ctx's deadline is skipped and the last failure returned instead. A non-2xx response that survives retries is returned as an *APIError; transport failures are returned wrapped, so errors.Is(err, context.DeadlineExceeded) detects a timeout. opts override the client's settings for this call only.

Example (ConfidenceGating)
package main

import (
	"context"
	"fmt"
	"log"

	typesafe "github.com/adibhanna/typesafego"
)

func main() {
	client, err := typesafe.NewClient()
	if err != nil {
		log.Fatal(err)
	}
	resp, err := client.SystemOne(context.Background(), typesafe.SystemOneRequest{
		State:     "Can I get my money back? The app crashed during checkout.",
		Questions: typesafe.Questions{"department": typesafe.ChoiceOf("Which team?", "billing", "technical")},
	})
	if err != nil {
		log.Fatal(err)
	}
	department, err := resp.Choice("department")
	if err != nil {
		log.Fatal(err)
	}
	if department.Confidence < 0.7 {
		fmt.Println("route to a human:", department.Ranked())
		return
	}
	fmt.Println("route to", department.Choice)
}

type ListModelsResponse

type ListModelsResponse struct {
	// Models lists the names your account can send in the model field. It
	// currently lists the aliases; versioned IDs such as "jev-1.13.0" are
	// accepted whether or not they appear here.
	Models []ModelInfo `json:"models"`

	// RequestID is the x-typesafe-request-id response header, if any.
	RequestID string `json:"-"`

	// Header holds the HTTP response headers.
	Header http.Header `json:"-"`
}

ListModelsResponse is the result of Client.ListModels.

type ModelInfo

type ModelInfo struct {
	// Name is the ID or alias accepted by the request's model field.
	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"`
}

ModelInfo describes a model or alias available to your account.

type NoulAnswer

type NoulAnswer struct {
	// Noul is the probability that the answer is yes, from 0 (no) to 1 (yes).
	Noul float64 `json:"noul"`
}

NoulAnswer answers a NoulQuestion.

func (*NoulAnswer) IsTrue

func (a *NoulAnswer) IsTrue(threshold float64) bool

IsTrue reports whether the probability of yes is at least threshold. Choose the threshold for the cost of being wrong: 0.5 treats the answer as a plain boolean; a higher value such as 0.8 acts only when the model is sure.

func (*NoulAnswer) MarshalJSON

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

MarshalJSON encodes the answer in the API's wire format.

func (*NoulAnswer) Type

func (*NoulAnswer) Type() QuestionType

Type returns TypeNoul.

type NoulCriteria

type NoulCriteria struct {
	// True describes what a yes (a value near 1) means.
	True any `json:"true,omitzero"`

	// False describes what a no (a value near 0) means.
	False any `json:"false,omitzero"`
}

NoulCriteria optionally describes what a yes and a no mean for a NoulQuestion. Either field may be a string, an object, an array, or nil to leave it out.

type NoulQuestion

type NoulQuestion struct {
	// Instructions is the yes/no question to evaluate. It may be a string or
	// JSON structure (an object or array); see
	// https://docs.typesafe.ai/primitives/advanced.
	Instructions any

	// Criteria optionally describes what a yes and a no mean. It is omitted
	// from the request when nil or empty.
	Criteria *NoulCriteria
}

NoulQuestion is a yes/no question. The answer is a *NoulAnswer holding the probability that the answer is yes.

func Noul

func Noul(instructions any) NoulQuestion

Noul builds a yes/no question.

typesafe.Noul("Does this convey urgency?")

func (NoulQuestion) MarshalJSON

func (q NoulQuestion) MarshalJSON() ([]byte, error)

MarshalJSON encodes the question in the API's wire format.

func (NoulQuestion) Type

func (NoulQuestion) Type() QuestionType

Type returns TypeNoul.

func (NoulQuestion) WithCriteria

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

WithCriteria returns a copy of the question that describes what a yes and a no mean. Pass nil for either side to leave it out.

typesafe.Noul("Does this convey urgency?").
	WithCriteria("Explicitly time-sensitive", "No urgency expressed")
Example
package main

import (
	typesafe "github.com/adibhanna/typesafego"
)

func main() {
	question := typesafe.Noul("Is the customer asking for a refund?").
		WithCriteria(
			"The customer explicitly asks for money back or a chargeback.",
			"The customer asks about anything else, including billing questions without a refund request.",
		)
	_ = typesafe.Questions{"is_refund": question}
}

type Option

type Option func(*config) error

Option configures a Client when passed to NewClient, or overrides the client's settings for one call when passed to Client.SystemOne or Client.ListModels.

func WithAPIKey

func WithAPIKey(key string) Option

WithAPIKey sets the API key sent in the Authorization header. It takes precedence over the TYPESAFE_API_KEY environment variable. An empty key is ignored.

func WithBaseURL

func WithBaseURL(baseURL string) Option

WithBaseURL sets the API root, such as "https://api.typesafe.ai" or the address of a proxy or test server. Paths are appended to it: the evaluation endpoint is <base>/v1/systemone. It takes precedence over the TYPESAFE_BASE_URL environment variable. An empty URL is ignored.

func WithHTTPClient

func WithHTTPClient(httpClient *http.Client) Option

WithHTTPClient sets the *http.Client used to send requests. The default is http.DefaultClient. Per-attempt timeouts are applied through the request context, so the client's own Timeout is not needed. A nil client is ignored.

func WithHeader

func WithHeader(key, value string) Option

WithHeader adds a header to every request. Calling it again with the same key replaces the earlier value. The Authorization, Content-Type and Accept headers are managed by the client and cannot be overridden; User-Agent can.

func WithLogger

func WithLogger(logger *slog.Logger) Option

WithLogger sets the logger. Requests and responses are logged at Debug level and retries at Info level; API keys and bodies are never logged. The default discards everything. A nil logger is ignored.

func WithMaxRetries

func WithMaxRetries(n int) Option

WithMaxRetries sets the number of retries after the first attempt, keeping the rest of the retry policy. 0 disables retries.

func WithModel

func WithModel(model string) Option

WithModel sets the model used by requests that do not name one, such as "jev-latest" or a pinned version like "jev-1.13.0". It takes precedence over the TYPESAFE_DEFAULT_MODEL environment variable. An empty model is ignored.

func WithRetryPolicy

func WithRetryPolicy(policy RetryPolicy) Option

WithRetryPolicy replaces the retry policy. Start from DefaultRetryPolicy and adjust it; the zero RetryPolicy disables retries.

func WithTimeout

func WithTimeout(d time.Duration) Option

WithTimeout sets the timeout for a single HTTP attempt, including reading the response body. The default is DefaultTimeout (10s). Each retry gets a fresh timeout; bound the whole call with the context deadline. 0 disables the per-attempt timeout.

type Question

type Question interface {
	json.Marshaler

	// Type returns the question's wire type.
	Type() QuestionType
}

Question is a typed question sent in the questions map of a request.

The three built-in implementations are NoulQuestion, ChoiceQuestion and ScoreQuestion, normally built with Noul, Choice, ChoiceOf and Score. RawQuestion sends an arbitrary JSON object for anything the typed forms do not cover.

type QuestionType

type QuestionType string

QuestionType identifies the kind of a Question and of the Answer it produces.

const (
	// TypeNoul is a yes/no question answered with the probability of yes.
	TypeNoul QuestionType = "noul"

	// TypeChoice selects one option from a set and returns the full
	// probability distribution over the options.
	TypeChoice QuestionType = "choice"

	// TypeScore rates the state against an ordered rubric and returns a
	// probability-weighted position on it.
	TypeScore QuestionType = "score"
)

The three System One question types.

type Questions

type Questions map[string]Question

Questions maps question ids to questions. You choose the ids; answers come back under the same ids. The id is not shown to the model.

func (Questions) Validate

func (q Questions) Validate() error

Validate reports the first problem found in the question set: an empty set, a nil question, a ChoiceQuestion without options, a ScoreQuestion with fewer than two levels, or a RawQuestion without a "type". Errors wrap ErrInvalidRequest. Client.SystemOne calls Validate before sending.

type RankedOption

type RankedOption struct {
	Option      string
	Probability float64
}

RankedOption is an option paired with its probability.

type RawQuestion

type RawQuestion map[string]any

RawQuestion sends an arbitrary JSON object as a question. Use it for fields or question types this package does not model. It must contain a string "type".

typesafe.RawQuestion{
	"type":         "noul",
	"instructions": "Is this spam?",
}

func (RawQuestion) MarshalJSON

func (q RawQuestion) MarshalJSON() ([]byte, error)

MarshalJSON encodes the map as a JSON object.

func (RawQuestion) Type

func (q RawQuestion) Type() QuestionType

Type returns the question's "type" entry, or "" if it is missing or not a string.

type RetryPolicy

type RetryPolicy struct {
	// MaxRetries is the number of retries after the first attempt. 0 disables
	// retries.
	MaxRetries int

	// InitialBackoff is the delay before the first retry. It doubles on every
	// retry up to MaxBackoff.
	InitialBackoff time.Duration

	// MaxBackoff caps the computed backoff delay.
	MaxBackoff time.Duration

	// Jitter is the fraction of each backoff delay to randomly subtract, from
	// 0 (none) to 1.
	Jitter float64

	// ShouldRetryStatus reports whether a response status is worth retrying.
	// nil means DefaultShouldRetryStatus.
	ShouldRetryStatus func(status int) bool

	// RespectRetryAfter honors retry-after-ms and Retry-After response
	// headers instead of the computed backoff.
	RespectRetryAfter bool

	// MaxRetryAfter is the longest server-requested delay to honor; longer
	// ones fall back to the computed backoff. Leaving it zero therefore
	// ignores every non-zero Retry-After; DefaultRetryPolicy uses 60s.
	MaxRetryAfter time.Duration

	// RetryConnectionErrors retries attempts that fail before a response is
	// read, such as refused connections, DNS failures, and interrupted bodies.
	RetryConnectionErrors bool

	// RetryTimeouts retries attempts that exceed the per-attempt timeout.
	RetryTimeouts bool
}

RetryPolicy controls how the client retries failed attempts.

Start from DefaultRetryPolicy and change what you need; the zero value disables retries entirely. A retry waits min(InitialBackoff·2^n, MaxBackoff) reduced by up to Jitter, where n is the zero-based retry number, unless the response carries an acceptable retry-after-ms or Retry-After header.

Each attempt has its own timeout (see WithTimeout); there is no total budget for retries. Use the context deadline to bound the whole call.

func DefaultRetryPolicy

func DefaultRetryPolicy() RetryPolicy

DefaultRetryPolicy returns the policy used by NewClient: 2 retries with backoff from 500ms doubling to 5s and 25% jitter, retrying HTTP 408, 429 and 5xx (including 529 Overloaded) as well as connection errors and timeouts, and honoring retry-after-ms or Retry-After headers of up to 60s.

type ScoreAnswer

type ScoreAnswer struct {
	// Score is the probability-weighted position on the rubric, from 0 to the
	// highest level. It can fall between levels: 1.6 means mostly level 2
	// with some weight on level 1.
	Score float64

	// Confidence, from 0 to 1, is how certain the model is about the score.
	// It is derived from the probability distribution; see
	// https://docs.typesafe.ai/confidence.
	Confidence float64

	// Legend holds the level descriptions from the question, indexed by
	// level. Each entry is a string or the JSON structure you sent.
	Legend []any

	// Probabilities holds the probability of each level, indexed by level.
	// They sum to 1. It is nil if the response omitted them.
	Probabilities []float64
}

ScoreAnswer answers a ScoreQuestion.

The API keys legend and probabilities by level number as strings ("0", "1", ...). This type stores them as slices indexed by level, so Legend[i] describes level i and Probabilities[i] is its probability.

func (*ScoreAnswer) Label

func (a *ScoreAnswer) Label() any

Label returns the legend entry for Level, or nil if the legend does not cover it.

func (*ScoreAnswer) LabelOf

func (a *ScoreAnswer) LabelOf(level int) any

LabelOf returns the legend entry for the given level, or nil if the legend does not cover it.

func (*ScoreAnswer) Level

func (a *ScoreAnswer) Level() int

Level returns the most probable level: the index of the highest probability, or the lowest such index on a tie. If the answer carries no probabilities it falls back to NearestLevel.

Level and NearestLevel differ when the distribution is split: with probabilities [0.5, 0, 0.5] the score is 1.0, so NearestLevel is 1 while Level is 0.

func (*ScoreAnswer) MarshalJSON

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

MarshalJSON encodes the answer in the API's wire format, with legend and probabilities keyed by level number.

func (*ScoreAnswer) MaxLevel

func (a *ScoreAnswer) MaxLevel() int

MaxLevel returns the highest level index, or -1 if the answer carries neither a legend nor probabilities.

func (*ScoreAnswer) NearestLevel

func (a *ScoreAnswer) NearestLevel() int

NearestLevel returns Score rounded to the nearest level, clamped to the rubric's range.

func (*ScoreAnswer) Normalized

func (a *ScoreAnswer) Normalized() float64

Normalized returns Score as a fraction of the highest level, from 0 to 1, so that scores from rubrics with different numbers of levels can be combined. It returns 0 for a rubric with fewer than two levels.

Example
package main

import (
	"fmt"

	typesafe "github.com/adibhanna/typesafego"
)

func main() {
	// Normalized puts scores from rubrics with different numbers of levels
	// on a common 0..1 scale so they can be combined.
	urgency := &typesafe.ScoreAnswer{Score: 2.4, Legend: []any{"Low", "Medium", "High", "Critical"}}
	anger := &typesafe.ScoreAnswer{Score: 1.0, Legend: []any{"Calm", "Frustrated", "Very angry"}}

	priority := 0.7*urgency.Normalized() + 0.3*anger.Normalized()
	fmt.Printf("%.2f\n", priority)
}
Output:
0.71

func (*ScoreAnswer) ProbabilityOf

func (a *ScoreAnswer) ProbabilityOf(level int) float64

ProbabilityOf returns the probability of the given level, or 0 if the level is out of range.

func (*ScoreAnswer) Type

func (*ScoreAnswer) Type() QuestionType

Type returns TypeScore.

func (*ScoreAnswer) UnmarshalJSON

func (a *ScoreAnswer) UnmarshalJSON(data []byte) error

UnmarshalJSON decodes the API's wire format, converting the string-keyed legend and probabilities into slices indexed by level. It fails with an error wrapping ErrInvalidResponse if a key is not a non-negative integer.

type ScoreQuestion

type ScoreQuestion struct {
	// Instructions says what the model should rate. It may be a string or
	// JSON structure.
	Instructions any

	// Levels describes the rubric from lowest (index 0) to highest. Each level
	// may be a string, JSON structure, or nil. At least two are required. They
	// are sent as the question's "criteria".
	Levels []any
}

ScoreQuestion rates the state along an ordered rubric. The answer is a *ScoreAnswer with a probability-weighted score that can fall between levels, the probability of every level, and a confidence.

func Score

func Score(instructions any, levels ...any) ScoreQuestion

Score builds a question that rates the state against the given ordered levels, from lowest to highest.

typesafe.Score("How frustrated is the customer?", "Calm", "Frustrated", "Very angry")

func (ScoreQuestion) MarshalJSON

func (q ScoreQuestion) MarshalJSON() ([]byte, error)

MarshalJSON encodes the question in the API's wire format.

func (ScoreQuestion) Type

func (ScoreQuestion) Type() QuestionType

Type returns TypeScore.

type SystemOneRequest

type SystemOneRequest struct {
	// State is the content to evaluate: a string for text, or any
	// JSON-marshalable value (struct, map, slice) for structured data such as
	// a chat log, a record, or your application's current state. Required.
	State any

	// Questions are the typed questions to evaluate against State, keyed by
	// ids you choose. Required; see Questions.Validate for the rules.
	Questions Questions

	// Model overrides the client's default model for this request. Leave it
	// empty to use the client's model.
	Model string

	// Extra holds additional top-level request fields to send as-is, for API
	// parameters this package does not model. Entries named "state",
	// "questions" or "model" are ignored in favor of the typed fields.
	Extra map[string]any
}

SystemOneRequest is the input to Client.SystemOne.

func (SystemOneRequest) MarshalJSON

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

MarshalJSON encodes the request body sent to POST /v1/systemone. The model field is omitted when Model is empty; Client.SystemOne fills it in before encoding.

type SystemOneResponse

type SystemOneResponse struct {
	// Model is the versioned ID of the model that answered, such as
	// "jev-1.13.0", even when the request named an alias.
	Model string `json:"model"`

	// Answers holds one answer per question, under the ids from the request.
	Answers Answers `json:"answers"`

	// Usage is the token usage of the request.
	Usage Usage `json:"usage"`

	// RequestID is the x-typesafe-request-id response header, useful when
	// contacting support. Empty if the server did not send one.
	RequestID string `json:"-"`

	// Header holds the HTTP response headers.
	Header http.Header `json:"-"`
}

SystemOneResponse is the result of Client.SystemOne.

It round-trips through encoding/json in the API's wire format, which makes it easy to build canned responses for tests.

func (*SystemOneResponse) Choice

func (r *SystemOneResponse) Choice(id string) (*ChoiceAnswer, error)

Choice returns the choice answer with the given id. See Answers.Choice.

func (*SystemOneResponse) Noul

func (r *SystemOneResponse) Noul(id string) (*NoulAnswer, error)

Noul returns the yes/no answer with the given id. See Answers.Noul.

func (*SystemOneResponse) Score

func (r *SystemOneResponse) Score(id string) (*ScoreAnswer, error)

Score returns the score answer with the given id. See Answers.Score.

type UnknownAnswer

type UnknownAnswer struct {
	AnswerType QuestionType
	Raw        json.RawMessage
}

UnknownAnswer holds an answer whose type this package does not know. Raw is the answer's JSON as received.

func (*UnknownAnswer) MarshalJSON

func (a *UnknownAnswer) MarshalJSON() ([]byte, error)

MarshalJSON returns the raw JSON as received.

func (*UnknownAnswer) Type

func (a *UnknownAnswer) Type() QuestionType

Type returns the answer's wire type.

type Usage

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

Usage reports the tokens consumed by a request. Only input tokens are billed.

Directories

Path Synopsis
examples
triage command
Command triage classifies a support ticket with TypeSafe's System One model and prints a routing decision.
Command triage classifies a support ticket with TypeSafe's System One model and prints a routing decision.

Jump to

Keyboard shortcuts

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