typesafe

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 19, 2026 License: MIT Imports: 20 Imported by: 0

README

typesafe-sdk-go

Go Reference MIT

typesafe-sdk-go is a Go SDK for the TypeSafe AI API.

TypeSafe answers typed questions about a piece of state and returns probability distributions, not prose. There is nothing to parse and no format to coax out of a model.

Install the module with:

go get github.com/Tangerg/typesafe-sdk-go@latest

It needs Go 1.25 or newer and has no third-party dependencies.

The package is named typesafe, so an import needs no alias in most editors but reads better with one:

import typesafe "github.com/Tangerg/typesafe-sdk-go"

Quickstart

Set TYPESAFE_API_KEY in your environment, then:

package main

import (
	"context"
	"fmt"
	"log"

	typesafe "github.com/Tangerg/typesafe-sdk-go"
)

func main() {
	client, err := typesafe.NewClient(nil) // reads TYPESAFE_API_KEY
	if err != nil {
		log.Fatal(err)
	}

	result, err := client.SystemOne(context.Background(), &typesafe.SystemOneRequest{
		State: "I was charged twice. Please fix this ASAP.",
		Questions: typesafe.Questions{
			"category": &typesafe.ChoiceQuestion{
				Instructions: "What is this ticket about?",
				Criteria: typesafe.ChoiceCriteria{
					"billing":   nil,
					"technical": nil,
					"other":     nil,
				},
			},
		},
	}, nil)
	if err != nil {
		log.Fatal(err)
	}

	category, err := result.Answers.Choice("category")
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(category.Choice, category.Confidence)
}

There is a runnable version in examples/demo:

TYPESAFE_API_KEY=sk-... go run ./examples/demo

Questions and answers

Three question types, mixable in one call. Each is evaluated in parallel and in isolation against the same state, so a question never sees another's answer. Keep each one atomic and combine them in your own code.

Question Asks Answer
NoulQuestion something yes-or-no Noul — the probability of yes
ChoiceQuestion which of these labels Choice, Confidence, Probabilities
ScoreQuestion where on this rubric Score, Confidence, Legend, Probabilities
result, err := client.SystemOne(ctx, &typesafe.SystemOneRequest{
	State: ticket,
	Questions: typesafe.Questions{
		"isBilling": &typesafe.NoulQuestion{Instructions: "Is this ticket about billing?"},
		"tone": &typesafe.ChoiceQuestion{
			Instructions: "What is the customer's tone?",
			Criteria:     typesafe.ChoiceCriteria{"calm": nil, "frustrated": nil, "angry": nil},
		},
		"urgency": &typesafe.ScoreQuestion{
			Instructions: "How urgent is this ticket?",
			Criteria:     typesafe.ScoreCriteria{"can wait", "this week", "today", "right now"},
		},
	},
}, nil)

A ChoiceCriteria value of nil leaves a label undescribed, which is the right choice when the label speaks for itself. A ScoreCriteria is a list, and its order is its meaning: position zero is the lowest score.

The service's own rules are checked before a request is sent, so they cost no round trip:

Rule
State is required JSON null, numbers, and booleans are refused; "", {} and [] are states
A question name may not be empty it is the key an answer comes back under
A NoulQuestion needs Instructions or Criteria nonempty instructions or at least one non-null criterion
ChoiceCriteria 1 to 255 labels
ScoreCriteria 1 to 10 levels, each non-null
Content entries text, object, array, or null; nested numbers and booleans are allowed

These rules were checked against the live API on 2026-09-20. The service accepts a one-level score (always zero) and rejects null score levels, although the API reference and TypeScript SDK describe a two-level minimum, and the TypeScript SDK allows null levels. This SDK follows the service. Empty noul instructions require a criterion; an empty string, object, or array supplied as that criterion is accepted.

Call Validate() on a SystemOneRequest or on Questions to check them yourself when a request is assembled far from where it is sent.

Validation checks the encoded JSON, including typed nils and custom json.Marshaler values. SystemOne encodes once and sends the bytes it checked; Validate() performs its own encoding and also checks Extra collisions.

A question's type decides its answer's, but Go cannot vary a map's value type by key, so the type is named at the point of use:

isBilling, err := result.Answers.Noul("isBilling")   // *NoulAnswer
tone, err := result.Answers.Choice("tone")           // *ChoiceAnswer
urgency, err := result.Answers.Score("urgency")      // *ScoreAnswer

Each accessor reports a clear error when the answer is absent or has a different type than you asked for.

An answer carries the readings you would otherwise compute from it:

if isBilling.Noul > 0.8 {        // a noul is a probability; the threshold is yours
	route(ticket)
}
tone.Choice                      // the selected label
tone.Probability()               // how likely that label was — not Confidence
urgency.Score                    // expected score, which may fall between levels
urgency.Level()                  // the rubric level it rounds to
urgency.Description()            // what the rubric says about that level

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

Configuration

NewClient takes its settings from ClientOptions, then from the environment, then from the SDK defaults. A blank environment value counts as unset.

Option Environment Default
APIKey TYPESAFE_API_KEY required
BaseURL TYPESAFE_BASE_URL https://api.typesafe.ai
DefaultModel TYPESAFE_DEFAULT_MODEL jev-latest
Logger TYPESAFE_LOG_LEVEL no logging
Timeout 10s per attempt
Retry DefaultRetryPolicy()
Header none
HTTPClient http.DefaultClient

The SDK copies the HTTP client's settings and shares its transport and cookie jar. By default, redirects return an *APIError with the original 3xx status: following a redirect could forward credentials or evaluation data, or change a POST into a GET. An explicitly supplied HTTPClient.CheckRedirect policy is honored.

A Client is safe for concurrent use and pools its connections. Create one for the life of the program and share it.

BaseURL must be an absolute HTTP(S) root, optionally with a path prefix. Credentials, query strings, and fragments are rejected at construction.

Timeouts and retries

Timeout bounds each attempt, not the call as a whole. Retries have no budget of their own, so bound a whole call with its context:

ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()

DefaultRetryPolicy() retries 408, 429, and every 5xx, along with connection failures and timeouts, twice each: exponential backoff from 500ms to 5s with up to 25% jitter, honoring a Retry-After header within a minute.

A RetryPolicy is a complete setting rather than a patch, so a zero field is never mistaken for an unset one. Start from the defaults and change what you need:

policy := typesafe.DefaultRetryPolicy()
policy.MaxRetries = 5
client, err := typesafe.NewClient(&typesafe.ClientOptions{Retry: &policy})

