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 21, 2026 License: MIT Imports: 24 Imported by: 0

README

jev

build status report card godocs

A Go client for the TypeSafe System One API and its model, Jev.

Jev does not write text. You send a state (a string, or JSON such as a ticket, a log, or the current state of a program) and one or more named questions. The call returns a typed answer and a probability for every question. Adding questions to the same call does not add a round trip.

This module is not affiliated with TypeSafe AI. The official clients are the JavaScript SDK and the Python SDK. The HTTP contract is the System One API.

Install

Go 1.27 or newer. The only dependency is golang.org/x/time/rate.

go get github.com/kataras/jev

Please star this open source project to attract more developers so that together we can improve it even more!

Questions

Question You provide You get back
Noul A yes/no question Probability of yes, from 0 to 1
Choice Named options, up to 255 The picked option, a probability for each option, and a confidence
Score An ordered list of levels, up to 10 A position on that list (it can fall between levels), a probability for each level, and a confidence

Question names are yours. They come back as the answer keys. They are not part of the prompt.

jev-latest is the default model, and that alias moves. Set WithModel("jev-1.13.0") when a later run has to hit the same model.

Usage

client, err := jev.New() // reads TYPESAFE_API_KEY
resp, err := client.SystemOne(ctx, jev.Request{
    State: "I was charged twice. Please fix this ASAP.",
    Questions: jev.Questions{
        "billing": jev.Noul{Instructions: "Is this ticket about billing?"},
        "team": jev.Choice{
            Instructions: "Which team should handle this?",
            Criteria: map[string]any{
                "billing":   "Payments, invoices, refunds",
                "technical": nil, // null means the label has no description
            },
        },
        "urgency": jev.Score{
            Instructions: "How urgent is this ticket?",
            Criteria:     []string{"can wait", "this week", "today"},
        },
    },
})

billing, _ := resp.Noul("billing")
team, _ := resp.Choice("team")
urgency, _ := resp.Score("urgency")

SystemOneAs decodes the same JSON into a struct. It is a generic method, which is why this module requires Go 1.27. JSON names are case-sensitive.

type ticket struct {
    Model string `json:"model"`
    Answers struct {
        Billing struct {
            Noul float64 `json:"noul"`
        } `json:"billing"`
    } `json:"answers"`
}

out, err := client.SystemOneAs[ticket](ctx, req)

A full program lives in examples/quickstart.

One question

Noul, Choice, and Score each send one question named answer through SystemOne. Classify is Choice with the question and the options as separate arguments. Rate is Score with the levels as a list. The As methods call SystemOneAs. In that JSON the question name is answer.

yes, err := client.Noul(ctx, ticket, "Is this about billing?")

team, err := client.Classify(ctx, ticket, "Which team?", map[string]any{
    "billing":   "Payments, invoices, refunds",
    "technical": nil,
})

urgency, err := client.Rate(ctx, ticket, "How urgent?", []string{"can wait", "this week", "today"})

Several questions about the same state still belong in one SystemOne call.

List the models on the account with ListModels. The list includes the jev-latest alias.

models, err := client.ListModels(ctx)

Configuration

Explicit options win over environment variables. A blank environment value is ignored.

Option Environment Default
WithAPIKey TYPESAFE_API_KEY required
WithBaseURL TYPESAFE_BASE_URL https://api.typesafe.ai
WithModel TYPESAFE_DEFAULT_MODEL jev-latest
WithTimeout 10s per attempt
WithRetry 2 retries, 500ms to 5s backoff
WithRateLimit 1,200 requests/min, 250,000 tokens/s
WithRateLimiter the limiter WithRateLimit builds
WithLogger TYPESAFE_LOG_LEVEL no logging
WithHTTPClient a client that does not follow redirects
WithHeader

The same options can be passed to one call. They override the client for that call only. WithRateLimit is the exception: it belongs to New, because a limiter built for one call starts with a full budget and paces nothing. For one call, pass WithRateLimiter with a limiter you own, or nil to skip pacing.

WithRetry replaces the whole policy. Copy DefaultRetryPolicy() and edit the copy. A zero RetryPolicy retries nothing, because its status list is empty.

Rate limits

The API limits each account to 1,200 requests per minute and 250,000 input tokens per second. A request over either limit is a 429. Retrying after a 429 works, but it costs a round trip and a backoff wait each time. The client stays under the limit instead: every attempt, retries included, first waits on a token bucket built with golang.org/x/time/rate, the same way kataras/httpclient paces its requests.

Requests are spread across the minute, with a burst of one second's worth (20 at the default). Tokens are estimated from the body size before the call and settled with usage.input_tokens after it, so the estimate corrects itself. When the server does answer 429 or 529 with Retry-After, every caller on that limiter pauses until then, not only the call that saw it. The pause is capped by RetryPolicy.MaxRetryAfter, 60s by default.

The limits are per account, so clients on the same key should share one limiter:

shared := jev.NewLimiter(jev.DefaultRateLimit())
latest, err := jev.New(jev.WithRateLimiter(shared))
pinned, err := jev.New(jev.WithModel("jev-1.13.0"), jev.WithRateLimiter(shared))
// or join an existing client's budget:
third, err := jev.New(jev.WithRateLimiter(latest.Limiter()))

TypeSafe adjusts the limits without notice, and enterprise plans get higher ones. Set your own figures, or divide the account's budget between processes:

client, err := jev.New(jev.WithRateLimit(jev.RateLimit{
    RequestsPerMinute: 300,
    TokensPerSecond:   60_000,
}))

A zero field disables that axis. jev.WithRateLimit(jev.RateLimit{}) or jev.WithRateLimiter(nil) turns pacing off. A plain *rate.Limiter also satisfies jev.Limiter; it paces requests only.

The limiter waits are not retries. A wait that outlives your context returns that context's error, and no request is sent.

Errors

if errors.Is(err, context.Canceled) {
    // the caller cancelled; this is not retried
}
if errors.Is(err, jev.ErrUnauthorized) {
    // HTTP 401
}
if errors.Is(err, jev.ErrRateLimited) {
    // HTTP 429, after retries
}
if errors.Is(err, jev.ErrOverloaded) {
    // HTTP 529; this also matches jev.ErrServer
}
var api *jev.APIError
if errors.As(err, &api) {
    fmt.Println(api.Status, api.RequestID, api.Message)
}
Sentinel When
ErrConfig Missing key, bad URL, bad option
ErrRequest Rejected locally, before HTTP
ErrResponse 2xx body that does not match the contract
ErrTimeout The per-attempt timeout fired
ErrConnection DNS, TLS, or a dropped connection
ErrBadRequest HTTP 400
ErrUnauthorized HTTP 401
ErrForbidden HTTP 403
ErrNotFound HTTP 404
ErrUnprocessable HTTP 422
ErrRateLimited HTTP 429
ErrOverloaded HTTP 529
ErrServer HTTP 5xx, including 529

A deadline on the context you passed is returned as that context error. It does not match ErrTimeout. ErrTimeout means this client's own per-attempt limit.

Retries

The default policy matches the official JavaScript SDK. It retries HTTP 408, 429, and 500 through 599, including 529. The first wait is 500ms, doubled up to 5s, with up to 25% of the wait subtracted at random. Retry-After-Ms is preferred over Retry-After. A server delay longer than 60s falls back to that backoff. Retries are the second line; the rate limiter is the first.

There is no budget across attempts unless you set RetryPolicy.MaxElapsed. The context you pass always applies. Cancellation during a wait stops the call and is not retried.

POST is retried because a System One call only evaluates. Each attempt still spends input tokens.

Logging

TYPESAFE_LOG_LEVEL may be debug, info, warn, error, or off. Info logs one line per attempt. Debug also logs headers and bodies. Authorization, Cookie, and X-Api-Key are replaced with [redacted]. Bodies are logged as sent, and they contain your state.

Wire format

A few choices differ from other Go clients, on purpose:

  • JSON is encoded with encoding/json/v2, and <, >, and & are left as themselves. encoding/json rewrites them to \u003c, \u003e, and \u0026, so the text the model reads is not the text you passed.
  • Object keys are sorted. The same request produces the same bytes.
  • Request.Extra cannot replace state, model, or questions.
  • A []byte state is sent as a JSON string. A plain byte slice would otherwise be base64.
  • Score criteria must be a JSON array. A map is rejected. That matches the official SDK change in v0.6.0 (15 Sep 2026).
  • Choice criteria must be a JSON object, with at most 255 options.

Live tests

TestLiveSystemOne and TestLiveModels call the real API and spend tokens. go test skips them when TYPESAFE_API_KEY is unset or blank, and always under go test -short. Every other test runs against an in-process server and needs no key.

For GitHub Actions, add a repository secret named TYPESAFE_API_KEY. The workflow runs the unit tests without the key, then runs -run '^TestLive' only when the secret is present. Pull requests from forks do not receive secrets, so there the live step prints a note and passes. Do not commit a key.

Agent skill

skills/jev/SKILL.md is an Agent Skill for this module. The signatures in it match the Go API above. Claude Code, Cursor, Codex, and Plexon all read that format.

Install it for every agent this machine already has, including Plexon (it adopts ~/.agents/skills):

npx skills add kataras/jev --skill jev -g -y

One project only: drop -g. The files land in .agents/skills/jev or .claude/skills/jev. Plexon reads both.

Pick agents by name when you do not want the full set:

npx skills add kataras/jev --skill jev -g -y -a claude-code -a cursor -a codex

Claude Code can install the same skill as a plugin. Inside a Claude Code session:

/plugin marketplace add kataras/jev
/plugin install jev@kataras-jev

License

MIT.

Documentation

Overview

Package jev is an HTTP client for the TypeSafe System One API.

