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

README

jev-sdk-go

A Go client for the TypeSafe AI API.

TypeSafe answers structured questions about a piece of content. You name each question; the answers come back under those names, with the probabilities behind them.

Zero dependencies — standard library only.

Install

go get github.com/HomayoonAlimohammadi/jev-sdk-go

Requires Go 1.24 or later.

Quickstart

Set TYPESAFE_API_KEY in your environment.

package main

import (
	"context"
	"fmt"
	"log"

	jev "github.com/HomayoonAlimohammadi/jev-sdk-go"
)

func main() {
	client, err := jev.New()
	if err != nil {
		log.Fatal(err)
	}

	resp, err := client.SystemOne(context.Background(), 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 {
		log.Fatal(err)
	}

	fmt.Println(resp.Choices["category"].Choice)
}

Questions

Kind Asks Answers with
jev.Noul a yes/no question a probability in NoulAnswer.Noul
jev.Choice which of these labels the label plus every label's probability
jev.Score rate this against a rubric an expected score, the rubric, and each level's probability
jev.RawQuestion anything the API accepts whatever comes back, via resp.Raw

A Choice's criteria maps each label to a description of when it applies. For labels that speak for themselves, jev.Labels saves writing the nils:

jev.Choice{
	Instructions: "What is this ticket about?",
	Criteria:     jev.Labels("billing", "technical", "other"),
}

Write the map out to describe a label. A description sharpens the boundary between labels that overlap, and it need not be a string — instructions and criteria take any JSON-marshalable value:

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,
	},
}

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",
	},
}

A Score's criteria is an ordered rubric: a description's position is its score, counting from zero. jev.Levels builds one from plain phrases.

jev.Score{
	Instructions: "How urgent is this ticket?",
	Criteria:     jev.Levels("can wait", "this week", "today"),
}

Answers

SystemOne returns every answer in resp.Answers, plus per-kind maps so you reach an answer you asked for without a type assertion:

resp.Nouls["billing"].Noul          // float64
resp.Choices["tone"].Choice         // string
resp.Choices["tone"].Probabilities  // map[string]float64
resp.Scores["urgency"].Score        // float64
resp.Scores["urgency"].Legend       // map[int]any

Every question you ask must come back answered, and answered in kind, or the call fails with a *jev.ResponseError naming it. So reaching straight into these maps is safe — a name you asked about is there. Without that check a missing answer would surface as NoulAnswer{}, and a Noul of 0.0 reads as a confident "no".

Answers carry the readings you would otherwise compute:

tone.Ranked()         // labels from most to least probable; [1] is the runner-up
urgency.Level()       // the rubric level the score rounds to, clamped to the rubric
urgency.Description() // what the rubric says about that level

A Noul has no Yes(threshold) on purpose: the threshold depends on what a wrong yes and a wrong no cost you, which is the one part of the decision the SDK cannot know.

An answer of a kind this SDK version does not model arrives as a jev.UnknownAnswer carrying its Type and Raw payload, so a RawQuestion works end to end and one unfamiliar answer never costs you the others.

Typed answers in your own types

ChoiceOf[T] and ScoreOf[L] take labels and levels of your own types. Register each question with jev.Ask, which returns a typed handle; the answer comes back as your type, so the compiler checks the switch over it:

type Team string

const (
	Billing   Team = "billing"
	Technical Team = "technical"
)

type Severity int

const (
	Cosmetic Severity = iota // a description's position is its level,
	Degraded                 // so iota lines up with the rubric
	Blocking
)

req := jev.SystemOneRequest{State: ticket}
team := jev.Ask(&req, "team", jev.ChoiceOf[Team]{Criteria: jev.LabelsOf(Billing, Technical)})
severity := jev.Ask(&req, "severity", jev.ScoreOf[Severity]{Criteria: jev.Levels("cosmetic", "degraded", "blocking")})

resp, err := client.SystemOne(ctx, req)
// ...
t, err := team.Answer(resp)     // jev.ChoiceAnswerOf[Team]: t.Choice is a Team
s, err := severity.Answer(resp) // jev.ScoreAnswerOf[Severity]: s.Level() is a Severity

Key.Answer also checks the answer against the question: a choice the question never offered, or a level outside its rubric, fails with a *jev.ResponseError rather than yielding a value that is none of your constants. Choice and Score are simply ChoiceOf[string] and ScoreOf[int], so they work with Ask too, and everything else is unchanged.

To decode into a type of your own, use SystemOneAs. The body is decoded as the API sends it, so the struct mirrors the wire shape:

type ticket 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[ticket](ctx, client, req)