A nil Retry in ClientOptions or RequestOptions inherits the level above it; a non-nil one replaces it outright.

Errors

result, err := client.SystemOne(ctx, request, nil)
switch {
case errors.Is(err, typesafe.ErrRateLimit):
	// back off and try later
case errors.Is(err, typesafe.ErrAuthentication):
	// check TYPESAFE_API_KEY
case errors.Is(err, context.Canceled):
	// the caller gave up
case err != nil:
	var apiErr *typesafe.APIError
	if errors.As(err, &apiErr) {
		log.Printf("request %s failed: %v", apiErr.RequestID, apiErr.Body)
	}
}
  • *APIError — a response the service refused to fulfil. Match its class with ErrBadRequest, ErrAuthentication, ErrPermissionDenied, ErrNotFound, ErrUnprocessableEntity, ErrRateLimit, and ErrServerError (any 5xx) rather than comparing status codes.
  • *ConnectionError — a request that produced no complete response. One that ran out of time also matches context.DeadlineExceeded.
  • The context's own error when the caller gives up, so context.Canceled means your cancellation and nothing else. A custom cancellation cause remains available through errors.Is alongside context.Canceled or context.DeadlineExceeded.

Logging

Pass an *slog.Logger to log activity: summaries at info, full headers and bodies at debug. Credential headers are redacted; bodies are not, and yours may hold the data you are evaluating.

logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelDebug}))
client, err := typesafe.NewClient(&typesafe.ClientOptions{Logger: logger})

With no logger the SDK is silent, unless TYPESAFE_LOG_LEVEL is set — which exists so a deployed program can be made to explain itself without a rebuild.

Raw responses

Every result carries a Meta with the request ID, status, headers, and the raw response body. The typed fields drop any property the SDK does not model, and Meta.Body is where to find one:

fmt.Println(result.Meta.RequestID)
fmt.Println(string(list.Meta.Body))

To send a request property the SDK does not model yet, use SystemOneRequest.Extra.

Testing

Unit tests need no credentials and never reach the network:

go test ./...

They run against payloads the live service actually sent, captured under testdata/. A fixture that stops matching is the service telling you its shape changed; refresh the file and read the diff.

Integration tests call the real API, so they are behind a build tag and cost real requests:

export TYPESAFE_API_KEY=sk-...
go test -tags integration -v ./...

A latency probe sits behind a second switch, because it makes a few hundred calls and reports percentiles rather than asserting anything:

TYPESAFE_LATENCY=1 go test -tags integration -run TestLatency -v ./...

It separates the two halves of a call — what the service spent, read from the gateway's X-Envoy-Upstream-Service-Time, and everything else, which is your network and this SDK. Only the first is a property of the API; read the second as a measurement of where you are sitting.

They assert what the SDK must get right — a probability in range, a distribution that sums to one, a label the question offered, the caller's cancellation coming back as theirs — not what the model happens to answer.

Keep a local key out of the repository. .env is gitignored for the purpose:

echo 'export TYPESAFE_API_KEY=sk-...' > .env && chmod 600 .env
. ./.env && go test -tags integration ./...

Design notes

A few choices are worth knowing before you build on them.

Models own their rules. Each question validates its decoded content; request and result types own their invariants, and APIError owns its classification and message. Validation decodes the bytes already encoded for transport, so it does not invoke user marshalers a second time. Types and all their receiver methods live together in the same source file; Client methods are in client.go, while each call owns its attempts in transport.go.

Results carry their HTTP metadata. Every call returns (value, error), and the value has a Meta with the request ID, status, headers, and the raw response body. The typed fields drop anything the SDK does not model; Meta.Body is where to find it.

Cancellation is the context's, and nothing else's. context.Canceled means you gave up. An attempt that ran out of time is a *ConnectionError and matches context.DeadlineExceeded. The two never get confused for each other, which matters when you are deciding whether to try again.

One error type per thing that can go wrong. A response the service refused is an *APIError, matched by class with errors.Is. A request that produced no complete response — a dropped connection or an attempt that timed out — is a *ConnectionError. There is no hierarchy to walk.

Settings are complete values, not patches. A nil *RetryPolicy inherits the level above it and a non-nil one replaces it outright, so no field needs a third state to distinguish "unset" from its zero. Start from DefaultRetryPolicy().

Questions are structs, and the compiler checks them. There are no builder functions: a rubric is a ScoreCriteria slice and a label set is a ChoiceCriteria map, so the shapes that would otherwise need a run-time check cannot be written down wrong.

An unrecognized payload is kept, not dropped. An answer whose type this release does not model arrives as an *UnknownAnswer carrying the original JSON, so one unfamiliar answer does not cost you the answers beside it. The typed accessors still refuse it.

Known answer types must contain their required, non-null fields. Missing values are errors rather than zero probabilities. A null or empty answers object also fails explicitly, with the service request ID included when available.

The same rules apply when decoding a concrete answer with json.Unmarshal, including its type field. A failed decode leaves the previous answer intact. Unknown answers keep only Raw; AnswerType() reads the type from that JSON, so it cannot disagree with the payload that marshaling returns.

Numbers inside structured ScoreAnswer.Legend descriptions and APIError.Body decode as json.Number, preserving large identifiers and values outside float64's range. Use Number.String(), Int64(), or Float64() according to the application's needs. Scores, confidence, and probabilities remain float64.

Documentation

License

MIT. See LICENSE.

Documentation

Overview

Package typesafe provides an SDK for the TypeSafe AI API.

TypeSafe answers typed questions about a piece of state and returns probability distributions, not prose. There is nothing to parse and no format to coax out of a model: a question comes back as a number your code can act on.

To get started, create a Client and call Client.SystemOne with the state to evaluate and the questions to ask:

client, err := typesafe.NewClient(nil) // reads TYPESAFE_API_KEY
if err != nil {
	log.Fatal(err)
}

result, err := client.SystemOne(ctx, &typesafe.SystemOneRequest{
	State: "I was charged twice. Please fix this ASAP.",
	Questions: typesafe.Questions{
		"category": &typesafe.ChoiceQuestion{
			Instructions: "What is this ticket about?",
			Criteria: typesafe.ChoiceCriteria{
				"billing":   nil,
				"technical": nil,
				"other":     nil,
			},
		},
	},
}, nil)
if err != nil {
	log.Fatal(err)
}

category, err := result.Answers.Choice("category")
if err != nil {
	log.Fatal(err)
}
fmt.Println(category.Choice, category.Confidence)

