8000 GitHub - kataras/jev: A Go client for the TypeSafe AI's System One API and its model, Jev. · GitHub
[go: up one dir, main page]

Skip to content
katarasPublic

About

A Go client for the TypeSafe AI's System One API and its model, Jev.

Topics

Resources

Stars

6 stars

Watchers

0 watching

Forks

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
< 7438 a title="integration_test.go" aria-label="integration_test.go, (File)" class="Link--primary" href="/kataras/jev/blob/main/integration_test.go" data-discover="true">integration_test.go
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

jev

build status report card godocs

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

Jev does not write text. You send a state (a string, or JSON such as a ticket, a log, or the current state of a program) and one or more named questions. The call returns a typed answer and a probability for every question. Adding questions to the same call does not add a round trip. Since Jev 1.14 the state can also hold a picture, and a box question says where in it one thing is.

This module is not affiliated with TypeSafe AI. The official clients are the JavaScript SDK and the Python SDK. The HTTP contract is the System One API.

Install

Go 1.27 or newer. The only dependency is golang.org/x/time/rate.

go get github.com/kataras/jev

Please star this open source project to attract more developers so that together we can improve it even more!

Questions

Question You provide You get back
Noul A yes/no question Probability of yes, from 0 to 1
Choice Named options, up to 255 The picked option, a probability for each option, and a confidence
Score An ordered list of levels, up to 10 A position on that list (it can fall between levels), a probability for each level, and a confidence
Box What to find, and in which image One bounding box, as fractions of the image, and a confidence

Question names are yours. They come back as the answer keys. They are not part of the prompt.

jev-latest is the default model, and that alias moves. Jev 1.14.0 came out on 2026-10-14; jev-latest still points at jev-1.13.0 and moves about a week after a release, while jev-preview already points at jev-1.14.0. The constants jev.ModelLatest, jev.ModelPreview, jev.ModelJev114, and jev.ModelJev113 hold those names. Set WithModel(jev.ModelJev114) when a later run has to hit the same model. Box questions need Jev 1.14, so pass WithModel(jev.ModelPreview) for them until jev-latest catches up. The model field of every response is the versioned id that answered. See aliases.

Usage

client, err := jev.New() // reads TYPESAFE_API_KEY
resp, err := client.SystemOne(ctx, jev.Request{
    State: "I was charged twice. Please fix this ASAP.",
    Questions: jev.Questions{
        "billing": jev.Noul{Instructions: "Is this ticket about billing?"},
        "team": jev.Choice{
            Instructions: "Which team should handle this?",
            Criteria: map[string]any{
                "billing":   "Payments, invoices, refunds",
                "technical": nil, // null means the label has no description
            },
        },
        "urgency": jev.Score{
            Instructions: "How urgent is this ticket?",
            Criteria:     []string{"can wait", "this week", "today"},
        },
    },
})

billing, _ := resp.Noul("billing")
team, _ := resp.Choice("team")
urgency, _ := resp.Score("urgency")

SystemOneAs decodes the same JSON into a struct. It is a generic method, which is why this module requires Go 1.27. JSON names are case-sensitive.

type ticket struct {
    Model string `json:"model"`
    Answers struct {
        Billing struct {
            Noul float64 `json:"noul"`
        } `json:"billing"`
    } `json:"answers"`
}

out, err := client.SystemOneAs[ticket](ctx, req)

A full program lives in examples/quickstart.

One question

Noul, Choice, Score, and Box each send one question named answer through SystemOne. Classify is Choice with the question and the options as separate arguments. Rate is Score with the levels as a list. Box takes the instructions alone or a jev.Box. The As methods call SystemOneAs. In that JSON the question name is answer.

yes, err := client.Noul(ctx, ticket, "Is this about billing?")

team, err := client.Classify(ctx, ticket, "Which team?", map[string]any{
    "billing":   "Payments, invoices, refunds",
    "technical": nil,
})

urgency, err := client.Rate(ctx, ticket, "How urgent?", []string{"can wait", "this week", "today"})

box, err := client.Box(ctx, map[string]any{"photo": photo}, "the coffee mug in `photo`", jev.WithModel(jev.ModelPreview))

Several questions about the same state still belong in one SystemOne call.

List the models on the account with ListModels. The list includes the jev-latest and jev-preview aliases.

models, err := client.ListModels(ctx)

Images

Jev 1.13 and 1.14 both look at PNG and JPEG images. Wrap the file with ImageFile, ImageBytes, or ImageReader and put the *Image anywhere a string can go: a value in the state, a question's instructions, a Choice option, a struct field, or a slice element. The model reads a reference string in its place, such as img-3f9a1c2b7d4e6f80-kitchen, and the bytes travel with the request. Box finds one thing in an image. It needs Jev 1.14, so pass WithModel(jev.ModelPreview) until jev-latest moves there.