The generic answer types carry JSON tags, so ChoiceAnswerOf[Team] and ScoreAnswerOf[Severity] work as fields here too.

SystemOneAs skips the answered-in-kind check, decoding whatever arrived, so it is also the way to accept a deliberately partial response.

The untouched body is always on resp.Raw.

Errors

var apiErr *jev.APIError

switch {
case errors.Is(err, jev.ErrRateLimited):
	errors.As(err, &apiErr)
	time.Sleep(apiErr.RetryAfter)
case errors.Is(err, jev.ErrUnauthorized):
	// check TYPESAFE_API_KEY
}

Three error types, each recoverable with errors.As:

  • *jev.APIError — an unsuccessful HTTP response. Carries the status, body, headers, request ID and the server's requested RetryAfter. errors.Is matches it against ErrBadRequest, ErrUnauthorized, ErrForbidden, ErrNotFound, ErrUnprocessable, ErrRateLimited and ErrServer.
  • *jev.TransportError — the request never reached the API. Timeout() reports whether it ran out of time.
  • *jev.ResponseError — the response body was missing data the SDK needs. Field names it, such as answers.tone.confidence.

Configuration

Explicit options beat the environment, which beats the default. An empty or whitespace-only 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 — DefaultRetryPolicy()
WithHeader — none
WithHTTPClient — a pooled client of its own
WithMaxResponseBytes — 1 MiB
WithBodyLogging — off
WithLogger — logs nothing

Requests are checked before they leave, so a request the API would refuse fails without a round trip:

  • State must encode to a string, object or array.
  • Every question needs a non-empty name.
  • A Choice needs 1 to 255 labels.
  • A Score needs 1 to 10 levels, none of them null.
  • A Noul must ask something: non-empty instructions, or a described outcome.

The caps are documented. The one-level floor and the Noul rule follow the service's reported behavior as of 2026-09-20, which departs from the documentation's two-level minimum; they are not yet confirmed by this SDK's own live tests.

Response bodies are read under a 1 MiB cap, so a runaway server cannot exhaust memory; raise or remove it with WithMaxResponseBytes.

Retries

By default a call is retried twice on 408, 429 and 5xx responses and on transport failures, with exponential backoff from 500ms to 5s, honoring the server's Retry-After, inside a 30 second budget.

policy := jev.DefaultRetryPolicy()
policy.MaxRetries = 4
policy.Budget = time.Minute

client, err := jev.New(jev.WithRetry(policy))

// Or replace it for one call. RetryPolicy{} disables retries.
resp, err := client.SystemOne(ctx, req, jev.WithCallRetry(jev.RetryPolicy{}))

RetryPolicy's zero value performs no retries, so build from DefaultRetryPolicy() rather than a partial literal.

A server that asks for a longer wait than MaxRetryAfter (a minute by default) is taken at its word: the call returns at once with the request in APIError.RetryAfter, for you to schedule, rather than parking a goroutine or retrying a server that asked you to back off. APIError.Attempts says how many attempts were made.

Concurrency

A *jev.Client is safe for concurrent use and keeps a pool of connections of its own, sized so concurrent calls reuse them instead of redialing. Create one, share it, and choose the parallelism yourself:

var wg sync.WaitGroup
for _, ticket := range tickets {
	wg.Add(1)
	go func() {
		defer wg.Done()
		resp, err := client.SystemOne(ctx, request(ticket))
		// ...
	}()
}
wg.Wait()

If you supply your own client with WithHTTPClient, its transport is used as is. Raise MaxIdleConnsPerHost on it for concurrent use: net/http's default keeps two idle connections per host, so a third call in flight closes and redials a connection every time.

Logging

The SDK logs nothing until you give it a *slog.Logger. URLs are stripped of credentials and credential-bearing headers are redacted.

client, err := jev.New(jev.WithLogger(slog.Default()))

Bodies are left out by default. The request body is the state you are evaluating and the response is the model's reading of it, so both carry whatever your content carries, and nothing in them is redacted. Opt in only where that is acceptable:

client, err := jev.New(jev.WithLogger(logger), jev.WithBodyLogging())

The SDK's own HTTP client refuses to follow redirects. If you supply one with WithHTTPClient, set CheckRedirect to return http.ErrUseLastResponse: net/http keeps an Authorization header across an https-to-http redirect to the same host, and keeps every header it does not recognize across a redirect to any host.

Observability

The SDK has no tracing dependency of its own. Wrap the transport of the *http.Client you pass to WithHTTPClient instead — with OpenTelemetry, for example:

httpClient := &http.Client{
	Transport: otelhttp.NewTransport(http.DefaultTransport),
	// Keep the SDK's own policy: never follow a redirect with credentials.
	CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse },
}