Questions and answers

There are three question types, and one call may mix them freely. Each is evaluated in parallel and in isolation against the same state, so a question never sees another's answer.

  • A NoulQuestion asks something yes-or-no and is answered with the probability of yes, so that the threshold is yours to choose.
  • A ChoiceQuestion selects one of a set of labels and reports a probability for each.
  • A ScoreQuestion rates the state against an ordered rubric and reports an expected score, which may fall between two levels.

Keep each question atomic. A question that asks two things at once has no single answer to report a probability for; ask both and combine them in your own code, which is cheaper to understand and to change than a longer prompt.

A question's type decides its answer's, but Go cannot vary a map's value type by key, so the answers arrive as an Answers map of an Answer interface. Name the type you expect with Answers.Noul, Answers.Choice, or Answers.Score, each of which reports a clear error if the service sent something else:

urgency, err := result.Answers.Score("urgency")

An answer owns the readings taken from it, so that the arithmetic a caller would otherwise repeat has one place to be right: ChoiceAnswer.Probability for how likely the selected label was, ScoreAnswer.Level and ScoreAnswer.Description for where an expected score lands on the rubric. NoulAnswer has no such helper on purpose — see its documentation.

Configuration

NewClient takes its settings from ClientOptions, then from the environment, then from the SDK defaults. See EnvAPIKey and the constants beside it for the variables, and DefaultBaseURL and DefaultModel for the defaults. A zero ClientOptions, or a nil one, configures a client entirely from the environment.

Timeouts, retries, and errors

ClientOptions.Timeout bounds each attempt and defaults to DefaultTimeout. Retries have no budget of their own, so bound a whole call with its context.Context:

ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()

DefaultRetryPolicy retries 408, 429, and every 5xx, along with connection failures and timeouts, twice each, backing off exponentially with jitter and honoring a Retry-After header within a minute. A RetryPolicy is a complete setting rather than a patch: start from DefaultRetryPolicy and change what you need.

Three kinds of error come back from a call, and errors.Is and errors.As tell them apart:

  • An *APIError is a response the service refused to fulfil. Match its class with ErrRateLimit and the sentinels beside it rather than comparing status codes.
  • A *ConnectionError is a request that produced no complete response, including one that ran out of time; those also match context.DeadlineExceeded.
  • The context's own error is returned when the caller gives up, so context.Canceled means your cancellation and nothing else.

Concurrency

A Client is safe for concurrent use and pools its connections, so create one for the life of the program and share it. Fanning many questions out across goroutines against one client is the intended way to use the API.

Example
package main

import (
	"context"
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"net/http/httptest"

	typesafe "github.com/Tangerg/typesafe-sdk-go"
)

// stubService stands in for api.typesafe.ai so that the examples below run
// offline and print the same thing every time. Point a real client at
// [typesafe.DefaultBaseURL] instead, which is where a client with no BaseURL
// goes.
func stubService(answer func(request map[string]any) any) *httptest.Server {
	return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		var request map[string]any
		_ = json.NewDecoder(r.Body).Decode(&request)
		w.Header().Set("Content-Type", "application/json")
		_ = json.NewEncoder(w).Encode(answer(request))
	}))
}

func main() {
	service := stubService(func(map[string]any) any {
		return map[string]any{
			"model": "jev-1.13",
			"answers": map[string]any{
				"category": map[string]any{
					"type": "choice", "choice": "billing", "confidence": 0.94,
					"probabilities": map[string]float64{"billing": 0.92, "technical": 0.05, "other": 0.03},
				},
			},
			"usage": map[string]any{"input_tokens": 41, "output_tokens": 3},
		}
	})
	defer service.Close()

	client, err := typesafe.NewClient(&typesafe.ClientOptions{
		APIKey:  "sk-example",
		BaseURL: service.URL,
	})
	if err != nil {
		log.Fatal(err)
	}

	result, err := client.SystemOne(context.Background(), &typesafe.SystemOneRequest{
		State: "I was charged twice. Please fix this ASAP.",
		Questions: typesafe.Questions{
			"category": &typesafe.ChoiceQuestion{
				Instructions: "What is this ticket about?",
				Criteria: typesafe.ChoiceCriteria{
					"billing":   nil,
					"technical": nil,
					"other":     nil,
				},
			},
		},
	}, nil)
	if err != nil {
		log.Fatal(err)
	}

	category, err := result.Answers.Choice("category")
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("%s (%.2f confidence)\n", category.Choice, category.Confidence)
}
Output:
billing (0.94 confidence)

Index

Examples

Constants

View Source
const (
	// DefaultBaseURL is the production API root.
	DefaultBaseURL = "https://api.typesafe.ai"
	// DefaultModel is the model used by a request that names none. The
	// "-latest" suffix tracks the newest release of that model family.
	DefaultModel = "jev-latest"
)

Defaults for the settings ClientOptions leaves unset and the environment does not supply.

View Source
const (
	// EnvAPIKey holds the API key. It is the only required setting, and
	// [ClientOptions.APIKey] takes precedence over it.
	EnvAPIKey = "TYPESAFE_API_KEY" //nolint:gosec // the name of a variable, not a key
	// EnvBaseURL holds the API root, defaulting to [DefaultBaseURL].
	EnvBaseURL = "TYPESAFE_BASE_URL"
	// EnvDefaultModel holds the model used by requests that omit one,
	// defaulting to [DefaultModel].
	EnvDefaultModel = "TYPESAFE_DEFAULT_MODEL"
	// EnvLogLevel holds a level name accepted by [ParseLevel]. It installs a
	// stderr logger only when [ClientOptions.Logger] is nil; a caller who
	// supplies a logger owns its leveling.
	EnvLogLevel = "TYPESAFE_LOG_LEVEL"
)

Environment variables read by NewClient when the matching ClientOptions field is unset. An empty or whitespace-only value counts as unset, so an exported-but-empty variable does not shadow an SDK default.

View Source
const (
	ErrBadRequest statusClassError = iota + 1
	ErrAuthentication
	ErrPermissionDenied
	ErrNotFound
	ErrUnprocessableEntity
	ErrRateLimit
	ErrServerError
)

Sentinels for the response classes the API distinguishes. Match them with errors.Is rather than by comparing APIError.Status, which is what most callers actually want to ask:

if errors.Is(err, typesafe.ErrRateLimit) {
	// back off and try later
}

ErrServerError matches every status from 500 to 599, so an overloaded 529 is caught alongside a plain 500.

