Documentation
¶
Overview ¶
Package jev is a Go client for the TypeSafe AI API.
TypeSafe answers structured questions about a piece of content, called the state. You name each question; the answers come back under those names, with the probabilities behind them.
Quickstart ¶
Set TYPESAFE_API_KEY in the environment, then:
client, err := jev.New()
if err != nil {
return err
}
resp, err := client.SystemOne(ctx, jev.SystemOneRequest{
State: "I was charged twice. Please fix this ASAP.",
Questions: map[string]jev.Question{
"category": jev.Choice{
Instructions: "What is this ticket about?",
Criteria: jev.Labels("billing", "technical", "other"),
},
},
})
if err != nil {
return err
}
fmt.Println(resp.Choices["category"].Choice)
Questions ¶
There are three kinds. Noul asks a yes/no question and answers with a probability. Choice selects one of several named labels. Score rates the state against an ordered rubric. RawQuestion passes an arbitrary object through, for question kinds the API accepts before this package models them.
Typed answers ¶
ChoiceOf and ScoreOf take labels and levels of the caller's own types. Register each question with Ask, which returns a typed Key; its answer comes back in that type, and is checked against what the question offered:
req := jev.SystemOneRequest{State: ticket}
team := jev.Ask(&req, "team", jev.ChoiceOf[Team]{Criteria: jev.LabelsOf(Billing, Technical)})
resp, err := client.SystemOne(ctx, req)
// ...
t, err := team.Answer(resp) // t.Choice is a Team
Choice and Score are ChoiceOf[string] and ScoreOf[int], so everything written against them works unchanged. Answers carry their readings: ChoiceAnswerOf.Ranked orders the labels by probability, and ScoreAnswerOf.Level rounds a score to its rubric level. An answer of a kind this version does not model arrives as an UnknownAnswer.
Errors ¶
Failures come back as one of three types, each recoverable with errors.As:
- *APIError for an unsuccessful HTTP response. Use errors.Is against ErrRateLimited, ErrUnauthorized and the other status sentinels to branch without inspecting status codes.
- *TransportError when the request never reached the API.
- *ResponseError when the response body was missing data the SDK needs, or left one of your questions unanswered. It names the offending field.
Concurrency ¶
A Client is safe for concurrent use and pools connections, so create one and share it. Parallelism is the caller's to choose: run calls in goroutines, bounded however the program needs.
Retries ¶
By default a call is retried twice on 408, 429 and 5xx responses and on transport failures, with exponential backoff, honoring the server's Retry-After up to a minute, within a 30 second budget. See RetryPolicy to change or disable that, per client or per call.
Logging ¶
The SDK logs nothing until WithLogger supplies a logger. URLs are stripped of credentials and credential-bearing headers are redacted. Request and response bodies are left out unless WithBodyLogging asks for them: the request body is the state being evaluated, so it carries whatever the caller's content carries.
Gateways and providers ¶
The client talks to anything that serves the System One API. For OpenRouter, set the base URL to https://openrouter.ai/api and use an OpenRouter key:
client, err := jev.New(
jev.WithBaseURL("https://openrouter.ai/api"),
jev.WithAPIKey(os.Getenv("OPENROUTER_API_KEY")),
)
ListModels does not work through OpenRouter, whose model catalog has a shape of its own. For a gateway of your own, point WithBaseURL at it and add its headers with WithHeader.
Observability ¶
There is no tracing dependency. Wrap the transport of the http.Client passed to WithHTTPClient, with OpenTelemetry's otelhttp for example, and quote the RequestID every response and error carries in a support ticket.
Learn what TypeSafe is and what it can do at https://docs.typesafe.ai.
Index ¶
- Constants
- Variables
- func Labels(names ...string) map[string]any
- func LabelsOf[T ~string](names ...T) map[T]any
- func Levels(descriptions ...string) []any
- func SystemOneAs[T any](ctx context.Context, c *Client, req SystemOneRequest, opts ...CallOption) (*T, error)
- type APIError
- type Answer
- type CallOption
- type Choice
- type ChoiceAnswer
- type ChoiceAnswerOf
- type ChoiceOf
- type Client
- type ClientOption
- func WithAPIKey(key string) ClientOption
- func WithBaseURL(rawURL string) ClientOption
- func WithBodyLogging() ClientOption
- func WithHTTPClient(client *http.Client) ClientOption
- func WithHeader(name, value string) ClientOption
- func WithLogger(l *slog.Logger) ClientOption
- func WithMaxResponseBytes(n int64) ClientOption
- func WithModel(model string) ClientOption
- func WithRetry(policy RetryPolicy) ClientOption
- func WithTimeout(d time.Duration) ClientOption
- type Key
- type ListModelsResponse
- type ModelMetadata
- type Noul
- type NoulAnswer
- type NoulCriteria
- type Question
- type RawQuestion
- type ResponseError
- type ResponseMeta
- type RetryPolicy
- type Score
- type ScoreAnswer
- type ScoreAnswerOf
- type ScoreLevel
- type ScoreOf
- type SystemOneRequest
- type SystemOneResponse
- type TransportError
- type TypedQuestion
- type UnknownAnswer
- type Usage
Examples ¶
Constants ¶
const ( APIKeyEnv = "TYPESAFE_API_KEY" BaseURLEnv = "TYPESAFE_BASE_URL" DefaultModelEnv = "TYPESAFE_DEFAULT_MODEL" )
Environment variables the SDK reads. An explicit option always wins, and an empty or whitespace-only value is ignored rather than shadowing the default.
const ( DefaultBaseURL = "https://api.typesafe.ai" DefaultModel = "jev-latest" // DefaultTimeout bounds each individual HTTP attempt, not the call as a // whole; see [RetryPolicy.Budget] for that. DefaultTimeout = 10 * time.Second // DefaultMaxResponseBytes caps how much of a response body is read into // memory, so a runaway or hostile server cannot exhaust it. The API's // responses are far smaller than this. DefaultMaxResponseBytes = 1 << 20 // 1 MiB )
Client defaults.
Variables ¶
var ( ErrNoAPIKey = errors.New("jev: no API key provided") ErrInvalidAPIKey = errors.New("jev: invalid API key") ErrInvalidTimeout = errors.New("jev: invalid timeout") ErrInvalidRetry = errors.New("jev: invalid retry policy") ErrInvalidBaseURL = errors.New("jev: invalid base URL") ErrInvalidOption = errors.New("jev: invalid option") ErrInvalidState = errors.New("jev: invalid state") ErrNoQuestions = errors.New("jev: at least one question is required") ErrInvalidQuestion = errors.New("jev: invalid question") ErrEncodeRequest = errors.New("jev: request body could not be encoded as JSON") // ErrResponseTooLarge reports a response body over the limit set by // [WithMaxResponseBytes]. It arrives wrapped in a [*ResponseError]. ErrResponseTooLarge = errors.New("jev: response body too large") )
Configuration and input errors, all reported before any request is sent.
var ( ErrBadRequest = errors.New("jev: bad request") ErrForbidden = errors.New("jev: permission denied") ErrNotFound = errors.New("jev: not found") ErrUnprocessable = errors.New("jev: unprocessable entity") ErrRateLimited = errors.New("jev: rate limited") ErrServer = errors.New("jev: server error") )
Status categories. An APIError matches these through errors.Is, so a caller can branch on the kind of failure without inspecting status codes:
if errors.Is(err, jev.ErrRateLimited) { ... }
Functions ¶
func Labels ¶
Labels builds Choice criteria for choices that need no description, so a call site is not padded out with nil values:
jev.Choice{
Instructions: "What is this ticket about?",
Criteria: jev.Labels("billing", "technical", "other"),
}
Describe a choice by writing the map out instead; a description sharpens the boundary between labels and is worth the extra line when they overlap. A label repeated here appears once in the request. For labels of your own type, see LabelsOf.
Example ¶
Labels builds criteria for choices that speak for themselves, so the common case is not padded out with nil values.
package main
import (
"encoding/json"
"fmt"
jev "github.com/HomayoonAlimohammadi/jev-sdk-go"
)
func main() {
question := jev.Choice{
Instructions: "What is this ticket about?",
Criteria: jev.Labels("billing", "technical", "other"),
}
encoded, err := json.Marshal(question)
if err != nil {
panic(err)
}
fmt.Println(string(encoded))
}
Output: {"type":"choice","instructions":"What is this ticket about?","criteria":{"billing":null,"other":null,"technical":null}}
Example (Described) ¶
A description sharpens the boundary between labels that overlap. Write the map out to mix described and undescribed choices.
package main
import (
"encoding/json"
"fmt"
jev "github.com/HomayoonAlimohammadi/jev-sdk-go"
)
func main() {
question := jev.Choice{
Instructions: "What is this ticket about?",
Criteria: map[string]any{
"billing": "Payments, invoices and refunds, but not delivery complaints",
"technical": map[string]any{"meaning": "The product misbehaved", "examples": []string{"500 error"}},
"other": nil,
},
}
encoded, err := json.Marshal(question)
if err != nil {
panic(err)
}
fmt.Println(string(encoded))
}
Output: {"type":"choice","instructions":"What is this ticket about?","criteria":{"billing":"Payments, invoices and refunds, but not delivery complaints","other":null,"technical":{"examples":["500 error"],"meaning":"The product misbehaved"}}}
func Levels ¶
Levels builds Score criteria from one plain description per level, so a rubric of short phrases reads as one:
jev.Score{
Instructions: "How urgent is this ticket?",
Criteria: jev.Levels("can wait", "this week", "today"),
}
Order is the meaning here, unlike Labels: a description's position is its score, counting from zero. Write the slice out to describe a level with something richer than a string.
Example ¶
Levels builds a score rubric from one phrase per level. Position is the score, counting from zero.
package main
import (
"encoding/json"
"fmt"
jev "github.com/HomayoonAlimohammadi/jev-sdk-go"
)
func main() {
question := jev.Score{
Instructions: "How urgent is this ticket?",
Criteria: jev.Levels("can wait", "this week", "today"),
}
encoded, err := json.Marshal(question)
if err != nil {
panic(err)
}
fmt.Println(string(encoded))
}
Output: {"type":"score","instructions":"How urgent is this ticket?","criteria":["can wait","this week","today"]}
func SystemOneAs ¶
func SystemOneAs[T any](ctx context.Context, c *Client, req SystemOneRequest, opts ...CallOption) (*T, error)
SystemOneAs answers the request and decodes the response body into T instead of SystemOneResponse.
The body is decoded as the API sends it, so T mirrors the wire shape:
type myAnswers struct {
jev.ResponseMeta // optional: filled in with the request ID and headers
Answers struct {
Billing jev.NoulAnswer `json:"billing"`
Tone jev.ChoiceAnswer `json:"tone"`
} `json:"answers"`
}
answers, err := jev.SystemOneAs[myAnswers](ctx, client, req)
It is a function rather than a method so the SDK builds on Go releases before 1.27, which cannot declare a generic method.
Unlike Client.SystemOne, nothing about the response is checked. A field the server omitted is left at its zero value, and an answer the server never sent is left at its zero value too, so a NoulAnswer{} is indistinguishable from a confident "no" and a ChoiceAnswer{} from an empty choice. Where that difference could decide anything, use pointer fields so absence is visible, or call Client.SystemOne, which rejects a response that leaves a question unanswered.
Example ¶
SystemOneAs decodes the response body into a type of your own. The body is decoded as the API sends it, so the struct mirrors the wire shape.
package main
import (
"context"
"fmt"
jev "github.com/HomayoonAlimohammadi/jev-sdk-go"
)
func main() {
type ticketAnswers struct {
jev.ResponseMeta // optional: filled in with the request ID and headers
Answers struct {
Billing jev.NoulAnswer `json:"billing"`
Tone jev.ChoiceAnswer `json:"tone"`
} `json:"answers"`
}
client, err := jev.New()
if err != nil {
panic(err)
}
answers, err := jev.SystemOneAs[ticketAnswers](context.Background(), client, jev.SystemOneRequest{
State: "I was charged twice.",
Questions: map[string]jev.Question{
"billing": jev.Noul{Instructions: "Is this about billing?"},
"tone": jev.Choice{Criteria: jev.Labels("calm", "angry")},
},
})
if err != nil {
panic(err)
}
fmt.Println(answers.Answers.Billing.Noul, answers.RequestID)
}
Output:
Types ¶
type APIError ¶
type APIError struct {
// StatusCode is the HTTP response status.
StatusCode int
// Message is the server's explanation, extracted from the response body.
Message string
// Body is the raw response body.
Body []byte
// Header holds the response headers.
Header http.Header
// RequestID is the x-typesafe-request-id response header, if present.
RequestID string
// Endpoint is the request method and URL, without credentials, query
// parameters or fragment.
Endpoint string
// RetryAfter is the delay the server asked for, from the retry-after or
// retry-after-ms headers. It is zero when the server did not ask.
RetryAfter time.Duration
// Attempts is how many attempts were made, retries included, before this
// response was returned.
Attempts int
}
APIError is an unsuccessful HTTP response from the API.
Example ¶
Errors carry the detail needed to decide what to do next.
package main
import (
"context"
"errors"
"fmt"
jev "github.com/HomayoonAlimohammadi/jev-sdk-go"
)
func main() {
client, err := jev.New()
if err != nil {
panic(err)
}
_, err = client.ListModels(context.Background())
var apiErr *jev.APIError
var transportErr *jev.TransportError
var responseErr *jev.ResponseError
switch {
case err == nil:
case errors.Is(err, jev.ErrRateLimited):
errors.As(err, &apiErr)
fmt.Printf("rate limited, retry in %v (request %s)\n", apiErr.RetryAfter, apiErr.RequestID)
case errors.Is(err, jev.ErrUnauthorized):
fmt.Println("check TYPESAFE_API_KEY")
case errors.As(err, &transportErr):
fmt.Printf("never reached the API after %d attempts: %v\n", transportErr.Attempts, transportErr.Err)
case errors.As(err, &responseErr):
fmt.Printf("unusable response at %s\n", responseErr.Field)
default:
fmt.Println(err)
}
}
Output:
type Answer ¶
type Answer interface {
// contains filtered or unexported methods
}
Answer is a single answer to a named question. It is a sealed union: only NoulAnswer, ChoiceAnswerOf, ScoreAnswerOf and UnknownAnswer implement it. Recover the concrete answer with a type switch, read it through a Key, or reach for one of the typed maps on SystemOneResponse.
type CallOption ¶
type CallOption func(*callConfig) error
CallOption overrides a client setting for one call.
func WithCallHeader ¶
func WithCallHeader(name, value string) CallOption
WithCallHeader sets a header for one call, overriding any client header of the same name.
func WithCallRetry ¶
func WithCallRetry(policy RetryPolicy) CallOption
WithCallRetry replaces the client's retry policy for one call. It does not merge with the client policy: the given one is used as written.
func WithCallTimeout ¶
func WithCallTimeout(d time.Duration) CallOption
WithCallTimeout overrides the per-attempt timeout for one call. Zero inherits the client's timeout.
type ChoiceAnswer ¶
type ChoiceAnswer = ChoiceAnswerOf[string]
ChoiceAnswer is a ChoiceAnswerOf with plain string labels.
type ChoiceAnswerOf ¶
type ChoiceAnswerOf[T ~string] struct { // Choice is the label with the highest probability. Choice T `json:"choice"` // Confidence in the selection, from 0 to 1. It is the API's own measure, // not the probability of Choice. Confidence float64 `json:"confidence"` // Probabilities holds every label's probability. They sum to about 1. Probabilities map[T]float64 `json:"probabilities"` }
ChoiceAnswerOf is a selected label and the probabilities of the alternatives, with labels of the caller's own string type.
See https://docs.typesafe.ai/primitives/choice.
func (ChoiceAnswerOf[T]) Ranked ¶
func (a ChoiceAnswerOf[T]) Ranked() []T
Ranked returns the labels from most to least probable, so the runner-up is at index 1: worth a look before acting on a low-confidence choice. Labels of equal probability keep a stable, lexical order.
Example ¶
Ranked orders the labels by probability, so the runner-up is one index away.
package main
import (
"fmt"
jev "github.com/HomayoonAlimohammadi/jev-sdk-go"
)
// Team is a caller's own label type for a routing question.
type Team string
const Technical Team = "technical"
func main() {
answer := jev.ChoiceAnswerOf[Team]{
Choice: Technical,
Confidence: 0.62,
Probabilities: map[Team]float64{Billing: 0.38, Technical: 0.62},
}
fmt.Println(answer.Ranked())
}
Output: [technical billing]
type ChoiceOf ¶
type ChoiceOf[T ~string] struct { // Instructions is what the model should decide. See [Noul.Instructions]. Instructions any `json:"instructions,omitempty"` // Criteria maps each label to a description of when it applies. A nil // description leaves the label to be interpreted by its name alone. // Between 1 and 255 labels are required. Criteria map[T]any `json:"criteria"` }
ChoiceOf is a question that selects one of several named alternatives, with labels of the caller's own string type. Its answer, read through Ask, comes back as that same type, so a switch over the choice is checked by the compiler:
type Team string
const (
Billing Team = "billing"
Technical Team = "technical"
)
team := jev.Ask(&req, "team", jev.ChoiceOf[Team]{Criteria: map[Team]any{Billing: nil, Technical: nil}})
See https://docs.typesafe.ai/primitives/choice.
func (ChoiceOf[T]) MarshalJSON ¶
MarshalJSON encodes the question with its "choice" type discriminator.
type Client ¶
type Client struct {
// contains filtered or unexported fields
}
Client talks to the TypeSafe AI API. It is safe for concurrent use, and a single client should be shared: it reuses connections through its underlying http.Client.
Example (Concurrent) ¶
The client is safe to share across goroutines; the caller chooses the parallelism.
package main
import (
"context"
"sync"
jev "github.com/HomayoonAlimohammadi/jev-sdk-go"
)
func main() {
client, err := jev.New()
if err != nil {
panic(err)
}
tickets := []string{"I was charged twice.", "How do I reset my password?"}
results := make([]*jev.SystemOneResponse, len(tickets))
var wg sync.WaitGroup
for i, ticket := range tickets {
wg.Add(1)
go func() {
defer wg.Done()
resp, err := client.SystemOne(context.Background(), jev.SystemOneRequest{
State: ticket,
Questions: map[string]jev.Question{"billing": jev.Noul{Instructions: "Is this about billing?"}},
})
if err != nil {
return
}
results[i] = resp
}()
}
wg.Wait()
}
Output:
func New ¶
func New(opts ...ClientOption) (*Client, error)
New creates a client. With no options it reads TYPESAFE_API_KEY from the environment and talks to the public API with the default retry policy.
client, err := jev.New()
if err != nil {
return err
}
func (*Client) CloseIdleConnections ¶
func (c *Client) CloseIdleConnections()
CloseIdleConnections releases connections the client is holding open but not using. Calling it is optional: a client needs no teardown, and dropping one leaks nothing. Unless the client came from WithHTTPClient, the connections are its own, and no other code in the process loses any.
func (*Client) ListModels ¶
func (c *Client) ListModels(ctx context.Context, opts ...CallOption) (*ListModelsResponse, error)
ListModels returns the models available to the account. A model's Name is accepted by SystemOneRequest.Model.
models, err := client.ListModels(ctx)
Example ¶
package main
import (
"context"
"fmt"
jev "github.com/HomayoonAlimohammadi/jev-sdk-go"
)
func main() {
client, err := jev.New()
if err != nil {
panic(err)
}
resp, err := client.ListModels(context.Background())
if err != nil {
panic(err)
}
for _, model := range resp.Models {
fmt.Printf("%s (%s): %s\n", model.Name, model.ReleaseDate, model.Description)
}
}
Output:
func (*Client) SystemOne ¶
func (c *Client) SystemOne(ctx context.Context, req SystemOneRequest, opts ...CallOption) (*SystemOneResponse, error)
SystemOne answers the request's questions about its state.
resp, err := client.SystemOne(ctx, jev.SystemOneRequest{
State: "I was charged twice. Please help.",
Questions: map[string]jev.Question{
"billing": jev.Noul{Instructions: "Is this about billing?"},
},
})
It returns ErrNoQuestions or ErrInvalidQuestion before sending anything if the request cannot be answered, an *APIError for an unsuccessful response, a *TransportError if the request never reached the API, and a *ResponseError if the response body was not usable.
Example ¶
package main
import (
"context"
"fmt"
jev "github.com/HomayoonAlimohammadi/jev-sdk-go"
)
func main() {
client, err := jev.New()
if err != nil {
panic(err)
}
resp, err := client.SystemOne(context.Background(), jev.SystemOneRequest{
State: "I was charged twice. Please fix this ASAP.",
Questions: map[string]jev.Question{
"billing": jev.Noul{Instructions: "Is this ticket about billing?"},
"tone": jev.Choice{
Instructions: "What is the customer's tone?",
Criteria: jev.Labels("calm", "frustrated", "angry"),
},
"urgency": jev.Score{
Instructions: "How urgent is this ticket?",
Criteria: jev.Levels("can wait", "this week", "today"),
},
},
})
if err != nil {
panic(err)
}
fmt.Println(resp.Nouls["billing"].Noul)
fmt.Println(resp.Choices["tone"].Choice)
fmt.Println(resp.Scores["urgency"].Score)
}
Output:
Example (RichCriteria) ¶
Criteria descriptions are not limited to strings: any JSON-marshalable value works, so a criterion can carry examples or structure.
package main
import (
"context"
jev "github.com/HomayoonAlimohammadi/jev-sdk-go"
)
func main() {
client, err := jev.New()
if err != nil {
panic(err)
}
_, err = client.SystemOne(context.Background(), jev.SystemOneRequest{
State: map[string]any{"subject": "Charged twice", "body": "I see two charges of $49."},
Questions: map[string]jev.Question{
"duplicate": jev.Noul{
Instructions: "Is this a duplicate charge?",
Criteria: &jev.NoulCriteria{
True: map[string]any{"meaning": "Billed more than once", "examples": []string{"charged twice"}},
False: "A single, expected charge",
},
},
},
})
if err != nil {
panic(err)
}
}
Output:
type ClientOption ¶
type ClientOption func(*clientConfig) error
ClientOption configures a Client. Options are applied in order, and each one overrides the environment.
func WithAPIKey ¶
func WithAPIKey(key string) ClientOption
WithAPIKey sets the API key, overriding TYPESAFE_API_KEY. Surrounding whitespace is trimmed.
func WithBaseURL ¶
func WithBaseURL(rawURL string) ClientOption
WithBaseURL sets the API root, overriding TYPESAFE_BASE_URL: point it at a gateway, or at https://openrouter.ai/api with an OpenRouter key. The scheme must be https or http, trailing slashes are trimmed, and a query or fragment is rejected because it would swallow the endpoint path. Credentials in the URL are sent but never logged. A plaintext http host other than loopback is logged as a warning, since it puts the API key on the wire in the clear.
func WithBodyLogging ¶
func WithBodyLogging() ClientOption
WithBodyLogging allows request and response bodies into debug logs.
It is off by default and should stay off in production: the request body is the state being evaluated and the response is the model's reading of it, so both carry whatever the caller's content carries. Nothing in a body is redacted, and turning this on makes a service-wide debug log level enough to persist customer content.
func WithHTTPClient ¶
func WithHTTPClient(client *http.Client) ClientOption
WithHTTPClient supplies the http.Client used for every request. Use it for proxies, custom TLS, connection tuning or instrumentation. The client is not modified, and it is the caller's to close.
Its transport is used as is, pool size included. For concurrent use, raise MaxIdleConnsPerHost: net/http's default keeps two idle connections per host, so every call beyond the second in flight redials. The SDK's own client, used when this option is absent, is sized for that already.
A supplied client follows redirects unless it sets its own CheckRedirect. Set it to return http.ErrUseLastResponse: net/http keeps an Authorization header across an https to http redirect to the same host, and keeps every header this SDK does not know about across a redirect to any host.
func WithHeader ¶
func WithHeader(name, value string) ClientOption
WithHeader sets a header on every request. It may be used repeatedly; a later call replaces an earlier one for the same name. Authentication, content negotiation and SDK identification headers cannot be replaced.
func WithLogger ¶
func WithLogger(l *slog.Logger) ClientOption
WithLogger sends the SDK's logs to l. URLs are stripped of credentials and credential-bearing headers are redacted. Request and response bodies are left out unless WithBodyLogging asks for them. Without this option the SDK logs nothing.
Example ¶
The SDK logs nothing until it is given a logger. Credential-bearing headers are redacted; bodies are logged verbatim at debug level.
package main
import (
"log/slog"
"os"
jev "github.com/HomayoonAlimohammadi/jev-sdk-go"
)
func main() {
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelInfo}))
client, err := jev.New(jev.WithLogger(logger))
if err != nil {
panic(err)
}
_ = client
}
Output:
func WithMaxResponseBytes ¶
func WithMaxResponseBytes(n int64) ClientOption
WithMaxResponseBytes caps how much of a response body is read into memory, guarding against a runaway or hostile server. A body over the cap fails with a *ResponseError wrapping ErrResponseTooLarge. Zero removes the cap; a negative value is refused with ErrInvalidOption.
func WithModel ¶
func WithModel(model string) ClientOption
WithModel sets the model used when a request does not name one, overriding TYPESAFE_DEFAULT_MODEL.
func WithRetry ¶
func WithRetry(policy RetryPolicy) ClientOption
WithRetry replaces the retry policy. Pass RetryPolicy{} to disable retries.
func WithTimeout ¶
func WithTimeout(d time.Duration) ClientOption
WithTimeout bounds each HTTP attempt. It must be positive. To bound a whole call including retries, set RetryPolicy.Budget or give the call a context with a deadline.
type Key ¶
type Key[A any] struct { // contains filtered or unexported fields }
Key is a typed handle on the answer to a question registered with Ask. Its zero value reads nothing: Key.Answer fails.
func Ask ¶
func Ask[A any](req *SystemOneRequest, name string, question TypedQuestion[A]) Key[A]
Ask registers question under name in req and returns a handle typed by the answer the question reads back as. Reading it needs no type assertion, and a ChoiceOf or ScoreOf answer comes back in the caller's own label or level type, so the compiler checks what is done with it:
req := jev.SystemOneRequest{State: ticket}
urgent := jev.Ask(&req, "urgent", jev.Noul{Instructions: "Is this urgent?"})
team := jev.Ask(&req, "team", jev.ChoiceOf[Team]{Criteria: jev.LabelsOf(Billing, Technical)})
resp, err := client.SystemOne(ctx, req)
// ...
t, err := team.Answer(resp) // t.Choice is a Team
A name already registered is replaced. A is inferred from question, which must therefore be a concrete question, not a Question interface value.
Example ¶
Ask returns a typed handle per question, so the answers come back as your own types and the compiler checks what you do with them.
package main
import (
"context"
"fmt"
jev "github.com/HomayoonAlimohammadi/jev-sdk-go"
)
// Team is a caller's own label type for a routing question.
type Team string
const Technical Team = "technical"
// Severity is a caller's own level type: iota numbering lines up with the
// rubric, where a description's position is its level.
type Severity int
const (
Cosmetic Severity = iota
Degraded
Blocking
)
func main() {
client, err := jev.New()
if err != nil {
panic(err)
}
req := jev.SystemOneRequest{State: "Our integration returns 500 on every request."}
urgent := jev.Ask(&req, "urgent", jev.Noul{Instructions: "Does this convey urgency?"})
team := jev.Ask(&req, "team", jev.ChoiceOf[Team]{
Instructions: "Which team should handle this?",
Criteria: map[Team]any{Billing: "Payments, invoices, refunds", Technical: "Bugs, outages, integrations"},
})
severity := jev.Ask(&req, "severity", jev.ScoreOf[Severity]{
Instructions: "How severe is the issue?",
Criteria: jev.Levels("cosmetic", "degraded, with a workaround", "blocking"),
})
resp, err := client.SystemOne(context.Background(), req)
if err != nil {
panic(err)
}
u, _ := urgent.Answer(resp)
t, _ := team.Answer(resp)
s, _ := severity.Answer(resp)
switch {
case t.Confidence < 0.5:
fmt.Println("route to a human; runner-up was", t.Ranked()[1])
case t.Choice == Technical && s.Level() == Blocking && u.Noul > 0.8:
fmt.Println("page on-call")
default:
fmt.Println("queue for", t.Choice)
}
}
Output:
func (Key[A]) Answer ¶
func (k Key[A]) Answer(resp *SystemOneResponse) (A, error)
Answer reads this question's answer out of resp.
It fails with a *ResponseError naming the field when the answer is absent, of another kind, or disagrees with the question: a choice the question never offered, or a level outside its rubric. Client.SystemOne already rejects a response that leaves a question unanswered or answered in the wrong kind, so against its result only the disagreements can surface here.
type ListModelsResponse ¶
type ListModelsResponse struct {
ResponseMeta
Models []ModelMetadata
}
ListModelsResponse is the models available to the account.
type ModelMetadata ¶
type ModelMetadata struct {
// Name is accepted by [SystemOneRequest.Model].
Name string `json:"name"`
// Description explains the model and its capabilities.
Description string `json:"description"`
// ReleaseDate is formatted as YYYY-MM-DD.
ReleaseDate string `json:"release_date"`
}
ModelMetadata describes one model available to the account.
type Noul ¶
type Noul struct {
// Instructions is the question or statement to evaluate, as a string, a
// JSON object, an array, or any other JSON-marshalable value. A nil value
// is omitted from the request; an explicitly empty one is sent as-is.
Instructions any `json:"instructions,omitempty"`
// Criteria optionally describes what counts as a yes and as a no.
Criteria *NoulCriteria `json:"criteria,omitempty"`
}
Noul is a yes/no question.
See https://docs.typesafe.ai/primitives/noul.
func (Noul) MarshalJSON ¶
MarshalJSON encodes the question with its "noul" type discriminator.
type NoulAnswer ¶
type NoulAnswer struct {
// Noul is the probability of a yes answer, from 0 to 1. Values near 0.5
// mean the model is unsure. The threshold to act on is yours: it depends on
// what a wrong yes and a wrong no each cost.
Noul float64 `json:"noul"`
}
NoulAnswer is a yes/no answer.
type NoulCriteria ¶
NoulCriteria describes the yes and no outcomes of a Noul. A nil field is left undescribed and omitted from the request.
type Question ¶
type Question interface {
// contains filtered or unexported methods
}
Question is a question to ask about the state. It is a sealed union: only Noul, ChoiceOf, ScoreOf and RawQuestion implement it. Choice and Score are the forms with plain string labels and plain int levels.
type RawQuestion ¶
RawQuestion is sent verbatim, including its "type" key. It is the escape hatch for question kinds or fields the API accepts before this SDK models them; the API, not the SDK, validates its contents.
Example ¶
A question kind or field the API accepts before this package models it can be sent through as a RawQuestion.
package main
import (
"context"
jev "github.com/HomayoonAlimohammadi/jev-sdk-go"
)
func main() {
client, err := jev.New()
if err != nil {
panic(err)
}
_, err = client.SystemOne(context.Background(), jev.SystemOneRequest{
State: "a support ticket",
Questions: map[string]jev.Question{
"experimental": jev.RawQuestion{
"type": "noul",
"instructions": "Is this urgent?",
"weight": 3,
},
},
})
if err != nil {
panic(err)
}
}
Output:
type ResponseError ¶
type ResponseError struct {
// Field is a dotted path to the offending value, such as
// "answers.tone.confidence" or "models[1].name". It is empty when the
// whole body was unusable.
Field string
// StatusCode is the HTTP response status.
StatusCode int
// Body is the raw response body.
Body []byte
// Header holds the response headers.
Header http.Header
// RequestID is the x-typesafe-request-id response header, if present.
RequestID string
// Endpoint is the request method and URL, without credentials.
Endpoint string
// Err is the underlying decoding failure, if there was one.
Err error
}
ResponseError is a successful HTTP response whose body was missing, or structurally invalid where the SDK requires data.
func (*ResponseError) Error ¶
func (e *ResponseError) Error() string
func (*ResponseError) Unwrap ¶
func (e *ResponseError) Unwrap() error
type ResponseMeta ¶
type ResponseMeta struct {
// RequestID is the x-typesafe-request-id response header, if the server
// sent one. Quote it in bug reports.
RequestID string
// StatusCode is the HTTP response status.
StatusCode int
// Header holds the response headers.
Header http.Header
// Raw is the untouched response body.
Raw []byte
}
ResponseMeta is the HTTP metadata attached to every response.
Embedding it in a type passed to SystemOneAs opts that type in: the SDK fills it after decoding the body.
type RetryPolicy ¶
type RetryPolicy struct {
// MaxRetries is how many attempts are made after the first one. Zero
// disables retries.
MaxRetries int
// BackoffInitial is the delay before the first retry. It doubles each
// attempt up to BackoffMax. Zero disables backoff.
BackoffInitial time.Duration
// BackoffMax caps the backoff delay. Zero disables backoff.
BackoffMax time.Duration
// BackoffJitter is the fraction of each delay, between 0 and 1, that is
// randomly subtracted to spread retries out.
BackoffJitter float64
// RetryStatuses lists the response status codes that are retried.
RetryStatuses []int
// RespectRetryAfter honors the retry-after and retry-after-ms response
// headers in place of the computed backoff.
RespectRetryAfter bool
// MaxRetryAfter caps how long a single server-requested delay is waited
// out when RespectRetryAfter is set. A response asking for longer is not
// retried: it is returned at once, with the requested delay in
// [APIError.RetryAfter], so the caller can schedule the retry instead of a
// goroutine being parked on it. Zero places no cap.
//
// Falling back to the computed backoff instead, as some clients do, would
// retry within a second a server that asked for minutes.
MaxRetryAfter time.Duration
// RetryTransport retries failures that produced no HTTP response, such as
// connection errors and timeouts.
RetryTransport bool
// Budget caps the total wall-clock time of one call, including the first
// attempt and every delay. A retry whose delay would reach the budget is
// not made, and the last error is returned instead. Zero disables the cap.
Budget time.Duration
// Retry opts additional failures in, on top of the rules above. It is
// called with the API error for a response, or the transport error for a
// failed attempt; exactly one is non-nil.
Retry func(apiErr *APIError, err error) bool
}
RetryPolicy controls how a call is retried.
The zero value performs no retries at all. Start from DefaultRetryPolicy and adjust from there; individual zero fields are used as written, because a zero BackoffInitial is a meaningful "no backoff" setting.
Example ¶
Retries are configured per client, and can be replaced for a single call.
package main
import (
"context"
"time"
jev "github.com/HomayoonAlimohammadi/jev-sdk-go"
)
func main() {
policy := jev.DefaultRetryPolicy()
policy.MaxRetries = 4
policy.BackoffMax = 10 * time.Second
policy.Budget = time.Minute
client, err := jev.New(jev.WithRetry(policy))
if err != nil {
panic(err)
}
// This one call must not be retried at all.
_, err = client.ListModels(context.Background(), jev.WithCallRetry(jev.RetryPolicy{}))
if err != nil {
panic(err)
}
}
Output:
func DefaultRetryPolicy ¶
func DefaultRetryPolicy() RetryPolicy
DefaultRetryPolicy returns the policy a client uses when none is set: two retries with exponential backoff from 500ms to 5s, honoring Retry-After up to a minute, within a 30s budget.
type ScoreAnswer ¶
type ScoreAnswer = ScoreAnswerOf[int]
ScoreAnswer is a ScoreAnswerOf with plain int levels.
type ScoreAnswerOf ¶
type ScoreAnswerOf[L ScoreLevel] struct { // Score is the probability-weighted average of the rubric levels, so it // may fall between them. See [ScoreAnswerOf.Level] for the level it rounds // to. Score float64 `json:"score"` // Confidence in the score, from 0 to 1. Confidence float64 `json:"confidence"` // Legend maps each level to the criterion it was given for. Legend map[L]any `json:"legend"` // Probabilities holds each level's probability. They sum to about 1. Probabilities map[L]float64 `json:"probabilities"` }
ScoreAnswerOf is an expected score with the rubric it was scored against, with levels of the caller's own integer type.
See https://docs.typesafe.ai/primitives/score.
func (ScoreAnswerOf[L]) Description ¶
func (a ScoreAnswerOf[L]) Description() any
Description returns what the rubric says about ScoreAnswerOf.Level.
func (ScoreAnswerOf[L]) Level ¶
func (a ScoreAnswerOf[L]) Level() L
Level returns the rubric level Score rounds to, clamped to the levels the answer reports, so it is always one the question offered.
Example ¶
A score is an expected value, so it can fall between levels. Level rounds it to the one it is nearest, in your own level type.
package main
import (
"fmt"
jev "github.com/HomayoonAlimohammadi/jev-sdk-go"
)
// Severity is a caller's own level type: iota numbering lines up with the
// rubric, where a description's position is its level.
type Severity int
const (
Cosmetic Severity = iota
Degraded
Blocking
)
func main() {
answer := jev.ScoreAnswerOf[Severity]{
Score: 1.8,
Legend: map[Severity]any{Cosmetic: "cosmetic", Degraded: "degraded", Blocking: "blocking"},
}
fmt.Println(answer.Level() == Blocking, answer.Description())
}
Output: true blocking
type ScoreLevel ¶
type ScoreLevel interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64
}
ScoreLevel is satisfied by the integer types a score level can be read back as. Level i of a rubric is the value i.
type ScoreOf ¶
type ScoreOf[L ScoreLevel] struct { // Instructions is what the model should rate. See [Noul.Instructions]. Instructions any `json:"instructions,omitempty"` // Criteria describes each score level. A description's index is its level, // counting from zero. Between 1 and 10 levels are required, none of them // null. Criteria []any `json:"criteria"` }
ScoreOf is a question that rates the state against an ordered rubric, with levels read back as the caller's own integer type. A description's position is its level, so an iota-numbered enum lines up with the rubric directly:
type Urgency int
const (
CanWait Urgency = iota
ThisWeek
Today
)
urgency := jev.Ask(&req, "urgency", jev.ScoreOf[Urgency]{Criteria: jev.Levels("can wait", "this week", "today")})
See https://docs.typesafe.ai/primitives/score.
func (ScoreOf[L]) MarshalJSON ¶
MarshalJSON encodes the question with its "score" type discriminator.
type SystemOneRequest ¶
type SystemOneRequest struct {
// State is what every question in the request refers to: a string, a map,
// a slice, a struct, or any other JSON-marshalable value.
//
// See https://docs.typesafe.ai/concepts/state.
State any
// Questions maps a name you choose to the question to ask. The answers
// come back under the same names. At least one is required.
Questions map[string]Question
// Model overrides the client's default model for this request.
Model string
// Extra adds top-level fields to the request body, shallow-merged over it.
// A key that collides with state, model or questions replaces it. It is
// the escape hatch for body fields the API accepts before this SDK models
// them.
Extra map[string]any
}
SystemOneRequest asks named questions about a piece of state.
type SystemOneResponse ¶
type SystemOneResponse struct {
ResponseMeta
// Model is the model that answered. It may differ from the alias asked for.
Model string
// Usage reports the tokens the request consumed.
Usage Usage
// Answers holds every answer, keyed by the question name from the request.
Answers map[string]Answer
// Nouls, Choices and Scores are Answers split by kind, so a caller that
// knows what it asked reaches its answer without a type assertion.
Nouls map[string]NoulAnswer
Choices map[string]ChoiceAnswer
Scores map[string]ScoreAnswer
}
SystemOneResponse is the answers to one System One request.
type TransportError ¶
type TransportError struct {
// Endpoint is the request method and URL, without credentials.
Endpoint string
// Attempts is how many attempts were made before giving up.
Attempts int
// Err is the underlying transport failure.
Err error
}
TransportError is a request that never produced an HTTP response.
func (*TransportError) Error ¶
func (e *TransportError) Error() string
func (*TransportError) Timeout ¶
func (e *TransportError) Timeout() bool
Timeout reports whether the request failed because it ran out of time, implementing the same convention as net.Error.
func (*TransportError) Unwrap ¶
func (e *TransportError) Unwrap() error
type TypedQuestion ¶
TypedQuestion is a question whose answer reads back as A. Noul, ChoiceOf and ScoreOf implement it, reading back as NoulAnswer, ChoiceAnswerOf and ScoreAnswerOf respectively. Pass one to Ask.
type UnknownAnswer ¶
type UnknownAnswer struct {
// Type is the answer's wire discriminator.
Type string
// Raw is the answer exactly as the API sent it.
Raw json.RawMessage
}
UnknownAnswer is an answer of a kind this SDK version does not model, kept rather than dropped so a RawQuestion works end to end: decode Raw yourself.
Example ¶
An answer of a kind this SDK version does not model is kept, not dropped, so a RawQuestion works end to end.
package main
import (
"context"
"encoding/json"
"fmt"
jev "github.com/HomayoonAlimohammadi/jev-sdk-go"
)
func main() {
client, err := jev.New()
if err != nil {
panic(err)
}
resp, err := client.SystemOne(context.Background(), jev.SystemOneRequest{
State: "a support ticket",
Questions: map[string]jev.Question{"future": jev.RawQuestion{"type": "aurora", "instructions": "?"}},
})
if err != nil {
panic(err)
}
if unknown, ok := resp.Answers["future"].(jev.UnknownAnswer); ok {
var decoded map[string]any
if err := json.Unmarshal(unknown.Raw, &decoded); err != nil {
panic(err)
}
fmt.Println(unknown.Type, decoded)
}
}
Output:
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
examples
|
|
|
basic
command
Command basic is the quickstart: it lists the models available to the account, then asks one of each kind of question about a support ticket and reads the answers back.
|
Command basic is the quickstart: it lists the models available to the account, then asks one of each kind of question about a support ticket and reads the answers back. |
|
batch
command
Command batch classifies many messages concurrently through one shared client, with the parallelism bounded by the caller rather than the SDK.
|
Command batch classifies many messages concurrently through one shared client, with the parallelism bounded by the caller rather than the SDK. |
|
config
command
Command config asks questions defined in data rather than code: a JSON document names each question and its criteria, and the program forwards them untouched.
|
Command config asks questions defined in data rather than code: a JSON document names each question and its criteria, and the program forwards them untouched. |
|
decode
command
Command decode reads the response straight into a struct of this program's own, with SystemOneAs.
|
Command decode reads the response straight into a struct of this program's own, with SystemOneAs. |
|
gateway
command
Command gateway reaches the API through a self-hosted gateway or proxy, the shape many platform teams put in front of third-party APIs to centralize credentials, quotas and audit.
|
Command gateway reaches the API through a self-hosted gateway or proxy, the shape many platform teams put in front of third-party APIs to centralize credentials, quotas and audit. |
|
moderation
command
Command moderation screens a forum post against several policies at once and turns the probabilities into allow, review or remove.
|
Command moderation screens a forum post against several policies at once and turns the probabilities into allow, review or remove. |
|
observability
command
Command observability watches the SDK at work: structured logs through a slog logger, and per-attempt metrics from a wrapped HTTP transport.
|
Command observability watches the SDK at work: structured logs through a slog logger, and per-attempt metrics from a wrapped HTTP transport. |
|
openrouter
command
Command openrouter calls Jev through OpenRouter, which serves the System One API at its own base URL, authenticated with an OpenRouter key.
|
Command openrouter calls Jev through OpenRouter, which serves the System One API at its own base URL, authenticated with an OpenRouter key. |
|
rerank
command
Command rerank orders search results by how well each answers a query, with every candidate rated in a single call.
|
Command rerank orders search results by how well each answers a query, with every candidate rated in a single call. |
|
resilience
command
Command resilience configures how calls are retried and bounded in time, and turns every kind of failure into a decision.
|
Command resilience configures how calls are retried and bounded in time, and turns every kind of failure into a decision. |
|
triage
command
Command triage routes support tickets to a team, using the typed questions: the answers come back as this program's own Team and Severity types, so the routing switch is checked by the compiler.
|
Command triage routes support tickets to a team, using the typed questions: the answers come back as this program's own Team and Severity types, so the routing switch is checked by the compiler. |
|
internal
|
|
|
fakeapi
Package fakeapi is an in-process stand-in for the System One API.
|
Package fakeapi is an in-process stand-in for the System One API. |
|
transport
Package transport moves bytes for the SDK.
|
Package transport moves bytes for the SDK. |