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

README

taurus-jev-sdk-go

Go Reference CI Go Report Card License MIT

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

Jev answers typed questions about a piece of state and returns calibrated probabilities. It generates no text, so every answer is a value your code can branch on directly.

Unofficial client, maintained independently of TypeSafe AI. Zero dependencies outside the standard library.

response, err := client.SystemOne(ctx, ticket, jev.Questions{
    "billing": jev.Noul{Instructions: "Is this about billing?"},
})
probability, _ := response.NoulOf("billing")

Why this client

  • Degraded responses are rejected, not coerced. A body missing a required field returns an error naming it, so a truncated response never reaches your branching logic as a confident zero.
  • The API key stays out of every string. Errors, error bodies, log records and the client's own rendering are all masked, in six spellings.
  • The bearer token does not follow redirects. Go's default policy would re-send it over cleartext to any subdomain of the same host.
  • A server cannot pin your goroutine. A retry-after wait is clamped, jittered and capped.
  • The pool is sized for a service. 128 idle connections per host against the stdlib default of two, worth 2.2x throughput at 200 concurrent calls.
  • Forward compatible. A primitive the API adds later is reachable through RawQuestion and Answer.Raw without waiting for a release.

Install

go get github.com/KKloudTarus/taurus-jev-sdk-go

Requires Go 1.22 or newer. Zero dependencies outside the standard library. The import path ends in taurus-jev-sdk-go; the package identifier is jev.

import jev "github.com/KKloudTarus/taurus-jev-sdk-go"

Quickstart

Set TYPESAFE_API_KEY, then ask three questions in one request:

client, err := jev.New()
if err != nil {
	return err
}

response, err := client.SystemOne(ctx,
	map[string]any{"subject": "Duplicate charge", "body": "I was charged twice."},
	jev.Questions{
		"billing": jev.Noul{Instructions: "Is this about billing?"},
		"tone": jev.Choice{
			Instructions: "What is the tone?",
			Criteria:     map[string]any{"angry": "upset or hostile", "calm": nil},
		},
		"urgency": jev.Score{
			Instructions: "How urgent is this?",
			Criteria:     []any{"Can wait", "This week", "Today"},
		},
	})
if err != nil {
	return err
}

if probability, ok := response.NoulOf("billing"); ok && probability > 0.9 {
	routeToBilling(ticket)
}
if tone, ok := response.ChoiceOf("tone"); ok && tone.Confidence < 0.6 {
	sendToHumanReview(ticket)
}

The three question types

Type Ask Answer
jev.Noul Is this statement true? Noul, from 0 to 1
jev.Choice Which label applies? Choice, Probabilities, Confidence
jev.Score Rate against ordered levels Score, Legend, Probabilities, Confidence

Instructions and every criterion accept a string, a map or a slice, so a question can carry structure rather than a sentence.

Cardinality limits are the API's, not this client's, so a limit raised server side needs no SDK upgrade.

Read one answer through NoulOf, ChoiceOf or ScoreOf, each returning a second result that reports whether the name was answered by that primitive. Read them in bulk through Nouls(), Choices(), Scores() and Unknown().

jev.RawQuestion sends a question shape this version does not model, for a primitive the API adds after this release.

Configuration

New reads TYPESAFE_API_KEY, TYPESAFE_BASE_URL and TYPESAFE_DEFAULT_MODEL. Options win over the environment.

client, err := jev.New(
	jev.WithAPIKey(key),
	jev.WithModel("jev-latest"),
	jev.WithTimeout(2*time.Second),
	jev.WithRetry(jev.DefaultRetry()),
	jev.WithLogger(slog.Default()),
)

Per call, UsingModel, UsingTimeout, UsingRetry, UsingHeader and UsingExtraBody override the client for that request only.

A Client is safe for concurrent use. Create one and share it so connections are pooled.

Errors

Classify with errors.Is, then read the details with errors.As:

switch {
case errors.Is(err, jev.ErrRateLimit), errors.Is(err, jev.ErrOverloaded):
	degradeToRules()
case errors.Is(err, jev.ErrAuthentication):
	log.Fatal("check TYPESAFE_API_KEY")
case errors.Is(err, jev.ErrTimeout):
	giveUp()
default:
	var apiErr *jev.APIError
	if errors.As(err, &apiErr) {
		log.Printf("status %d request %s: %s", apiErr.Status, apiErr.RequestID, apiErr.Message)
	}
}

