jev

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

jev

Type-safe Go client for TypeSafe's System One API and its model, Jev.

Jev answers questions whose possible answers you define up front, with a probability for each. This package makes those answers Go types: a choice over your own enum returns that enum, and invalid questions or answers that don't fit are caught before your code branches on them.

go get github.com/anilsenay/jev

Unofficial. Requires an API key from TypeSafe's early access (TYPESAFE_API_KEY).

Quick start

type Intent string

const (
	Refund   Intent = "refund"
	Shipping Intent = "shipping"
	Other    Intent = "other"
)

// Declare questions once.
var intentQ = jev.Choice("What does the customer want?",
	jev.Opt(Refund, "wants money back"),
	jev.Opt(Shipping, "asks where the order is"),
	jev.Opt(Other, "anything else"),
)

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

ans, err := jev.Ask(ctx, client, ticket, intentQ) // ans is jev.ChoiceAnswer[Intent]
if err != nil {
	return err
}

switch intent, sure := ans.Sure(0.9); {
case sure && intent == Refund:
	return autoRefund(ticket)
case !sure:
	return escalate(ticket, ans.Probs)
default:
	return route(intent)
}

Question kinds

Constructor Answer Use for
jev.Noul(q) / jev.Noul(q, jev.NoulCriteria{True, False}) NoulAnswer{P} whether a condition holds; one per label for multi-label
jev.Choice(q, jev.Opt(v, desc)...) / jev.OneOf(q, v...) ChoiceAnswer[T]{Value, Probs, Confidence} exactly one of a set you define
jev.Score(q, levels...) ScoreAnswer{Value, Probs, Levels, Legend, Confidence} a degree along ordered levels; can land between them

Instructions are optional, as in the official SDKs: a choice or score whose criteria say everything can pass nil. Like those SDKs, this package validates only what it can be sure of before sending — at least one question, at least two score levels, and no duplicate or empty choice labels. The API's own limits (MaxChoiceOptions, MaxScoreLevels) are exported for reference but not enforced, so a raised server limit never blocks a valid request.

Sure(threshold) is available on noul and choice answers. Low confidence is not an error: thresholds are policy and live in your code.

Confidence is the API's own field: how peaked the distribution is. It is not a calibrated probability that the answer is right — use Probs[Value] (or P) for that. A noul carries no separate confidence; its probability is the signal.

Noul — is this true?

A yes/no judgment where the probability itself is the signal. Describe both ends when the boundary is subtle; one noul per label when several labels can apply at once.

spam := jev.Noul("Is this review spam or advertising?", jev.NoulCriteria{
	True:  "promotes an unrelated product, or links to another site",
	False: "a genuine opinion about the product",
})

ans, err := jev.Ask(ctx, client, review, spam) // jev.NoulAnswer
if err != nil {
	return err
}

switch yes, sure := ans.Sure(0.9); {
case sure && yes:
	return hide(review)      // ans.P == 0.99 for the advertising example below
case sure:
	return publish(review)
default:
	return queueForModeration(review, ans.P)
}
state: "BEST DEAL EVER!!! visit cheap-watches-online.example for 90% off luxury watches"
→ P = 0.99   Sure(0.9) = true, true

Sure(t) answers in both directions: (true, true) when P >= t, (false, true) when 1-P >= t, and (false, false) in the uncertain middle. Use a threshold above 0.5.

Choice — which one of these?

One option out of a set you define, returned as your own enum. The model cannot answer outside the set, so include a catch-all when the list might not cover every input.

type Team string

const (
	Billing   Team = "billing"
	Shipping  Team = "shipping"
	Technical Team = "technical"
	Other     Team = "other"
)

route := jev.Choice("Which team should handle this ticket?",
	jev.Opt(Billing, "Charges, invoices, refunds, subscriptions"),
	jev.Opt(Shipping, "Delivery status, delays, lost packages"),
	jev.Opt(Technical, "Bugs, outages, integrations"),
	jev.Opt(Other, nil), // nil when the name speaks for itself
)

ans, err := jev.Ask(ctx, client, ticket, route) // jev.ChoiceAnswer[Team]
if err != nil {
	return err
}

team, sure := ans.Sure(0.8) // team is a Team, checked at compile time
if !sure {
	return escalate(ticket, ans.Ranked()) // options, most probable first
}
return assign(ticket, team)
state: "I was charged twice for order A-104 last Friday. Please refund the duplicate."
→ Value = billing   Confidence = 1.00
  Probs  = map[billing:1 other:0 shipping:0 technical:0]
  Ranked = [billing other shipping technical]

jev.OneOf("Which team?", Billing, Shipping, Technical, Other) is the same question without descriptions. Probs always holds every option, including the zero ones.

Score — where on this scale?

A position along ordered levels, lowest first. The answer is probability-weighted, so it can land between two levels — which is information, not noise.

severity := jev.Score("How severe is the reported issue?",
	"Cosmetic; no impact to functionality",
	"Broken or degraded feature, but a workaround exists",
	"Blocking issue; no workaround exists",
)