They are constants of an unexported type rather than error variables, because a package-level var is writable by every importer, and one that reassigned it would silently change how every other importer's errors.Is behaved. A constant cannot be made to mean anything else.

View Source
const (
	TypeNoul   = "noul"
	TypeChoice = "choice"
	TypeScore  = "score"
)

The wire discriminants of the three question and answer types. The service calls a yes/no question a noul.

View Source
const DefaultTimeout = 10 * time.Second

DefaultTimeout is the per-attempt timeout used when ClientOptions.Timeout is zero.

View Source
const LevelOff = slog.Level(math.MaxInt32)

LevelOff is above every level slog defines, so a handler thresholded at it emits nothing. ParseLevel returns it for "off".

View Source
const Version = "0.1.0"

Version is the version of this SDK, reported in the User-Agent and X-TypeSafe-SDK request headers. It versions this module, not the API it speaks to.

Variables

View Source
var ErrNoAPIKey = errors.New(errPrefix + "no API key")

ErrNoAPIKey reports that NewClient found no API key in either ClientOptions or the environment.

Functions

func ParseLevel

func ParseLevel(name string) (slog.Level, error)

ParseLevel converts a level name, in any capitalization, to the level the SDK logs at. The names are debug, info, warn, error, and off; "warn" maps to slog.LevelWarn and "off" to LevelOff.

func StatusRange

func StatusRange(lo, hi int) []int

StatusRange returns the status codes from lo to hi inclusive, for building RetryPolicy.HTTPStatuses. It returns nil when hi is below lo.

Types

type APIError

type APIError struct {
	// Status is the HTTP response status code.
	Status int
	// Header holds the HTTP response headers.
	Header http.Header
	// Body is the parsed JSON body, the response text when the body is not
	// JSON, or nil when the body is empty. JSON numbers decode as [json.Number]
	// so unmodeled data keeps its original precision and range.
	Body any
	// RequestID is the x-typesafe-request-id header, or "" when absent. Quote
	// it in bug reports: it identifies the request in the service's own logs.
	RequestID string
}

An APIError is an unsuccessful HTTP response from the API, returned once the request's retries are exhausted or the status is not one the policy retries.

Example

Match a failure by its class rather than by its status code. The request ID is worth logging: it is what identifies the call in the service's own logs.

package main

import (
	"context"
	"errors"
	"fmt"
	"log"
	"net/http"
	"net/http/httptest"

	typesafe "github.com/Tangerg/typesafe-sdk-go"
)

func main() {
	service := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		w.Header().Set("X-Typesafe-Request-Id", "req_01HQ")
		w.WriteHeader(http.StatusTooManyRequests)
		_, _ = fmt.Fprint(w, `{"error":{"message":"rate limit exceeded"}}`)
	}))
	defer service.Close()

	// Retrying is on by default; it is off here so the example does not wait.
	noRetries := typesafe.DefaultRetryPolicy()
	noRetries.MaxRetries = 0
	client, err := typesafe.NewClient(&typesafe.ClientOptions{
		APIKey: "sk-example", BaseURL: service.URL, Retry: &noRetries,
	})
	if err != nil {
		log.Fatal(err)
	}

	_, err = client.ListModels(context.Background(), nil)
	switch {
	case errors.Is(err, typesafe.ErrRateLimit):
		var apiErr *typesafe.APIError
		errors.As(err, &apiErr)
		fmt.Printf("rate limited (request %s): %s\n", apiErr.RequestID, err)
	case errors.Is(err, typesafe.ErrAuthentication):
		fmt.Println("check TYPESAFE_API_KEY")
	case err != nil:
		fmt.Println("failed:", err)
	}
}
Output:
rate limited (request req_01HQ): typesafe: 429 rate limit exceeded

func (*APIError) Error

func (e *APIError) Error() string

func (*APIError) Is

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

Is matches the class sentinels above, so that a caller can ask about the kind of failure without reaching for the status code.

func (*APIError) RetryAfter

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

RetryAfter reports the server's requested delay from the Retry-After or retry-after-ms response header, and false when neither carries a valid delay. It is most useful on a 429, which is the status the service documents it for.

type Answer

type Answer interface {
	// AnswerType reports the wire discriminant: [TypeNoul], [TypeChoice],
	// [TypeScore], or whatever unrecognized type the service sent.
	AnswerType() string
	// contains filtered or unexported methods
}

An Answer is one of *NoulAnswer, *ChoiceAnswer, *ScoreAnswer, or *UnknownAnswer.

A question's type decides its answer's, but Go cannot give a map values of different types per key, so the type arrives at run time instead: use Answers.Noul, Answers.Choice, and Answers.Score, which name the type you expect and report a clear error when the service disagrees.

type Answers

type Answers map[string]Answer

Answers are the answers to one request, keyed by the names the request's Questions used.

func (Answers) Choice

func (a Answers) Choice(name string) (*ChoiceAnswer, error)

Choice returns the answer to the named choice question, on the same terms as Answers.Noul.

func (Answers) Noul

func (a Answers) Noul(name string) (*NoulAnswer, error)

Noul returns the answer to the named noul question. It reports an error when the response holds no answer under that name, or answered it with a different type than the question asked for.

func (Answers) Score

func (a Answers) Score(name string) (*ScoreAnswer, error)

Score returns the answer to the named score question, on the same terms as Answers.Noul.

func (*Answers) UnmarshalJSON

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

UnmarshalJSON decodes each answer into the type its discriminant names.

type ChoiceAnswer

type ChoiceAnswer struct {
	// Choice is the selected label, one of the keys of the question's
	// [ChoiceCriteria].
	Choice string `json:"choice"`
	// Confidence is the service's reported confidence in Choice, from zero to
	// one. It is not the same as the selected label's probability: see
	// Probabilities.
	Confidence float64 `json:"confidence"`
	// Probabilities holds one probability per label of the question.
	Probabilities map[string]float64 `json:"probabilities"`
}

A ChoiceAnswer answers a ChoiceQuestion.

func (*ChoiceAnswer) AnswerType

func (a *ChoiceAnswer) AnswerType() string

AnswerType returns TypeChoice.

func (*ChoiceAnswer) MarshalJSON

func (a *ChoiceAnswer) MarshalJSON() ([]byte, error)

func (*ChoiceAnswer) Probability

func (a *ChoiceAnswer) Probability() float64

Probability reports how likely the selected label was, which is the reading of a choice that callers reach for and the one that is easy to confuse with Confidence. It is zero if the service selected a label it reported no probability for, which a well-formed answer never does.