Sentinels: ErrNoAPIKey, ErrInvalidConfig, ErrInvalidRequest, ErrInvalidResponse, ErrConnection, ErrTimeout, ErrBadRequest, ErrAuthentication, ErrPermissionDenied, ErrNotFound, ErrUnprocessableEntity, ErrRateLimit, ErrOverloaded, ErrInternalServer.

Detail types: *APIError for an unsuccessful status, *ConnectionError for a request that never got a response, and *ResponseValidationError for a success whose body did not match the schema.

RequestID carries the x-typesafe-request-id header on all three. Quote it in a support report. SystemOneResponse also carries Status, Header and RawBody.

Responses are validated, not coerced

A field the API documents as required is enforced. A response that omits noul would decode to 0.0 in a plain float, which reads as a maximally confident no, so the client returns a *ResponseValidationError naming the field instead:

jev: POST https://api.typesafe.ai/v1/systemone: invalid response data at "answers.spam.noul"

A body that is null, empty, or missing model, answers or usage is rejected the same way. A truncated or degraded response never reaches your branching logic as a zero value.

Retries

DefaultRetry retries 408, 429 and 5xx twice, plus connection failures, with jittered exponential backoff from 500ms to 5s under a 30s total budget.

It honors retry-after and retry-after-ms, with two bounds. The requested wait is clamped by MaxBackoff, so a hostile or misconfigured upstream cannot pin your goroutine for a day, and it is jittered, so a fleet handed the same retry-after does not retry in lockstep. A wait that would exhaust Budget ends the call instead of sleeping through it.

jev.NoRetry() sends one attempt. A cancelled or expired context stops retrying immediately and reports ErrTimeout alongside context.DeadlineExceeded, with the failure that was in flight still reachable through errors.As.

Credentials

The API key is masked in every string this package produces: error messages, error bodies, log records, and the rendering of the Client itself under %v, %+v, %#v and log/slog. Masking covers the raw key, the Bearer form, and its Go-quoted, JSON-escaped and percent-encoded spellings. URLs in errors are stripped of userinfo, query and fragment.

ConnectionError.Unwrap returns a sanitized node: its message is redacted and it has no Unwrap of its own, so an error reporter that walks the chain cannot reach the raw transport error, while errors.Is and errors.As still see through to it.

The SDK's own HTTP client does not follow redirects. Go's default policy re-sends Authorization to any subdomain of the same host and ignores scheme and port, which would leak the bearer token over cleartext or to a co-hosted service. A client supplied through WithHTTPClient keeps its own policy and is responsible for this itself.

WithBaseURL requires https, or http on a loopback host, and rejects a URL carrying credentials, a query or a fragment.

Connection pool

The SDK builds its own http.Transport with 128 idle connections per host. The stdlib default is two, which forces a fresh TCP and TLS handshake on most requests once more than two calls are in flight. Measured against an upstream with a 20ms RTT at 200 concurrent calls, the default gave 1593 rps and a p50 of 106ms; this pool gave 3509 rps and a p50 of 36.6ms.

A client supplied through WithHTTPClient is used as given, transport included.

Forward compatibility

An answer type this version does not model does not fail the response. It arrives with Known() == false and its bytes in Answer.Raw, so a primitive added to the API later cannot break a service already in production. Reach those answers through Unknown().

A malformed answer is rejected rather than passed through, so a broken payload is never mistaken for a future primitive.

To send a question shape this version does not model, use jev.RawQuestion. For a response shape it does not model, SystemOneAs[T] decodes the body into your own type. UsingExtraBody adds top-level request fields.

A decoded response round-trips through encoding/json, so it can be cached or forwarded and decoded again.

Development

go test ./...          # unit tests, no network
go test -race ./...    # same under the race detector
go vet ./...
gofmt -l .             # must print nothing

Every test runs against httptest or a local listener, so the suite needs no API key and no network.

The retry loop is covered by mutation testing rather than by line coverage alone. Reusing one bytes.Reader across attempts, reporting a connection failure as non-retryable, dropping the MaxBackoff clamp on retry-after, and returning a bare error after a deadline are each caught by a named test.