ans, err := jev.Ask(ctx, client, report, severity) // jev.ScoreAnswer
if err != nil {
	return err
}

level, description := ans.Nearest()
log.Printf("severity %.2f — nearest level %d: %s", ans.Value, level, jev.Text(description))

if ans.Value >= 1.5 || ans.Probs[2] > 0.4 { // thresholds are yours
	return page(onCall)
}
state: "The export button crashes the settings page in Safari. Works in Chrome,
        but some customers only use Safari."
→ Value = 1.47   Confidence = 0.30
  Probs = [0 0.53 0.47]
  Nearest = 1, "Broken or degraded feature, but a workaround exists"

That answer sits almost exactly between "has a workaround" and "blocking", and the low Confidence says so. Rounding it to a single level would throw that away; thresholding Value or reading Probs directly keeps it. Scores need at least two levels, and the API accepts at most ten.

Questions can carry structure

instructions, choice option descriptions, score levels and noul criteria are all jev.Content: a string, or any value that marshals to a JSON object or array. Use structure when a question has several parts, or when the supporting data is already JSON.

jev.Choice("Which department does this product belong to?",
	jev.Opt(Sporting, map[string]any{
		"Cycling": []string{"Bike Bottles & Cages", "Helmets"},
		"Fitness": []string{"Yoga Mats"},
	}),
	jev.Opt(Drinkware, map[string]any{
		"what":    "Everyday water bottles, travel mugs, tumblers",
		"not_for": "Sport-specific gear",
	}),
	jev.Opt(Other, nil), // nil description is sent as JSON null
)

jev.Text(c) renders a Content for logging: a string as it is, anything else as compact JSON.

When the API moves first

New API fields and question kinds land before a release of this package does. RawQuestion sends a Spec exactly as given and hands the answer back undecoded, so nothing blocks you in the meantime — the counterpart of the official SDKs' raw question dictionaries and extra_body.

q := jev.RawQuestion(jev.Spec{
	Type:         "rank",                          // a kind this package predates
	Instructions: "Rank these passages by relevance",
	Criteria:     []jev.Content{"passage A", "passage B"},
	Extra:        map[string]any{"beam_width": 4},  // a field it predates too
})

h := jev.Add(b, q)  // *jev.Handle[jev.RawAnswer]
if _, err := b.Run(ctx); err != nil {
	return err
}
raw, err := h.Get() // read raw.Choice, raw.Probabilities, raw.Score yourself

Nothing is validated or narrowed here, so reach for a typed constructor whenever one fits. Extra works on any Spec; type, instructions and criteria are reserved and cannot be overridden from it.

Many questions, one request

Independent questions about the same state should share a batch. They run in parallel on the model and cost one call.

b := client.Batch(review)
spam     := jev.Add(b, jev.Noul("Is this review spam or advertising?"))
intent   := jev.Add(b, intentQ)
severity := jev.Add(b, jev.Score("How severe is the complaint?", "None", "Mild", "Serious", "Severe"))

meta, err := b.Run(ctx) // meta.Model, meta.Usage, meta.RequestID, meta.Latency

s, err := spam.Get()     // jev.NoulAnswer
i, err := intent.Get()   // jev.ChoiceAnswer[Intent]
v, err := severity.Get() // jev.ScoreAnswer
level, name := v.Nearest()

If one answer doesn't fit its question, Run returns an error wrapping jev.ErrMalformedAnswer, but the other handles can still be read.

Models

models, err := client.Models(ctx) // []jev.ModelCard{Name, Description, ReleaseDate}

Needs a provider that implements ModelLister, which the default HTTP provider does; a fake that does not returns ErrNoModelList. Middleware does not hide it.

Errors

One sentinel per status the official SDKs distinguish, matched with errors.Is:

Status Sentinel Retried
400 ErrBadRequest, ErrInvalidRequest no
401 ErrAuth no
403 ErrPermissionDenied no
404 ErrNotFound no
408 – yes
422 ErrUnprocessable, ErrInvalidRequest no
429 ErrRateLimit yes
529 ErrOverloaded, ErrInternalServer yes
other 5xx ErrInternalServer yes

Below HTTP: ErrConnection for a request that never got a response, and ErrTimeout (which also matches ErrConnection) for one that ran out of time.

Before any request: ErrInvalidQuestion, ErrNoAPIKey, ErrNotRun, ErrBatchUsed, ErrNoModelList. After one: ErrMalformedAnswer for an answer that does not fit its question.

APIError parses the API's body, so Message carries the human-readable reason and Fields lists the offending fields of a 422:

var apiErr *jev.APIError
if errors.As(err, &apiErr) {
	log.Printf("%s (%s) request=%s", apiErr.Message, apiErr.Type, apiErr.RequestID)
}

Configuration