func (*ChoiceAnswer) UnmarshalJSON added in v0.1.0

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

type ChoiceCriteria

type ChoiceCriteria map[string]Entry

ChoiceCriteria maps each label a ChoiceQuestion may select to a description of what it means. A nil description leaves the label undescribed, which is the right choice when the label speaks for itself:

typesafe.ChoiceCriteria{"billing": nil, "technical": nil, "other": nil}

A label with no description is still an alternative; only labels present here can be selected. The service accepts up to 255 of them.

type ChoiceQuestion

type ChoiceQuestion struct {
	// Instructions is the question. A nil Instructions is sent as null.
	Instructions Entry `json:"instructions"`
	// Criteria are the alternatives to select between.
	Criteria ChoiceCriteria `json:"criteria"`
}

A ChoiceQuestion selects one of a set of named alternatives. See ChoiceAnswer.

func (*ChoiceQuestion) MarshalJSON

func (q *ChoiceQuestion) MarshalJSON() ([]byte, error)

func (*ChoiceQuestion) QuestionType

func (q *ChoiceQuestion) QuestionType() string

QuestionType returns TypeChoice.

type Client

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

A Client is a connection-pooled handle for the TypeSafe API. It is safe for concurrent use, and one client should be shared for the life of a program so that its underlying connections are reused.

func NewClient

func NewClient(options *ClientOptions) (*Client, error)

NewClient returns a client for the TypeSafe API.

Options may be nil, which configures the client entirely from the environment. It returns an error when no API key is available, or when a setting is one this package refuses.

func (*Client) BaseURL

func (c *Client) BaseURL() string

BaseURL reports the API root the client sends to, without a trailing slash.

func (*Client) DefaultModel

func (c *Client) DefaultModel() string

DefaultModel reports the model used by requests that name none.

func (*Client) ListModels

func (c *Client) ListModels(ctx context.Context, options *RequestOptions) (*ModelList, error)

ListModels reports the models available to the account.

Example
package main

import (
	"context"
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"net/http/httptest"
	"strings"

	typesafe "github.com/Tangerg/typesafe-sdk-go"
)

// stubService stands in for api.typesafe.ai so that the examples below run
// offline and print the same thing every time. Point a real client at
// [typesafe.DefaultBaseURL] instead, which is where a client with no BaseURL
// goes.
func stubService(answer func(request map[string]any) any) *httptest.Server {
	return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		var request map[string]any
		_ = json.NewDecoder(r.Body).Decode(&request)
		w.Header().Set("Content-Type", "application/json")
		_ = json.NewEncoder(w).Encode(answer(request))
	}))
}

func main() {
	service := stubService(func(map[string]any) any {
		return map[string]any{"models": []any{
			map[string]any{"name": "jev-1.13", "description": "general purpose", "release_date": "2026-07-01"},
			map[string]any{"name": "jev-latest", "description": "alias for the newest jev", "release_date": "2026-07-01"},
		}}
	})
	defer service.Close()

	client, err := typesafe.NewClient(&typesafe.ClientOptions{APIKey: "sk-example", BaseURL: service.URL})
	if err != nil {
		log.Fatal(err)
	}

	list, err := client.ListModels(context.Background(), nil)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(strings.Join(list.Names(), ", "))
	for _, model := range list.Models {
		fmt.Printf("%s: %s\n", model.Name, model.Description)
	}
}
Output:
jev-1.13, jev-latest
jev-1.13: general purpose
jev-latest: alias for the newest jev

func (*Client) Retry

func (c *Client) Retry() RetryPolicy

Retry reports a copy of the client's retry policy. Changing it does not affect the client; pass a policy to NewClient or RequestOptions instead.

func (*Client) SystemOne

func (c *Client) SystemOne(ctx context.Context, request *SystemOneRequest, options *RequestOptions) (*SystemOneResult, error)

SystemOne answers named questions about text or structured state.

Each question is evaluated in parallel and in isolation against the same state, and comes back as a probability distribution rather than as prose to parse. Retrieve the answers with Answers.Noul, Answers.Choice, and Answers.Score, which name the type each question should have produced.

It returns an *APIError for a non-2xx response that outlived the retry policy, a *ConnectionError for a request that never completed, and ctx's error when the caller gives up.

Example

A single call may mix all three question types. They are evaluated in parallel and in isolation, so keep each one atomic and combine the answers in your own code rather than in a longer instruction.

package main

import (
	"context"
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"net/http/httptest"

	typesafe "github.com/Tangerg/typesafe-sdk-go"
)

// stubService stands in for api.typesafe.ai so that the examples below run
// offline and print the same thing every time. Point a real client at
// [typesafe.DefaultBaseURL] instead, which is where a client with no BaseURL
// goes.
func stubService(answer func(request map[string]any) any) *httptest.Server {
	return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		var request map[string]any
		_ = json.NewDecoder(r.Body).Decode(&request)
		w.Header().Set("Content-Type", "application/json")
		_ = json.NewEncoder(w).Encode(answer(request))
	}))
}