Project

License

MIT. See LICENSE.

Documentation

Overview

Package jev is a dependency-free client for the TypeSafe AI System One API and its Jev model.

Jev answers typed questions about a piece of state and returns calibrated probabilities. It generates no text, so every answer is a value your code can branch on directly.

Send state and named questions:

client, err := jev.New(jev.WithAPIKey(os.Getenv(jev.APIKeyEnv)))
if err != nil {
	return err
}
response, err := client.SystemOne(ctx, "I was charged twice.", jev.Questions{
	"billing": jev.Noul{Instructions: "Is this about billing?"},
})
if err != nil {
	return err
}
probability, _ := response.NoulOf("billing")

The three question types are Noul (yes/no), Choice (one label out of many) and Score (a rating against ordered levels). Each has a matching answer type reached through SystemOneResponse.NoulOf, SystemOneResponse.ChoiceOf and SystemOneResponse.ScoreOf.

This is an unofficial client, maintained independently of TypeSafe AI. The wire contract follows https://docs.typesafe.ai/api.

Example

Route a support ticket on three questions asked in one request.

package main

import (
	"context"
	"fmt"
	"log"

	jev "github.com/KKloudTarus/taurus-jev-sdk-go"
)

func main() {
	client, err := jev.New()
	if err != nil {
		log.Fatal(err)
	}
	response, err := client.SystemOne(context.Background(),
		map[string]any{"subject": "Duplicate charge", "body": "I was charged twice. Please help."},
		jev.Questions{
			"billing": jev.Noul{Instructions: "Is this about billing?"},
			"tone": jev.Choice{
				Instructions: "What is the tone?",
				Criteria:     map[string]any{"angry": "upset or hostile", "calm": nil},
			},
			"urgency": jev.Score{
				Instructions: "How urgent is this?",
				Criteria:     []any{"Can wait", "This week", "Today"},
			},
		})
	if err != nil {
		log.Fatal(err)
	}

	if probability, ok := response.NoulOf("billing"); ok && probability > 0.9 {
		fmt.Println("route to billing")
	}
	if tone, ok := response.ChoiceOf("tone"); ok {
		fmt.Println("tone:", tone.Choice, "confidence:", tone.Confidence)
	}
	if urgency, ok := response.ScoreOf("urgency"); ok && urgency.Score > 1.5 {
		fmt.Println("escalate")
	}
}

Index

Examples

Constants

View Source
const (
	AnswerNoul   = "noul"
	AnswerChoice = "choice"
	AnswerScore  = "score"
)

Answer types the API defines today. An answer carrying any other type is kept raw rather than rejected.

View Source
const (
	// DefaultBaseURL is the API root used when none is configured.
	DefaultBaseURL = "https://api.typesafe.ai"
	// DefaultModel is the model alias used when none is configured.
	DefaultModel = "jev-latest"

	// APIKeyEnv holds the API key.
	APIKeyEnv = "TYPESAFE_API_KEY"
	// BaseURLEnv overrides the API root.
	BaseURLEnv = "TYPESAFE_BASE_URL"
	// ModelEnv overrides the default model.
	ModelEnv = "TYPESAFE_DEFAULT_MODEL"
)

Wire constants and environment variables.

View Source
const DefaultTimeout = 30 * time.Second

DefaultTimeout bounds one HTTP attempt when no timeout is configured.

View Source
const Version = "0.1.0"

Version is this client's release, sent in the User-Agent header.

Variables

View Source
var (
	// ErrNoAPIKey is returned by New when no key is configured.
	ErrNoAPIKey = errors.New("jev: no API key")
	// ErrInvalidConfig is returned by New for an unusable option.
	ErrInvalidConfig = errors.New("jev: invalid configuration")
	// ErrInvalidRequest marks input rejected before any request is sent.
	ErrInvalidRequest = errors.New("jev: invalid request")
	// ErrInvalidResponse marks a success status whose body did not match the
	// documented schema.
	ErrInvalidResponse = errors.New("jev: invalid response")
	// ErrConnection marks a request that never produced an HTTP response.
	ErrConnection = errors.New("jev: connection failed")
	// ErrTimeout marks a call that ran out of time.
	ErrTimeout = errors.New("jev: request timed out")

	ErrBadRequest          = errors.New("jev: bad request")           // 400
	ErrAuthentication      = errors.New("jev: authentication failed") // 401
	ErrPermissionDenied    = errors.New("jev: permission denied")     // 403
	ErrNotFound            = errors.New("jev: not found")             // 404
	ErrUnprocessableEntity = errors.New("jev: unprocessable entity")  // 422
	ErrRateLimit           = errors.New("jev: rate limit exceeded")   // 429
	ErrOverloaded          = errors.New("jev: service overloaded")    // 529
	ErrInternalServer      = errors.New("jev: server error")          // 5xx
)

