Decision Models¶
A decision model answers typed questions about a single document in one forward pass and returns a probability distribution for each question without generating free-form text.
Continuum Router supports decision models in two ways:
- Client-facing API: Exposes decision inference directly at
POST /v1/decisions(aliasPOST /v1/systemone). - Internal consumers: Uses decision models across five router subsystems:
- Content screening (Guardrail provider)
- Request classification (Smart-routing classifier)
- Agent tool-call evaluation (Action safety)
- Answer-quality fallback (
fallback.quality_checkin Model Fallback) - Post-response quality telemetry (
usage.quality_judgment)
A decision model's reported confidence measures how concentrated the output distribution is, not its empirical accuracy on your workload. Always calibrate thresholds against labeled traffic from your own deployment before enabling enforcement. Every shipped default in Continuum Router is derived from a reproducible calibration run committed to this repository, and any consumer whose measured accuracy does not beat a constant-answer baseline defaults to monitor-only or shadow mode.
Supported models¶
The router implements the System One protocol across four provider profiles (generic, typesafe, foundry, and llama_cpp; see Provider profiles and experimental media) and includes built-in catalog metadata for both hosted services and self-hosted open-weights checkpoints:
| Model / family | Catalog IDs & checkpoints | Where it runs (protocol) | Capabilities & constraints |
|---|---|---|---|
| TypeSafe Jev 1.13 | jev-1.13.0 (jev-latest alias) | TypeSafe hosted (https://api.typesafe.ai/v1/systemone, protocol: typesafe) | decisions. $0.042 / 1M input tokens, free output; 64k request / 32k state+question context (262k with typesafe_long_context: true); opt-in experimental image assets (typesafe_images: true); no box. |
| TypeSafe Jev 1.14 | jev-1.14.0 (jev-preview alias) | TypeSafe hosted (https://api.typesafe.ai/v1/systemone, protocol: typesafe) | decisions. Same pricing and context limits as Jev 1.13, plus experimental image assets and box localization questions when typesafe_images: true. |
| Microsoft-Decision-1 | microsoft-decision-1 (addressed via your Foundry deployment name) | Azure AI Foundry (<resource>/providers/microsoft/v1/systemone, protocol: foundry) | decisions. Text/JSON input up to 32K input tokens; noul, choice, and score questions; authenticates with api-key (auth: api_key / auto) or an Entra Bearer token (auth: entra). |
| Laya | laya (convaiinnovations/laya, ggml-org/Laya-GGUF) | Self-hosted (laya-serve or llama-server, protocol: generic / llama_cpp) | decisions. Three encoder checkpoints in one bundle: english (ModernBERT-large, 512 tokens), multilingual (mmBERT-base, 1,024 tokens), typed-decisions. Stock laya-serve routes each request to english or multilingual by script detection. |
| Kev | kev-0.8b, kev-4b (jaredpalmer/kev-0.8b, jaredpalmer/kev-4b, ggml-org/Kev-4B-GGUF) | Self-hosted (kev.serve or llama-server, protocol: generic / llama_cpp) | decisions. LoRA adapter plus pointer head on a Qwen3.5 base with built-in temperature; runs on MLX on Apple silicon or GGUF on llama.cpp. |
| Julia-1 & lev | julia-1 (SupersonicLabs/Julia-1, ggml-org/Julia-1-GGUF), lev (interfaze-ai/lev, ggml-org/lev-GGUF) | Self-hosted (llama-server, protocol: llama_cpp / generic) | decisions (text). |
| OpenJev | openjev (openjev/openjev, ggml-org/OpenJev-GGUF) | Self-hosted (llama-server, protocol: llama_cpp) | decisions, image input. Enforces a 52-option choice limit (set model_family: openjev when served behind a custom deployment alias). |
| Clef & pplx-decider v1.2 | clef (Cloudflare/clef, ggml-org/Clef-GGUF), pplx-decider-v1.2 (perplexity-ai/pplx-decider-v1.2) | Self-hosted (llama-server, protocol: llama_cpp) | decisions, image input. Clef evaluates questions jointly rather than independently. |
| Bespoke Nimble 9B v3 | bespoke-nimble-9b-v3 (bespokelabs/Bespoke-Nimble-9B-v3, ggml-org/Bespoke-Nimble-9B-v3-GGUF) | Self-hosted (llama-server, protocol: llama_cpp / generic) | decisions (choice and noul). |
| vLLM SR Decision 2.0 | decision-2.0-{eos-0.8b,kai-0.6b,lux-9b,nox-4b,sol-2b,vega-27b} (vllm-sr/Decision-2.0-*) | Self-hosted (llama-server, protocol: llama_cpp / generic) | decisions (text). Uses normalized-entropy confidence. |
| vLLM SR d3 | d3, d3-{edge,lite,nano,mini,flash} (vllm-sr/d3*, ggml-org/vllm-d3*-GGUF) | Self-hosted (llama-server, protocol: llama_cpp) | decisions, image/video input. Accepts null state when model_family: vllm_d3 (or matched by model ID). |
| Liquid AI d1 | d1-3b (LiquidAI/d1-3B, LiquidAI/d1-3B-GGUF), d1-omni-600m (LiquidAI/d1-omni-600M, LiquidAI/d1-omni-600M-GGUF) | Self-hosted (llama-server, protocol: llama_cpp) | decisions; d1-3b supports image input (model_family: lfm2_d1), d1-omni-600m supports image and audio input (model_family: lfm2_d1_omni). Both accept null state. |
| IBM Granite Switch 4.1 | granite-switch-4.1-8b, granite-switch-4.1-30b (ibm-granite/granite-switch-4.1-*) | Self-hosted (llama-server, protocol: llama_cpp) | Dual chat and decisions capabilities, plus audio input. Eligible for both native decisions and explicitly configured text generation. |
Wire protocol¶
POST /v1/decisions (or POST /v1/systemone) accepts a model, a state (string, JSON object, JSON array, or null on supported llama_cpp families), and a questions map. Each question uses one of four types:
noul: Binary probability (0.0to1.0) that a statement is true, with optionalcriteria: {"true": ..., "false": ...}.choice: Categorical probability distribution over 1 to 255 named options (1 to 52 foropenjev), returning the argmaxchoice,probabilities, and optionalconfidence.score: Ordinal probability distribution over ordered levels (2to10levels ongeneric,foundry, andllama_cpp;1to10levels ontypesafe), returning the probability-weightedscoreindex, optionallegend,probabilities, and optionalconfidence.box: Normalized bounding box (box: {top, bottom, left, right}in0.0to1.0plus optionalconfidence), supported onprotocol: typesafewhentypesafe_images: trueandmodelisjev-1.14.0(jev-preview).
{
"model": "jev-latest",
"state": {"request": "Write a Rust function that parses a CSV file", "message_count": 1},
"questions": {
"domain": {
"type": "choice",
"instructions": "What domain does `request` belong to?",
"criteria": {"general": "...", "code": "...", "reasoning": "..."}
},
"complexity": {
"type": "score",
"instructions": "How hard is `request`?",
"criteria": ["trivial", "simple", "moderate", "complex", "expert"]
}
}
}
The response echoes the question IDs, the resolved model, and usage (input_tokens and output_tokens).
Internal consumers¶
Continuum Router uses decision models across five internal consumers:
| Consumer | Configuration | Questions asked | How the answer is used | Default mode |
|---|---|---|---|---|
| Guardrail provider | guardrails.providers[].type: systemone | One noul per router category (violence, hate_speech, sexual_content, self_harm, harassment, dangerous, jailbreak, pii, profanity) plus a 4-level severity score | Blocks when p >= category_thresholds[c]; flags when p >= review_threshold (upgraded to block at severity >= 2) | mode: monitor |
| Smart-routing classifier | smart_routing.classifier.method: system_one / hybrid_system_one, or shadow: true with rule / llm / hybrid | complexity (score over 5 levels) and domain (choice over 6 domains) | Uses the decision result when min(confidence) >= min_confidence; otherwise falls back to the rule classifier (or LLM classifier under hybrid_system_one) | method: rule (shadow opt-in) |
| Action safety | safeguards.action_safety.evaluator: systemone | action (choice: approve / confirm / block), irreversible (noul), and injected_instruction (noul) | Deny when probabilities.block or injected_instruction >= deny_threshold; Allow when action == approve, confidence >= min_confidence, and probabilities.approve >= allow_threshold; otherwise Indeterminate | mode: advisory |
| Answer-quality fallback | fallback.quality_check | answered, refusal, and truncated (noul) over the last user message and a completed non-streaming chat reply | Advances to the next model in the fallback chain when 1 - answered, refusal, or truncated reaches threshold | Disabled (mode: shadow when enabled) |
| Usage quality judgment | usage.quality_judgment | answered and refusal (noul), plus language_match (when the request contains letters) and grounded (when include_tool_results: true and the turn includes tool results) | Attaches basis-point probabilities and judge_model to the control-plane usage record for Hub analytics; never alters the client response | Disabled |
See also: Guardrails, System One Classifier, Claude Code Integration, and Model Fallback.
Usage accounting for decision calls¶
Every decision call that reaches an upstream server (including retries) is recorded as a usage event linked to the parent request with a consumer-specific purpose:
guardrailfor the System One guardrail providerclassifierfor the smart-routing classifieraction_safetyfor the action-safety evaluatorquality_judgmentfor bothusage.quality_judgmentandfallback.quality_checkclientfor direct client requests toPOST /v1/decisions(see "Router-initiated calls" in Architecture)
The target setting count_toward_budgets controls only the counted label on smart_routing_system_one_classifier_input_tokens_total.
Usage quality judgment (usage.quality_judgment)¶
usage.quality_judgment evaluates served chat replies asynchronously and attaches the resulting probabilities (in basis points, alongside the resolved judge_model) to the request's control-plane usage record. This allows Continuum Hub to compare response quality across models, API keys, and experiment arms without adding latency to the client path.
Asynchronous execution and late vs. skipped outcomes¶
- Non-blocking background task: Evaluation runs on a background task after the reply is served and never blocks usage accounting or batch uploads.
latejudgments: If a judgment finishes after its usage record's upload batch has already been sealed, the judgment is dropped and counted aslate. Because slower judgments are more likely to miss the batch cutoff, a highlaterate skews attached scores toward faster requests. To reducelatedrops, increasecontrol_plane.usage_push_interval_secsor use a low-latency self-hosted decision model.- Streaming sampling: Streamed replies are sampled immediately after the input guardrail and evaluated as soon as the stream-end usage record is emitted. Only the head and tail of the client-visible text (bounded by
max_reply_bytes, excluding reasoning tokens) are retained in memory. - Unified
streamlabel: Every outcome inusage_quality_judgments_totalcarriesstream="true"orstream="false".
Route-by-route extraction and skip rules¶
| Route | Inspected user turn | Inspected assistant reply | Skipped or excluded cases |
|---|---|---|---|
/v1/chat/completions (including responses_only bridge) | Last user message (max_request_bytes head+tail cap). Tool results included only when include_tool_results: true. | Buffered reply text, or head+tail of streamed reply text (max_reply_bytes cap). Reasoning text is excluded. | Input-blocked requests (never sampled); replies blocked by output guardrail; streamed replies masked or truncated by output guardrail; mid-stream fallback hops; stream errors/timeouts; client disconnects before stream end; disaggregated_serving or Dynamo shim; streamed native Anthropic/Gemini/Bedrock or Unix-socket paths; streamed response-cache replays. (Buffered replies masked by the output guardrail are judged on the masked text the client received.) |
/anthropic/v1/messages (all backend paths: native HTTP/Unix, Bedrock mantle/runtime/converse, OpenAI/Gemini conversion, responses_only) | Last role: user message that contains no tool_result and has non-blank text after stripping <system-reminder> blocks. Top-level system and role: system messages are never read. tool_result content is sent only when include_tool_results: true. | Concatenated text blocks delivered to the client (thinking blocks are never included). stop_reason: max_tokens is judged normally. | Replies containing tool_use or server_tool_use blocks; stop_reason: pause_turn; streamed error events or early client disconnects; web_search_20250305 server-tool emulation; response-cache hits. |
/v1/responses (OpenAI/Azure passthrough, responses_only, and Chat/Anthropic/Gemini conversions) | Last user message (or bare text item) in the request's own input with non-blank text after stripping harness blocks (<system-reminder>, <user_instructions>, <environment_context>, # AGENTS.md instructions). previous_response_id continuations use only the current request's input. instructions, system, developer, and reasoning items are never read; function_call_output is sent only when include_tool_results: true. | Concatenated output_text parts of message items (or refusal parts when no text is present). Streamed replies are read from the terminal response.completed or response.incomplete event. status: incomplete (e.g., max_output_tokens) is judged normally. | input containing only function_call_output items; item_reference appearing after the last user turn; replies containing function_call, custom_tool_call, computer_call, or local_shell_call items; status: failed or cancelled; response-cache hits; /v1/responses/compact. |
| Other routes | Not applicable | Not applicable | /v1/completions is never judged. |
See the usage section in the Complete Configuration Example for configuration syntax.
Call sharing with fallback.quality_check¶
For non-streaming /v1/chat/completions requests, fallback.quality_check and usage.quality_judgment can share a single upstream decision call when all of the following hold:
- Both consumers are enabled and reference the same
targetconfiguration. - Both configure identical
max_request_bytesandmax_reply_byteslimits. - The request does not send tool results to
usage.quality_judgment(include_tool_resultsisfalseor the turn has no tool results).
When a call is shared:
- The quality check includes
language_match(when applicable) alongsideanswered,refusal, andtruncatedin the same forward pass. A missing or malformedlanguage_matchanswer never fails the fallback check. - If the initial reply passes the quality check,
usage.quality_judgmentreuses the returned probabilities immediately without consuming amax_in_flightslot or risking alatedrop. - If the shared call omits an answer for a required usage question,
usage.quality_judgmentissues a follow-up call for only the missing question(s) and merges the results (dropping the judgment asinvalidif the follow-up resolves a differentjudge_model). - If
fallback.quality_checktriggers a fallback hop to another model,usage.quality_judgmentrecordsskippedand makes no extra call. - If
fallback.quality_checkdid not run (for example, no fallback hop was available or its budget was exhausted),usage.quality_judgmentperforms its own normal background call. - Shared calls increment
systemone_requests_total{consumer="fallback_quality"}, whileusage_quality_judgments_total{outcome}still records one outcome per sampled request.
Serving decisions to clients¶
Clients can call POST /v1/decisions (alias POST /v1/systemone) directly under the router's standard API key authentication, scopes, rate limits, budgets, and fallback chains:
- Native routing: Models listed under
decisions.targets[]are forwarded directly to the configured System One server. - Chat-model emulation: Any model not listed in
decisions.targets[]is emulated on a chat backend using a forcedemit_decisiontool call. Emulated responses return one-hot probabilities, omitconfidence, and setemulated: trueso callers can distinguish emulated answers from native probability distributions.
The public endpoint acts purely as a proxy or emulator and does not apply router thresholds. See API Reference for request and response schemas.
Egress and operational caveats¶
- Data egress: Every decision call sends a state object constructed from the request:
- Guardrail: Last user turn and a bounded conversation history (
history_turns,max_turn_bytes). - Smart-routing classifier: Last user message and a 200-character system prompt excerpt. On
/v1/chat/completions, the classifier state (including shadow mode) is built after the input guardrail stage, so blocked requests are never sent and PII-masked prompts are sent in their masked form. - Action safety: Proposed tool name, full arguments, and the user's latest instruction.
- Answer-quality fallback & Usage quality judgment: Head-and-tail-capped user message and assistant reply (plus current-turn tool results for
usage.quality_judgmentonly wheninclude_tool_results: true). - Deployments that prohibit sending prompt or reply excerpts to third parties should point all consumers at self-hosted endpoints rather than hosted services such as TypeSafe Jev.
- Guardrail: Last user turn and a bounded conversation history (
- Adversarial robustness: Decision models are fast screening and telemetry signals, not standalone adversarial defenses. On the reference guardrail corpus, uncalibrated Laya checkpoints score at or below the constant-answer baseline on most categories at a
0.5cutoff and frequently misclassify non-Latin text with high confidence. Use decision models inmonitormode or alongside dedicated safety models (such as Llama Guard or Qwen3Guard) onenforcepaths. - Script sensitivity: The Laya
englishcheckpoint does not support non-Latin scripts. Stocklaya-serveroutes non-Latin inputs to themultilingualcheckpoint, which has lower accuracy on English and Korean routing tasks. Always evaluate English, Korean (ko), andnon_latinslices separately before enabling enforcement.
When to use a decision model¶
| Question shape | Recommended? | Rationale |
|---|---|---|
Closed criterion with direct evidence in state (e.g., "Does this tool call write outside the working directory?", "Is this request about code?") | Yes | Matches the model's training objective and can be calibrated against ground-truth labels. |
| Fixed-category screening for observability and review queues | Yes (monitor mode) | Output probabilities allow you to define separate flag and block bands after calibrating on your own traffic. |
| Open-ended rubric (e.g., "Is this response helpful or well-written?") | No | Lacks a closed criterion; use an LLM judge or human evaluation. |
| Cross-step causal attribution (e.g., "Did an earlier tool output cause this action?") | No | The decision state captures a single step; action safety evaluates only the proposed tool call against the user's request. |
| Predicting downstream model failure (e.g., "Will model X fail on this prompt?") | No | Decision models have no signal for another model's capability boundaries; the classifier estimates prompt complexity and domain instead. |
Provider profiles and experimental media¶
Each System One target block selects an explicit wire profile via protocol (generic, typesafe, foundry, or llama_cpp), independent of the model alias:
generic(default): Text-only System One contract.typesafe: Enables TypeSafe's structured criteria schemas and opt-in feature headers (typesafe_images,typesafe_long_context).foundry: Uses Azure AI Foundry's provider path (/providers/microsoft/v1/systemone) and deployment naming.llama_cpp: Enablesllama-servermultimodal data URLs and checkpoint-family constraints (model_family).
decisions:
enabled: true
targets:
- name: jev
protocol: typesafe
endpoint: https://api.typesafe.ai/v1/systemone
api_key_env: TYPESAFE_API_KEY
model: jev-1.14.0
typesafe_images: false # opt in for JSON assets and Box
typesafe_long_context: false # independent opt-in
- name: microsoft-decision
protocol: foundry
endpoint: https://RESOURCE.services.ai.azure.com
auth: api_key # auto also selects api-key for Foundry
api_key_env: AZURE_FOUNDRY_API_KEY
model: support-decisions # deployment name, not a catalog alias
- name: local-decision
protocol: llama_cpp
endpoint: http://127.0.0.1:8080/v1/systemone
model: local-decision
model_family: generic # openjev enforces its 52-choice bound
Target configuration reference (SystemOneTargetConfig)¶
The same target schema is shared across decisions.targets[], guardrails.providers[].systemone, smart_routing.classifier.system_one, safeguards.action_safety.systemone, fallback.quality_check.target, and usage.quality_judgment.target:
| Field | Type | Default | Description |
|---|---|---|---|
endpoint | string | unset | Full HTTP(S) URL of the decision endpoint (mutually exclusive with backend). Must not contain embedded credentials, query parameters, or fragments. For generic, typesafe, and llama_cpp, the path must end in /v1/systemone unless allow_custom_endpoint_path: true. For foundry, the resource root, /providers/microsoft, /providers/microsoft/v1, and /providers/microsoft/v1/systemone all resolve to /providers/microsoft/v1/systemone. |
backend | string | unset | Name of a self-hosted backends[] entry (vllm, sglang, tensorrt-llm, ollama, lmstudio, llamacpp, mlxcel, generic, continuumrouter) configured with auth.type: api_key or basic and an http(s) URL. Mutually exclusive with endpoint and api_key_env; not supported with protocol: foundry. |
protocol | string | generic | Wire profile: generic, typesafe, foundry, or llama_cpp. |
auth | string | auto | Authentication scheme for inline endpoint targets: auto (api_key on foundry, bearer on others), bearer, api_key (api-key: <secret> header, foundry only), or entra (Authorization: Bearer <token> from api_key_env, foundry only). Must remain auto when backend is used. |
api_key_env | string | unset | Environment variable holding the secret or token for an inline endpoint (never the raw secret). Rejected when backend is set. |
model | string | jev-latest on typesafe / *.typesafe.ai, unset otherwise | Model or deployment ID sent in the request body. Required when protocol: foundry (must match the Foundry deployment name). |
model_family | string | generic | Explicit checkpoint family for protocol: llama_cpp when a custom deployment alias hides the model name: generic, openjev (52-option choice cap), lfm2_d1, lfm2_d1_omni, or vllm_d3 (the last three permit state: null). Rejected on non-llama_cpp protocols. |
typesafe_images | boolean | false | Requires protocol: typesafe. Enables top-level JSON assets and sends TypeSafe-Danger-Zone-Images: true. box questions also require model to be jev-1.14.0 or jev-preview. |
typesafe_long_context | boolean | false | Requires protocol: typesafe. Sends TypeSafe-Danger-Zone-Long-Context: true, raising TypeSafe's context limit from 64k/32k to 262k. |
max_questions | integer | 64 | Maximum questions per native decision call (1 to 256). |
max_state_bytes | integer | 16384 | Maximum serialized state size for internal consumers (256 to 1048576 bytes). |
max_media_bytes | integer | 4194304 | Maximum decoded inline media bytes per request (1 to 16777216 bytes, 4 MiB default; also bounded by the 2 MiB public JSON body limit on /v1/decisions). |
timeout_ms | integer | 1500 | Per-call wall-clock timeout in milliseconds, including the single bounded retry (100 to 60000). |
max_concurrent | integer | 32 | Maximum concurrent in-flight decision calls to this target (1 to 1024). |
count_toward_budgets | boolean | false | Controls the counted label on smart_routing_system_one_classifier_input_tokens_total. |
allow_custom_endpoint_path | boolean | false | Allows an inline endpoint whose path does not end in /v1/systemone on non-Foundry profiles. |
Protocol profile comparison¶
| Profile | Endpoint & transport rules | Auth headers | Question & schema rules | Media & special state |
|---|---|---|---|---|
generic | http(s) URL ending in /v1/systemone (or backend: reference) | Authorization: Bearer <key> (or backend's configured auth) | String instructions required; choice 1-255; score 2-10 levels | Text/JSON state only (null state and media fields rejected) |
typesafe | https:// required for typesafe.ai and subdomains; custom gateways keep their scheme | Authorization: Bearer <key>, plus opt-in TypeSafe-Danger-Zone-Images: true / TypeSafe-Danger-Zone-Long-Context: true | Preserves structured (string/object/array), null, and omitted instructions, noul criteria, choice descriptions, and score levels; score 1-10 levels; box on jev-1.14.0 / jev-preview | Top-level assets map of base64 images when typesafe_images: true |
foundry | Inline endpoint only (https:// required except loopback localhost, 127/8, ::1); resolves /providers/microsoft/v1/systemone | api-key: <key> (auth: api_key or auto) or Authorization: Bearer <token> (auth: entra) | Preserves structured/null/omitted question fields; choice 1-255; score 2-10 levels; requires explicit model deployment name | Text/JSON state only |
llama_cpp | http(s) URL ending in /v1/systemone (or backend: reference) | Authorization: Bearer <key> (or backend's configured auth) | Preserves structured/null/omitted question fields; choice 1-255 (1-52 for openjev); score 2-10 levels | Top-level files / images data-URL arrays or multimodal message state (image/audio data URLs); state: null allowed for lfm2_d1, lfm2_d1_omni, vllm_d3 |
Profile details and wire examples¶
Foundry authentication and transport¶
- Endpoint resolution: Resource roots,
/providers/microsoft,/providers/microsoft/v1, and/providers/microsoft/v1/systemoneall normalize to/providers/microsoft/v1/systemone. Foundry requires an inlineendpoint(backend:references are rejected). Remote endpoints must use HTTPS; HTTP is permitted only for loopback addresses (localhost,127.0.0.0/8,::1). - Token lifecycle:
auth: entrareads an externally provisioned Entra token fromapi_key_envat client build time and sends it as a Bearer token. It does not fetch or refresh tokens automatically; HTTP401/403responses are treated as terminal errors without retry.
Payload limits and TypeSafe box / assets¶
- Structural bounds: Individual string fields are capped at 8 KiB, JSON nesting at 64 levels, and
scorequestions at 10 levels. Public ingress accepts up to 2 MiB of uncompressed JSON (multipart and compressed request bodies are rejected with HTTP415). - TypeSafe images and
box: Settingtypesafe_images: trueenables top-levelassetsand sendsTypeSafe-Danger-Zone-Images: true.boxquestions additionally requirejev-1.14.0orjev-preview. Client-supplied headers are never forwarded to enable disabled features.
{
"model": "jev",
"state": {"photo": "asset-photo-1"},
"assets": {
"asset-photo-1": {
"type": "image",
"format": "png",
"data_base64": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII="
}
},
"questions": {
"location": {
"type": "box",
"instructions": "Locate the red rectangle in photo."
}
}
}
A box answer returns normalized coordinates (0.0 to 1.0) and optional confidence:
{
"model": "jev-1.14.0",
"answers": {
"location": {
"type": "box",
"box": {"top": 0.12, "bottom": 0.64, "left": 0.2, "right": 0.85},
"confidence": 0.91
}
},
"usage": {"input_tokens": 310, "output_tokens": 12}
}
llama.cpp multimodal inputs¶
protocol: llama_cpp accepts inline files or images data URLs (or multimodal message state with image/audio data URLs) up to max_media_bytes (default 4 MiB, max 16 MiB). External http(s) media URLs, local file paths, and native video transports are rejected.
{
"model": "local-decision",
"state": "Does this receipt show a total above $50?",
"images": [
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII="
],
"questions": {
"over_fifty": {"type": "noul", "instructions": "Is the receipt total above $50?"}
}
}
Model discovery, fallback, and accounting¶
- Model discovery (
/v1/models): The model fetcher normalizes both TypeSafe'sGET /v1/modelsformat andllama.cpp'sarchitecture.output_modalitiesarray. Models advertising"decisions"receive thedecisionscapability; models advertising both"text"and"decisions"(such as Granite Switch 4.1) receive bothchatanddecisions. - Fallback and emulation gates: Every fallback hop validates profile compatibility. Requests using media,
state: null, orboxquestions cannot fall back to incompatible profiles or text-only chat emulation. Decision-only catalog models require a configured native target and are never emulated via chat completions. - Token accounting: Both
input_tokensandoutput_tokensare recorded in usage statistics even when a provider prices output tokens at $0. Missingconfidencefields remain absent rather than synthesized.
Smart-routing classifier rollout¶
Smart routing uses the same target profiles under smart_routing.classifier.system_one. To evaluate a decision model safely before switching routing authority, keep method: rule (or llm / hybrid) and enable shadow: true:
# TypeSafe Jev (pin a specific calibrated version)
smart_routing:
classifier:
method: rule
system_one:
shadow: true
protocol: typesafe
endpoint: https://api.typesafe.ai/v1/systemone
api_key_env: TYPESAFE_API_KEY
model: jev-1.14.0
# Azure AI Foundry (model is your Foundry deployment name)
smart_routing:
classifier:
method: rule
system_one:
shadow: true
protocol: foundry
endpoint: https://RESOURCE.services.ai.azure.com
auth: api_key
api_key_env: AZURE_FOUNDRY_API_KEY
model: support-decisions
# Self-hosted llama.cpp
smart_routing:
classifier:
method: rule
system_one:
shadow: true
protocol: llama_cpp
endpoint: http://127.0.0.1:8080/v1/systemone
model: kev-local
Key rollout behaviors:
- Shadow evaluation: Shadow mode leaves routing decisions unchanged while recording agreement (
smart_routing_shadow_agreement_total{field, agree}), latency, and token usage (purpose: classifier). - Deterministic fallback: Under
method: system_one, any call that fails, times out, omitsconfidence, or returnsmin(confidence) < min_confidencefalls back to the rule classifier. Undermethod: hybrid_system_one, low-confidence answers fall back to the configured LLM classifier first, and then to the rule classifier. - Candidate pool filtering: Decision-only models (
decisionswithoutchat/text) are automatically excluded from generation candidate pools and reported underexcluded_incompatible_modelsonGET /admin/smart-routing/status. - Cache invalidation: Classifier cache keys incorporate the bounded prompt state, target profile, model name, question battery, and backend topology generation (
cache_target_identity), invalidating cached entries automatically on config or credential changes.
Calibrating a decision model on your own traffic¶
Because confidence is an uncalibrated concentration statistic, you should measure accuracy on your own labeled dataset before switching any consumer from monitor/shadow/advisory to enforcement.
1. Start a decision endpoint¶
By default, make calibrate-systemone launches the self-hosted CPU laya-serve container defined in deploy/docker-compose.systemone.yml on 127.0.0.1:8000 with all three Laya checkpoints preloaded.
To benchmark a single pinned Laya checkpoint (bypassing laya-serve's automatic script router) or a local Kev server, start the server manually:
# Pinned single Laya checkpoint
uv run --no-project --with 'laya-serve[inference]==0.1.1' \
python scripts/systemone/laya_pinned_serve.py --checkpoint english --device cpu --port 8011
# Kev (uses MLX automatically on Apple silicon)
git clone https://github.com/jaredpalmer/kev && cd kev
uv sync --extra serve && uv run python -m kev.serve --run jaredpalmer/kev-0.8b --port 8008
2. Run the calibration harness¶
# Calibrate all three consumers against the default compose service (CPU)
make calibrate-systemone
# Run only the routing corpus (200 rows)
SYSTEMONE_CONSUMERS=routing make calibrate-systemone
# Run against a custom endpoint
SYSTEMONE_ENDPOINT=http://127.0.0.1:8008/v1/systemone SYSTEMONE_RUN_LABEL=kev-0.8b \
SYSTEMONE_MODEL=kev-latest SYSTEMONE_CHECKPOINT=jaredpalmer/kev-0.8b \
SYSTEMONE_CHECKPOINT_REVISION=54f4f8777356cd5bbbb6c6919c657f26e6f2f6d8 \
make calibrate-systemone
make calibrate-systemone runs scripts/systemone/calibrate-compose.sh, executing the consumer calibration tests in tests/systemone_calibration_test.rs and writing raw results and derived thresholds to tests/fixtures/systemone/calibration/<date>/<label>/ (or SYSTEMONE_REPORT_DIR). Each row is replayed through the consumer's exact state builder and question battery. You can tune concurrency, timeout, and row count with SYSTEMONE_CONCURRENCY (default 1), SYSTEMONE_TIMEOUT_SECS (default 120), and SYSTEMONE_LIMIT.
To run a minimal live connectivity check without the full corpus, use the ignored smoke test:
SYSTEMONE_SMOKE_ENDPOINT=http://127.0.0.1:8000/v1/systemone \
SYSTEMONE_SMOKE_MODEL=laya-english \
SYSTEMONE_SMOKE_PROFILE=generic \
cargo test --test systemone live_smoke_test::configured_native_endpoint_answers_a_text_decision -- --ignored --exact
3. Threshold derivation rules¶
tests/systemone_support/derive.rs sweeps a fixed threshold grid for each consumer and selects conservative thresholds where minimum evidence requirements are met (retaining vendor defaults otherwise):
- Guardrail
category_thresholds.<c>(block): Smallest threshold in0.50..=0.95(step0.05) achieving at least0.90block precision with at least 3 predicted blocks and 5 labeled positives. - Guardrail
review_threshold(flag): Largest threshold in0.05..=0.50maintaining at least0.90row-level recall across unsafe rows. - Classifier
min_confidence: Threshold in0.00..=1.00maximizing combineddomainandcomplexityaccuracy for thesystem_one->rulefallback chain (ties broken toward the higher threshold). If no threshold beats the standalone rule classifier, no value is derived. - Action safety: Derives thresholds only if
actionaccuracy beats the constant-answer baseline.deny_thresholdis the smallest value in0.30..=0.95with at least0.90deny precision on at least 3 denies;allow_thresholdandmin_confidencemaximize allows with zero false-allowblocklabels, at least0.95allow precision, and at least 5 allows.
Shipped defaults¶
The shipped defaults are derived from the all slice of the reference run 2026-09-24/laya-serve-compose-cpu (stock laya-serve 0.1.1 on CPU with convaiinnovations/laya@55cf4c4ebb4e). tests/systemone_calibration_test.rs (shipped_defaults_match_the_reference_run_and_the_docs) verifies that the library constants and this table match the committed derivation.
| Setting | Default | Source |
|---|---|---|
guardrail.review_threshold | 0.40 | derived from laya-serve-compose-cpu: row recall 0.913 at 0.40 (was 0.35) |
guardrail.category_thresholds.violence | 0.75 | derived from laya-serve-compose-cpu: block precision reaches 0.90 at 0.75 (was 0.70) |
guardrail.category_thresholds.hate_speech | 0.70 | vendor value: block precision never reaches 0.90 on the run |
guardrail.category_thresholds.sexual_content | 0.70 | vendor value: 4 labelled positives, fewer than the 5 required |
guardrail.category_thresholds.self_harm | 0.70 | vendor value: block precision never reaches 0.90 on the run |
guardrail.category_thresholds.harassment | 0.70 | vendor value: block precision never reaches 0.90 on the run |
guardrail.category_thresholds.dangerous | 0.70 | vendor value: block precision never reaches 0.90 on the run |
guardrail.category_thresholds.jailbreak | 0.70 | vendor value: block precision never reaches 0.90 on the run |
guardrail.category_thresholds.pii | 0.70 | vendor value: block precision never reaches 0.90 on the run |
guardrail.category_thresholds.profanity | 0.70 | vendor value: block precision never reaches 0.90 on the run |
classifier.min_confidence | 0.15 | derived from laya-serve-compose-cpu: chain accuracy 0.510 at 0.15, 0.460 at 0.5, rule alone 0.475 (was 0.5) |
action_safety.allow_threshold | 0.85 | vendor value: action accuracy 0.319 is below its 0.440 baseline, nothing derived |
action_safety.deny_threshold | 0.60 | vendor value: action accuracy 0.319 is below its 0.440 baseline, nothing derived |
action_safety.min_confidence | 0.6 | vendor value: action accuracy 0.319 is below its 0.440 baseline, nothing derived |
These values map to guardrails.providers[].options.review_threshold, guardrails.providers[].category_thresholds, smart_routing.classifier.system_one.min_confidence, and safeguards.action_safety.systemone.{allow_threshold, deny_threshold, min_confidence}. Explicit values in config.yaml always take precedence.
Monitor-only findings¶
On the reference laya-serve-compose-cpu run, several consumers or language slices do not beat a constant-answer baseline and therefore default to non-enforcing modes:
- Guardrail (
mode: monitoron all slices): At derived defaults, 70.8% of clean rows are flagged or blocked (allslice, block precision0.652), and over 92% of cleankoandnon_latinrows are flagged. Keepmode: monitorand use category probabilities as observability signals rather than standalone block gates. - Smart-routing classifier (
shadow: truerecommended for Korean/non-Latin traffic): On the English (en) and combined (all) slices, thesystem_one+rulechain atmin_confidence: 0.15outperforms the standalone rule classifier (0.628vs.0.544onen;0.510vs.0.475onall). Onko(0.405vs.0.487) andnon_latin(0.418vs.0.428), Laya does not beat the rule classifier; Korean-heavy deployments should retainmethod: rulewithshadow: trueunless using a model such askev-0.8bthat outperforms the rule baseline on Korean (0.639chain accuracy). - Action safety (
mode: advisory): Ontool_calls.jsonl,actionaccuracy (0.319) is below the constant baseline (0.440). Keepmode: advisory. - Unmeasured targets and consumers: Hosted TypeSafe Jev, Microsoft-Decision-1,
kev-4b, andfallback.quality_check(default threshold0.8inmode: shadow) were not calibrated in the committed offline run. Always calibrate against your target endpoint and pin explicit model versions (such asjev-1.14.0rather thanjev-latest) before enforcing.
Results (2026-09-24)¶
The summary tables below are generated from the committed calibration fixtures under tests/fixtures/systemone/calibration/2026-09-24/ across three corpora:
guardrail.jsonl: 244 rows (223 replayed live; 111 English, 66 Korean, 106 non-Latin)routing.jsonl: 200 rows (90 English, 79 Korean, 104 non-Latin)tool_calls.jsonl: 207 rows (English)
To regenerate the full per-question Brier, ECE, AUROC, and slice breakdown tables from the committed JSON files, run python3 scripts/systemone/results_tables.py 2026-09-24.
Evaluated endpoints¶
| Run | Checkpoint | Revision | Server | Device | Resolved model |
|---|---|---|---|---|---|
kev-0.8b | jaredpalmer/kev-0.8b (LoRA on Qwen/Qwen3.5-0.8B-Base, T=2.41) | 54f4f8777356 | kev.serve (62c9183) | Apple M4 Pro (mps, bf16) | kev-latest |
laya-auto | convaiinnovations/laya (auto script routing) | 55cf4c4ebb4e | laya-serve==0.1.1 (laya 0.3.20) | Apple M4 Pro (mps) | laya-auto@55cf4c4ebb4e |
laya-english | convaiinnovations/laya (english) | 55cf4c4ebb4e | laya-serve==0.1.1 (laya_pinned_serve.py) | Apple M4 Pro (mps) | laya-english@55cf4c4ebb4e |
laya-multilingual | convaiinnovations/laya (multilingual) | 55cf4c4ebb4e | laya-serve==0.1.1 (laya_pinned_serve.py) | Apple M4 Pro (mps) | laya-multilingual@55cf4c4ebb4e |
laya-serve-compose-cpu | convaiinnovations/laya (auto script routing) | 55cf4c4ebb4e | deploy/docker-compose.systemone.yml | CPU (concurrency 1) | laya-english |
laya-typed-decisions | convaiinnovations/laya (typed-decisions) | 55cf4c4ebb4e | laya-serve==0.1.1 (laya_pinned_serve.py) | Apple M4 Pro (mps) | laya-typed-decisions@55cf4c4ebb4e |
End-to-end latency and token usage by consumer¶
| Run | Guardrail p50 / p95 (ms) | Guardrail input tokens | Routing p50 / p95 (ms) | Routing input tokens | Action safety p50 / p95 (ms) | Action safety input tokens |
|---|---|---|---|---|---|---|
kev-0.8b | 235.2 / 319.1 | 679 | 84.3 / 160.2 | 351 | 83.2 / 145.6 | 308 |
laya-auto | 189.2 / 263.3 | 1081 | 47.0 / 61.3 | 341 | 63.3 / 77.7 | 280 |
laya-english | 238.6 / 332.8 | 1222 | 61.7 / 72.1 | 409 | 62.6 / 75.8 | 280 |
laya-multilingual | 92.9 / 115.1 | 1079 | 33.4 / 45.8 | 341 | 31.2 / 39.2 | 298 |
laya-serve-compose-cpu | 786.2 / 1734.9 | 1081 | 291.3 / 579.6 | 341 | 256.8 / 469.9 | 280 |
laya-typed-decisions | 242.5 / 322.7 | 1222 | 62.3 / 74.1 | 409 | 62.2 / 73.8 | 280 |
Routing shadow comparison¶
Comparison of standalone System One accuracy, standalone rule classifier accuracy, classifier agreement (smart_routing_shadow_agreement_total), and combined system_one -> rule chain accuracy across language slices:
| Run | Slice | Rows | System One (domain / complexity) | Rule (domain / complexity) | Agreement (domain / complexity) | Chain at 0.5 | Derived floor | Chain at derived |
|---|---|---|---|---|---|---|---|---|
kev-0.8b | all | 200 | 0.700 / 0.525 | 0.440 / 0.510 | 0.370 / 0.420 | 0.600 | 0.25 | 0.618 |
kev-0.8b | en | 90 | 0.722 / 0.511 | 0.556 / 0.533 | 0.511 / 0.378 | 0.633 | 0.65 | 0.656 |
kev-0.8b | ko | 79 | 0.671 / 0.557 | 0.430 / 0.544 | 0.304 / 0.506 | 0.614 | 0.25 | 0.639 |
kev-0.8b | non_latin | 104 | 0.673 / 0.538 | 0.356 / 0.500 | 0.250 / 0.462 | 0.587 | 0.25 | 0.625 |
laya-auto | all | 200 | 0.610 / 0.390 | 0.440 / 0.510 | 0.410 / 0.475 | 0.460 | 0.15 | 0.510 |
laya-auto | en | 90 | 0.722 / 0.489 | 0.556 / 0.533 | 0.511 / 0.533 | 0.539 | 0.15 | 0.628 |
laya-auto | ko | 79 | 0.519 / 0.291 | 0.430 / 0.544 | 0.392 / 0.443 | 0.462 | none (rule wins) | n/a |
laya-auto | non_latin | 104 | 0.538 / 0.298 | 0.356 / 0.500 | 0.346 / 0.423 | 0.409 | none (rule wins) | n/a |
laya-english | all | 200 | 0.595 / 0.460 | 0.440 / 0.510 | 0.390 / 0.470 | 0.478 | 0.15 | 0.535 |
laya-english | en | 90 | 0.722 / 0.489 | 0.556 / 0.533 | 0.511 / 0.533 | 0.539 | 0.15 | 0.628 |
laya-english | ko | 79 | 0.456 / 0.443 | 0.430 / 0.544 | 0.342 / 0.468 | 0.500 | 0.50 | 0.500 |
laya-english | non_latin | 104 | 0.500 / 0.433 | 0.356 / 0.500 | 0.288 / 0.433 | 0.438 | 0.05 | 0.476 |
laya-multilingual | all | 200 | 0.550 / 0.345 | 0.440 / 0.510 | 0.350 / 0.460 | 0.450 | none (rule wins) | n/a |
laya-multilingual | en | 90 | 0.589 / 0.400 | 0.556 / 0.533 | 0.378 / 0.489 | 0.517 | none (rule wins) | n/a |
laya-multilingual | ko | 79 | 0.519 / 0.291 | 0.430 / 0.544 | 0.392 / 0.443 | 0.462 | none (rule wins) | n/a |
laya-multilingual | non_latin | 104 | 0.538 / 0.298 | 0.356 / 0.500 | 0.346 / 0.423 | 0.409 | none (rule wins) | n/a |
laya-serve-compose-cpu | all | 200 | 0.610 / 0.390 | 0.440 / 0.510 | 0.410 / 0.475 | 0.460 | 0.15 | 0.510 |
laya-serve-compose-cpu | en | 90 | 0.722 / 0.489 | 0.556 / 0.533 | 0.511 / 0.533 | 0.539 | 0.15 | 0.628 |
laya-serve-compose-cpu | ko | 79 | 0.519 / 0.291 | 0.430 / 0.544 | 0.392 / 0.443 | 0.462 | none (rule wins) | n/a |
laya-serve-compose-cpu | non_latin | 104 | 0.538 / 0.298 | 0.356 / 0.500 | 0.346 / 0.423 | 0.409 | none (rule wins) | n/a |
laya-typed-decisions | all | 200 | 0.580 / 0.375 | 0.440 / 0.510 | 0.355 / 0.360 | 0.475 | 0.10 | 0.490 |
laya-typed-decisions | en | 90 | 0.756 / 0.422 | 0.556 / 0.533 | 0.533 / 0.433 | 0.544 | 0.10 | 0.600 |
laya-typed-decisions | ko | 79 | 0.430 / 0.342 | 0.430 / 0.544 | 0.241 / 0.392 | 0.487 | none (rule wins) | n/a |
laya-typed-decisions | non_latin | 104 | 0.442 / 0.337 | 0.356 / 0.500 | 0.202 / 0.317 | 0.428 | none (rule wins) | n/a |