Skip to content

Architecture

Independent implementation of the documented System One wire format — not affiliated with TypeSafe.

One call in, typed answers out. system_one(state=..., questions=...) validates a rubric, renders a prompt, sends a request per pass — one normally, two in two-step mode, more when a retry or a capability re-ask is needed — reads a distribution off the reply, and returns answers, reasoning, usage and a debug record. This page is the map: what each layer owns, what crosses each boundary, and which decision is made where. Internals is the implementation reference behind it — the generator mechanics, the accounting rules and the invariant list.

The layers

flowchart TD
    app["Your code: state, questions, options"] --> facade["Facade: validate, orchestrate, retry, account"]
    facade --> prompts["prompts.py: the message list"]
    facade --> methods["methods.py: request spec, readout"]
    facade --> transport["transport.py: per-surface request and result"]
    prompts --> shape["types.py: questions, answers, usage"]
    methods --> shape
    transport --> client["Your client object"]
    client --> wire["Provider HTTP API"]

The dependencies run one way: the facade drives, prompts.py and methods.py never reach a provider, and transport.py is the only module that touches the client; the full import graph is in Internals. No provider SDK is imported — the one runtime dependency is pydantic — so a local server, a hosted API and a hand-written stub are the same code path, and the same SystemOneResponse comes back from all three.

The client contract. jevper reads chat.completions.create, responses.create or messages.create off the object you pass, as attributes, and calls it with keyword arguments. The reply is read more loosely: any field is taken as an attribute or a mapping key, so a client may return the SDK's typed model or a plain dict. The blocking facade needs blocking methods and the async facade needs coroutines; handing one the other raises ClientCapabilityError naming the class to use. The complete example has a client that is not an SDK at all.

Two decisions, made once per call

A surface says where the request goes; a method says how the decision is elicited. They are independent, and each is resolved before the first request.

Surface — the API dialect on the wire: chat_completions, responses or messages. With api="auto" the client is asked what it exposes, in that order: responses.create, then chat.completions.create, then messages.create. A pinned api is checked against the same three and refused with ClientCapabilityError before anything is sent. grammar is Chat-only, so it pins the surface itself.

Method — how the model is made to answer: logprobs reads the distribution off the first answer token, grammar constrains that token to one label per option, structured asks for a JSON object carrying a probability per option, and discrete for one option per answer. With method="auto" the choice is made per (model, surface): the method this client has already learned works there, otherwise logprobs, and structured where the provider cannot supply logprobs at all. auto never resolves to anything but logprobs or structured — see Methods.

Method Chat Completions Responses Messages
logprobs logprobs + top_logprobs include with message.output_text.logprobs refused: that API has no logprobs
grammar GBNF grammar in extra_body refused: no such field refused
structured response_format JSON schema text.format JSON schema output_config.format in extra_body, plus the schema in the prompt
discrete as structured as structured as structured

A method that needs a field the surface does not have is refused locally, never silently downgraded: UnsupportedMethodError for a pinned method, one surface move for auto on the OpenAI surfaces, and the structured fallback for a provider that has no logprobs to give.

What one request contains

flowchart LR
    subgraph prefix["Shared by every call about this question"]
        system["System prompt"] --> shots["Few-shot turns"] --> block["Question block"]
    end
    block --> state["State turns"]
    state --> cue["Answer cue and correction turn, two_step and retries only"]

The state comes last because it is the only part that changes from call to call: a provider reuses a cached prefix up to the first differing token, and with the state earlier every new state reprocessed the whole prompt. The question block is a turn of its own, so a chat-list state stays verbatim, roles included. The two exceptions are deliberate — a state's own system/developer turns are folded into the leading system prompt, because the servers here reject a late instruction turn, and a state whose final turn is the assistant's puts the question block last again, so the request still ends on a question.

Each question's prompt is rendered once and assembled per pass, so a two-step call and a corrective retry share the same rendered prefix. prompt_cache_key is derived from that prefix — model, method, example turns and question block, never the state — unless the caller supplies one.

The life of a call

sequenceDiagram
    participant app as Your code
    participant jevper as jevper
    participant sdk as Your client
    participant api as Provider
    app->>jevper: system_one with state and questions
    jevper->>jevper: validate, render state, resolve method and surface
    par one sequence per question, concurrently
        jevper->>sdk: analysis request, two_step only
        sdk->>api: POST
        api-->>sdk: completion
        sdk-->>jevper: reply
        jevper->>sdk: answer request
        sdk->>api: POST
        api-->>sdk: completion with logprobs or JSON
        sdk-->>jevper: reply
    end
    jevper->>jevper: readout, normalize, finalize
    jevper-->>app: SystemOneResponse

Validation happens before the first request, so a bad rubric costs nothing. A generation the provider itself cut short, filtered or refused raises IncompleteAnswerError or ModelRefusalError rather than reading as a weak answer; an answer that arrived unreadable gets a correction turn, up to n_retry_malformed of them, and a request the provider refused gets the ladder below. The attempt history lands in debug["llm_attempts"] either way.

When a server refuses a field

jevper adds request fields for capability — the Responses include list, the schema carrier, the reasoning parameters, the cache key, the Messages thinking field. A refusal of a field is answered by dropping it one rung and asking again, in this order, because none of them is needed to answer:

flowchart TD
    refuse["4xx naming a field jevper added"] --> include["include"]
    include --> schema["Schema carrier"]
    schema --> reasoning["Reasoning parameters"]
    reasoning --> key["prompt_cache_key"]
    key --> thinking["thinking"]
    thinking --> done["Answered, limit remembered"]

The ladder is finite, so a server that refuses everything still ends in a ProviderError, and the same call is re-asked with the request rebuilt from the limit the transport was built with. A refusal of a value is not a field refusal and moves nothing: a budget_tokens below a server's minimum travels back as that server's own error, and a top_logprobs cap below the default is answered with a logprob-free method under auto — either way nothing is remembered, so the next call tries logprobs again.

What a client remembers

Three verdicts live on the client for its lifetime: which method works for a (model, surface), which fields that server accepts for it, and which surfaces answer 404 for a route the server does not have — that last one remembered per surface, since a route is the server's, not the model's. They only ever change what auto and api="auto" do; a pinned method sends byte-identical requests whatever the client has learned. A readable distribution retires a remembered absence, so a server that recovers is tried again, and the full record is in debug.

Concurrency and accounting

Questions are independent, so each runs its own request sequence: a thread pool for the blocking client, a semaphore for the async one, both bounded by max_concurrency and reused across calls. Each worker keeps its own usage record and the results are merged in question order, so the response is deterministic whatever order the requests finished in; the first failure in that same order is raised after every question has settled, and no partial answers is returned.

usage.n_calls counts successful provider results — a failed attempt or a fallback probe is recorded in debug["llm_attempts"], not billed as a call. Token counts sum across the call, and a count is None when any provider response omitted it, because an unreported count is not a zero.

Extending

A new method, a new surface or a new answer type each touch one module above the transport; the client, the prompts and the readouts stay as they are. Internals names the exact symbols to add.