photo, err := jev.ImageFile("kitchen.jpg")
client, err := jev.New(jev.WithModel(jev.ModelPreview))
resp, err := client.SystemOne(ctx, jev.Request{
    State: map[string]any{"photo": photo},
    Questions: jev.Questions{
        "mug_visible": jev.Noul{Instructions: "Is there a coffee mug in `photo`?"},
        "mug":         jev.Box{Instructions: "the coffee mug in `photo`"},
    },
})

mug, ok := resp.Box("mug")
fmt.Println(mug.Confidence, mug.Rect(width, height))

BoxAnswer holds Top, Bottom, Left, and Right as fractions from 0 to 1, measured from the top-left corner, and a Confidence. Rect(width, height) turns the edges into a stdlib image.Rectangle for cropping. A box always comes back, even when the thing is not in the picture, so ask a Noul in the same call when it might be missing. Box has no criteria field.

To mention an image inside a sentence, build the text with TextOf. The image part becomes its reference:

jev.Noul{Instructions: jev.TextOf("Is the dog in `photo` the same dog as in this picture: ", poster, "?")}

The client reads the file signature and accepts PNG and JPEG only; convert GIF, WebP, and HEIC first. One image may be 16 MiB and the whole request 32 MiB, and both are checked before sending. The API also asks for 280 to 1568 pixels per side, which the client does not check. The same image used twice is uploaded once.

The header TypeSafe-Danger-Zone-Images: true is added whenever a request carries an image or a box question; without it the API answers 400. Three limits: a nil *Image encodes as JSON null, an image inside pre-encoded jsontext.Value state is not seen, and an image in Request.Extra is rejected. The rate limiter does not count image bytes as tokens. It reserves a flat 1,600 tokens per image before the call and settles with the real usage after it.

A program that draws a rectangle and asks where it is lives in examples/box.

Configuration

Explicit options win over environment variables. A blank environment value is ignored.

Option Environment Default
WithAPIKey TYPESAFE_API_KEY required
WithBaseURL TYPESAFE_BASE_URL https://api.typesafe.ai
WithModel TYPESAFE_DEFAULT_MODEL jev-latest
WithLongContext off
WithTimeout 10s per attempt
WithRetry 2 retries, 500ms to 5s backoff
WithRateLimit 1,200 requests/min, 250,000 tokens/s
WithRateLimiter the limiter WithRateLimit builds
WithLogger TYPESAFE_LOG_LEVEL no logging
WithHTTPClient a client that does not follow redirects
WithHeader

The same options can be passed to one call. They override the client for that call only. WithRateLimit is the exception: it belongs to New, because a limiter built for one call starts with a full budget and paces nothing. For one call, pass WithRateLimiter with a limiter you own, or nil to skip pacing.

WithRetry replaces the whole policy. Copy DefaultRetryPolicy() and edit the copy. A zero RetryPolicy retries nothing, because its status list is empty.

WithLongContext(true) raises the input limit from 64k tokens per request and 32k per question to 262k, by sending TypeSafe-Danger-Zone-Long-Context: true. The price per token is the same, but the API documents long context as slower and less accurate, so turn it on for the calls that need it. Jev 1.13 and 1.14 both support it. See context length.

yes, err := client.Noul(ctx, longContract, "Does the `state` include a termination clause?", jev.WithLongContext(true))

Rate limits

The API limits each account to 1,200 requests per minute and 250,000 input tokens per second. A request over either limit is a 429. Retrying after a 429 works, but it costs a round trip and a backoff wait each time. The client stays under the limit instead: every attempt, retries included, first waits on a token bucket built with golang.org/x/time/rate, the same way kataras/httpclient paces its requests.

Requests are spread across the minute, with a burst of one second's worth (20 at the default). Tokens are estimated from the body size before the call and settled with usage.input_tokens after it, so the estimate corrects itself. When the server does answer 429 or 529 with Retry-After, every caller on that limiter pauses until then, not only the call that saw it. The pause is capped by RetryPolicy.MaxRetryAfter, 60s by default.

The limits are per account, so clients on the same key should share one limiter:

shared := jev.NewLimiter(jev.DefaultRateLimit())
latest, err := jev.New(jev.WithRateLimiter(shared))
pinned, err := jev.New(jev.WithModel(jev.ModelJev114), jev.WithRateLimiter(shared))
// or join an existing client's budget:
third, err := jev.New(jev.WithRateLimiter(latest.Limiter()))

TypeSafe adjusts the limits without notice, and enterprise plans get higher ones. Set your own figures, or divide the account's budget between processes:

client, err := jev.New(jev.WithRateLimit(jev.RateLimit{
    RequestsPerMinute: 300,
    TokensPerSecond:   60_000,
}))