client, err := jev.New(
	jev.WithAPIKey(key),                 // default: $TYPESAFE_API_KEY
	jev.WithBaseURL(url),                // default: $TYPESAFE_BASE_URL, then https://api.typesafe.ai
	jev.WithModel("jev-preview"),        // default: $TYPESAFE_DEFAULT_MODEL, then jev-latest
	jev.WithTimeout(2*time.Second),      // per attempt, via context; default 10s
	jev.WithRetry(jev.DefaultRetry),     // or WithRetries(n) to change only the count
	jev.WithHeaders(map[string]string{"X-Tenant": "acme"}),
	jev.WithLogger(slog.Default()),      // one line per request; off by default
	jev.WithHTTPClient(hc),              // never modified
	jev.WithCache(jev.NewMemoryCache(10_000)),
	jev.WithMiddleware(metrics),
)

Explicit options win over environment variables, which win over the defaults — the same precedence as the Python and JavaScript SDKs.

Reading configuration from somewhere other than the process environment needs no ceremony:

// godotenv, viper, koanf, a secret manager — anything that hands you a string.
client, err := jev.NewWithKey(cfg.TypeSafeKey, jev.WithTimeout(5*time.Second))

Once WithAPIKey (or NewWithKey) is given a key, TYPESAFE_API_KEY is not read at all, so an empty key is ErrNoAPIKey rather than a silent fall back to whatever the machine happens to hold.

Retries

DefaultRetry matches the official SDKs: two retries, 500ms of backoff doubling to 5s with 25% jitter subtracted, honouring retry-after-ms and Retry-After up to one minute (a longer delay falls back to backoff), retrying 408, 429 and every 5xx, plus connection failures and timeouts.

jev.WithRetry(jev.Retry{
	MaxRetries:        3,
	BackoffInitial:    time.Second,
	BackoffMax:        20 * time.Second,
	BackoffJitter:     0.25,
	Statuses:          []int{429, 503}, // nil means 408, 429 and 5xx
	RespectRetryAfter: true,
	MaxRetryAfter:     time.Minute,
	RetryConnection:   true,
	RetryTimeout:      true,
})

Every request carries User-Agent, X-TypeSafe-SDK and X-TypeSafe-Runtime, and retries carry X-TypeSafe-Retry-Count, as the official clients do.

Provider and Middleware are small interfaces, so logging, metrics, rate limiting or an alternative backend can be plugged in without touching call sites.

Testing

jevtest is a rule-based fake. A question that matches no rule fails the request, so a missing rule becomes a test failure instead of a silent "no".

fake := jevtest.New().
	On(jevtest.Instructions("spam"), jevtest.Yes(0.02)).
	On(jevtest.Kind(jev.KindChoice), jevtest.Pick("refund", 0.93)).
	On(jevtest.State("broken"), jevtest.Level(2, 0.8))

client, _ := jev.New(jev.WithProvider(fake))
// ... exercise your code, then inspect fake.Calls()

Matchers: Any, Instructions, Kind, Option, State, All, Not. Answers: Yes, Pick, Level, Fail.

A note on trust

Typed output guarantees the shape of an answer, not its truth. Calibration is a property of predictions in aggregate: measure accuracy per confidence band on your own labelled data before choosing thresholds for production.

License

MIT

Documentation

Overview

Package jev is a type-safe Go client for TypeSafe's System One API and its model Jev.

Jev reads natural language and structured state like an LLM, but instead of generating text it answers questions whose possible answers you define in advance, with a probability for each. This package turns those questions into Go types, so the answer to a Choice over your own enum comes back as that enum.

Asking one question

type Intent string

const (
	Refund   Intent = "refund"
	Shipping Intent = "shipping"
	Other    Intent = "other"
)

var intentQ = jev.Choice("What does the customer want?",
	jev.Opt(Refund, "wants money back"),
	jev.Opt(Shipping, "asks where the order is"),
	jev.Opt(Other, "anything else"),
)

ans, err := jev.Ask(ctx, client, ticket, intentQ)
if err != nil {
	return err
}
if intent, sure := ans.Sure(0.9); sure && intent == Refund {
	return autoRefund(ticket)
}

Asking many questions in one call

Independent questions about the same state belong in one Batch. They are evaluated in parallel and cost one request. Each Add returns a typed Handle:

b := client.Batch(review)
spam := jev.Add(b, jev.Noul("Is this review spam or advertising?"))
intent := jev.Add(b, intentQ)
if _, err := b.Run(ctx); err != nil {
	return err
}
s, err := spam.Get()     // jev.NoulAnswer
i, err := intent.Get()   // jev.ChoiceAnswer[Intent]

Questions can carry structure

Instructions, option descriptions, score levels and noul criteria are all Content: a string, or any value that marshals to JSON. Structure sharpens a boundary the model keeps getting wrong, and lets a taxonomy or a schema be passed through instead of flattened into a sentence:

jev.Choice("Which department does this product belong to?",
	jev.Opt(Sporting, map[string]any{
		"Cycling": []string{"Bike Bottles & Cages", "Helmets"},
		"Fitness": []string{"Yoga Mats"},
	}),
	jev.Opt(Drinkware, map[string]any{
		"what":    "Everyday water bottles, travel mugs, tumblers",
		"not_for": "Sport-specific gear",
	}),
)

