Inspiration
A new kind of model API is showing up: native Decisions APIs. Chat completions decode tokens one at a time, and you hope the JSON parses. Native decision models such as OpenAI (/v1/decisions), Google Gemini (gemini-decision-1), TypeSafe (Jev) and OpenRouter (Alpha Decisions) work differently. They evaluate structured predicates, discrete choices and ordered rubrics in a single forward pass and return probabilities and confidence scores directly.
The trouble is that every provider shaped its API differently:
- OpenAI expects an ordered
questionsarray ofpredicate,choice(option values may mixbooleanandstring) andscore(levels indexed0..N-1), plus role-basedinputmessages (system,developer,user,assistant,input_json,input_image), authenticated withAuthorization: Bearer. - Google Gemini authenticates with
x-goog-api-key. It takes a keyedquestionsmap (noul,choice,score), limits choice option keys to 1–128 ASCII characters with string-only descriptions, requiresstring[]rubric criteria, and reports single-pass usage astotal_input_tokensandinput_tokens_by_modality. - OpenRouter and TypeSafe (Jev) expect a top-level
statestring and keyedquestionswithnoulandcriteria. Their token accounting is different again (billable_inputvs.provider_reported).
Some "unified gateways" quietly fall back to chat completions and make up the probabilities. We call these phantom probabilities. We built Decora to keep them out: a single-tenant Rust gateway that stitches all four native Decisions APIs together behind strict, verified contracts. It never uses chat-completion workarounds and never fabricates numbers.
What it does
Decora exposes four inbound endpoints, one per official dialect (plus GET /healthz):
| Endpoint | Dialect | Request shape |
|---|---|---|
POST /v1/decisions |
OpenAI | model / input / questions[] |
POST /v1beta/decisions |
Google Gemini | model / input / questions{} |
POST /v0/decisions |
TypeSafe (Jev) | model / state / questions{} |
POST /api/v1/decisions |
OpenRouter | model / state / questions{} |
Existing SDKs keep sending their native requests unchanged. Each endpoint strictly validates its own dialect: sending state to /v1/decisions returns 400 invalid_request. Any public model alias can then be routed to any compatible upstream.
Hub-and-spoke protocol translation (Dialect ↔ Canonical Decision IR ↔ Adapter)
- Same-dialect passthrough. When the inbound dialect matches the upstream (e.g. Gemini → Gemini), Decora replaces only the top-level
model. It keeps the original question keys, option keys andcriteria, still verifies every number in the response, and returns the native response unchanged. - Cross-dialect translation and fallback. When a request crosses dialects, or a same-dialect primary fails with 429/5xx/timeout and falls back to a different provider, Decora switches per attempt to translation through the canonical IR. It maps
predicate↔noul, ordered arrays ↔ collision-free keyed maps (q0..qN-1), and restores the original question order and nullable or duplicate names. - Multi-role message arrays and
input_jsonpayloads are encoded into a deterministic envelope (openai_decisions_text_v1) for upstreams that only accept flat text or state. Clients never flatten data or set conversion flags. - Typed choices survive the round trip:
truevs."true",falsevs."false", empty strings, and Unicode labels like"售后". Internally they travel as collision-proof keys (decora_choice_0..N-1).
- Same-dialect passthrough. When the inbound dialect matches the upstream (e.g. Gemini → Gemini), Decora replaces only the top-level
Mathematical verification of every probability Decora never re-normalizes, clamps or invents upstream numbers. It checks every response before the client sees it:
- Every probability $p_i$ and every
confidencemust be a finite number in $[0, 1]$. - For a
choicequestion over options $C = {c_0, \dots, c_{k-1}}$, the distribution must cover $C$ exactly once, and $$\left| \sum_{i=0}^{k-1} p(c_i) - 1 \right| \le \epsilon, \quad \epsilon = 10^{-4}$$ The returned choice $c^$ must have the highest probability: $p(c^) = \max_i p(c_i)$. - For a
scorequestion with $N$ ordered levels $0, \dots, N-1$, Decora checks both the sum and the expected score $S$: $$\left| \sum_{i=0}^{N-1} p_i - 1 \right| \le 10^{-4} \quad \text{and} \quad \left| S - \sum_{i=0}^{N-1} i \cdot p_i \right| \le 10^{-4}$$ Any violation returns502 upstream_protocol_errorand is not retried, so bad data never reaches your business logic. The same checks apply in passthrough mode, including thescore.legendand token-sum checks.
- Every probability $p_i$ and every
Honest usage accounting (
compatiblevs.strict) Decora doesn't fill missing cache or reasoning token counts with0. In the defaultcompatiblepolicy, cross-dialect responses keep the upstream counts that can be verified, omit unknown breakdowns, and record where each number came from inusage.decora(billable_inputfor TypeSafe,total_input_tokens/input_tokens_by_modalityfor Gemini,provider_reported/provider_costfor OpenRouter). Same-dialect passthrough returns the nativeusageuntouched. If a deployment needs exact usage schemas, an operator can setusage_policy: strict. Cross-dialect candidates that can't satisfy it are then rejected before any network call is made.Capability-aware weighted routing with bounded fallback Candidates are filtered by real capabilities (
question_types,images,prompt_cache,safety_identifier,max_questions, usage policy) and ordered by priority, then weighted sampling without replacement. Retries are bounded by a totalmax_attempts, and429 Retry-Aftercounts against the time budget.
How we built it (with Kiro)
Decora is written in Rust (2024 edition) with axum, tokio, reqwest (rustls, redirects and proxy auto-discovery disabled), serde, serde-saphyr, and subtle for constant-time key comparison.
Kiro shaped how the project was built:
- Steering as the single source of truth (
.kiro/steering/). Five steering documents (product.md,protocol-contract.md,security.md,structure.md,tech.md) hold the architectural invariants.protocol-contract.mdusesinclusion: fileMatchon{src/adapter.rs,src/dialect.rs,src/protocol.rs,src/gateway.rs,tests/offline.rs,docs/PROTOCOL.md}. Whenever protocol code is open in the Kiro IDE, Kiro automatically loads the dialect schemas, the passthrough rules, the probability tolerance ($\epsilon = 10^{-4}$) and the security boundaries. Those rules didn't have to be repeated in every prompt, and they didn't drift between sessions. - Spec-driven development (
.kiro/specs/). Each of the six milestones has a Kirorequirements.md→design.md→tasks.mdspec:decisions-gateway,native-upstreams-and-usage,unified-contract-and-lowering,gemini-backend,multi-dialect-endpointsanddialect-passthrough. Fromgemini-backendonward we wrote the spec first, then had Kiro implement it task by task, checking offtasks.mdas it went. (.kiro/was accidentally git-ignored early in the project and re-tracked on Nov 13, which is why the folder's git history starts there.) The acceptance criteria are written as testable behaviour, such as "a tamperedscore.legendreturns 502 and is not retried", and we turned them into assertions in the offline integration suite. Compared with vibe coding, the specs made large refactors safe, because the tests pinned the contract while the code underneath changed. The move from one endpoint to the four-dialect hub-and-spoke design was one such refactor. - Agent hooks (
.kiro/hooks/).rust-fmt-check-on-save.jsonrunscargo fmt --checkevery time a.rsfile is saved.verify-reminder-on-stop.jsonis an agent hook that fires when Kiro finishes a turn. It requires the full pipeline (fmt --check,check --locked,test --locked,clippy -D warnings) to have actually run, and README,docs/PROTOCOL.md,config.example.yamlandHANDOFF.mdto be in sync. It also forbids claiming a check passed if it wasn't run. - The same
.kiro/folder works in Kiro CLI. Kiro CLI (V3) reads the same folder:/configlists the steering files (withprotocol-contractasfileMatch) and both hooks. Asked about the probability tolerance, Kiro readsprotocol-contract.mdand answers0.0001with the file cited. The Stop hook fires in CLI sessions too: after answering, Kiro adds which checks it did not run and refuses to claim they passed. - Handoff across agents.
HANDOFF.md, together with each spec'stasks.mdchecklist, let Kiro sessions and subagents pick up complex protocol refactors from where the last one stopped without breaking the security or contract invariants.
Challenges we ran into
- Type collisions and ASCII key limits across providers. OpenAI lets choice options mix booleans (
true,false) with arbitrary strings ("true","false","","售后"). OpenRouter and TypeSafe representcriteriaas a JSON object, whose keys can only be strings, and Gemini limits option keys to 1–128 ASCII characters. Our fix was deterministic internal indexing (decora_choice_0..N-1) plus structured criteria serialization in the adapter. The exact typed value comes back on the way out without collisions, even when a client literally passes"decora_choice_0"as an option value. - Preventing false multimodal fallbacks.
{"type": "input_json", "data": {"image_url": "https://..."}}is ordinary JSON data, not an image. When a request contains a realinput_imagepart, a text-only adapter must never turn the URL into a string and pretend it looked at the image. We inspect the request structure before lowering it, so real images are filtered accurately while JSON fields with the same name pass through untouched. - Security and retry hazards. If a response body stream drops after a
200 OKheader, retrying can bill the request twice. Ordinary HTTP client debug logs or error echoes can also leak API keys and user prompts. So we use constant-time inbound authentication (one credential per request,Bearerorx-goog-api-key), outbound clients that never follow redirects, a fixed error envelope ({"error":{"message","type","param":null,"code"}}), and a strict no-retry rule once upstream response headers have arrived.
Accomplishments that we're proud of
- Four dialects in, four native engines out. The same OpenAI-dialect request (system/user roles, nested
input_json, mixed boolean and string choices) gets back the sameanswerscontract whether it's served by the OpenAI, Gemini, OpenRouter or TypeSafe adapter: the same question order and names, the same typed choice values, and the same answer fields. Only the models' own probabilities differ, and every one of them is verified. Gemini, TypeSafe and OpenRouter clients can also call Decora in their own dialects, with passthrough and automatic cross-dialect fallback. - A live end-to-end run. The demo script (
demo/run.sh) sends the same OpenAI-dialect support-ticket request through Decora to realgpt-6-luna(OpenAI) and realgemini-decision-1(Gemini): two different models, two different opinions, one answer shape, every number verified. The Gemini → Gemini passthrough also ran live with native keys preserved. TypeSafe and OpenRouter, the fallback, the phantom (Σp = 0.93 →502) and the JSON-as-data cases run against labelled loopback mocks, and the video labels every rowreal APIorloopback mock. - An offline verification suite that costs nothing to run. 43 tests (42 end-to-end integration tests against loopback TCP servers that emulate each provider's official schema, plus 1 unit test) cover translation, passthrough, fallback, retry budgets, tampered probabilities and usage policies. The suite finishes in under a second. The full gate passes:
cargo fmt --check,cargo check --locked,cargo test --locked,cargo clippy --all-targets -- -D warnings, andcargo auditacross 182 locked crates with 0 vulnerabilities. - Being explicit about limits. The docs separate what Decora guarantees (dialect validation, the answer contract, the probability checks) from where upstream semantics really differ (
usage.decoraprovenance, role hierarchy vs. text encoding). They also state clearly that compatible usage is not a perfect copy of every native usage schema.
What we learned
- Native decision models are fast, cheap and inconsistent. Single-pass models like
gemini-decision-1and Jev produce no output tokens at all (output_tokens = 0), but their REST contracts differ in small, sharp ways: auth headers, rubric formats, token accounting. - A canonical IR keeps the work linear. Adding the Gemini/TypeSafe/OpenRouter inbound dialects meant writing one dialect module each, not $M \times N$ pairwise converters. Passthrough fell out naturally as the case where the dialect and the adapter are the same.
- Spec-first collaboration between agents works. With the rules in
.kiro/steering/and progress tracked in.kiro/specs/<feature>/tasks.mdandHANDOFF.md, Kiro and its subagents could work together through large protocol refactors without drifting from the security or contract invariants.
What's next for Decora
- Live coverage for every provider. OpenAI and Gemini have been exercised live through Decora (see below); TypeSafe and OpenRouter are so far verified only against schema-accurate mocks. Next: live runs on those two, plus a billing cross-check on all four.
- Native image mapping for Gemini. Verified
data:image/...;base64payloads should map directly to Geminiinline_datawhen crossing dialects, with explicit MIME checks. - Privacy-preserving OpenTelemetry metrics. Export latency, fallback counts and error-code counters without ever logging payloads or prompts.
- Production hardening. A TLS reverse proxy, rate limiting and process supervision guide for public deployments.