DGX LLM Chat Gateway

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

HeaderTypePurpose
SPASS-AugmentRFC 8941 dictionaryCaller-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-IdstringMulti-tenant override (only valid for unbound tokens).
SPASS-Scope-IdstringMemory + system-prompt scope-binding.
SPASS-User-IdstringMemory + system-prompt user-binding.
SPASS-Stt-Modeenum chunked | continuousSTT-File-Upload mode (ADR 0005 v2). Only on /v1/audio/transcriptions.
SPASS-Request-IdUUID v4Caller-supplied correlation id. Server echoes it back in the response. Dual-accept with X-Request-Id.
SPASS-Cloud-ConsentRFC 8941-nahe ListeGoCreate #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

HeaderFormatNotes
SPASS-VersionSemVer + 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

HeaderFormatNotes
SPASS-Request-IdUUID v4Internal 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-IdUUID v4RFC-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)

HeaderFormatNotes
SPASS-Augment-AppliedRFC 8941 dictionarysystem-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-Appliedcomma-listSilent adjustments applied to the request (e.g. max_tokens_floored=200, response_format_stripped=empty).
SPASS-Tools-Executedcomma-list name:iter,…Server-side tool-loop trace: which tools fired in which iteration.
SPASS-Stt-ModelstringCut 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.

HeaderFormatNotes
SPASS-Attachments-AppliedintegerCR-012 — number of image attachments injected into the user turn (model is vision-capable). Only set when ≥ 1 applied.
SPASS-Attachments-Droppedreason 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-AppliedintegerCR-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-Totalinteger#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-Modetext | vision | bothCR-0015 — effective processing mode across all documents: extracted text only, page-images only (scan), or both (tables/layout/hybrid).
SPASS-Doc-Truncatedpages | text | bothCR-0015 — set when the 32k-budget guard trimmed content (page-images reduced from the back, text cut, or both).
SPASS-Doc-Errorcomma-list fetch_failed/render_failed/too_large/empty_extractionCR-0015 — a document could not be processed (chat continues anyway). Distinct values comma-joined for multiple docs.
SPASS-Doc-Needs-Vision1CR-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)

HeaderValuesNotes
SPASS-Resolved-Modelcanonical aliasNon-streaming: zuverlässig. Streaming: best-effort.
SPASS-Resolved-Backendlocal-fp4 | local-fp8 | local-bf16 | cloud-1 | cloud-2 | cloud-3Generic backend-slot, ADR 0006 v2 compliant — keine implementation-Names.
SPASS-Resolved-Reasonprimary-up | primary-down-fallback | quant-pin-explicit | cloud-pin-explicit | hardware-pin-explicit | legacy-alias | unknownErklärt warum der resolved-backend der ist der er ist. Cockpit-followup-7 wishlist.
SPASS-Fallback-Usedtrue | false | unknownAus actual != primary abgeleitet. unknown bei streaming oder unbekanntem Modell.
SPASS-Cache-Hittrue | falseStack-cache (Redis) hit.

Cost-Pipeline V2 (ADR 0010)

HeaderFormatNotes
SPASS-Cost-Eur0.01 (2 decimals, ceil_to_cent)Final EUR mit allen markups. Tenant-billing-authoritative.
SPASS-Cost-Usd0.01 (2 decimals, round_to_cent)USD = EUR ÷ ECB-rate (ohne markups). Display-only.
SPASS-Cost-Availabletrue | falseOb ein cost-Wert ableitbar war. false nur bei unknown source.
SPASS-Cost-Sourcezero | free | upstream | unknownWoher der Wert kommt.
SPASS-Cost-Exchange-Rate0.9300 (4 decimals)ECB-rate (ohne markup) für caller-Verifikation.
SPASS-Cost-Exchange-Rate-Sourceecb-YYYY-MM-DD | fallback-30d-max-... | fallback-hardcodedLookup-Hierarchie-Indikator.
SPASS-Cost-Sub-Callsinteger (≥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)

HeaderFormatNotes
SPASS-Caller-Depthinteger (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-IdUUID 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

HeaderFormatNotes
SPASS-Conversation-IdUUID v4Persistent conversation ID. Caller submits it on follow-up turns to load history.
spass-system-prompt-ignoredalready_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)

HeaderFormatNotes
SPASS-Session-Compactedsuccessor 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-TokensintegerCompanion 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)

HeaderFormatNotes
Retry-Afterinteger (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

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.

ValueMeaning
tool_loop_max_iterationsThe 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_synthesisedThe 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_reasoningA valid tool call was recovered out of the model's reasoning text
prior_recovery_markerA 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.