func main() {
	service := stubService(func(map[string]any) any {
		return map[string]any{
			"model": "jev-1.13",
			"answers": map[string]any{
				"isBilling": map[string]any{"type": "noul", "noul": 0.97},
				"tone": map[string]any{
					"type": "choice", "choice": "frustrated", "confidence": 0.81,
					"probabilities": map[string]float64{"calm": 0.04, "frustrated": 0.79, "angry": 0.17},
				},
				"urgency": map[string]any{
					"type": "score", "score": 2.6, "confidence": 0.7,
					"legend":        map[string]any{"0": "can wait", "1": "this week", "2": "today", "3": "right now"},
					"probabilities": map[string]float64{"0": 0.02, "1": 0.08, "2": 0.18, "3": 0.72},
				},
			},
			"usage": map[string]any{"input_tokens": 88, "output_tokens": 9},
		}
	})
	defer service.Close()

	client, err := typesafe.NewClient(&typesafe.ClientOptions{APIKey: "sk-example", BaseURL: service.URL})
	if err != nil {
		log.Fatal(err)
	}

	ticket := map[string]any{
		"subject": "Charged twice this month",
		"body":    "I see two charges of $49 on my card for August. Please fix this ASAP.",
	}
	result, err := client.SystemOne(context.Background(), &typesafe.SystemOneRequest{
		State: ticket,
		Questions: typesafe.Questions{
			"isBilling": &typesafe.NoulQuestion{Instructions: "Is this ticket about billing?"},
			"tone": &typesafe.ChoiceQuestion{
				Instructions: "What is the customer's tone?",
				Criteria:     typesafe.ChoiceCriteria{"calm": nil, "frustrated": nil, "angry": nil},
			},
			"urgency": &typesafe.ScoreQuestion{
				Instructions: "How urgent is this ticket?",
				Criteria:     typesafe.ScoreCriteria{"can wait", "this week", "today", "right now"},
			},
		},
	}, nil)
	if err != nil {
		log.Fatal(err)
	}

	isBilling, err := result.Answers.Noul("isBilling")
	if err != nil {
		log.Fatal(err)
	}
	tone, err := result.Answers.Choice("tone")
	if err != nil {
		log.Fatal(err)
	}
	urgency, err := result.Answers.Score("urgency")
	if err != nil {
		log.Fatal(err)
	}

	// A noul is a probability, not a verdict: the threshold is yours.
	fmt.Printf("billing:  %.2f (route it: %v)\n", isBilling.Noul, isBilling.Noul > 0.8)
	fmt.Printf("tone:     %s at p=%.2f\n", tone.Choice, tone.Probability())
	fmt.Printf("urgency:  %.1f of 3, nearest %q\n", urgency.Score, urgency.Description())
	fmt.Printf("tokens:   %d in, %d out\n", result.Usage.InputTokens, result.Usage.OutputTokens)
}
Output:
billing:  0.97 (route it: true)
tone:     frustrated at p=0.79
urgency:  2.6 of 3, nearest "right now"
tokens:   88 in, 9 out

func (*Client) Timeout

func (c *Client) Timeout() time.Duration

Timeout reports the per-attempt timeout.

type ClientOptions

type ClientOptions struct {
	// APIKey authenticates every request. It falls back to [EnvAPIKey], and
	// [NewClient] fails with [ErrNoAPIKey] when neither supplies one.
	APIKey string
	// BaseURL is an absolute HTTP(S) API root, optionally with a path prefix,
	// without credentials, query, or fragment. It falls back to [EnvBaseURL]
	// and then [DefaultBaseURL]. Trailing slashes are removed.
	BaseURL string
	// DefaultModel answers requests that leave [SystemOneRequest.Model] empty.
	// It falls back to [EnvDefaultModel] and then [DefaultModel].
	DefaultModel string
	// Timeout bounds each attempt, not the call as a whole: a retried call may
	// take several times as long. Bound the whole call with its
	// [context.Context]. Zero means [DefaultTimeout].
	Timeout time.Duration
	// Retry replaces the SDK's retry policy. Nil means [DefaultRetryPolicy].
	Retry *RetryPolicy
	// Header is sent with every request. Per-call headers in
	// [RequestOptions.Header] override it, and neither can displace the headers
	// this package sets to authenticate and identify the request.
	Header http.Header
	// HTTPClient supplies HTTP settings. Nil uses [http.DefaultClient].
	// The client is copied; its Transport and Jar remain shared. Without an
	// explicit CheckRedirect, redirects are returned as [*APIError] responses
	// so credentials and state stay at the configured endpoint.
	//
	// Any timeout on it applies to the whole attempt including the body, as
	// Timeout already does. Set its Transport to route through a proxy, pin
	// TLS, or observe traffic.
	HTTPClient *http.Client
	// Logger records request and response activity: summaries at info, and
	// full headers and bodies at debug. Credential headers are redacted;
	// bodies are not, and yours may hold the data you are evaluating.
	//
	// Nil means no logging, unless [EnvLogLevel] is set, which installs a
	// stderr logger at that level.
	Logger *slog.Logger
}

ClientOptions configures a Client. The zero value is usable as long as EnvAPIKey is set in the environment.

Every string setting falls back to its environment variable and then to the SDK default, in that order.

type ConnectionError

type ConnectionError struct {
	// Timeout is the per-attempt timeout that elapsed, and zero when the
	// SDK's deadline did not elapse. A transport timeout has no known duration
	// and leaves this field zero; use errors.Is with context.DeadlineExceeded.
	Timeout time.Duration
	// Err is the underlying transport or context error.
	Err error
}

A ConnectionError reports that an attempt produced no complete HTTP response: the connection failed, the server closed it mid-body, or the attempt ran out of time.

A timeout is the same failure as a dropped connection from the caller's point of view — no answer arrived — so both use this type. Ask errors.Is for context.DeadlineExceeded to identify a timeout, including one imposed by the configured HTTP client or transport.

func (*ConnectionError) Error

func (e *ConnectionError) Error() string

func (*ConnectionError) Unwrap

func (e *ConnectionError) Unwrap() error

type Entry

type Entry = any

An Entry is a JSON value the API accepts wherever it takes free-form content: the state under evaluation, a question's instructions, and the description of an outcome.

It is a string for plain text, a map or slice for structured content, and nil for JSON null, which leaves a noul or choice outcome undescribed. State and score levels must be non-null. The alias names this contract in signatures; validation checks the encoded shape. Numbers and booleans are allowed inside objects and arrays, but not as an Entry on their own.

type Meta

type Meta struct {
	// RequestID is the x-typesafe-request-id header, or "" when absent. Quote
	// it in bug reports: it identifies the request in the service's own logs.
	RequestID string
	// Status is the HTTP response status code, which for a result is 2xx.
	Status int
	// Header holds the HTTP response headers.
	Header http.Header
	// Body is the raw response body. The typed fields of a result drop any
	// property this SDK does not model, and this is where to find one: a model
	// attribute added after this release, for instance.
	Body []byte
}

Meta reports HTTP metadata about a response from the API. It reaches a caller on a result; an unsuccessful response carries the same facts on an *APIError.

type ModelCard

type ModelCard struct {
	// Name is the identifier to put in [SystemOneRequest.Model].
	Name string `json:"name"`
	// Description is a human-readable summary of what the model is for.
	Description string `json:"description"`
	// ReleaseDate is the model's release date, as the service formats it.
	ReleaseDate string `json:"release_date"`
}

A ModelCard describes one model available to the account.

type ModelList

type ModelList struct {
	// Models are the available models. An account with none is answered with
	// an empty list rather than an error.
	Models []ModelCard `json:"models"`
	// Meta reports HTTP metadata about the response. Its Body holds any model
	// attribute [ModelCard] does not name, such as one added after this
	// release or one visible only to the service's own accounts.
	Meta Meta `json:"-"`
}

A ModelList holds the models available to the account.

func (*ModelList) Names