Jev is TypeSafe's System One model. You send it a state (text or JSON) and one or more named questions. It answers every question in that call and returns a probability with each answer. It does not generate text. The HTTP contract is the API reference; the ideas behind it are under System One and primitives.

Create a client with New. The API key comes from WithAPIKey or from the TYPESAFE_API_KEY environment variable.

client, err := jev.New()
resp, err := client.SystemOne(ctx, jev.Request{
	State: "I was charged twice. Please fix this ASAP.",
	Questions: jev.Questions{
		"billing": jev.Noul{Instructions: "Is this ticket about billing?"},
		"team": jev.Choice{
			Instructions: "Which team should handle this?",
			Criteria: map[string]any{
				"billing":   "Payments, invoices, refunds",
				"technical": nil,
			},
		},
		"urgency": jev.Score{
			Instructions: "How urgent is this ticket?",
			Criteria:     []string{"can wait", "this week", "today"},
		},
	},
})
if billing, ok := resp.Noul("billing"); ok {
	fmt.Println(billing.Noul)
}

Client.SystemOneAs decodes that same JSON into a struct of your own. It is a generic method, so it needs Go 1.27.

One question at a time can use Client.Noul, Client.Choice, Client.Score, Client.Classify, or Client.Rate. Each calls SystemOne with the question named "answer". The As forms call SystemOneAs.

Rate limits

The API limits each account to a number of requests per minute and input tokens per second; the current figures are under current models. The client paces itself under those figures by default, with a Limiter built from DefaultRateLimit, so a burst of calls queues instead of failing with 429. When the server does answer 429 or 529 with Retry-After, every caller on that limiter pauses until then. Set WithRateLimit to change the figures, or share one NewLimiter between clients on the same key with WithRateLimiter. See handling rate limits.

Retries and errors

The client retries 408, 429, and 5xx responses, including 529, with exponential backoff, as the API asks. Context cancellation is not retried. Failed responses are an *APIError and match a status sentinel such as ErrRateLimited through errors.Is; see errors. Credential headers are redacted from logs. Request and response bodies are not.

Example
package main

import (
	"context"
	"fmt"

	"github.com/kataras/jev"
)

func main() {
	client, err := jev.New()
	if err != nil {
		fmt.Println(err)
		return
	}
	resp, err := client.SystemOne(context.Background(), jev.Request{
		State: "I was charged twice. Please fix this ASAP.",
		Questions: jev.Questions{
			"billing": jev.Noul{Instructions: "Is this ticket about billing?"},
			"team": jev.Choice{
				Instructions: "Which team should handle this?",
				Criteria: map[string]any{
					"billing":   "Payments, invoices, refunds",
					"technical": nil,
				},
			},
			"urgency": jev.Score{
				Instructions: "How urgent is this ticket?",
				Criteria:     []string{"can wait", "this week", "today"},
			},
		},
	})
	if err != nil {
		fmt.Println(err)
		return
	}
	if billing, ok := resp.Noul("billing"); ok {
		fmt.Println(billing.Noul > 0.5)
	}
}

Index

Examples

Constants

View Source
const (
	// DefaultRequestsPerMinute is the documented request limit per account.
	DefaultRequestsPerMinute = 1200
	// DefaultTokensPerSecond is the documented input-token limit per account.
	DefaultTokensPerSecond = 250_000
)

The documented limits for jev-1.13.0. TypeSafe adjusts them without notice, and enterprise plans get higher ones, so they are a starting point and not a contract. See current models.

View Source
const AnswerKey = "answer"

AnswerKey is the question name used by the single-question methods (Client.Noul, Client.Choice, Client.Score, Client.Classify, Client.Rate). In a struct passed to the As methods, read it as answers.answer. The name is a key of the questions map in the request body; the API does not see it as part of the prompt.

View Source
const Version = "0.1.0"

Version is the client version sent in User-Agent and X-TypeSafe-SDK. The official SDKs send the same header from their own VERSION constant.

Variables

View Source
var (
	// ErrConfig is wrapped by errors from [New] and from invalid options.
	ErrConfig = errors.New("jev: invalid configuration")
	// ErrRequest is wrapped by errors for calls rejected before any HTTP attempt.
	ErrRequest = errors.New("jev: invalid request")
	// ErrResponse is wrapped when a 2xx body does not match the System One contract.
	ErrResponse = errors.New("jev: invalid response")
	// ErrBodyTooLarge is wrapped when a response body exceeds 16 MiB.
	// It is not retried.
	ErrBodyTooLarge = errors.New("jev: response body exceeds 16 MiB")
)

Configuration and request sentinels.