When the API moves first

RawQuestion sends a Spec exactly as given and returns the answer undecoded, so a question kind or field that lands before a release of this package does is still reachable. Spec.Extra carries unmodelled fields.

Policy stays in your code

Low confidence is not an error. Answers carry the full distribution, and the thresholds that decide what to do with it belong to the caller. Errors are reserved for invalid questions, transport and API failures, and answers that do not fit the question.

Typed output guarantees the shape of an answer, not its truth. Validate your thresholds on labelled data before trusting them in production.

Configuration

New reads TYPESAFE_API_KEY, TYPESAFE_BASE_URL and TYPESAFE_DEFAULT_MODEL when the matching option is not given. Callers whose configuration comes from somewhere else pass the key directly:

client, err := jev.NewWithKey(cfg.TypeSafeKey)

An explicitly supplied key disables the environment fallback entirely, so an empty one is ErrNoAPIKey rather than a silent fall back.

Testing

Package github.com/anilsenay/jev/jevtest provides a rule-based fake Provider, so code that branches on Jev can be tested without the network.

Index

Constants

View Source
const (
	DefaultBaseURL = "https://api.typesafe.ai"
	DefaultModel   = "jev-latest"
	DefaultTimeout = 10 * time.Second

	// APIKeyEnv is read when no key is passed with WithAPIKey.
	APIKeyEnv = "TYPESAFE_API_KEY"
	// BaseURLEnv is read when no URL is passed with WithBaseURL.
	BaseURLEnv = "TYPESAFE_BASE_URL"
	// DefaultModelEnv is read when no model is passed with WithModel.
	DefaultModelEnv = "TYPESAFE_DEFAULT_MODEL"
)

Defaults used when an option is not supplied. They match the official TypeSafe SDKs.

View Source
const (
	// MaxChoiceOptions is the largest number of options one choice may offer.
	MaxChoiceOptions = 255
	// MaxScoreLevels is the largest number of levels one score may define.
	MaxScoreLevels = 10
)

Limits the API enforces on a single question. Like the official SDKs, this package does not check them before sending: a question that exceeds one comes back as an APIError matching ErrBadRequest. They are exported so callers building option lists programmatically can check for themselves.

View Source
const (
	// StatusOverloaded is the non-standard status the API returns when overloaded.
	StatusOverloaded = 529
)
View Source
const Version = "0.1.0"

Version is the package version, sent in the User-Agent and X-TypeSafe-SDK headers.

Variables

View Source
var (
	// ErrNoAPIKey means no key was supplied and TYPESAFE_API_KEY was unset.
	ErrNoAPIKey = errors.New("jev: no API key: set TYPESAFE_API_KEY or use WithAPIKey")
	// ErrInvalidQuestion means a question was built with invalid arguments,
	// for example a score with one level. It is detected before any request.
	ErrInvalidQuestion = errors.New("jev: invalid question")
	// ErrMalformedAnswer means the provider returned an answer that does not fit
	// its question: a missing answer, the wrong kind, or an option that was
	// never offered. It is the counterpart of the official SDKs' response
	// validation error.
	ErrMalformedAnswer = errors.New("jev: malformed answer")
	// ErrNotRun means a Handle was read before its Batch ran.
	ErrNotRun = errors.New("jev: batch has not run")
	// ErrBatchUsed means a Batch was run twice or added to after running.
	ErrBatchUsed = errors.New("jev: batch already run")
	// ErrNoModelList means the client's provider cannot list models.
	ErrNoModelList = errors.New("jev: provider does not list models")
)

Errors raised before a request is sent.

View Source
var (
	// ErrConnection means the request never produced an HTTP response: DNS, TLS,
	// a refused connection, or a body that stopped mid-flight.
	ErrConnection = errors.New("jev: connection error")
	// ErrTimeout means an attempt exceeded its timeout. It also matches
	// ErrConnection, as it does in the official SDKs.
	ErrTimeout = errors.New("jev: request timed out")
)

Errors raised by the transport, below HTTP. They mirror the official SDKs' APIConnectionError and APITimeoutError.

View Source
var (
	// ErrBadRequest is 400: the API rejected the request.
	ErrBadRequest = errors.New("jev: bad request")
	// ErrAuth is 401: the API key is missing, wrong or revoked.
	ErrAuth = errors.New("jev: authentication failed")
	// ErrPermissionDenied is 403: the key is valid but not allowed to do this.
	ErrPermissionDenied = errors.New("jev: permission denied")
	// ErrNotFound is 404.
	ErrNotFound = errors.New("jev: not found")
	// ErrUnprocessable is 422: the request body failed server validation.
	ErrUnprocessable = errors.New("jev: unprocessable entity")
	// ErrInvalidRequest matches both 400 and 422.
	ErrInvalidRequest = errors.New("jev: invalid request")
	// ErrRateLimit is 429.
	ErrRateLimit = errors.New("jev: rate limited")
	// ErrOverloaded is 529, the status TypeSafe returns when it is overloaded.
	ErrOverloaded = errors.New("jev: overloaded")
	// ErrInternalServer is any 5xx.
	ErrInternalServer = errors.New("jev: internal server error")
)