Sentinel errors for classification with errors.Is. Details live on APIError, ConnectionError and ResponseValidationError, reached with errors.As.

Functions

func RetryableStatus

func RetryableStatus(status int) bool

RetryableStatus reports whether a status is worth retrying: 408, 429 and any 5xx, including the non-standard 529 the API uses for overload.

func SystemOneAs

func SystemOneAs[T any](ctx context.Context, client *Client, state any, questions Questions, options ...CallOption) (T, error)

SystemOneAs answers named questions and decodes the body into T, for a response shape this version does not model or a narrower one you declare yourself. It is a function rather than a method because Go methods take no type parameters.

Example

Decode a response shape this version does not model, such as a field added to the API after this release.

package main

import (
	"context"
	"fmt"
	"log"

	jev "github.com/KKloudTarus/taurus-jev-sdk-go"
)

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

	type response struct {
		Model   string `json:"model"`
		Answers struct {
			Spam struct {
				Noul float64 `json:"noul"`
			} `json:"spam"`
		} `json:"answers"`
	}
	result, err := jev.SystemOneAs[response](context.Background(), client,
		"Buy cheap watches now", jev.Questions{"spam": jev.Noul{Instructions: "Is this spam?"}})
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(result.Answers.Spam.Noul > 0.9)
}

Types

type APIError

type APIError struct {
	// Status is the HTTP status code.
	Status int
	// Message is the server's explanation, extracted from the error body.
	Message string
	// Body is the response body, truncated to 8 KiB. Any configured credential
	// is masked, as in Message.
	Body []byte
	// RequestID is the x-typesafe-request-id header, empty when absent.
	RequestID string
	// Endpoint is the method and URL, without credentials or query.
	Endpoint string
	// RetryAfter is the server's requested wait, zero when not supplied.
	RetryAfter time.Duration
}

APIError is an unsuccessful HTTP response. Classify it with errors.Is against the status sentinels, and read the details after errors.As.

Example

Classify a failure before deciding whether to fall back or to fail the request.

package main

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

	jev "github.com/KKloudTarus/taurus-jev-sdk-go"
)

func main() {
	client, err := jev.New()
	if err != nil {
		log.Fatal(err)
	}
	_, err = client.SystemOne(context.Background(), "state", jev.Questions{"q": jev.Noul{}})

	switch {
	case err == nil:
	case errors.Is(err, jev.ErrRateLimit), errors.Is(err, jev.ErrOverloaded):
		// Retries are already exhausted here; shed load instead.
		fmt.Println("degrade to the rule-based path")
	case errors.Is(err, jev.ErrAuthentication):
		log.Fatal("check TYPESAFE_API_KEY")
	case errors.Is(err, jev.ErrTimeout):
		fmt.Println("give up on this ticket for now")
	default:
		var apiErr *jev.APIError
		if errors.As(err, &apiErr) {
			fmt.Printf("status %d, request %s: %s\n", apiErr.Status, apiErr.RequestID, apiErr.Message)
		}
	}
}

func (*APIError) Error

func (e *APIError) Error() string

func (*APIError) Is

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

Is reports whether this status matches a sentinel. A 529 matches both ErrOverloaded and ErrInternalServer.

type Answer

type Answer struct {
	// Type is the wire discriminator.
	Type string
	// Noul is set when Type is [AnswerNoul].
	Noul *NoulAnswer
	// Choice is set when Type is [AnswerChoice].
	Choice *ChoiceAnswer
	// Score is set when Type is [AnswerScore].
	Score *ScoreAnswer
	// Raw is the answer object exactly as the server sent it.
	Raw json.RawMessage
	// contains filtered or unexported fields
}

Answer holds exactly one of Noul, Choice or Score, selected by Type.