View Source
var (
	ErrBadRequest    = errors.New("jev: bad request")       // HTTP 400
	ErrUnauthorized  = errors.New("jev: unauthorized")      // HTTP 401
	ErrForbidden     = errors.New("jev: forbidden")         // HTTP 403
	ErrNotFound      = errors.New("jev: not found")         // HTTP 404
	ErrUnprocessable = errors.New("jev: unprocessable")     // HTTP 422
	ErrRateLimited   = errors.New("jev: rate limited")      // HTTP 429
	ErrOverloaded    = errors.New("jev: overloaded")        // HTTP 529
	ErrServer        = errors.New("jev: server error")      // HTTP 5xx, including 529
	ErrStatus        = errors.New("jev: unexpected status") // any other non-2xx
)

Status sentinels matched by *APIError through errors.Is. A 529 matches both ErrOverloaded and ErrServer. The statuses the API documents, and what each one means, are under errors. 429 and 529 ask for backoff, which the default RetryPolicy applies; see handling rate limits.

View Source
var (
	// ErrConnection matches every ConnectionError.
	ErrConnection = errors.New("jev: connection error")
	// ErrTimeout matches a ConnectionError caused by the per-attempt timeout.
	// A deadline on the caller's context is returned as that context error
	// and does not match ErrTimeout.
	ErrTimeout = errors.New("jev: request timed out")
)

Transport sentinels matched by *ConnectionError through errors.Is.

Functions

This section is empty.

Types

type APIError

type APIError struct {
	Status    int
	Method    string
	URL       string
	Header    http.Header
	Body      []byte
	Message   string
	RequestID string
	Attempts  int
}

APIError is an unsuccessful HTTP response, returned after retries. Status is the HTTP status; the API's table of statuses is under errors. Message is pulled from the JSON body, including the field path for a 422. RequestID is the server's X-Typesafe-Request-Id, for support tickets.

func (*APIError) Error

func (e *APIError) Error() string

func (*APIError) Is

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

Is reports whether target is the status sentinel for this response.

func (*APIError) RetryAfter

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

RetryAfter returns the delay from Retry-After-Ms or Retry-After. A 429 or 529 may carry one; see handling rate limits. The client already waited it out when it retried, and a Limiter from NewLimiter pauses every caller for it, so read it here for logging or to plan the next call.

type Answer

type Answer interface {
	Type() string
}

Answer is the result of one question: NoulAnswer, ChoiceAnswer, ScoreAnswer, or UnknownAnswer. Every answer carries a type matching its question; see answer types.

type Choice

type Choice struct {
	Instructions any
	Criteria     any
}

Choice picks one option from a set you define. Criteria is a JSON object: a map[string]any, a map[string]string, or a struct with json tags. A nil description leaves that option undescribed and is sent as null. At least one option is required, and more than 255 is rejected. The wire shape is under choice; structured choice options shows how a description can be an object rather than a string.

type ChoiceAnswer

type ChoiceAnswer struct {
	Choice        string
	Confidence    float64
	Probabilities map[string]float64
}

ChoiceAnswer is the selected option, a probability for every option, and a confidence derived from that distribution. See choice answer, and confidence for how confidence differs from the winning probability.

func (ChoiceAnswer) Type

func (ChoiceAnswer) Type() string

type Client

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

Client calls the TypeSafe System One API. It is safe for concurrent use. Create it with New. The HTTP contract it speaks is the API reference.

func New

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

New returns a client. Options override environment variables, which override the defaults. The API key is required; get one from the quick start.

The precedence, the environment variable names, and the defaults match the official SDKs. See client config for the JavaScript SDK's version of the same table.

func (*Client) BaseURL

func (c *Client) BaseURL() string

BaseURL returns the API root without a trailing slash. The production root is https://api.typesafe.ai; see the evaluation endpoint.

func (*Client) Choice

func (c *Client) Choice(ctx context.Context, state any, q Choice, opts ...Option) (ChoiceAnswer, error)

Choice asks one choice question. See choice and choice answer. The call is one Client.SystemOne request; several questions about one state belong in one SystemOne call, see ask more than one question per call.

func (*Client) ChoiceAs

func (c *Client) ChoiceAs[T any](ctx context.Context, state any, q Choice, opts ...Option) (T, error)

ChoiceAs asks one choice question and decodes the response JSON into T. It calls Client.SystemOneAs. The question name in that JSON is AnswerKey. The shape to mirror is under choice answer.

func (*Client) Classify

func (c *Client) Classify(ctx context.Context, state any, instructions any, criteria any, opts ...Option) (ChoiceAnswer, error)

Classify picks one option. instructions is the question. criteria maps each option to a description, or nil when the option needs none; see choice. It calls Client.Choice.

func (*Client) ClassifyAs

func (c *Client) ClassifyAs[T any](ctx context.Context, state any, instructions any, criteria any, opts ...Option) (T, error)

ClassifyAs picks one option and decodes the response JSON into T. It calls Client.ChoiceAs. The question name in that JSON is AnswerKey. The shape to mirror is under choice answer.

func (*Client) CloseIdleConnections

func (c *Client) CloseIdleConnections()

CloseIdleConnections closes idle connections on the underlying HTTP client. It does not cancel calls that are in flight.

func (*Client) Limiter