Errors matching an API status, one per status the official SDKs distinguish. ErrInvalidRequest is an umbrella over both request-validation statuses.

View Source
var DefaultRetry = Retry{
	MaxRetries:        2,
	BackoffInitial:    500 * time.Millisecond,
	BackoffMax:        5 * time.Second,
	BackoffJitter:     0.25,
	RespectRetryAfter: true,
	MaxRetryAfter:     time.Minute,
	RetryConnection:   true,
	RetryTimeout:      true,
}

DefaultRetry is the policy used when none is supplied.

Functions

func Ask

func Ask[A any](ctx context.Context, c *Client, state any, q Question[A]) (A, error)

Ask evaluates a single question about state. For several independent questions about the same state, use a Batch: it costs one request.

func Text

func Text(c Content) string

Text renders a Content for logs and messages: a string is returned as it is, anything else as compact JSON.

Types

type APIError

type APIError struct {
	// Status is the HTTP status code.
	Status int
	// Body is the response body, truncated to a readable length.
	Body string
	// Header is the response header, for anything this type does not model.
	Header http.Header
	// RequestID is the x-typesafe-request-id header. Quote it in a bug report.
	RequestID string
	// RetryAfter is the delay the server asked for, from retry-after-ms or
	// Retry-After. It is zero when the response carried neither.
	RetryAfter time.Duration
	// Endpoint is the method and path of the request, for context.
	Endpoint string

	// Type is the API's error_type, such as "authentication_error" or
	// "api_usage_error". It is empty when the body carried none.
	Type string
	// Message is the API's human-readable explanation, extracted from the body.
	Message string
	// Fields lists the offending fields of a validation failure.
	Fields []FieldError
}

APIError is a non-2xx response from the API. errors.Is matches it against the sentinel for its status.

func (*APIError) Error

func (e *APIError) Error() string

func (*APIError) Is

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

Is implements errors.Is, matching the sentinels for the status. A status may match more than one: 400 and 422 both match ErrInvalidRequest, and 529 matches ErrInternalServer as well as ErrOverloaded.

func (*APIError) Retryable

func (e *APIError) Retryable() bool

Retryable reports whether sending the same request again could succeed. It covers 408, 429 and every 5xx, matching the official SDKs' default policy.

type Batch

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

Batch collects questions about one state and evaluates them in a single request. Build it with Client.Batch, add questions with Add, then call Batch.Run once. A Batch is not safe for concurrent use.

func (*Batch) Len

func (b *Batch) Len() int

Len reports how many questions have been added.

func (*Batch) Run

func (b *Batch) Run(ctx context.Context) (Meta, error)

Run sends the batch. It returns an error when a question was invalid, the provider failed, or any answer did not fit its question; in the last case the handles whose answers were fine can still be read.

func (*Batch) UseModel

func (b *Batch) UseModel(model string) *Batch

UseModel overrides the client's model for this batch.

type Cache

type Cache interface {
	Get(key string) (*Response, bool)
	Set(key string, resp *Response)
}

Cache stores responses by request key. Implementations must be safe for concurrent use and must treat stored responses as read-only.

type ChoiceAnswer

type ChoiceAnswer[T ~string] struct {
	// Value is the most probable option.
	Value T
	// Probs holds a probability for every option, including zero ones.
	Probs map[T]float64
	// Confidence is the API's confidence: how peaked the distribution is, from
	// 0 to 1. It is not a calibrated probability that the answer is right; use
	// Probs[Value] for that.
	Confidence float64
}

ChoiceAnswer is the selected option and the distribution over all options.

func (ChoiceAnswer[T]) Ranked

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

Ranked returns the options from most to least probable.

func (ChoiceAnswer[T]) Sure

func (a ChoiceAnswer[T]) Sure(threshold float64) (T, bool)

Sure returns Value and whether its probability is at least threshold.

type ChoiceOption

type ChoiceOption[T ~string] struct {
	Value       T
	Description Content
}

ChoiceOption is one choice option with an optional description. The description may be a string, structured Content, or nil.

func Opt

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

Opt builds a ChoiceOption. T is inferred from value, so passing your own string enum makes the whole choice typed. Pass nil as the description when the option name speaks for itself.

type Client

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

Client evaluates questions through a Provider. It is safe for concurrent use.

func New

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

New builds a client. Without WithProvider it talks to the TypeSafe API and returns ErrNoAPIKey when no key is available.

Explicit options win over environment variables, which win over the defaults.

func NewWithKey

func NewWithKey(apiKey string, opts ...Option) (*Client, error)

NewWithKey is New with the API key supplied directly, for callers that read their configuration through something other than the process environment.

client, err := jev.NewWithKey(cfg.TypeSafeKey, jev.WithTimeout(5*time.Second))