An answer whose type this version does not model leaves all three nil and keeps Raw, so a primitive added to the API later does not fail the response your service is already handling. A malformed answer is rejected instead: SystemOneResponse reports it as a ResponseValidationError naming the field, so a broken payload is never mistaken for a future primitive.

func (Answer) Known

func (a Answer) Known() bool

Known reports whether this version models the answer's type.

func (Answer) MarshalJSON

func (a Answer) MarshalJSON() ([]byte, error)

MarshalJSON writes the answer exactly as the server sent it, so a decoded response round-trips through encoding/json.

func (*Answer) UnmarshalJSON

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

UnmarshalJSON selects the variant named by the type discriminator. A type this version does not model keeps its bytes in Raw; a malformed payload is recorded and reported by the enclosing response, which knows the question name.

type CallOption

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

CallOption overrides client configuration for a single call. The interface is closed; use the Using* constructors.

func UsingExtraBody

func UsingExtraBody(fields map[string]any) CallOption

UsingExtraBody merges top-level fields into the request body after state, model and questions are set. Last write wins, and values replace rather than merge. Use it to reach a field this version does not model.

func UsingHeader

func UsingHeader(name, value string) CallOption

UsingHeader adds a header to this call.

func UsingModel

func UsingModel(model string) CallOption

UsingModel overrides the model for this call.

func UsingRetry

func UsingRetry(policy RetryPolicy) CallOption

UsingRetry overrides the retry policy for this call.

func UsingTimeout

func UsingTimeout(timeout time.Duration) CallOption

UsingTimeout bounds each HTTP attempt of this call. The value must be positive. Use RetryPolicy.Budget, or the context, to bound the whole call.

type Choice

type Choice struct {
	// Instructions is what the model should decide. Optional.
	Instructions any
	// Criteria holds the labels to choose between. Required.
	Criteria map[string]any
}

Choice selects one label. Criteria maps each label to a description, or to nil for a label interpreted by its name alone.

The number of labels is bounded by the API, not by this client, so a limit raised server side needs no SDK upgrade.

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

func (Choice) MarshalJSON

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

MarshalJSON builds the wire object by hand so an unset field is absent rather than null, while a nil description inside Criteria stays an explicit null.

type ChoiceAnswer

type ChoiceAnswer struct {
	// Choice is the label with the highest probability.
	Choice string `json:"choice"`
	// Confidence in the selection, from 0 to 1.
	Confidence float64 `json:"confidence"`
	// Probabilities per label, summing to approximately 1.
	Probabilities map[string]float64 `json:"probabilities"`
}

ChoiceAnswer is the selected label with the distribution behind it.

func (*ChoiceAnswer) UnmarshalJSON

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

UnmarshalJSON rejects a payload missing any required field, so an absent label or confidence is reported rather than decoded as an empty string or a zero probability.

type Client

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

Client is a TypeSafe AI API client. It is safe for concurrent use and should be created once and shared, so connections are pooled.

Its String and LogValue methods mask the API key, so printing or logging a client cannot disclose the credential.

func New

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

New creates a client. It reads TYPESAFE_API_KEY, TYPESAFE_BASE_URL and TYPESAFE_DEFAULT_MODEL, each overridable by an option.

It returns ErrNoAPIKey when no key is configured and ErrInvalidConfig for an unusable option.

Example (Options)

Tune retries and logging for a request path with a tight latency budget.

package main

import (
	"log"
	"log/slog"
	"os"
	"time"

	jev "github.com/KKloudTarus/taurus-jev-sdk-go"
)

func main() {
	client, err := jev.New(
		jev.WithAPIKey(os.Getenv(jev.APIKeyEnv)),
		jev.WithModel("jev-latest"),
		jev.WithTimeout(2*time.Second),
		jev.WithRetry(jev.RetryPolicy{
			MaxRetries:        1,
			InitialBackoff:    100 * time.Millisecond,
			MaxBackoff:        time.Second,
			Jitter:            0.25,
			Budget:            3 * time.Second,
			RetryStatus:       jev.RetryableStatus,
			RespectRetryAfter: true,
			RetryConnection:   true,
		}),
		jev.WithLogger(slog.Default()),
	)
	if err != nil {
		log.Fatal(err)
	}
	_ = client
}