func (c *Client) Limiter() Limiter

Limiter returns the client-side rate limiter, or nil when pacing is off. Pass it to WithRateLimiter on another client so both share one budget, which is what the per-account limits under current models ask for.

func (*Client) ListModels

func (c *Client) ListModels(ctx context.Context, opts ...Option) ([]ModelInfo, error)

ListModels returns the models available to the account with GET /v1/models. The list holds the aliases, such as jev-latest. Versioned IDs such as jev-1.13.0 are accepted by the model field whether or not they appear here. See listing models and aliases.

func (*Client) Model

func (c *Client) Model() string

Model returns the model used when a request does not name one. The names and aliases the API accepts are listed under models.

func (*Client) Noul

func (c *Client) Noul(ctx context.Context, state any, question any, opts ...Option) (NoulAnswer, error)

Noul asks one yes/no question and returns the probability of yes. question is a Noul value, or the instructions alone: a string, or any value that encodes as a JSON object or array. See noul and reading a noul.

The call is one Client.SystemOne request. For several questions about the same state, use SystemOne; see ask more than one question per call.

func (*Client) NoulAs

func (c *Client) NoulAs[T any](ctx context.Context, state any, question any, opts ...Option) (T, error)

NoulAs asks one yes/no question and decodes the response JSON into T. It calls Client.SystemOneAs. The question name in that JSON is AnswerKey. The shape to mirror is under noul answer.

func (*Client) Rate

func (c *Client) Rate(ctx context.Context, state any, instructions any, levels any, opts ...Option) (ScoreAnswer, error)

Rate places the state on an ordered rubric. levels is a JSON array, usually a []string, lowest level first; see score and writing good levels. It calls Client.Score.

func (*Client) RateAs

func (c *Client) RateAs[T any](ctx context.Context, state any, instructions any, levels any, opts ...Option) (T, error)

RateAs places the state on an ordered rubric and decodes the response JSON into T. It calls Client.ScoreAs. The question name in that JSON is AnswerKey. The shape to mirror is under score answer.

func (*Client) Score

func (c *Client) Score(ctx context.Context, state any, q Score, opts ...Option) (ScoreAnswer, error)

Score asks one score question. See score, levels, and score answer. The call is one Client.SystemOne request.

func (*Client) ScoreAs

func (c *Client) ScoreAs[T any](ctx context.Context, state any, q Score, opts ...Option) (T, error)

ScoreAs asks one score question and decodes the response JSON into T. It calls Client.SystemOneAs. The question name in that JSON is AnswerKey. The shape to mirror is under score answer.

func (*Client) SystemOne

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

SystemOne evaluates req.State against req.Questions with one POST to the evaluation endpoint. One answer comes back for each question, under the same name; see response body.

Several questions about one state belong in one call. The model reads the state once and answers them in parallel, so adding a question does not add a round trip; see ask multiple questions together.

func (*Client) SystemOneAs

func (c *Client) SystemOneAs[T any](ctx context.Context, req Request, opts ...Option) (T, error)

SystemOneAs evaluates the request and decodes the JSON response into T. Object names are case-sensitive. Members that T does not declare are ignored. The JSON shape to mirror in T is documented under response body and answer types.

type ConnectionError

type ConnectionError struct {
	Method    string
	URL       string
	Timeout   time.Duration
	Attempts  int
	RequestID string
	Err       error
}

ConnectionError is a request that failed without a usable HTTP response, returned after retries. It is the Go form of the official SDKs' connection errors: APIConnectionError, and APITimeoutError when Timeout is set.

func (*ConnectionError) Error

func (e *ConnectionError) Error() string

func (*ConnectionError) Is

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

Is reports whether target is ErrConnection or, for a timed-out attempt, ErrTimeout.

func (*ConnectionError) Unwrap

func (e *ConnectionError) Unwrap() error

Unwrap exposes the transport error. Attempt timeouts do not unwrap to context.DeadlineExceeded, so that sentinel stays reserved for the caller's own deadline.

type Limiter

type Limiter interface {
	Wait(ctx context.Context) error
}

A Limiter blocks until the next request may be sent, or until ctx ends.

*rate.Limiter from golang.org/x/time/rate satisfies this as written, and NewLimiter returns one that also paces input tokens. An implementation must be safe for concurrent use: the client waits on it from every goroutine that sends, and a Limiter shared between clients is waited on by all of them.

The API counts requests and tokens per account, so two clients on the same key share one budget. Hand them the same Limiter with WithRateLimiter. See handling rate limits.

func NewLimiter

func NewLimiter(limit RateLimit) Limiter

NewLimiter returns a Limiter for limit. Pass it to WithRateLimiter on every client that shares the account, so they share one budget:

shared := jev.NewLimiter(jev.DefaultRateLimit())
fast, _ := jev.New(jev.WithModel("jev-latest"), jev.WithRateLimiter(shared))
pinned, _ := jev.New(jev.WithModel("jev-1.13.0"), jev.WithRateLimiter(shared))

