Response-Header Reference
Every /v1/chat/completions response carries a set of SPASS-* headers that
expose stack-internal state in a machine-friendly way. They follow the
convention from ADR 0006:
proper-cased on the wire (Spass-Augment-Applied), case-insensitive per
RFC 9110, no X--prefix per RFC 6648, structured-field-values per RFC 8941.
Two categories: request-side (what the caller can send) and
response-side (what the server emits).
Request-side headers
| Header | Type | Purpose |
SPASS-Augment | RFC 8941 dictionary | Caller-controlled server-side augmentation. Format: system-prompt=off, server-tools=off, memory=off. Each key is default (= aktive injection) or off. Default-Verhalten ohne header: alle drei aktiv. |
SPASS-Tenant-Id | string | Multi-tenant override (only valid for unbound tokens). |
SPASS-Scope-Id | string | Memory + system-prompt scope-binding. |
SPASS-User-Id | string | Memory + system-prompt user-binding. |
SPASS-Stt-Mode | enum chunked | continuous | STT-File-Upload mode (ADR 0005 v2). Only on /v1/audio/transcriptions. |
SPASS-Request-Id | UUID v4 | Caller-supplied correlation id. Server echoes it back in the response. Dual-accept with X-Request-Id. |
SPASS-Cloud-Consent | RFC 8941-nahe Liste | GoCreate #4 — per-document cloud-upload consent. Value = list of content_hash tokens (sha256-hex), each optionally ;persist for a permanent grant: SPASS-Cloud-Consent: <hash>;persist, <hash2>. Only effective for tenants with c1_cloud_doc_consent: true. Clears a cloud_consent_required (409). |
Response-side headers
Build/version
| Header | Format | Notes |
SPASS-Version | SemVer + build-metadata, e.g. 0.51.0+gabc1234 (ADR 0028) | Canonical stack version. Stamped globally on every response via SetResponseHeaderLayer in main.rs. Single-source: Cargo.toml / VERSION. Same value as /api/version's full. |
Request-correlation
| Header | Format | Notes |
SPASS-Request-Id | UUID v4 | Internal SPASS form. Always set. Cross-Layer-Tracing: propagiert sich als parent_request_id in audit-events von /a1-sub-calls und ist in jedem audit.jsonl-record als top-level Feld. Caller können den Header echo'en (Bearer-trace-Korrelation). |
X-Request-Id | UUID v4 | RFC-de-facto compat alias, same value as SPASS-Request-Id. |
Caller-Tip (Cut 2.23c+): Wenn ihr einen X-Request-Id-Header zum Server schickt, übernimmt der Server diesen Wert für audit.jsonl — euer eigener Trace-Identifier ist dann in unseren Logs für post-mortem-Korrelation findbar. Bei 5xx-Errors wird der request_id im dgx_code-Feld der Error-Response mitgeliefert (siehe errors.md).
Augmentation visibility (ADR 0006)
| Header | Format | Notes |
SPASS-Augment-Applied | RFC 8941 dictionary | system-prompt=Nitems, server-tools=<comma-list-or-off>, memory=Nitems, stream-usage=injected (when F.1 default-injected include_usage). Lets the caller audit what the server actually did. |
SPASS-Applied | comma-list | Silent adjustments applied to the request (e.g. max_tokens_floored=200, response_format_stripped=empty). |
SPASS-Tools-Executed | comma-list name:iter,… | Server-side tool-loop trace: which tools fired in which iteration. |
SPASS-Stt-Model | string | Cut 2.50 — effektiv genutztes STT-Modell auf /v1/audio/transcriptions (resolved Slug, nie auto). Nur dgx-intern/Debug — der nachgelagerte Caller-Proxy verwirft Upstream-Response-Header. Für convert=1 ist das JSON-Body-Feld model maßgeblich. |
Attachment + document processing (CR-012 + CR-0015)
Set on chat responses that carried attachments. Absent when no attachment was processed.
| Header | Format | Notes |
SPASS-Attachments-Applied | integer | CR-012 — number of image attachments injected into the user turn (model is vision-capable). Only set when ≥ 1 applied. |
SPASS-Attachments-Dropped | reason string (e.g. model_not_vision) | CR-012 — image attachments were stripped. Since 2026-08-22 this header no longer appears for text-only models: sending an image to a model without a vision encoder is now HTTP 400 model_not_vision_capable on both channels (image_url content-part and attachments). The header remains for the other strip reasons (e.g. an image URL blocked by the SSRF guard). |
SPASS-Doc-Pages-Applied | integer | CR-0015 — number of rendered document page-images hung into the user turn (summed over all kind:"document" attachments). 0 = pure-text path. |
SPASS-Doc-Pages-Total | integer | #14 — total source-page count of the rendered documents (sum of n_pages). With SPASS-Doc-Pages-Applied the frontend can show "N of M pages evaluated". |
SPASS-Doc-Mode | text | vision | both | CR-0015 — effective processing mode across all documents: extracted text only, page-images only (scan), or both (tables/layout/hybrid). |
SPASS-Doc-Truncated | pages | text | both | CR-0015 — set when the 32k-budget guard trimmed content (page-images reduced from the back, text cut, or both). |
SPASS-Doc-Error | comma-list fetch_failed/render_failed/too_large/empty_extraction | CR-0015 — a document could not be processed (chat continues anyway). Distinct values comma-joined for multiple docs. |
SPASS-Doc-Needs-Vision | 1 | CR-0015 — a scanned document (no text layer) went to a text-only model and could not be read. Frontend can prompt the user to pick a vision-capable model. |
Resolution + routing (ADR 0007 + 0008)
| Header | Values | Notes |
SPASS-Resolved-Model | canonical alias | Non-streaming: zuverlässig. Streaming: best-effort. |
SPASS-Resolved-Backend | local-fp4 | local-fp8 | local-bf16 | cloud-1 | cloud-2 | cloud-3 | Generic backend-slot, ADR 0006 v2 compliant — keine implementation-Names. |
SPASS-Resolved-Reason | primary-up | primary-down-fallback | quant-pin-explicit | cloud-pin-explicit | hardware-pin-explicit | legacy-alias | unknown | Erklärt warum der resolved-backend der ist der er ist. Cockpit-followup-7 wishlist. |
SPASS-Fallback-Used | true | false | unknown | Aus actual != primary abgeleitet. unknown bei streaming oder unbekanntem Modell. |
SPASS-Cache-Hit | true | false | Stack-cache (Redis) hit. |
Cost-Pipeline V2 (ADR 0010)
| Header | Format | Notes |
SPASS-Cost-Eur | 0.01 (2 decimals, ceil_to_cent) | Final EUR mit allen markups. Tenant-billing-authoritative. |
SPASS-Cost-Usd | 0.01 (2 decimals, round_to_cent) | USD = EUR ÷ ECB-rate (ohne markups). Display-only. |
SPASS-Cost-Available | true | false | Ob ein cost-Wert ableitbar war. false nur bei unknown source. |
SPASS-Cost-Source | zero | free | upstream | unknown | Woher der Wert kommt. |
SPASS-Cost-Exchange-Rate | 0.9300 (4 decimals) | ECB-rate (ohne markup) für caller-Verifikation. |
SPASS-Cost-Exchange-Rate-Source | ecb-YYYY-MM-DD | fallback-30d-max-... | fallback-hardcoded | Lookup-Hierarchie-Indikator. |
SPASS-Cost-Sub-Calls | integer (≥0) | /a1 only. How many internal /v1 sub-calls contributed to the aggregated cost. ≥1 for normal completions; can be larger for tool-loops. |
On /a1/agents/<name>/chat and /a1/agents/<name>/sessions/<sid>/messages, all of the above are summed across the rig agent's internal /v1 sub-calls (Cut 2.7). The dominant Source wins (Upstream > Free > Zero > Unknown); Available is false if ANY sub-call was Unknown.
Recursion + audit-correlation (Cut 2.3)
| Header | Format | Notes |
SPASS-Caller-Depth | integer (in-only) | Optional incoming header. The /a1 handler refuses requests at ≥ 3 (recursion_depth_exceeded). The handler propagates incoming + 1 on its rig sub-call. |
SPASS-Parent-Request-Id | UUID v4 (in-only) | Set by /a1 on the rig sub-call so /v1's audit-event carries parent_request_id = outer-rid. Lets jq reconstruct call-trees from audit.jsonl. |
/c1/chat only
| Header | Format | Notes |
SPASS-Conversation-Id | UUID v4 | Persistent conversation ID. Caller submits it on follow-up turns to load history. |
spass-system-prompt-ignored | already_present (Cut 2.33, CR-0003) | Set when the request carries a system-prompt field (system_prompt / system_prompt_ref / additional_system_prompt) but the conversation already has a role=system-row from an earlier turn. Server ignores the new value; this header is the explicit signal so the caller can debug why their late system-prompt change didn't take effect. Use POST /c1/conversations/{id}/system-prompt to append a new system-row deliberately. |
/a1 agent-sessions only (Cut 2.12)
| Header | Format | Notes |
SPASS-Session-Compacted | successor session-id (UUID v4) | Set on the /a1/.../sessions/<sid>/messages response when the handler triggered an auto-compaction before the chat-loop. Value = the successor session-id the answer was actually generated on. Clients remember this id for the next request; the old sid returns 308 → successor from then on. |
SPASS-Session-Compacted-Tokens | integer | Companion to SPASS-Session-Compacted. The pre-compact estimated_tokens value that acted as the trigger (operator diagnose + tuning hint). |
Quota signaling (Cut 2.32, CR-0005)
| Header | Format | Notes |
Retry-After | integer (seconds) | Set on HTTP 429 responses with dgx_code: "openrouter_daily_quota_exhausted". Value = seconds until next 00:00 UTC. Standard RFC 9110 retry-hint. |
Caller-pattern: cost-tracking
function requestCostEur(headers: Headers): number | null {
if (headers.get("spass-cost-available") !== "true") return null;
return parseFloat(headers.get("spass-cost-eur") ?? "0");
}
Local-zero responses set Spass-Cost-Eur: 0.00 + Spass-Cost-Source: zero
explicitly so the caller doesn't need to special-case missing headers.
Caller-pattern: routing-diagnose
const backend = headers.get("spass-resolved-backend");
const reason = headers.get("spass-resolved-reason");
if (reason === "primary-down-fallback") {
// T1 unprefixed alias fell to cloud — log + maybe retry differently
}
Spass-Resolved-Reason removes the need to compare Spass-Resolved-Backend
to the catalog manually — the server already did that.
Cross-references
- ADR 0006 — naming convention
- ADR 0007 — cost + backend-resolution V1
- ADR 0008 — slug tier-schema (T1/T2/T3/T4)
- ADR 0010 — cost-pipeline V2 (EUR + tenant-markup)
/v1/info — model catalog with primary backend + capabilities
/api/version — current build SHA + timestamp
SPASS-Notice — the answer is complete, but here is how it came about
Since 2026-08-22 a successful response (HTTP 200) may carry a SPASS-Notice
header. It means the answer is usable but the server took a non-obvious path to
produce it. There is no error, nothing failed, and no retry is needed.
| Value | Meaning |
tool_loop_max_iterations | The tool loop hit its per-request cap. The server made one extra synthesis call over the tool results already collected, so you get a narrative answer instead of a dangling tool_calls response |
tool_loop_anti_loop_synthesised | The model repeated an identical tool call; the server broke the loop with a synthesis call |
hallucinated_tool_stripped_<reason> | The model emitted something shaped like a tool call where none was requested. It was removed before the answer was returned. The names are in the body field stripped_tool_names |
tool_feedback_recovery_after_<reason> | A first attempt produced an unusable tool call; a corrective round succeeded. feedback_retries in the body says how many |
tool_feedback_exhausted_safety_fallback_<reason> | The corrective rounds did not succeed either; the answer is a safety fallback |
invalid_response_safety_fallback_<source> | The model returned nothing usable at all; the text you see was substituted by the gateway so the caller never faces an empty answer |
tool_call_salvaged_from_reasoning | A valid tool call was recovered out of the model's reasoning text |
prior_recovery_marker | A recovery marker from an earlier turn was carried forward |
Streaming: a header cannot be set once the first chunk is out, so the same
information arrives as a named SSE event before the final content —
spass.tool-cap, spass.tool-anti-loop, spass.tool-stripped,
spass.tool-feedback-recovery, spass.tool-feedback-exhausted,
spass.safety-fallback, spass.tool-salvaged, spass.prior-recovery.
Until 2026-08-22 only the first four were emitted; the rest were dropped
silently in streaming mode although they reached the caller in the unary body.
Migrating from dgx_code: the value is identical — read the header (or the
SSE event) instead of the body field. Payload stays in the body:
stripped_tool_names, feedback_retries.