func (l *ModelList) Names() []string

Names reports the model names, which is what a caller needs to offer a choice or to check that the model they mean to use is one of them.

type NoulAnswer

type NoulAnswer struct {
	// Noul is the probability that the answer is yes, from zero to one.
	//
	// It is a probability and not a verdict, which is the point of the
	// primitive: pick the threshold your application needs instead of
	// accepting one the service picked for you.
	Noul float64 `json:"noul"`
}

A NoulAnswer answers a NoulQuestion.

It deliberately offers no Yes or Above helper. A threshold is the one part of a yes/no decision this package does not know: it depends on what a wrong yes and a wrong no each cost you. A helper would make `answer.Yes()` the obvious thing to write and bury that choice at whatever default it shipped with.

func (*NoulAnswer) AnswerType

func (a *NoulAnswer) AnswerType() string

AnswerType returns TypeNoul.

func (*NoulAnswer) MarshalJSON

func (a *NoulAnswer) MarshalJSON() ([]byte, error)

func (*NoulAnswer) UnmarshalJSON added in v0.1.0

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

type NoulCriteria

type NoulCriteria struct {
	// True describes the yes outcome.
	True Entry `json:"true,omitzero"`
	// False describes the no outcome.
	False Entry `json:"false,omitzero"`
}

NoulCriteria describes the two outcomes of a NoulQuestion. Either side may be left nil, which omits it and leaves that outcome undescribed.

type NoulQuestion

type NoulQuestion struct {
	// Instructions is the question. A nil Instructions is sent as null, which
	// asks the service to judge the state against Criteria alone — so a question
	// with neither is refused. Empty text, objects, and arrays also need criteria.
	Instructions Entry `json:"instructions"`
	// Criteria optionally describes what the two outcomes mean. A nil Criteria
	// is omitted.
	Criteria *NoulCriteria `json:"criteria,omitzero"`
}

A NoulQuestion asks a yes/no question and is answered with the probability of yes, rather than with a yes or a no. See NoulAnswer.

func (*NoulQuestion) MarshalJSON

func (q *NoulQuestion) MarshalJSON() ([]byte, error)

func (*NoulQuestion) QuestionType

func (q *NoulQuestion) QuestionType() string

QuestionType returns TypeNoul.

type Question

type Question interface {
	// QuestionType reports the wire discriminant.
	QuestionType() string
	// contains filtered or unexported methods
}

A Question is one of *NoulQuestion, *ChoiceQuestion, or *ScoreQuestion.

The set is closed to the three question types in this package.

type Questions

type Questions map[string]Question

Questions are the questions of one request, keyed by the names their answers are returned under. The names are yours; the service echoes them back in SystemOneResult.Answers.

Each question is evaluated in parallel and in isolation against the same state, so questions do not see one another's answers. Keep each one atomic and combine them in your own code.

func (Questions) Validate

func (q Questions) Validate() error

Validate reports whether the question set can be sent.

It checks the encoded values using the same rules as Client.SystemOne. Call it when questions are assembled separately from sending the request.

type RequestOptions

type RequestOptions struct {
	// Timeout replaces [ClientOptions.Timeout] for each attempt of this call.
	Timeout time.Duration
	// Retry replaces the client's retry policy for this call, in full: it is a
	// complete policy and not a patch. See [RetryPolicy].
	Retry *RetryPolicy
	// Header is merged over [ClientOptions.Header] for this call.
	Header http.Header
}

RequestOptions overrides client settings for one call. A nil *RequestOptions, and every zero field of a non-nil one, inherits the client's setting.

type RetryPolicy

type RetryPolicy struct {
	// MaxRetries is the number of retries after the first attempt. Zero
	// disables retrying.
	MaxRetries int
	// BackoffInitial is the delay before the first retry, doubled before each
	// later one up to BackoffMax.
	BackoffInitial time.Duration
	// BackoffMax caps the doubling.
	BackoffMax time.Duration
	// BackoffJitter is the fraction of each delay, from 0 to 1, that is
	// randomly subtracted so that concurrent clients do not retry in lockstep.
	BackoffJitter float64
	// HTTPStatuses are the response status codes that are retried. A nil slice
	// retries no status; use [StatusRange] to build a contiguous run.
	HTTPStatuses []int
	// RespectRetryAfter honors a Retry-After or retry-after-ms response header
	// in place of the computed backoff.
	RespectRetryAfter bool
	// MaxRetryAfter is the longest server-requested delay that is honored. A
	// longer one falls back to backoff rather than parking the call.
	MaxRetryAfter time.Duration
	// RetryConnection retries a [ConnectionError] that is not a timeout.
	RetryConnection bool
	// RetryTimeout retries an attempt or transport timeout.
	RetryTimeout bool
}

A RetryPolicy decides which failures are retried and how long to wait first.

A policy is a complete setting rather than a patch: a nil *RetryPolicy in ClientOptions or RequestOptions inherits the level above it, and a non-nil one replaces it outright. Start from DefaultRetryPolicy and change what you need, so that a zero field is never mistaken for an unset one:

policy := typesafe.DefaultRetryPolicy()
policy.MaxRetries = 5

Retries have no shared deadline. ClientOptions.Timeout bounds each attempt, and a whole call is bounded by its context.Context.

Example

A RetryPolicy is a complete setting, not a patch: start from the defaults and change what you need, so that a zero field is never mistaken for an unset one.

package main

import (
	"fmt"
	"log"
	"net/http"

	typesafe "github.com/Tangerg/typesafe-sdk-go"
)

func main() {
	policy := typesafe.DefaultRetryPolicy()
	policy.MaxRetries = 5
	policy.HTTPStatuses = append(policy.HTTPStatuses, http.StatusConflict)

	client, err := typesafe.NewClient(&typesafe.ClientOptions{APIKey: "sk-example", Retry: &policy})
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(client.Retry().MaxRetries, client.Retry().RetriesStatus(http.StatusConflict))
}
Output:
5 true

func DefaultRetryPolicy

func DefaultRetryPolicy() RetryPolicy

DefaultRetryPolicy returns the SDK's default policy: two retries of 408, 429, and every 5xx, and of connection failures and timeouts, with exponential backoff from 500ms to 5s and up to 25% jitter.

It returns a fresh value on each call, so the returned policy can be modified freely.

func (RetryPolicy) Delay

func (p RetryPolicy) Delay(attempt int, header http.Header, random func() float64) time.Duration

Delay reports how long to wait before a zero-based retry attempt, given the response headers that prompted it, or nil when no response arrived.