Besides pacing, the limiter honors Retry-After. When a response carries one, every goroutine waiting on this limiter pauses until that time. A 429 tells the whole account to slow down, not only the call that saw it. See handling rate limits.

A zero limit returns nil, which disables pacing. The result is the interface rather than a concrete type on purpose: a nil pointer inside a non-nil interface would pass a nil check and panic on the first Wait.

type ModelInfo

type ModelInfo struct {
	Name        string `json:"name"`
	Description string `json:"description"`
	ReleaseDate string `json:"release_date"`
}

ModelInfo is one model the account can call, as returned by listing models. ReleaseDate is whatever string the API returns. The docs describe a calendar date; the service currently returns a full timestamp.

type Noul

type Noul struct {
	// Instructions is the question: a string, or any value that encodes as a
	// JSON object or array. Nil omits the field. See [structured instructions].
	//
	// [structured instructions]: https://docs.typesafe.ai/primitives/advanced#structured-instructions
	Instructions any
	// Criteria optionally describes what a yes and a no mean.
	Criteria *NoulCriteria
}

Noul asks a yes-or-no question. The answer is the probability that the answer is yes, from 0 to 1. The wire shape is under noul, and writing a noul question has advice on phrasing.

type NoulAnswer

type NoulAnswer struct {
	Noul float64
}

NoulAnswer is the probability that a yes/no question is yes, from 0 (no) to 1 (yes). See noul answer and reading a noul.

func (NoulAnswer) Type

func (NoulAnswer) Type() string

type NoulCriteria

type NoulCriteria struct {
	True  any `json:"true,omitzero"`
	False any `json:"false,omitzero"`
}

NoulCriteria describes the two outcomes of a Noul: what a value near 1 means, and what a value near 0 means. Nil fields are omitted. See noul and structured noul criteria.

type Option

type Option func(*config) error

Option configures a Client. Pass options to New, or to a single call. A call option overrides the client for that call only. An empty string means "not set" and leaves the current value in place.

The settings, their environment variables, and their defaults are the ones the official SDKs use; see client config.

func WithAPIKey

func WithAPIKey(key string) Option

WithAPIKey sets the bearer token sent as Authorization: Bearer <key>. The default is the TYPESAFE_API_KEY environment variable, the same variable the official SDKs read; see API_KEY_ENV. Blank and whitespace-only values are ignored.

A missing or invalid key is a 401, returned as ErrUnauthorized; see errors.

func WithBaseURL

func WithBaseURL(baseURL string) Option

WithBaseURL sets the API root, for a proxy or a mock. It must be an http or https URL without credentials, a query, or a fragment. The default is TYPESAFE_BASE_URL, then https://api.typesafe.ai, the host of the evaluation endpoint. See BASE_URL_ENV.

func WithHTTPClient

func WithHTTPClient(client *http.Client) Option

WithHTTPClient uses client for requests. Per-attempt deadlines are set on the request context, so a Timeout on client is a second, shorter limit. Nil is ignored. The client is not closed.

This is the place for a proxy, a custom TLS setup, or an in-process transport in tests. It plays the role of fetch in the JavaScript SDK.

func WithHeader

func WithHeader(key, value string) Option

WithHeader sets a header sent on every request, like defaultHeaders in the JavaScript SDK. Authorization, Accept, Content-Type, User-Agent, and the X-TypeSafe-* headers are reserved.

func WithLogger

func WithLogger(logger *slog.Logger) Option

WithLogger sets the logger. Info records one line per attempt. Debug adds headers and bodies. Authorization and cookie headers are redacted. Bodies are logged as sent, which includes your state. Nil is ignored. Without a logger, TYPESAFE_LOG_LEVEL selects one on stderr, and an unset or "off" level logs nothing. The levels and the redaction rule follow the official SDKs; see logLevel and LOG_LEVEL_ENV.

func WithModel

func WithModel(model string) Option

WithModel sets the model used when Request.Model is empty. The default is TYPESAFE_DEFAULT_MODEL, then jev-latest; see DEFAULT_MODEL_ENV.

jev-latest is an alias and it moves with each release. Pin a version such as jev-1.13.0 when two runs must hit the same model, for example after tuning confidence thresholds. See aliases and current models.

func WithRateLimit

func WithRateLimit(limit RateLimit) Option

WithRateLimit paces requests and input tokens to limit, on top of retries. The default is DefaultRateLimit, the documented account limit, so a client stays under it instead of finding it through 429 responses.

Pass the zero RateLimit to disable pacing. Pass it to New only: a limiter built for one call would start with a full budget and pace nothing. For one call, use WithRateLimiter with a limiter you own, or nil to skip pacing.

The limit is per account. Several clients on the same key should share one limiter through NewLimiter and WithRateLimiter. See current models.

Example

A plan with other limits, or one process out of several on the same key.

package main

import (
	"fmt"

	"github.com/kataras/jev"
)