func (*Client) Batch

func (c *Client) Batch(state any) *Batch

Batch starts a batch of questions about state. State may be a string or any value that marshals to JSON; named fields let questions refer to parts of it.

func (*Client) Model

func (c *Client) Model() string

Model reports the client's default model.

func (*Client) Models

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

Models lists the models the account may use, through GET /v1/models.

It needs a provider that implements ModelLister, which the default HTTP provider does; otherwise it returns ErrNoModelList. Middleware does not hide it: the underlying provider is asked directly, so cached or instrumented clients still list models.

type Content

type Content any

Content is anything that may be sent as instructions, as a choice option's description, as a score level or as a noul criterion: a plain string, or any value that marshals to a JSON object or array.

Structure is not decoration. A question with several parts reads more clearly as an object with named keys, and a taxonomy or a schema is already JSON, so it can be passed through instead of flattened into a sentence. A nil Content is sent as JSON null, which the API reads as "no description".

type FieldError

type FieldError struct {
	// Type is the kind of violation, such as "missing" or "too_short".
	Type string
	// Loc is the path to the offending field, such as ["body", "model"].
	Loc []string
	// Msg is the human-readable explanation.
	Msg string
}

FieldError is one entry of a validation failure, as the API reports it on a 422 response.

func (FieldError) String

func (f FieldError) String() string

String renders the entry as "path: message", dropping the leading "body" segment, as the official SDKs do.

type Handle

type Handle[A any] struct {
	// contains filtered or unexported fields
}

Handle is a typed slot for one answer in a Batch.

func Add

func Add[A any](b *Batch, q Question[A]) *Handle[A]

Add appends q to b and returns a handle for its answer. An invalid question does not panic: its error is returned by Batch.Run and by Handle.Get.

func (*Handle[A]) Get

func (h *Handle[A]) Get() (A, error)

Get returns the typed answer. It returns ErrNotRun before the batch ran.

type Kind

type Kind string

Kind is the wire type of a question and of its answer.

const (
	KindNoul   Kind = "noul"
	KindChoice Kind = "choice"
	KindScore  Kind = "score"
)

The question kinds the System One API accepts.

type MemoryCache

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

MemoryCache is an in-process LRU Cache.

func NewMemoryCache

func NewMemoryCache(size int) *MemoryCache

NewMemoryCache returns an LRU cache holding up to size responses. A size below 1 defaults to 1024.

func (*MemoryCache) Get

func (c *MemoryCache) Get(key string) (*Response, bool)

Get implements Cache.

func (*MemoryCache) Set

func (c *MemoryCache) Set(key string, resp *Response)

Set implements Cache.

type Meta

type Meta struct {
	Model string
	Usage Usage
	// RequestID identifies the request in TypeSafe's logs. Quote it in a bug
	// report. It is empty when the provider reports none.
	RequestID string
	Latency   time.Duration
}

Meta describes the request a Batch made.

When the response came from a cache, Model, Usage and RequestID are those of the original request; only Latency describes this call.

type Middleware

type Middleware func(next Provider) Provider

Middleware wraps a Provider, for caching, logging, metrics or rate limiting.

func CacheMiddleware

func CacheMiddleware(cache Cache) Middleware

CacheMiddleware serves identical requests (same model, state and questions) from cache. Only successful responses are stored. Cached responses report the usage of the original request.

type ModelCard

type ModelCard struct {
	Name        string    `json:"name"`
	Description string    `json:"description"`
	ReleaseDate time.Time `json:"release_date"`
}

ModelCard describes one model the account may use.

type ModelLister

type ModelLister interface {
	Models(ctx context.Context) ([]ModelCard, error)
}

ModelLister is the optional half of Provider, implemented by providers that can also list models. Client.Models needs it. Middleware that wraps a provider hides it unless the wrapper implements it too.

type NoulAnswer

type NoulAnswer struct {
	P float64
}

NoulAnswer is the probability that the answer is yes. A noul carries no separate confidence: the probability itself is the signal.

func (NoulAnswer) Sure

func (a NoulAnswer) Sure(threshold float64) (yes, ok bool)

Sure reports the answer and whether it clears threshold in either direction: (true, true) when P >= threshold, (false, true) when 1-P >= threshold, and (false, false) otherwise. Use a threshold above 0.5.

type NoulCriteria

type NoulCriteria struct {
	// True is what a value near 1 means.
	True Content
	// False is what a value near 0 means.
	False Content
}

NoulCriteria describes the two ends of a Noul. Either side may be left nil, and either may be structured Content when the boundary is subtle.

type Option

type Option func(*config)

Option configures a Client.

func WithAPIKey

func WithAPIKey(key string) Option

WithAPIKey sets the API key. Once it is given, TYPESAFE_API_KEY is not read at all: passing an empty key returns ErrNoAPIKey rather than quietly falling back to whatever the environment happens to hold. This is what the official SDKs do, and it keeps a client from reaching the API with a key its caller never chose.

func WithBaseURL