func (Client) GoString

func (c Client) GoString() string

GoString renders the client without its credential, for the %#v verb.

func (Client) LogValue

func (c Client) LogValue() slog.Value

LogValue renders the client without its credential, for log/slog.

func (*Client) Models

func (c *Client) Models(ctx context.Context, options ...CallOption) ([]Model, error)

Models lists the models available to the account.

func (Client) String

func (c Client) String() string

String renders the client without its credential.

func (*Client) SystemOne

func (c *Client) SystemOne(ctx context.Context, state any, questions Questions, options ...CallOption) (*SystemOneResponse, error)

SystemOne answers named questions about state.

State is the content every question refers to: a string, or any value that encodes to a JSON object or array. Each answer is keyed by its question name.

Errors are classified with errors.Is against ErrRateLimit, ErrTimeout and the other sentinels; details come from errors.As into *APIError, *ConnectionError or *ResponseValidationError.

type ConnectionError

type ConnectionError struct {
	// Endpoint is the method and URL, without credentials or query.
	Endpoint string
	// Timeout reports whether the failure was a deadline rather than a refusal.
	Timeout bool
	// contains filtered or unexported fields
}

ConnectionError is a request that never produced an HTTP response.

Every string this type exposes is redacted, including the one reached through Unwrap. The chain stops at a sanitized node, so an error reporter that walks Unwrap cannot reach the raw transport error, while errors.Is and errors.As still see through to it.

func (*ConnectionError) Error

func (e *ConnectionError) Error() string

func (*ConnectionError) Is

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

Is reports whether the target is ErrConnection, or ErrTimeout when the failure was a deadline.

func (*ConnectionError) Unwrap

func (e *ConnectionError) Unwrap() error

Unwrap returns the sanitized transport error. Its message is redacted and it has no Unwrap of its own, so the raw error is not reachable by walking the chain. errors.Is and errors.As still reach the original cause.

type Model

type Model struct {
	// Name or alias accepted by a request's model field.
	Name string `json:"name"`
	// Description of the model and its capabilities.
	Description string `json:"description"`
	// ReleaseDate formatted as YYYY-MM-DD.
	ReleaseDate string `json:"release_date"`
}

Model describes one model available to the account.

type Noul

type Noul struct {
	// Instructions is the question or statement to evaluate, as a string, a map
	// or a slice. Optional.
	Instructions any
	// Criteria clarifies the two outcomes. Optional.
	Criteria *NoulCriteria
}

Noul asks a yes/no question. The answer is the probability that the statement is true, from 0 to 1.

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

func (Noul) MarshalJSON

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

MarshalJSON builds the wire object by hand so an unset field is absent rather than null; the API reads an explicit null as a supplied value.

type NoulAnswer

type NoulAnswer struct {
	// Noul runs from 0 to 1. Values near 0.5 mean the model is undecided.
	Noul float64 `json:"noul"`
}

NoulAnswer is the probability that a yes/no statement is true.

func (*NoulAnswer) UnmarshalJSON

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

UnmarshalJSON rejects a payload missing the required field. Decoding into a plain float would turn an absent probability into 0, which reads as a maximally confident no.

type NoulCriteria

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

NoulCriteria describes what counts as a yes and what counts as a no. Either field may be a string, a map or a slice. Leave a field nil to send nothing.

type Option

type Option func(*Client)

Option configures a Client in New. Explicit options win over environment variables.

func WithAPIKey

func WithAPIKey(key string) Option

WithAPIKey sets the API key, overriding TYPESAFE_API_KEY.

func WithBaseURL

func WithBaseURL(baseURL string) Option

WithBaseURL sets the API root, overriding TYPESAFE_BASE_URL. It must be an absolute https URL, or http for a loopback host, and must carry no credentials, query or fragment.

func WithHTTPClient

func WithHTTPClient(httpClient *http.Client) Option

WithHTTPClient supplies the HTTP client. The SDK does not close it and does not change its transport, timeout or redirect policy, so a client supplied here is responsible for its own connection pool and for not following a redirect that would carry the Authorization header to another host.

func WithHeader

func WithHeader(name, value string) Option

WithHeader adds a header to every request. Authentication, Accept, the SDK identification headers and the SDK's own retry-count header are set afterwards and cannot be overridden.