A server-requested delay within the policy's ceiling is used exactly, on the grounds that the server knows when it will be ready and jitter would only make the client early. Everything else uses capped exponential backoff, from which jitter is subtracted.

random must return a value in [0, 1); pass math/rand/v2.Float64 outside of a test that needs a fixed delay.

func (RetryPolicy) RetriesStatus

func (p RetryPolicy) RetriesStatus(status int) bool

RetriesStatus reports whether the policy retries a response status code.

type ScoreAnswer

type ScoreAnswer struct {
	// Score is the expected score over the rubric, so it may fall between two
	// levels: a question split evenly between levels 1 and 3 scores 2, which no
	// single level was chosen for.
	Score float64 `json:"score"`
	// Confidence is the service's reported confidence in Score, from zero to
	// one.
	Confidence float64 `json:"confidence"`
	// Legend is the question's rubric echoed back, keyed by score, so that a
	// score can be rendered without holding on to the request.
	// Numbers inside structured descriptions decode as [json.Number] to
	// preserve their precision and range.
	Legend map[int]Entry `json:"legend"`
	// Probabilities holds one probability per level of the rubric.
	Probabilities map[int]float64 `json:"probabilities"`
}

A ScoreAnswer answers a ScoreQuestion.

func (*ScoreAnswer) AnswerType

func (a *ScoreAnswer) AnswerType() string

AnswerType returns TypeScore.

func (*ScoreAnswer) Description

func (a *ScoreAnswer) Description() Entry

Description reports what the rubric says about ScoreAnswer.Level, and nil when that level was left undescribed.

func (*ScoreAnswer) Level

func (a *ScoreAnswer) Level() int

Level reports the rubric level the expected score lands on, rounding a score that fell between two of them.

Round to render a score, not to decide on one: rounding 1.5 and 2.4 to the same level discards the difference the expected score exists to express.

func (*ScoreAnswer) MarshalJSON

func (a *ScoreAnswer) MarshalJSON() ([]byte, error)

func (*ScoreAnswer) UnmarshalJSON added in v0.1.0

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

type ScoreCriteria

type ScoreCriteria []Entry

ScoreCriteria is a rubric: descriptions of each level in ascending order, scored from zero by position. The service accepts one to ten non-null levels. A single level always scores zero. This follows the running service; the documentation and TypeScript SDK currently require at least two levels.

typesafe.ScoreCriteria{"can wait", "this week", "today", "right now"}

scores 0 through 3. The answer's expected score may land between levels; see ScoreAnswer.Score.

type ScoreQuestion

type ScoreQuestion struct {
	// Instructions is the question. A nil Instructions is sent as null.
	Instructions Entry `json:"instructions"`
	// Criteria is the rubric, which must describe at least one level.
	Criteria ScoreCriteria `json:"criteria"`
}

A ScoreQuestion rates the state against an ordered rubric. See ScoreAnswer.

func (*ScoreQuestion) MarshalJSON

func (q *ScoreQuestion) MarshalJSON() ([]byte, error)

func (*ScoreQuestion) QuestionType

func (q *ScoreQuestion) QuestionType() string

QuestionType returns TypeScore.

type SystemOneRequest

type SystemOneRequest struct {
	// State is what the questions are asked about, and is required: text, a
	// JSON object, or a JSON array. The service refuses a null state, and
	// refuses one that encodes as a number or a boolean.
	//
	// An empty string, object, or array is a state; nil is not.
	State Entry `json:"state"`
	// Questions are the questions to answer, keyed by the names their answers
	// come back under. At least one is required.
	Questions Questions `json:"questions"`
	// Model overrides the client's default model for this request.
	Model string `json:"model"`
	// Extra carries additional top-level request properties, for using a
	// service feature this SDK does not model yet. Its keys are merged into the
	// request body and may not collide with the fields above.
	Extra map[string]any `json:"-"`
}

A SystemOneRequest asks named questions about a piece of state.

func (*SystemOneRequest) MarshalJSON

func (r *SystemOneRequest) MarshalJSON() ([]byte, error)

MarshalJSON validates the encoded request and merges SystemOneRequest.Extra.

The fields are encoded and decoded again rather than being listed here, so that the struct tags stay the only statement of what this request sends. A second list would be a second owner of that fact, and the collision check below would start passing keys that do collide.

func (*SystemOneRequest) Validate

func (r *SystemOneRequest) Validate() error

Validate reports whether the request can be sent.

It encodes the request and checks the same JSON rules as Client.SystemOne, including Extra collisions. Call it when assembling a request separately from sending it. Custom JSON marshalers must be safe to call again.

type SystemOneResult

type SystemOneResult struct {
	// Model is the model that answered, resolved from an alias such as
	// "jev-latest" to the release it names.
	Model string `json:"model"`
	// Answers holds one answer per question, under the same names.
	Answers Answers `json:"answers"`
	// Usage reports the tokens the request consumed.
	Usage Usage `json:"usage"`
	// Meta reports HTTP metadata about the response.
	Meta Meta `json:"-"`
}

A SystemOneResult holds the answers to a SystemOneRequest.

type UnknownAnswer

type UnknownAnswer struct {
	// Raw is the answer's JSON object, including its nonempty type field.
	// It is the sole source of both [UnknownAnswer.AnswerType] and the encoded
	// answer, so editing it cannot leave a separate type field out of sync.
	Raw json.RawMessage
}

An UnknownAnswer carries an answer whose type this SDK does not model, which is what a service newer than the SDK produces.

Decoding keeps it rather than failing, so that one unrecognized answer does not cost you the answers beside it in the same response. The accessors still refuse it: an answer the SDK cannot interpret is never silently treated as one it can.

func (*UnknownAnswer) AnswerType

func (a *UnknownAnswer) AnswerType() string

AnswerType reads the discriminant from Raw. It returns an empty string if Raw is malformed, which UnknownAnswer.MarshalJSON reports as an error.

func (*UnknownAnswer) MarshalJSON

func (a *UnknownAnswer) MarshalJSON() ([]byte, error)

MarshalJSON returns the bytes the service sent. An answer this package could not interpret is one it has no business rewriting.

func (*UnknownAnswer) UnmarshalJSON added in v0.1.0

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

type Usage

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

Usage reports the tokens a request consumed.

Directories

Path Synopsis
examples
demo command
The demo command asks a support ticket a few questions of each kind.
The demo command asks a support ticket a few questions of each kind.

Jump to

Keyboard shortcuts

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