Skip to content

Wire formats

How the proxy (and the SDK's log_exchange) turns an upstream request and response into decision records (record/1).

1. Sources

Read on 2026-11-10 from each vendor's own documentation: - TypeSafe: API reference; the noul, choice and score pages; and models. - OpenAI: the Decisions guide and the create reference. - Ollaya: docs/api.md. - Ollama: api/systemone and capabilities/decision.

2. Two formats, chosen by the upstream

Format Upstreams Questions Answers
systemone TypeSafe Jev (api.typesafe.ai), Ollaya (:11435), Ollama 0.35.0+ (:11434) Object keyed by names the caller picks; instructions + criteria Object keyed by the same names
openai-decisions OpenAI (gpt-6-luna) Array of 1–400; name, instructions, choices or levels Array in question order

The upstream's format, set by the operator with the upstream (ADR 0011), decides the format, not the path. Ollaya serves the systemone format at /v1/decisions too. The proxy forwards the path it received, unchanged.

Models in the app's own process. A decision model run locally, for example a Hugging Face pipeline("decision"), takes the systemone request without model and returns the answers object bare: no answers wrapper and no model. No HTTP call is made, so the proxy can't see it; the SDK records it (instrument() or log_exchange(), sdk.md §2). The caller names the served model as <repo>@<revision>, for example autotrust/GEV-26B-Decide@<commit>: a pinned revision can't change underneath the app, and when someone updates the weights, the new revision shows up as a new served model (FR-39).

3. From a response to records

One record per answered question. In systemone, answers are matched to questions by name; in OpenAI's format, by position.

Type systemone answer OpenAI answer probabilities in the record
noul {"type":"noul","noul":p} {"type":"predicate","probability":p} [p]: the probability of yes
choice probabilities: object keyed by option name probabilities: array of {value, probability} One per option, in the question's option order. OpenAI values are matched by JSON equality, so true ≠ "true".
score probabilities: object keyed "0", "1", … probabilities: array of {label, probability, value} One per level, level 0 first. OpenAI's value is the index.
refusal — {"type":"refusal","name":…} null, with refusal: true

Probabilities: - trimset keeps the full distribution. It ignores the vendors' confidence (Ollaya and Ollama compute it with different formulas) and score's expected score. - Vendors round to 4 decimals: Ollama's documented example sums to 0.9999. A record is valid when its probabilities sum to 1 within 1e-3. They are stored as received and renormalised before any statistics or gating. - A record whose answer is missing an option or level is rejected and counted, never guessed.

Model names: - model_requested is the request's model; model_served is the response's model. - What model_served means depends on the runtime, and so does silent-upgrade detection (FR-39):

Runtime Response model Silent upgrades detectable
TypeSafe The versioned id, for example jev-1.14.0 behind jev-latest Yes. jev-latest moved from 1.13.0 to 1.14.0 on 2026-10-14.
Ollaya The canonical name, for example laya:en behind laya Only when the routed tag changes. Re-pulled weights under the same tag are invisible.
Ollama Echoes the requested name No
OpenAI Echoes gpt-6-luna No

Where detection is impossible, the docs tell users to pin a version, and drift checks (FR-38) are the safety net.

Cost: - cost_usd per request = (input_tokens − cached) × input + cached × cached_input + output_tokens × output, using the project's per-model prices (USD per million tokens). - OpenAI reports cached tokens in usage.input_tokens_details.cached_tokens. - The request's cost is split equally across its questions. A model without a price gives null.

Latency: latency_ms is the time from sending to the upstream until its full response, the same for every decision in the request. Ollaya's X-Ollaya-*-Duration headers are passed through, not recorded.

Not logged: - non-2xx responses, which go to the upstream error counters (FR-40); - GET /v1/models; - requests whose body can't be parsed (forwarded anyway, and counted).

4. Headers

Direction Rule
App → upstream Forward every end-to-end header unchanged, including Authorization (passthrough mode) and TypeSafe-Danger-Zone-*. Remove hop-by-hop headers (Connection, Keep-Alive, TE, Trailer, Transfer-Encoding, Upgrade, Proxy-*) and every X-Trimset-*. Rewrite Host.
Upstream → app Forward every end-to-end header. Add X-Trimset-Decision-Id: one id per question, comma-separated, in question order (systemone: the order of the questions keys). A question trimset can't read still gets an id, which never gets a record (ADR 0015).

Optional request headers read by trimset: - X-Trimset-Key: header mode (ADR 0011). - X-Trimset-External-Ref: applies to every question in the request. - X-Trimset-Tags: comma-separated key=value pairs, URL-encoded, at most 20.

5. Body and multipart

  • Body size: up to 8 MiB by default, the same as Ollaya (the project setting proxy_max_body_mb). This replaces the 1 MB in ADR 0011, which would have refused real image requests.
  • Multipart: for systemone images (multipart/form-data), the request part is parsed as JSON. Every part is forwarded unchanged. Image bytes are never stored or hashed.
  • Runtime-only fields: some runtimes accept fields others ignore (Ollama's keep_alive and images; OpenAI's prompt_cache_key and safety_identifier). They are forwarded unchanged and not interpreted.

6. Errors trimset itself returns on the proxy

Some errors originate in trimset: a bad trimset key, an unreachable upstream, a timeout. These use the upstream's error format, so the app's own client parses them:

Format Body Status codes
systemone {"detail": "<message>"} (TypeSafe's 400 shape) 401, 502, 504
openai-decisions {"error": {"message": "<message>", "type": "trimset_error", "code": "<code>", "param": null}} 401, 502, 504

OpenAI's Decisions pages don't document an error body. Confirmed on 2026-11-11 with a call carrying a made-up key: /v1/decisions answers 401 with {"error": {"message", "type": "invalid_request_error", "code": "invalid_api_key", "param": null}, "status": 401}, served as text/plain. trimset's own errors carry the same four error fields.

TypeSafe answers a made-up key with 401 {"detail": {"error_type": "authentication_error", "message": "..."}}. Its detail is a string (400), a list (422) or an object (401), so its clients already read all three; trimset's string form is the 400 one.

Upstream errors pass through unchanged. That includes Ollaya's {"error", "code"}, Ollama's {"error"}, TypeSafe's 422 {"detail": [...]}, and 429 with retry-after.

7. Limits worth knowing (from the vendors)

TypeSafe Ollaya Ollama (local) OpenAI
Questions per request No stated maximum (context-bound) 1–256 Not stated (cloud: 64) 1–400
Choice options 1–255 2–255 2–255 in its schema; 2–26 in its guide 2–255
Score levels 1–10 in its schema; "at least two" in its prose 2–10 2–10 2–10
Auth Bearer None unless OLLAYA_API_KEY None (cloud: Bearer) Bearer

trimset accepts whatever its upstream accepts. The compiler needs at least two levels or options to produce a policy.