func WithLogger

func WithLogger(logger *slog.Logger) Option

WithLogger enables logging. Requests log at debug, retries at info. Headers are never logged and any configured credential is masked.

func WithModel

func WithModel(model string) Option

WithModel sets the default model, overriding TYPESAFE_DEFAULT_MODEL.

func WithRetry

func WithRetry(policy RetryPolicy) Option

WithRetry replaces the retry policy. Use NoRetry to send one attempt.

func WithTimeout

func WithTimeout(timeout time.Duration) Option

WithTimeout bounds each HTTP attempt. It composes with WithHTTPClient rather than replacing it: the timeout is applied through the request context, so a supplied client keeps its transport, and its own Timeout still applies alongside this one. Use RetryPolicy.Budget to bound a call across its retries.

The value must be positive.

type Question

type Question interface {
	json.Marshaler
	// contains filtered or unexported methods
}

Question is one of Noul, Choice, Score or RawQuestion.

type Questions

type Questions map[string]Question

Questions maps a name you choose to the question asked under it. The same names key the answers in the response.

type RawQuestion

type RawQuestion struct {
	// Type is the wire discriminator. Required.
	Type string
	// Fields are the remaining members of the question object. A "type" key
	// here is ignored in favor of Type.
	Fields map[string]any
}

RawQuestion sends a question shape this version does not model, such as a primitive added to the API after this release. Fields are written verbatim alongside the type discriminator.

Its answer arrives with Known() false and its payload in Answer.Raw.

func (RawQuestion) MarshalJSON

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

MarshalJSON writes Fields verbatim with Type as the discriminator. A "type" key in Fields is ignored, so the discriminator cannot be overwritten.

type ResponseValidationError

type ResponseValidationError struct {
	// FieldPath is the dotted path to the offending field, such as
	// "answers.tone.confidence". It is empty when the whole body is unusable.
	FieldPath string
	// Status is the HTTP status code, which was a success.
	Status int
	// Body is the response body, truncated to 8 KiB and redacted.
	Body []byte
	// RequestID is the x-typesafe-request-id header, empty when absent.
	RequestID string
	// Endpoint is the method and URL, without credentials or query.
	Endpoint string
	// contains filtered or unexported fields
}

ResponseValidationError is a success status whose body did not match the documented schema. FieldPath names the first offending field, so a truncated or degraded response is reported instead of decoding to a zero value.

func (*ResponseValidationError) Error

func (e *ResponseValidationError) Error() string

func (*ResponseValidationError) Is

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

Is reports whether the target is ErrInvalidResponse.

type RetryPolicy

type RetryPolicy struct {
	// MaxRetries is the number of retries after the initial attempt.
	MaxRetries int
	// InitialBackoff is the first delay, doubled each attempt up to MaxBackoff.
	// Zero disables backoff.
	InitialBackoff time.Duration
	// MaxBackoff caps every delay between attempts, including one a server asks
	// for through retry-after. Zero disables backoff.
	MaxBackoff time.Duration
	// Jitter is the fraction of each delay randomly subtracted, from 0 to 1. It
	// applies to a server-supplied wait as well, so callers that received the
	// same retry-after do not wake at the same instant.
	Jitter float64
	// Budget is the total wall-clock allowance for one call including delays.
	// Zero disables the limit. An attempt is not started when the delay before
	// it would exhaust the budget.
	Budget time.Duration
	// RetryStatus reports whether a status code should be retried. Nil retries
	// no status.
	RetryStatus func(status int) bool
	// RespectRetryAfter honors the retry-after and retry-after-ms headers,
	// clamped by MaxBackoff and jittered.
	RespectRetryAfter bool
	// RetryConnection retries a request that produced no HTTP response.
	RetryConnection bool
}

RetryPolicy controls how a failed attempt is retried. The zero value retries nothing; start from DefaultRetry and adjust.

func DefaultRetry

func DefaultRetry() RetryPolicy

DefaultRetry retries 408, 429 and 5xx twice, plus connection failures, with jittered exponential backoff from 500ms to 5s and a 30s total budget.

func NoRetry

func NoRetry() RetryPolicy

NoRetry sends one attempt and returns its outcome.

type Score