func WithBaseURL(url string) Option

WithBaseURL points the HTTP provider at another host, for proxies and tests. It overrides TYPESAFE_BASE_URL.

func WithCache

func WithCache(cache Cache) Option

WithCache caches responses for identical requests. See NewMemoryCache.

func WithHTTPClient

func WithHTTPClient(hc *http.Client) Option

WithHTTPClient supplies the http.Client used for requests. The client is never modified; per-attempt timeouts are applied through the request context.

func WithHeaders

func WithHeaders(h map[string]string) Option

WithHeaders adds headers to every request. Authentication, content type and the SDK headers cannot be overridden.

func WithLogger

func WithLogger(l *slog.Logger) Option

WithLogger logs one line per request at info level, and retries as they happen. Nothing is logged without it.

func WithMiddleware

func WithMiddleware(m ...Middleware) Option

WithMiddleware wraps the provider. The first middleware given is outermost.

func WithModel

func WithModel(model string) Option

WithModel sets the default model, overriding TYPESAFE_DEFAULT_MODEL. Batch.UseModel overrides it per batch.

func WithProvider

func WithProvider(p Provider) Option

WithProvider replaces the HTTP provider, for fakes or alternative backends. No API key is required when a provider is supplied.

func WithRetries

func WithRetries(n int) Option

WithRetries sets how many times a retryable failure is retried, leaving the rest of the policy alone. Zero disables retries.

func WithRetry

func WithRetry(r Retry) Option

WithRetry replaces the retry policy. See DefaultRetry.

func WithTimeout

func WithTimeout(d time.Duration) Option

WithTimeout bounds each HTTP attempt. Zero disables the per-attempt bound; the caller's context still applies.

func WithUserAgent

func WithUserAgent(ua string) Option

WithUserAgent sets the User-Agent header.

type Provider

type Provider interface {
	Evaluate(ctx context.Context, req *Request) (*Response, error)
}

Provider evaluates requests. The default provider talks to the TypeSafe HTTP API; tests use github.com/anilsenay/jev/jevtest. Implementations must be safe for concurrent use.

type ProviderFunc

type ProviderFunc func(ctx context.Context, req *Request) (*Response, error)

ProviderFunc adapts a function to Provider.

func (ProviderFunc) Evaluate

func (f ProviderFunc) Evaluate(ctx context.Context, req *Request) (*Response, error)

Evaluate implements Provider.

type Question

type Question[A any] interface {
	// contains filtered or unexported methods
}

Question is a typed question whose answer decodes to A. Build one with Noul, Choice, OneOf or Score. Questions are immutable values and are meant to be declared once, typically as package-level variables.

The interface is sealed: only this package can implement it, because the API accepts only these kinds.

func Choice

func Choice[T ~string](instructions Content, options ...ChoiceOption[T]) Question[ChoiceAnswer[T]]

Choice picks exactly one of options and returns the distribution over them. Include a catch-all option when none of the others may fit: the model cannot answer outside the set.

func Noul

func Noul(instructions Content, criteria ...NoulCriteria) Question[NoulAnswer]

Noul asks whether a condition holds and answers with the probability that it does. Pass a NoulCriteria to describe the two ends; filling them in usually sharpens the boundary. The API needs at least one of instructions and criteria.

Use one Noul per label when several labels can apply at once. A value near 0.5 means the model finds yes and no about equally likely; it does not mean "medium". Use a Score to place something on a spectrum.

func OneOf

func OneOf[T ~string](instructions Content, values ...T) Question[ChoiceAnswer[T]]

OneOf is Choice without descriptions, for options whose names speak for themselves.

func RawQuestion

func RawQuestion(spec Spec) Question[RawAnswer]

RawQuestion sends a hand-built Spec exactly as given and hands the answer back undecoded. Nothing about it is validated or narrowed.

It exists so the API can move ahead of this package: a question kind or field that lands before a release does is still reachable, through Spec.Extra for new fields and a new Kind for a new type.

q := jev.RawQuestion(jev.Spec{
	Type:         jev.KindChoice,
	Instructions: "Which team should handle this?",
	Criteria:     map[string]jev.Content{"billing": nil, "shipping": nil},
	Extra:        map[string]any{"beam_width": 4},
})
h := jev.Add(b, q)     // *Handle[RawAnswer]
raw, err := h.Get()    // read raw.Choice, raw.Probabilities yourself

Reach for a typed constructor whenever one fits; this gives up the type safety that is the point of the package.

func Score

func Score(instructions Content, levels ...Content) Question[ScoreAnswer]

Score rates the state against ordered levels, lowest first. Each level should describe a concrete situation, as a string or as structured Content. The answer is a probability-weighted position, so it can land between levels.

type RawAnswer

type RawAnswer struct {
	Type   Kind     `json:"type"`
	Noul   *float64 `json:"noul,omitempty"`
	Choice string   `json:"choice,omitempty"`
	Score  *float64 `json:"score,omitempty"`
	// Legend maps each score level index to the description that was sent. Its
	// values are raw JSON because a level may be structured [Content].
	Legend        map[string]json.RawMessage `json:"legend,omitempty"`
	Probabilities map[string]float64         `json:"probabilities,omitempty"`
	Confidence    *float64                   `json:"confidence,omitempty"`
}