client, err := jev.New(jev.WithHTTPClient(httpClient))

Every response, and every *APIError and *ResponseError, carries the x-typesafe-request-id as RequestID; quote it in a support ticket.

Gateways and providers

The SDK works with anything that serves the System One API: TypeSafe itself (the default), OpenRouter, or a gateway of your own. Pointing it elsewhere is configuration, not code.

OpenRouter

OpenRouter serves the System One API at its own base URL, authenticated with an OpenRouter key sent as the bearer token (OpenRouter's guide):

client, err := jev.New(
	jev.WithBaseURL("https://openrouter.ai/api"),
	jev.WithAPIKey(os.Getenv("OPENROUTER_API_KEY")),
)

Setting TYPESAFE_BASE_URL=https://openrouter.ai/api and TYPESAFE_API_KEY to the OpenRouter key does the same without code.

  • The default model, jev-latest, works as-is: OpenRouter maps it onto ~typesafe/jev-latest.
  • OpenRouter adds id, provider and usage.cost to each response. They do no harm, and resp.Raw has them.
  • ListModels does not work there. OpenRouter's /v1/models is its own catalog, so the call fails with a *jev.ResponseError.
  • OpenRouter's separate Decisions API (/api/alpha/decisions) uses a different request format and is not supported.
Your own gateway

Any gateway or proxy that forwards the System One request format works. Point WithBaseURL at it (a path prefix is fine), add the headers it needs with WithHeader or per call with WithCallHeader, and pass a tuned *http.Client with WithHTTPClient if it needs a proxy or custom TLS. A gateway that rewrites the request into another format is not supported.

Examples

examples/ holds a runnable program per use case, each tested against an in-process fake of the API:

Use case Example
First call basic
Routing with your own enums triage
Content moderation moderation
Search relevance rerank
High volume batch
Questions from configuration config
Decoding into your own struct decode
Retries and error handling resilience
Logging and metrics observability
OpenRouter openrouter
A gateway of your own gateway

For coding agents

An installable skill teaches coding agents to use this SDK the way it is meant to be used:

npx skills add HomayoonAlimohammadi/jev-sdk-go --skill jev-sdk-go

It is skills/jev-sdk-go/SKILL.md. For designing the questions themselves, TypeSafe publishes its own skill: npx skills add typesafe-ai/skills --skill typesafe-ai.

Performance

The SDK's own cost is about 27µs and 210 allocations per call, measured against a local server with go test -bench .. A real call spends orders of magnitude longer on the network and in the model, so this matters only for garbage-collector pressure in very high-throughput services. Validation allocates nothing, and with logging off the SDK builds no log records at all.

Development

make test        # go test ./... -race
go test -bench . # the SDK's own per-call cost
make cover       # coverage summary
make vet lint    # go vet, golangci-lint
make integration # live API tests, needs TYPESAFE_API_KEY

License

MIT

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:

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

Examples

Constants

View Source
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.

View Source
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

View Source
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.

View Source
var (
	ErrBadRequest    = errors.New("jev: bad request")
	ErrUnauthorized  = errors.New("jev: authentication failed")
	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

func Labels(names ...string) map[string]any

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 LabelsOf

func LabelsOf[T ~string](names ...T) map[T]any

LabelsOf is Labels for a ChoiceOf with labels of the caller's own type.

func Levels

func Levels(descriptions ...string) []any

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)
}

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)
	}
}

func (*APIError) Error

func (e *APIError) Error() string

func (*APIError) Is

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

Is maps the response status onto the status category sentinels.

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 Choice

type Choice = ChoiceOf[string]

Choice is a ChoiceOf with plain string labels.

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

func (c ChoiceOf[T]) MarshalJSON() ([]byte, error)

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()
}

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)
	}
}

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)
}
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)
	}
}

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
}

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)
	}
}

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.

func (Key[A]) Name

func (k Key[A]) Name() string

Name is the question's name: the key its answer comes back under.

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

func (n Noul) MarshalJSON() ([]byte, error)

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.

See https://docs.typesafe.ai/primitives/noul.

type NoulCriteria

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

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

type RawQuestion map[string]any

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)
	}
}

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)
	}
}

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 Score

type Score = ScoreOf[int]

Score is a ScoreOf with plain int levels.

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

func (s ScoreOf[L]) MarshalJSON() ([]byte, error)

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.

See https://docs.typesafe.ai/concepts/system-one.

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.

See https://docs.typesafe.ai/concepts/system-one.

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

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

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)
	}
}

type Usage

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

Usage reports the tokens a request consumed. A field is nil when the API did not report it.

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.

Jump to

Keyboard shortcuts

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