type Score struct {
	// Instructions is what the model should rate. Optional.
	Instructions any
	// Criteria holds the ordered level descriptions. Required, at least one.
	Criteria []any
}

Score rates the state against ordered levels. A level's position is its score, counting from zero.

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

func (Score) MarshalJSON

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

MarshalJSON builds the wire object by hand so an unset field is absent rather than null.

type ScoreAnswer

type ScoreAnswer struct {
	// Score is the probability-weighted average level, so it may fall between
	// two integer levels.
	Score float64 `json:"score"`
	// Confidence in the score, from 0 to 1.
	Confidence float64 `json:"confidence"`
	// Legend maps each level to the criterion that defined it.
	Legend map[int]any `json:"legend"`
	// Probabilities per level, summing to approximately 1.
	Probabilities map[int]float64 `json:"probabilities"`
}

ScoreAnswer is the expected score with the rubric it was scored against.

Legend and Probabilities are keyed by integer level. The wire format sends those keys as JSON strings; encoding/json converts them back.

func (*ScoreAnswer) UnmarshalJSON

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

UnmarshalJSON rejects a payload missing any required field, so an absent score or rubric is reported rather than decoded as zero.

type SystemOneResponse

type SystemOneResponse struct {
	// Model that answered, which may differ from the alias that was requested.
	Model string `json:"model"`
	// Answers keyed by question name.
	Answers map[string]Answer `json:"answers"`
	// Usage for this evaluation.
	Usage Usage `json:"usage"`

	// RequestID is the x-typesafe-request-id header. Quote it in a support
	// report.
	RequestID string `json:"-"`
	// Status is the HTTP status code the response arrived with.
	Status int `json:"-"`
	// Header is the response header, for values this version does not model.
	Header http.Header `json:"-"`
	// RawBody is the response body exactly as it arrived.
	RawBody json.RawMessage `json:"-"`
}

SystemOneResponse holds one answer per question, keyed by the names supplied in the request.

func (*SystemOneResponse) ChoiceOf

func (r *SystemOneResponse) ChoiceOf(name string) (ChoiceAnswer, bool)

ChoiceOf returns the choice answer for a question. The second result is false when the name is absent or answered by another primitive.

Example

Confidence is the point of a System One model: a low-confidence answer is a signal to ask a person rather than to guess.

package main

import (
	"fmt"

	jev "github.com/KKloudTarus/taurus-jev-sdk-go"
)

func main() {
	var response *jev.SystemOneResponse // from client.SystemOne

	tone, ok := response.ChoiceOf("tone")
	switch {
	case !ok:
		fmt.Println("no answer for that question")
	case tone.Confidence < 0.6:
		fmt.Println("uncertain, send to a human reviewer")
	default:
		fmt.Println("acting on", tone.Choice)
	}
}

func (*SystemOneResponse) Choices

func (r *SystemOneResponse) Choices() map[string]ChoiceAnswer

Choices returns every choice answer, keyed by question name.

func (*SystemOneResponse) NoulOf

func (r *SystemOneResponse) NoulOf(name string) (float64, bool)

NoulOf returns the probability for a yes/no question. The second result is false when the name is absent or answered by another primitive.

func (*SystemOneResponse) Nouls

func (r *SystemOneResponse) Nouls() map[string]NoulAnswer

Nouls returns every yes/no answer, keyed by question name.

func (*SystemOneResponse) ScoreOf

func (r *SystemOneResponse) ScoreOf(name string) (ScoreAnswer, bool)

ScoreOf returns the score answer for a question. The second result is false when the name is absent or answered by another primitive.

func (*SystemOneResponse) Scores

func (r *SystemOneResponse) Scores() map[string]ScoreAnswer

Scores returns every score answer, keyed by question name.

func (*SystemOneResponse) Unknown

func (r *SystemOneResponse) Unknown() map[string]Answer

Unknown returns every answer whose type this version does not model, keyed by question name. Their payloads are available through Answer.Raw.

func (*SystemOneResponse) UnmarshalJSON

func (r *SystemOneResponse) UnmarshalJSON(data []byte) error

UnmarshalJSON enforces the fields the API documents as required, so a truncated or degraded body is reported rather than decoded into zero values.

type Usage

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

Usage reports the tokens billed for one evaluation. Output tokens are currently free of charge.

Jump to

Keyboard shortcuts

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