func main() {
	client, err := jev.New(jev.WithRateLimit(jev.RateLimit{
		RequestsPerMinute: 300,
		TokensPerSecond:   60_000,
	}))
	if err != nil {
		fmt.Println(err)
		return
	}
	_ = client
}

func WithRateLimiter

func WithRateLimiter(l Limiter) Option

WithRateLimiter uses a Limiter you own, in place of the one WithRateLimit builds. Clients given the same Limiter share one budget, which is what an account-wide quota asks for. Nil disables pacing.

A plain *rate.Limiter paces requests only. The Limiter from NewLimiter also paces tokens and honors Retry-After. See handling rate limits.

As a call option it replaces the client's limiter for that call.

Example

The API limits are per account. Two clients on the same key share one limiter, so together they stay under the limit.

package main

import (
	"fmt"

	"github.com/kataras/jev"
)

func main() {
	shared := jev.NewLimiter(jev.DefaultRateLimit())

	latest, err := jev.New(jev.WithRateLimiter(shared))
	if err != nil {
		fmt.Println(err)
		return
	}
	pinned, err := jev.New(jev.WithModel("jev-1.13.0"), jev.WithRateLimiter(latest.Limiter()))
	if err != nil {
		fmt.Println(err)
		return
	}
	_, _ = latest, pinned
}

func WithRetry

func WithRetry(policy RetryPolicy) Option

WithRetry replaces the retry policy. Copy DefaultRetryPolicy and edit it. The zero RetryPolicy retries nothing.

The API asks clients to retry 429 and 529 with exponential backoff; see handling rate limits. The default policy does that, and WithRateLimit keeps the client under the limit in the first place.

func WithTimeout

func WithTimeout(timeout time.Duration) Option

WithTimeout sets the limit for one HTTP attempt, including reading the body. The default is 10 seconds, the same as the official SDKs; see timeout. There is no budget across retries unless RetryPolicy.MaxElapsed is set. The context you pass to the call also applies.

type Question

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

Question is a Noul, Choice, Score, or Raw. The three kinds and how to pick one are described under question types and choose a question type.

type Questions

type Questions map[string]Question

Questions maps the name you choose to a question. It is the questions member of the request body. Answers come back under the same names. Names are not part of the prompt. On the wire, names are sorted so two equal requests encode to the same bytes.

type RateLimit

type RateLimit struct {
	// RequestsPerMinute caps how many requests this limiter lets through.
	// Requests are spread across the minute: the burst is one second's worth,
	// so a quiet client can send about RequestsPerMinute/60 at once.
	RequestsPerMinute int
	// TokensPerSecond caps input tokens. The body size is not known in tokens
	// before the call, so the limiter estimates from the body length, then
	// corrects itself with usage.input_tokens from each response.
	// The burst is one second's worth; a request larger than that waits for
	// a full second's budget.
	TokensPerSecond int
}

RateLimit is the client-side pacing for one account. Start from DefaultRateLimit and change the fields you care about. A zero field disables that axis; the zero RateLimit paces nothing.

The numbers to match are on the models page. They are per account, so if several processes share a key, divide the budget between them.

func DefaultRateLimit

func DefaultRateLimit() RateLimit

DefaultRateLimit is the documented limit for jev-1.13.0: 1,200 requests per minute and 250,000 input tokens per second. See current models.

func (RateLimit) IsZero

func (l RateLimit) IsZero() bool

IsZero reports whether the limit paces nothing.

type Raw

type Raw map[string]any

Raw is a question object sent as-is, for a kind this version does not model. It must include a non-empty string field named type. Known kinds are validated the same way as Noul, Choice, and Score. The current kinds are listed under question types.

type Request

type Request struct {
	// State is the content every question reads. A string, a map, a slice, or
	// a struct with json tags. A []byte is sent as text, not as base64.
	// Nil is sent as JSON null. What to put in it, and how to point a
	// question at a nested field, is under [state].
	//
	// [state]: https://docs.typesafe.ai/concepts/state
	State any
	// Questions is the non-empty set of named questions.
	Questions Questions
	// Model overrides the client default when non-empty. See [models].
	//
	// [models]: https://docs.typesafe.ai/models#current-models
	Model string
	// Extra adds top-level JSON fields this version does not model.
	// Keys state, model, and questions are reserved and rejected.
	Extra map[string]any
}

Request is the input to Client.SystemOne: the request body of one POST to /v1/systemone.

type Response

type Response struct {
	Model     string
	Answers   map[string]Answer
	Usage     Usage
	RequestID string
	Attempts  int
	// contains filtered or unexported fields
}

Response is a successful System One result: the response body of one call. Model is the versioned ID that answered, even when the request named an alias. Attempts counts HTTP attempts including retries.

func (*Response) As

func (r *Response) As[T any]() (T, error)

As decodes the response JSON into T. JSON object names are case-sensitive. Unknown object members are ignored. The names to mirror in T are under response body and answer types.

func (*Response) Choice