RawAnswer is an answer as it arrives over the wire, before it is narrowed to the type of its question.

type Request

type Request struct {
	// Model is required by the API. [Batch.Run] fills it from the client.
	Model     string          `json:"model"`
	State     json.RawMessage `json:"state"`
	Questions map[string]Spec `json:"questions"`
}

Request is one evaluation: a state and a battery of questions keyed by id.

type Response

type Response struct {
	Model   string               `json:"model"`
	Answers map[string]RawAnswer `json:"answers"`
	Usage   Usage                `json:"usage"`
	// RequestID identifies the request in TypeSafe's logs. It is read from the
	// response headers rather than the body, so it is not part of the JSON.
	RequestID string `json:"-"`
}

Response is the result of one evaluation.

type Retry

type Retry struct {
	// MaxRetries is how many attempts follow the first. Zero disables retries.
	MaxRetries int
	// BackoffInitial is the first delay, doubled on each attempt up to
	// BackoffMax. Zero disables backoff.
	BackoffInitial time.Duration
	// BackoffMax caps a single backoff delay.
	BackoffMax time.Duration
	// BackoffJitter is the fraction of each delay randomly subtracted, from 0
	// to 1. It stops parallel callers from retrying in lockstep.
	BackoffJitter float64
	// Statuses lists the HTTP status codes to retry. Nil means the default:
	// 408, 429 and every 5xx.
	Statuses []int
	// RespectRetryAfter honours the retry-after-ms and Retry-After headers.
	RespectRetryAfter bool
	// MaxRetryAfter is the longest server delay that is honoured. A longer one
	// falls back to backoff.
	MaxRetryAfter time.Duration
	// RetryConnection retries failures that produced no HTTP response.
	RetryConnection bool
	// RetryTimeout retries attempts that ran out of time.
	RetryTimeout bool
}

Retry configures how a failed request is retried. Its zero value is not useful; start from DefaultRetry and change what you need.

The defaults mirror the official TypeSafe SDKs: two retries, 500ms of initial backoff doubling to 5s with 25% jitter subtracted, honouring a server delay of up to a minute, retrying 408, 429 and every 5xx.

type ScoreAnswer

type ScoreAnswer struct {
	// Value runs from 0 (the first level) to len(Levels)-1.
	Value float64
	// Probs holds one probability per level, lowest level first.
	Probs []float64
	// Levels are the level descriptions from the question, in order.
	Levels []Content
	// Legend is the level descriptions the API echoed back, keyed by level
	// index. It is nil when the answer carried none.
	Legend map[int]Content
	// Confidence is the API's confidence; see [ChoiceAnswer.Confidence].
	Confidence float64
}

ScoreAnswer is a probability-weighted position across the levels of a Score.

func (ScoreAnswer) Nearest

func (a ScoreAnswer) Nearest() (int, Content)

Nearest returns the index and description of the closest whole level. Use Text to render the description when the levels are structured.

func (ScoreAnswer) NearestIndex

func (a ScoreAnswer) NearestIndex() int

NearestIndex returns the index of the closest whole level.

type Spec

type Spec struct {
	Type Kind `json:"type"`
	// Instructions is optional, as it is in the official SDKs: a choice or a
	// score whose criteria say everything can leave it nil, and it is then
	// omitted from the request.
	Instructions Content `json:"instructions,omitempty"`
	// Criteria is *noulCriteria for noul questions, map[string]Content for
	// choices (a nil description is sent as null) and []Content for scores.
	Criteria any `json:"criteria,omitempty"`
	// Extra carries question fields this package does not model, for API
	// features that land before a release does. Its entries are marshalled
	// alongside the three above, sorted by key; "type", "instructions" and
	// "criteria" are reserved and ignored here. The typed constructors never
	// set it; see [RawQuestion].
	Extra map[string]any `json:"-"`
}

Spec is one question as it is sent over the wire. Most users never build a Spec directly; the typed constructors Noul, Choice, OneOf and Score produce them. Providers and fakes read them.

func (Spec) ChoiceOptions

func (s Spec) ChoiceOptions() []string

ChoiceOptions returns the option names of a choice, sorted. It returns nil for other kinds.

func (Spec) MarshalJSON

func (s Spec) MarshalJSON() ([]byte, error)

MarshalJSON implements json.Marshaler, merging Spec.Extra into the question object.

func (Spec) ScoreLevels

func (s Spec) ScoreLevels() int

ScoreLevels reports how many levels a score has. It returns 0 for other kinds.

type Usage

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

Usage counts the tokens a request consumed.

Directories

Path Synopsis
Package jevtest provides a rule-based fake jev.Provider for tests.
Package jevtest provides a rule-based fake jev.Provider for tests.

Jump to

Keyboard shortcuts

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