A zero field disables that axis. jev.WithRateLimit(jev.RateLimit{}) or jev.WithRateLimiter(nil) turns pacing off. A plain *rate.Limiter also satisfies jev.Limiter; it paces requests only.

The limiter waits are not retries. A wait that outlives your context returns that context's error, and no request is sent.

Errors

if errors.Is(err, context.Canceled) {
    // the caller cancelled; this is not retried
}
if errors.Is(err, jev.ErrUnauthorized) {
    // HTTP 401
}
if errors.Is(err, jev.ErrRateLimited) {
    // HTTP 429, after retries
}
if errors.Is(err, jev.ErrOverloaded) {
    // HTTP 529; this also matches jev.ErrServer
}
var api *jev.APIError
if errors.As(err, &api) {
    fmt.Println(api.Status, api.RequestID, api.Message)
}
Sentinel When
ErrConfig Missing key, bad URL, bad option
ErrRequest Rejected locally, before HTTP
ErrResponse 2xx body that does not match the contract
ErrTimeout The per-attempt timeout fired
ErrConnection DNS, TLS, or a dropped connection
ErrBadRequest HTTP 400
ErrUnauthorized HTTP 401
ErrForbidden HTTP 403
ErrNotFound HTTP 404
ErrUnprocessable HTTP 422
ErrRateLimited HTTP 429
ErrOverloaded HTTP 529
ErrServer HTTP 5xx, including 529

A deadline on the context you passed is returned as that context error. It does not match ErrTimeout. ErrTimeout means this client's own per-attempt limit.

Retries

The default policy matches the official JavaScript SDK. It retries HTTP 408, 429, and 500 through 599, including 529. The first wait is 500ms, doubled up to 5s, with up to 25% of the wait subtracted at random. Retry-After-Ms is preferred over Retry-After. A server delay longer than 60s falls back to that backoff. Retries are the second line; the rate limiter is the first.

There is no budget across attempts unless you set RetryPolicy.MaxElapsed. The context you pass always applies. Cancellation during a wait stops the call and is not retried.

POST is retried because a System One call only evaluates. Each attempt still spends input tokens.

Logging

TYPESAFE_LOG_LEVEL may be debug, info, warn, error, or off. Info logs one line per attempt. Debug also logs headers and bodies. Authorization, Cookie, and X-Api-Key are replaced with [redacted]. Bodies are logged as sent, and they contain your state. Image bytes are the exception: the JSON part of a multipart request is logged, the file parts are not.

Wire format

A few choices differ from other Go clients, on purpose:

  • JSON is encoded with encoding/json/v2, and <, >, and & are left as themselves. encoding/json rewrites them to \u003c, \u003e, and \u0026, so the text the model reads is not the text you passed.
  • Object keys are sorted. The same request produces the same bytes.
  • Request.Extra cannot replace state, model, questions, or assets.
  • A []byte state is sent as a JSON string. A plain byte slice would otherwise be base64.
  • Score criteria must be a JSON array. A map is rejected. That matches the official SDK change in v0.6.0 (15 Sep 2026).
  • Choice criteria must be a JSON object, with at most 255 options.
  • A request that carries an image goes out as multipart/form-data: first a part named request with the JSON, which gains a top-level assets map, then one file part per image, named by its reference, with Content-Type image/png or image/jpeg. Every other request is application/json.
  • Two headers are the client's job. TypeSafe-Danger-Zone-Images: true goes with any image or box question. TypeSafe-Danger-Zone-Long-Context: true goes with WithLongContext(true).

Live tests

TestLiveSystemOne, TestLiveBox, and TestLiveModels call the real API and spend tokens. TestLiveBox draws a red rectangle on a PNG and asks jev-preview where it is. go test skips all three when TYPESAFE_API_KEY is unset or blank, and always under go test -short. Every other test runs against an in-process server and needs no key.

For GitHub Actions, add a repository secret named TYPESAFE_API_KEY. The workflow runs the unit tests without the key, then runs -run '^TestLive' only when the secret is present. Pull requests from forks do not receive secrets, so there the live step prints a note and passes. Do not commit a key.

Agent skill

skills/jev/SKILL.md is an Agent Skill for this module. The signatures in it match the Go API above. Claude Code, Cursor, Codex, and Plexon all read that format.

Install it for every agent this machine already has, including Plexon (it adopts ~/.agents/skills):

npx skills add kataras/jev --skill jev -g -y

One project only: drop -g. The files land in .agents/skills/jev or .claude/skills/jev. Plexon reads both.

Pick agents by name when you do not want the full set:

npx skills add kataras/jev --skill jev -g -y -a claude-code -a cursor -a codex

Claude Code can install the same skill as a plugin. Inside a Claude Code session:

/plugin marketplace add kataras/jev
/plugin install jev@kataras-jev

License

MIT.

About

A Go client for the TypeSafe AI's System One API and its model, Jev.

Topics

Resources

Stars

6 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

0