func (r *Response) Choice(name string) (ChoiceAnswer, bool)

Choice returns the choice answer named name. See choice answer.

func (*Response) Noul

func (r *Response) Noul(name string) (NoulAnswer, bool)

Noul returns the yes/no answer named name. See noul answer.

func (*Response) Raw

func (r *Response) Raw() []byte

Raw returns a copy of the response JSON, in the shape of the response body.

func (*Response) Score

func (r *Response) Score(name string) (ScoreAnswer, bool)

Score returns the score answer named name. See score answer.

type ResponseError

type ResponseError struct {
	Method    string
	URL       string
	RequestID string
	Field     string
	Body      []byte
	Err       error
}

ResponseError is a 2xx body that failed validation, or a body over the size limit. Field is the JSON path that failed, such as answers.team.choice. The contract it checks is the response body and its answer types; the official SDKs raise the same thing as response validation errors.

func (*ResponseError) Error

func (e *ResponseError) Error() string

func (*ResponseError) Is

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

func (*ResponseError) Unwrap

func (e *ResponseError) Unwrap() error

type RetryPolicy

type RetryPolicy struct {
	// MaxRetries is how many retries follow the first attempt. 0 disables retries.
	// SDK name: maxRetries.
	MaxRetries int
	// InitialBackoff is the first backoff delay, doubled on each retry up to MaxBackoff.
	// SDK name: backoffInitialMs.
	InitialBackoff time.Duration
	// MaxBackoff caps the computed backoff. SDK name: backoffMaxMs.
	MaxBackoff time.Duration
	// Jitter is the fraction of each backoff delay that may be subtracted, from 0 to 1.
	// SDK name: backoffJitter.
	Jitter float64
	// Statuses are the HTTP status codes that are retried. SDK name: httpStatuses.
	Statuses []int
	// RespectRetryAfter honors Retry-After-Ms and Retry-After when the delay
	// is within MaxRetryAfter. A longer server delay falls back to backoff.
	// SDK name: respectRetryAfter.
	RespectRetryAfter bool
	// MaxRetryAfter is the longest server-requested delay that is honored,
	// by retries and by the pause a [Limiter] from [NewLimiter] applies to
	// every caller. SDK name: maxRetryAfterMs.
	MaxRetryAfter time.Duration
	// RetryConnection retries connection failures. SDK name: apiConnectionError.
	RetryConnection bool
	// RetryTimeout retries an attempt that hit the per-attempt timeout.
	// SDK name: apiTimeoutError.
	RetryTimeout bool
	// MaxElapsed is a budget for the whole call, including waits between attempts.
	// Zero means there is no budget. The caller's context always applies.
	MaxElapsed time.Duration
	// contains filtered or unexported fields
}

RetryPolicy controls retries after the first attempt. Start from DefaultRetryPolicy and change the fields you care about. A policy built from the zero value retries nothing, because Statuses is empty.

The fields mirror the official SDKs' RetryPolicy, with Go names and durations. The API's own advice on what to retry is under handling rate limits.

func DefaultRetryPolicy

func DefaultRetryPolicy() RetryPolicy

DefaultRetryPolicy matches the official JavaScript SDK: 2 retries, 500ms backoff doubling to 5s, 25% jitter, and retries on 408, 429, and 500-599. Retry-After is honored up to 60s. There is no total time budget unless you set MaxElapsed. The defaults are listed field by field under RetryPolicy.

type Score

type Score struct {
	Instructions any
	Criteria     any
}

Score rates the state against an ordered rubric. Criteria is a JSON array of level descriptions, lowest first, such as a []string or []any. The API schema requires at least one level. The public docs cap a score at 10 levels, and this client enforces that cap. A map is rejected: since SDK 0.6.0, score criteria are a list, not an object. The wire shape is under score; writing good levels has advice on the rubric.

type ScoreAnswer

type ScoreAnswer struct {
	Score         float64
	Confidence    float64
	Legend        map[string]any
	Probabilities map[string]float64
}

ScoreAnswer is a probability-weighted position along the rubric. Score can fall between levels. Legend maps the level index, as a string, back to the description you sent. Probabilities uses those same string keys. See score answer and reading a score.

func (ScoreAnswer) Type

func (ScoreAnswer) Type() string

type UnknownAnswer

type UnknownAnswer struct {
	Kind string
	Raw  []byte
}

UnknownAnswer is a question kind this version does not model. Raw is the JSON object for that answer. The kinds this version knows are listed under answer types.

func (UnknownAnswer) Type

func (a UnknownAnswer) Type() string

type Usage

type Usage struct {
	InputTokens  int
	OutputTokens int
}

Usage counts tokens for one call. Input tokens are billed and count toward the token rate limit; output tokens are free. See usage and current models.

Directories

Path Synopsis
examples
quickstart command
Command quickstart calls System One with one of each question type.
Command quickstart calls System One with one of each question type.

Jump to

Keyboard shortcuts

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