Skip to content

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

typesafego

A Go client for the TypeSafe AI API.

TypeSafe's System One model, Jev, evaluates typed questions against a state and returns structured, probabilistic answers instead of generated text:

  • Noul (yes/no) → a probability that the answer is yes
  • Choice → the selected option, the probability of every option, and a confidence
  • Score → a probability-weighted position on an ordered rubric, the probability of every level, and a confidence

Every question in a request is evaluated independently against the same state, so you can ask all the questions your code needs in one call.

The library depends only on the Go standard library.

Install

go get github.com/adibhanna/typesafego

Requires Go 1.24 or later. The package name is typesafe:

import typesafe "github.com/adibhanna/typesafego"

Quick start

package main

import (
	"context"
	"fmt"
	"log"

	typesafe "github.com/adibhanna/typesafego"
)

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

	resp, err := client.SystemOne(context.Background(), typesafe.SystemOneRequest{
		State: "My payment failed twice and I still got charged. Fix this now!",
		Questions: typesafe.Questions{
			"is_urgent": typesafe.Noul("Does the customer convey urgency?"),
			"department": typesafe.Choice("Which team should handle this?", map[string]any{
				"billing":   "Payments, invoices, refunds",
				"technical": "Bugs, outages, integrations",
				"sales":     "Pricing, upgrades, new accounts",
			}),
			"frustration": typesafe.Score("How frustrated is the customer?",
				"Calm", "Frustrated", "Very angry"),
		},
	})
	if err != nil {
		log.Fatal(err)
	}

	urgent, _ := resp.Noul("is_urgent")
	department, _ := resp.Choice("department")
	frustration, _ := resp.Score("frustration")

	fmt.Println(urgent.IsTrue(0.5), urgent.Noul)                 // true 0.93
	fmt.Println(department.Choice, department.Confidence)        // billing 0.81
	fmt.Println(frustration.Score, frustration.Label())          // 1.7 Very angry
	fmt.Println(resp.Model, resp.Usage.InputTokens)              // jev-1.13.0 312
}

A complete, runnable version lives in examples/triage:

export TYPESAFE_API_KEY=sk-...
go run ./examples/triage "My card was charged twice and support is ignoring me!"

Questions

State can be any JSON-encodable value: a string, a struct, a map, or a slice. Question instructions and criteria can be strings or structured JSON values (objects, arrays), which the model reads as-is.

Noul (yes/no)

typesafe.Noul("Is the customer asking for a refund?")

// Optionally spell out what a yes and a no mean.
typesafe.Noul("Is the customer asking for a refund?").WithCriteria(
	"The customer explicitly asks for money back or a chargeback.",
	"Anything else, including billing questions without a refund request.",
)

Answer: *NoulAnswer with Noul (probability of yes) and IsTrue(threshold), which is Noul >= threshold.

Choice

// Options with descriptions. A nil description sends null.
typesafe.Choice("Which team should handle this?", map[string]any{
	"billing":   "Payments, invoices, refunds",
	"technical": map[string]any{"handles": []string{"bugs", "outages"}},
	"sales":     nil,
})

// Options without descriptions.
typesafe.ChoiceOf("What is the sentiment?", "positive", "neutral", "negative")

Answer: *ChoiceAnswer with Choice, Probabilities (option → probability), Confidence, plus ProbabilityOf(option) and Ranked() (options sorted by probability, highest first).

Score

typesafe.Score("How frustrated is the customer?",
	"Calm",         // level 0
	"Frustrated",   // level 1
	"Very angry",   // level 2
)

Levels are ordered from lowest to highest and each may be a string or a structured value. At least two are required.

Answer: *ScoreAnswer with Score (probability-weighted, may fall between levels), Legend (level → description), Probabilities (level → probability), and Confidence, plus:

Method Returns
Level() the most probable level (lowest index on a tie)
NearestLevel() Score rounded to the nearest level, clamped to the rubric
Label() / LabelOf(level) the legend entry for Level() / a given level
ProbabilityOf(level) the probability of a level
Normalized() Score / MaxLevel(), a 0..1 value comparable across rubrics of different sizes
MaxLevel() the highest level index

Level() and NearestLevel() differ when the distribution is split: with probabilities [0.5, 0, 0.5] the score is 1.0, so NearestLevel() is 1 while Level() is 0.

Raw questions

RawQuestion sends an arbitrary JSON object for fields or question types this package does not model. It must contain a string "type".

typesafe.RawQuestion{"type": "noul", "instructions": "Is this spam?"}

Answers of a type this package does not know decode to *UnknownAnswer carrying the raw JSON, so new answer types never break existing code.

Reading answers

SystemOneResponse.Answers is a map[string]Answer. The typed getters return an error wrapping ErrAnswerNotFound if the id is missing and ErrAnswerType if the answer has another type:

department, err := resp.Choice("department")
if err != nil {
	return err // typesafe: answer not found: "department"
}

Confidence-gated routing

department, _ := resp.Choice("department")
if department.Confidence < 0.7 {
	return routeToHuman(ticket, department.Ranked())
}
return routeTo(department.Choice)

Composite scores

Normalized() puts rubrics with different numbers of levels on the same 0..1 scale so they can be weighted together with Noul probabilities:

priority := 0.4*frustration.Normalized() + 0.35*urgent.Noul + 0.25*churn.Noul

Configuration

NewClient reads its defaults from the environment, and explicit options take precedence:

Option Environment variable Default
WithAPIKey(key) TYPESAFE_API_KEY required
WithBaseURL(url) TYPESAFE_BASE_URL https://api.typesafe.ai
WithModel(model) TYPESAFE_DEFAULT_MODEL jev-latest
WithTimeout(d) 10s per attempt (0 disables)
WithRetryPolicy(p) / WithMaxRetries(n) DefaultRetryPolicy()
WithHTTPClient(c) http.DefaultClient
WithHeader(key, value)
WithLogger(l) discard
policy := typesafe.DefaultRetryPolicy()
policy.MaxRetries = 4

client, err := typesafe.NewClient(
	typesafe.WithAPIKey(os.Getenv("MY_TYPESAFE_KEY")),
	typesafe.WithModel(typesafe.ModelJevPreview),
	typesafe.WithTimeout(20*time.Second),
	typesafe.WithRetryPolicy(policy),
	typesafe.WithLogger(slog.Default()),
)

The same options can be applied to a single call without changing the client:

resp, err := client.SystemOne(ctx, req, typesafe.WithModel("jev-1.13.0"), typesafe.WithMaxRetries(0))

The model used for a request is, in order of precedence: SystemOneRequest.Model, a per-call WithModel, the client's WithModel, TYPESAFE_DEFAULT_MODEL, then jev-latest.

Models

ModelJevLatest (jev-latest) follows the most recent stable release and ModelJevPreview (jev-preview) the most recent release of any kind. Pin a versioned ID such as jev-1.13.0 if you have tuned thresholds against a specific version; the response's Model field always reports the versioned ID that answered. client.ListModels(ctx) returns the available models.

Timeouts, retries, and errors

WithTimeout bounds a single HTTP attempt (default 10s). Each retry gets a fresh timeout; use the context deadline to bound the whole call. When a retry's delay would run past that deadline, the client returns the last error (for example the 429 *APIError) right away instead of waiting only to fail with context.DeadlineExceeded.

DefaultRetryPolicy() retries up to 2 times with exponential backoff from 500ms doubling to 5s with 25% jitter. It retries HTTP 408, 429, and all 5xx responses (including TypeSafe's 529 Overloaded), connection errors, and timeouts, and it honors retry-after-ms and Retry-After headers of up to 60s. Retried attempts carry an X-Typesafe-Retry-Count header. Every field of RetryPolicy can be changed; WithMaxRetries(0) disables retries.

Errors fall into three groups:

var apiErr *typesafe.APIError
switch {
case errors.As(err, &apiErr):
	// Non-2xx response after retries. StatusCode, Message, Body, Header,
	// and RequestID are available; RetryAfter() reads the retry headers.
	log.Printf("%d %s (request id %s)", apiErr.StatusCode, apiErr.Message, apiErr.RequestID)
case errors.Is(err, typesafe.ErrInvalidRequest):
	// Rejected locally before sending: nil state, no questions, a choice
	// without options, a score with fewer than two levels, ...
case errors.Is(err, typesafe.ErrInvalidResponse):
	// The body could not be decoded.
default:
	// Transport failure or context cancellation; errors.Is works on the
	// underlying error, e.g. errors.Is(err, context.DeadlineExceeded).
}

APIError.Error() reads like typesafe: POST https://api.typesafe.ai/v1/systemone: 401 Unauthorized: Invalid API key (request id req_123). FastAPI-style validation errors are flattened to questions.q.criteria: Field required.

Testing your code

The client is an ordinary HTTP client, so point it at an httptest.Server:

server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json")
	io.WriteString(w, `{"model":"jev-1.13.0","answers":{"is_urgent":{"type":"noul","noul":0.9}},"usage":{"input_tokens":10,"output_tokens":1}}`)
}))
defer server.Close()

client, err := typesafe.NewClient(
	typesafe.WithAPIKey("test"),
	typesafe.WithBaseURL(server.URL),
	typesafe.WithMaxRetries(0),
)

Answer types are plain structs, so code that consumes them can be tested without a server:

answer := &typesafe.ScoreAnswer{Score: 1.6, Legend: []any{"Calm", "Frustrated", "Very angry"}}

Development

gofmt -l .
go vet ./...
go test -race ./...

License

MIT

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages