{
  "openapi": "3.1.0",
  "info": {
    "title": "SandLogic LLM Router",
    "summary": "Enterprise Model Routing, Qualification and Governance Platform",
    "version": "1.25.0",
    "description": "The serving API. Two properties of this document are enforced by tests in packages/contracts/tests/test_openapi_baseline.py rather than by review, because both are the kind of thing that erodes one pull request at a time.\n\nFirst, no request property carries a `default`. An OpenAPI `default` is a silent default: it tells a generated client to send a value the caller never chose, and downstream that value is indistinguishable from a deliberate choice. ADR-10 forbids exactly this, so the absence of the keyword is checked mechanically across the whole document.\n\nSecond, the request properties and the error code enumeration must match the frozen Python contracts exactly. A schema that drifts from the code it describes is worse than no schema, because clients are generated from it.\n\nAn omitted optional property means the caller expressed no preference. It is carried as absent through the decision record and the provider applies its own default, recorded as the provider's choice. It is never resolved into a value attributed to the caller.\n\nCR-004 (v1.2.0) aligned this document with the operation surface the enterprise architecture publishes, and the alignment was not cosmetic. Three things were wrong and one of them was material.\n\nThe version now sits in each path rather than in the server URL, because one operation must NOT carry the router's prefix: `POST /v1/chat/completions` is OpenAI's chat path, and being byte-identical to the path an existing client already calls is the entire content of the compatibility claim. A server prefix would have made every path share a shape that one of them must not have.\n\nBefore CR-004 the compatible surface was published as `/v1/completions` - OpenAI's *legacy text-completion* path - carrying a chat body requiring `messages`, plus `tenant_id`, `application_id` and `purpose` required in the body, which no OpenAI client sends. A chat call 404'd, a legacy call was refused, and the tenancy fields could not have been supplied either way. The claim was undeliverable three independent ways at once, and nothing caught it because the baseline tests checked this document against itself and never against the design volume that promised the surface.\n\nCR-038 (v1.9.0) publishes agent sessions: POST /routing/v1/sessions opens one for the calling principal and the application its credential names, under an id the router makes, at version 0. A completion continues it by sending two headers, SL-Session-Id and SL-Session-Version (the version last read), and answers with both, carrying the version to present next. A session is keyed by tenant, application and caller, so another caller's session and a session nobody opened are one refusal. Continuations compare and set on the version: of two computed against one version, one commits and the other is a STATE_CONFLICT. While a tool call the model returned is outstanding, the session keeps the model that returned it; a withdrawn pin inside that round is refused rather than re-selected, and a named model cannot take the round. Headers rather than body fields, because the OpenAI-compatible body refuses a field it does not define. No new error code.\n\nCR-040 (v1.10.0) publishes `response_format`, OpenAI's structured output request, on the completion request (MP-022, UC-11, DC-04). A caller who needs JSON asks for it by type: `json_object` needs a server that guarantees syntactically valid JSON, and `json_schema` needs a server that ENFORCES the supplied schema, whatever `strict` says - a caller who supplied a schema is routed only where it is enforced, because a server that promises JSON syntax alone would be a silent downgrade of what was asked. A configuration that cannot give the guarantee is excluded before ranking, with its own reason, and a request no candidate can serve is NO_CAPABILITY_ELIGIBLE_MODEL. An adapter not qualified for the guarantee refuses with UNQUALIFIED_FIELD rather than send the request without it. A malformed value is INVALID_REQUEST, naming the part that is wrong and never a value in it. No new error code.\n\nCR-041 (v1.10.1) corrects the description of `max_output_tokens` to what the router has done since MP-090 (ADR-34): an omitted or null value is capped at the application's approved maximum, which the router sends to the provider; a value above that maximum is refused REQUEST_TOO_LARGE and never clamped. The description said 'Omit to express no preference', which stopped being true when the allowance shipped. Behaviour is unchanged; only the published sentence is.\n\nCR-042 (v1.11.0) adds ROUTER_BUNDLE_UNAVAILABLE (HTTP 503): the router bundle this tenant activated cannot be loaded - missing, altered since it was pinned, or unverifiable where a verified signature is required - so no request under it can be judged until an operator re-publishes or re-activates it. It was answered as POLICY_INVALID, which names a policy the estate cannot act on and sent operators to the wrong place. A tenant that has activated no bundle is unchanged: its requests end in a typed no-route naming the unavailable estimate.\n\nCR-043 (v1.12.0) answers createChatCompletion (/v1/chat/completions) in OpenAI's shapes, as the product owner decided for MP-124: the response is OpenAI's chat.completion and every refusal OpenAI's error object, carrying the router's published code as `code`. The router's evidence travels in X-SL-* headers. Every refusal carries x-should-retry, which OpenAI's SDKs read before retrying on their own, so an execution whose outcome is uncertain is never sent twice by the client library. The request gains OpenAI's max_completion_tokens (one of it and max_tokens, never both) and response_format. A streamed response (stream: true) is refused with UNSUPPORTED_FIELD until the stream path answers in OpenAI's chunk shape. The operation had never been served, so no client depended on the shape it replaces.\n\nCR-044 (v1.13.0) answers stream: true on createChatCompletion as OpenAI's chunk stream (text/event-stream): data frames of chat.completion.chunk - the assistant's role, each piece of text as the provider wrote it, each complete tool call as one delta, a last chunk with finish_reason and usage - then data: [DONE]. The router's evidence rides on the last chunk under x_sl, because a stream's headers are sent before any of it exists. A refusal decided before any text keeps its status and error object; an answer interrupted after it began ends with an error frame (STREAM_INTERRUPTED) rather than a stop.\n\nCR-047 (v1.16.0) adds ADMISSION_PAUSED (HTTP 503, retryable): this environment's operator has stopped admitting new requests. It is returned by createCompletion, dispatchDecision and createChatCompletion before anything is reserved or sent, and says so; no Retry-After is sent, because nobody knows when an operator will resume and a guessed interval would be a promise the router cannot keep. A recommendation (createSelection) sends nothing and reserves nothing, so it is still answered, and work admitted before the pause finishes and settles as it would have. ADMISSION_LIMIT_EXCEEDED keeps its meaning - too much work already in flight - and its 429: a caller told that would back off and retry into a pause its own traffic did not cause.\n\nCR-046 (v1.25.0) makes a completion's `idempotency_key` mean what a caller expects (EXE-04); until now it was accepted and read by nothing, so a request sent again with the same key was answered again and charged again. A key is scoped to the tenant, the application and the key. The first request under it claims it, with the request's digest, before anything is sent, and the claim is a unique key in the database, so two requests arriving together under one key cannot both be sent. The same key with the same request never reaches a provider again. While the first is still being answered it is refused STATE_CONFLICT, retryable. Once the first has finished, createCompletion answers 200 with that operation's record - its decision id, the configuration that served it, its state and its usage - and no output (CompletionRecord, `answer_kept: false`), because the router keeps no answer: capture is the only store of content and is off unless approved. A request that was refused before anything was sent is refused the same way again, and one whose outcome was never established is EXECUTION_UNCERTAIN again; a new attempt takes a new key. A refusal marked retryable (for example ADMISSION_PAUSED) is the exception: it releases the key, so the same key may be sent again once the condition clears. The same key with a different request is IDEMPOTENCY_CONFLICT. A key is kept for 24 hours and is then free again. A request without a key, createSelection and createChatCompletion - which has no idempotency field and is given none - are unchanged, and so is a receipt's single use at dispatch (CR-045). No new error code."
  },
  "servers": [
    {
      "url": "https://{host}",
      "description": "Host only. The major version is in each path, not here: the OpenAI-compatible operation must be exactly /v1/chat/completions and cannot inherit a router prefix, so a shared server prefix would be a lie about one of the operations. See contracts.versioning.",
      "variables": {
        "host": {
          "default": "api.example.invalid",
          "description": "Deployment host. A server variable, not a request property."
        }
      }
    }
  ],
  "paths": {
    "/routing/v1/selections": {
      "post": {
        "operationId": "createSelection",
        "summary": "Ask which configuration would be chosen, with no admission and no charge",
        "description": "The recommendation half of the separation REQ INT-01 requires. It runs the same selection as a dispatch - policy-eligible, capability-supported, quality-qualified, then preference - and stops there. No admission is taken, no capacity or budget is encumbered, no provider is called, and there is therefore no charge. A receipt is issued only when the caller asks for one with X-SL-Receipt: issue (CR-045): a JSON Web Token the router signs, stating the decision so the caller's own gateway can enforce it and /routing/v1/dispatch can carry it out. It is still not a charge and still commits nothing, and it is issued only for a decision that could be carried out: a principal who may only ask is refused one as an execution would refuse them (DESTINATION_FORBIDDEN), and a deployment holding no receipt-signing key refuses the header (POLICY_INVALID). A caller who may ask what would be chosen is not thereby a principal who may spend money, which is why this is a separate operation and a separate authorization rather than a flag on the dispatch. The response deliberately cannot carry a charge: if it could, a recommendation would be indistinguishable from a dispatch that happened to be cheap. Re-pathed by CR-004 from /selections under a /v1 server prefix: the design volume namespaces by subsystem and the plural resource noun is the better REST shape, so this takes both rather than either document capitulating.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CompletionRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "A configuration would be chosen. Nothing was admitted or spent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SelectionResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request was refused before selection.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Authentication is required.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Authorization was refused or has been revoked. With X-SL-Receipt: issue, also a principal that may ask what would be chosen but not execute (DESTINATION_FORBIDDEN): a receipt is for a decision the caller could have carried out.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "The payload exceeds the limit. It was refused, not truncated.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "No configuration qualified. The body names the limiting class of constraint and never which control fired for which candidate. With X-SL-Receipt: issue, also POLICY_INVALID where this deployment holds no receipt-signing key, or where the decision could not be carried out (no budget account, no approved output maximum, a configuration that cannot be bounded).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "A mandatory governance dependency is unavailable, so the request was not admitted. An unrecordable decision is not a decision.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "X-SL-Receipt",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "issue"
              ]
            },
            "description": "Ask for a signed receipt for this decision (CR-045, ADR-41). A header and not a body field, because the body is the request the receipt is bound to and is presented again, unchanged, at dispatch. Absent, nothing changes: the response carries no receipt. Any other value is refused with INVALID_REQUEST."
          }
        ]
      }
    },
    "/routing/v1/completions": {
      "post": {
        "operationId": "createCompletion",
        "summary": "Route one request to a qualified model configuration",
        "description": "Selects among policy-eligible, capability-supported, quality-qualified configurations. When no configuration qualifies the response is a typed refusal naming the limiting class of constraint; the router never relaxes a constraint to find a route. Re-pathed by CR-004 from /completions under a /v1 server prefix. That path collided with OpenAI's legacy text-completion endpoint while carrying a chat body, and no design volume listed a native execute operation at all; EA section 17 now carries a row for it. Idempotency (CR-046): a request carrying an idempotency_key claims it before anything is sent, and a repeat under the same key is never sent again. A repeat of a request that already finished is answered 200 with CompletionRecord - that operation's decision, serving configuration, state and usage, and no output, because the router keeps none. A repeat while the first is still being answered is 409 STATE_CONFLICT, retryable; a different request under the key is 409 IDEMPOTENCY_CONFLICT; a request refused before anything was sent, or whose outcome was never established, is answered with the same error again - except a refusal marked retryable (for example ADMISSION_PAUSED), which releases the key, so the same key may be sent again once the condition clears.",
        "parameters": [
          {
            "name": "SL-Session-Id",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "minLength": 1
            },
            "description": "The agent session this request continues, as openSession returned it. Sent together with SL-Session-Version or not at all; one without the other is refused with INVALID_REQUEST. Absent, the request continues no session."
          },
          {
            "name": "SL-Session-Version",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "pattern": "^[0-9]+$"
            },
            "description": "The session's version as last read: 0 after openSession, then the value the previous completion answered with. A version the session has moved past is refused with STATE_CONFLICT before anything is dispatched. A session that is closed (CR-048) is refused with STATE_CONFLICT before anything is dispatched: open a new one."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CompletionRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "A configuration was selected and the request was admitted - or, for a repeat of a request under an idempotency_key that already finished, that operation's record and no output (CompletionRecord, answer_kept false).",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/CompletionResponse"
                    },
                    {
                      "$ref": "#/components/schemas/CompletionRecord"
                    }
                  ]
                }
              }
            },
            "headers": {
              "SL-Session-Id": {
                "description": "The session this completion continued. Absent where it continued none.",
                "schema": {
                  "type": "string",
                  "minLength": 1
                }
              },
              "SL-Session-Version": {
                "description": "The version to present on the next completion of this session. Absent where the request continued no session, or served nothing and so changed nothing.",
                "schema": {
                  "type": "string",
                  "pattern": "^[0-9]+$"
                }
              }
            }
          },
          "400": {
            "description": "The request was refused before selection.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Authentication is required.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Authorization was refused or has been revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "The request conflicts with recorded state: a session version the session has moved past, an idempotency_key already bound to a different request (IDEMPOTENCY_CONFLICT), or one whose first request is still being answered (STATE_CONFLICT, retryable - nothing was sent).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "The payload exceeds the limit. It was refused, not truncated.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "No configuration qualified. The body names the limiting class of constraint and never which control fired for which candidate.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "A mandatory governance dependency is unavailable, so the request was not admitted. An unrecordable decision is not a decision. Or (ADMISSION_PAUSED, retryable) this environment's operator has stopped admitting new requests: nothing was reserved or sent, and the same request may be sent again later.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/routing/v1/sessions": {
      "post": {
        "operationId": "openSession",
        "summary": "Open an agent session",
        "description": "Opens a session for the calling principal and the application its credential names, at version 0, under an id the router makes: a caller never names a session into existence. There is no request body. The session is continued by createCompletion with the SL-Session-Id and SL-Session-Version headers, and only a principal that may execute has a session to open.",
        "responses": {
          "201": {
            "description": "The session is open at version 0.",
            "headers": {
              "SL-Session-Id": {
                "description": "The session this completion continued. Absent where it continued none.",
                "schema": {
                  "type": "string",
                  "minLength": 1
                }
              },
              "SL-Session-Version": {
                "description": "The version to present on the next completion of this session. Absent where the request continued no session, or served nothing and so changed nothing.",
                "schema": {
                  "type": "string",
                  "pattern": "^[0-9]+$"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SessionOpened"
                }
              }
            }
          },
          "401": {
            "description": "Authentication is required.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "This principal may not execute, so it has no session to continue.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "A mandatory governance dependency is unavailable, so no session was opened.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/routing/v1/dispatch": {
      "post": {
        "operationId": "dispatchDecision",
        "summary": "Carry out a decision already taken, presenting its signed receipt",
        "description": "The enforcing-gateway integration. A caller that ran /routing/v1/selections with X-SL-Receipt: issue, enforced the decision at its own egress and now wants the generation performed presents the original request together with the id of the receipt that decision was answered with.\n\nServed by CR-045 (ADR-41), and checked in this order. The receipt must exist in the caller's tenant; an unknown id and another tenant's id get the same refusal. Its SIGNATURE is checked against the router's keys exactly as a gateway would check it, so a stored receipt altered after it was issued is refused. Its CLAIMS must bind this deployment, this caller, tenant and application, and this request - the router re-derives the request's digest - and it must be unexpired, with the request's deadline not passed and the configuration still at the endpoint the receipt names. Every way a receipt can fail to be ours or bound to this request is RECEIPT_INVALID with one message; expiry and a moved endpoint say what they are, because neither is a secret. Then the EPOCHS, inside admission: authority revoked since the decision is AUTHORIZATION_REVOKED, authority or the model changed since is STATE_CONFLICT.\n\nExactly the receipt's configuration is carried out, at the receipt's endpoint only - there is no fallback to another candidate, because the gateway approved one destination - with retries at it as the operator configured, and the output cap the receipt names. The receipt id is admission's single-use key, so a receipt is carried out at most once: presenting it again is IDEMPOTENCY_CONFLICT.\n\nThe receipt is REQUIRED, and this is a separate operation rather than an optional field on the native execute, because an optional receipt would make 'receipt absent' and 'receipt invalid' the same code path - the collapse ADR-08 forbids. A receipt is evidence of a decision, never an authorization.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DispatchRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The decision the receipt names was carried out. The response names the decision it carried out, not a new one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompletionResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request was refused before selection.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Authentication is required.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Authorization was refused or has been revoked: a principal that may not execute (DESTINATION_FORBIDDEN), or authority revoked since the decision (AUTHORIZATION_REVOKED).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "The receipt was already carried out (IDEMPOTENCY_CONFLICT), or authority or the model changed since the decision (STATE_CONFLICT): ask for a new decision.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "The payload exceeds the limit. It was refused, not truncated.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The receipt is not one this router issued for this request, or it expired, or its configuration no longer runs at the endpoint it names (RECEIPT_INVALID); or this deployment holds no receipt keys (POLICY_INVALID).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "A mandatory governance dependency is unavailable, so the request was not admitted. An unrecordable decision is not a decision. Or (ADMISSION_PAUSED, retryable) this environment's operator has stopped admitting new requests: nothing was reserved or sent, and the same request may be sent again later.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/chat/completions": {
      "post": {
        "operationId": "createChatCompletion",
        "summary": "OpenAI-compatible chat completions (drop-in base-URL swap)",
        "description": "The supported managed proxy contract. This path carries no router prefix on purpose: its whole value is being byte-identical to the path an existing OpenAI-compatible client already calls, so that adopting the router is a base-URL change and nothing else. A prefixed path would be a different endpoint that merely resembles one.\n\nConsequently the request carries NO tenancy fields. `tenant_id`, `application_id` and `purpose` are not properties here, because no OpenAI client would send them; they are resolved from the authenticated principal and from the configuration bound to the API credential (C02). That is the substantive design consequence of CR-004 and is recorded as ADR-29.\n\nTwo router rules still hold and are visible to the caller. `model` names a logical routing profile, not a provider model - a provider model id is refused with UNSUPPORTED_MODEL_OVERRIDE rather than honoured, because silently serving a named model the policy did not authorize is the failure this product exists to prevent. And `n` is absent from this schema: more than one completion is not expressible, refused rather than reduced to 1 (ADR-10). The upstream provider refuses n>1 only under greedy sampling and returns two choices under sampling, so this refusal is the router's and cannot be delegated.",
        "parameters": [
          {
            "name": "SL-Session-Id",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "minLength": 1
            },
            "description": "The agent session this request continues, as openSession returned it. Sent together with SL-Session-Version or not at all; one without the other is refused with INVALID_REQUEST. Absent, the request continues no session."
          },
          {
            "name": "SL-Session-Version",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "pattern": "^[0-9]+$"
            },
            "description": "The session's version as last read: 0 after openSession, then the value the previous completion answered with. A version the session has moved past is refused with STATE_CONFLICT before anything is dispatched. A session that is closed (CR-048) is refused with STATE_CONFLICT before anything is dispatched: open a new one."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ChatCompletionRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OpenAI's chat.completion in one body, or - for stream: true - OpenAI's chunk stream (CR-044): data frames of chat.completion.chunk ended by data: [DONE].",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ChatCompletion"
                }
              },
              "text/event-stream": {
                "schema": {
                  "$ref": "#/components/schemas/ChatCompletionChunk"
                }
              }
            },
            "headers": {
              "SL-Session-Id": {
                "description": "The session this completion continued. Absent where it continued none.",
                "schema": {
                  "type": "string",
                  "minLength": 1
                }
              },
              "SL-Session-Version": {
                "description": "The version to present on the next completion of this session. Absent where the request continued no session, or served nothing and so changed nothing.",
                "schema": {
                  "type": "string",
                  "pattern": "^[0-9]+$"
                }
              },
              "X-SL-Request-Id": {
                "description": "The router's id for this request; the body's id is chatcmpl- followed by it.",
                "schema": {
                  "type": "string"
                }
              },
              "X-SL-Selected-Configuration": {
                "description": "The configuration this router selected.",
                "schema": {
                  "type": "string"
                }
              },
              "X-SL-Served-Configuration": {
                "description": "The configuration that produced the answer. Differs from the selected one after an authorized fallback. Absent where none did.",
                "schema": {
                  "type": "string"
                }
              },
              "X-SL-Policy-Generation": {
                "description": "The policy generation the request was judged under.",
                "schema": {
                  "type": "string"
                }
              },
              "X-SL-Decision-Id": {
                "description": "The recorded decision this answer followed.",
                "schema": {
                  "type": "string"
                }
              },
              "X-SL-Finish": {
                "description": "The router's own stop reason (StopReason), beside OpenAI's finish_reason.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "The request was refused before selection.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OpenAIError"
                }
              }
            },
            "headers": {
              "x-should-retry": {
                "description": "true or false: the router's own verdict on retrying. OpenAI's SDKs read it before retrying 409, 429 and 5xx on their own; an execution whose outcome is uncertain says false, so the library does not send the same money twice.",
                "schema": {
                  "type": "string",
                  "enum": [
                    "true",
                    "false"
                  ]
                }
              },
              "X-SL-Correlation-Id": {
                "description": "Where to trace this refusal.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "description": "Authentication is required.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OpenAIError"
                }
              }
            },
            "headers": {
              "x-should-retry": {
                "description": "true or false: the router's own verdict on retrying. OpenAI's SDKs read it before retrying 409, 429 and 5xx on their own; an execution whose outcome is uncertain says false, so the library does not send the same money twice.",
                "schema": {
                  "type": "string",
                  "enum": [
                    "true",
                    "false"
                  ]
                }
              },
              "X-SL-Correlation-Id": {
                "description": "Where to trace this refusal.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "403": {
            "description": "Authorization was refused or has been revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OpenAIError"
                }
              }
            },
            "headers": {
              "x-should-retry": {
                "description": "true or false: the router's own verdict on retrying. OpenAI's SDKs read it before retrying 409, 429 and 5xx on their own; an execution whose outcome is uncertain says false, so the library does not send the same money twice.",
                "schema": {
                  "type": "string",
                  "enum": [
                    "true",
                    "false"
                  ]
                }
              },
              "X-SL-Correlation-Id": {
                "description": "Where to trace this refusal.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "408": {
            "description": "The deadline passed before an answer.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OpenAIError"
                }
              }
            },
            "headers": {
              "x-should-retry": {
                "description": "true or false: the router's own verdict on retrying. OpenAI's SDKs read it before retrying 409, 429 and 5xx on their own; an execution whose outcome is uncertain says false, so the library does not send the same money twice.",
                "schema": {
                  "type": "string",
                  "enum": [
                    "true",
                    "false"
                  ]
                }
              },
              "X-SL-Correlation-Id": {
                "description": "Where to trace this refusal.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "409": {
            "description": "The request conflicts with recorded state.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OpenAIError"
                }
              }
            },
            "headers": {
              "x-should-retry": {
                "description": "true or false: the router's own verdict on retrying. OpenAI's SDKs read it before retrying 409, 429 and 5xx on their own; an execution whose outcome is uncertain says false, so the library does not send the same money twice.",
                "schema": {
                  "type": "string",
                  "enum": [
                    "true",
                    "false"
                  ]
                }
              },
              "X-SL-Correlation-Id": {
                "description": "Where to trace this refusal.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "413": {
            "description": "The payload exceeds the limit. It was refused, not truncated.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OpenAIError"
                }
              }
            },
            "headers": {
              "x-should-retry": {
                "description": "true or false: the router's own verdict on retrying. OpenAI's SDKs read it before retrying 409, 429 and 5xx on their own; an execution whose outcome is uncertain says false, so the library does not send the same money twice.",
                "schema": {
                  "type": "string",
                  "enum": [
                    "true",
                    "false"
                  ]
                }
              },
              "X-SL-Correlation-Id": {
                "description": "Where to trace this refusal.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "422": {
            "description": "No configuration qualified. The body names the limiting class of constraint and never which control fired for which candidate.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OpenAIError"
                }
              }
            },
            "headers": {
              "x-should-retry": {
                "description": "true or false: the router's own verdict on retrying. OpenAI's SDKs read it before retrying 409, 429 and 5xx on their own; an execution whose outcome is uncertain says false, so the library does not send the same money twice.",
                "schema": {
                  "type": "string",
                  "enum": [
                    "true",
                    "false"
                  ]
                }
              },
              "X-SL-Correlation-Id": {
                "description": "Where to trace this refusal.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "429": {
            "description": "Admission is at its limit, or the provider throttled every destination.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OpenAIError"
                }
              }
            },
            "headers": {
              "x-should-retry": {
                "description": "true or false: the router's own verdict on retrying. OpenAI's SDKs read it before retrying 409, 429 and 5xx on their own; an execution whose outcome is uncertain says false, so the library does not send the same money twice.",
                "schema": {
                  "type": "string",
                  "enum": [
                    "true",
                    "false"
                  ]
                }
              },
              "X-SL-Correlation-Id": {
                "description": "Where to trace this refusal.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "502": {
            "description": "The provider's outcome is uncertain. Not retryable: it may have run.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OpenAIError"
                }
              }
            },
            "headers": {
              "x-should-retry": {
                "description": "true or false: the router's own verdict on retrying. OpenAI's SDKs read it before retrying 409, 429 and 5xx on their own; an execution whose outcome is uncertain says false, so the library does not send the same money twice.",
                "schema": {
                  "type": "string",
                  "enum": [
                    "true",
                    "false"
                  ]
                }
              },
              "X-SL-Correlation-Id": {
                "description": "Where to trace this refusal.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "503": {
            "description": "A mandatory governance dependency is unavailable, so the request was not admitted. An unrecordable decision is not a decision. Or (ADMISSION_PAUSED, retryable) this environment's operator has stopped admitting new requests: nothing was reserved or sent, and the same request may be sent again later.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OpenAIError"
                }
              }
            },
            "headers": {
              "x-should-retry": {
                "description": "true or false: the router's own verdict on retrying. OpenAI's SDKs read it before retrying 409, 429 and 5xx on their own; an execution whose outcome is uncertain says false, so the library does not send the same money twice.",
                "schema": {
                  "type": "string",
                  "enum": [
                    "true",
                    "false"
                  ]
                }
              },
              "X-SL-Correlation-Id": {
                "description": "Where to trace this refusal.",
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/routing/v1/receipt-keys": {
      "get": {
        "operationId": "listReceiptKeys",
        "summary": "The public keys a receipt may be signed with",
        "description": "A JSON Web Key Set (RFC 7517; RFC 8037 for Ed25519): the router's current receipt-signing key and every previous one its operator still lists after a rotation, a revoked key left out. A gateway checks a receipt with these keys alone - on a sealed network it pins them once. Empty where the deployment signs no receipts, which accepts none. Added by CR-045.",
        "responses": {
          "200": {
            "description": "The keys a receipt may be signed with.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReceiptKeySet"
                }
              }
            }
          },
          "401": {
            "description": "Authentication is required.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/routing/v1/sessions/{session_id}/close": {
      "post": {
        "operationId": "closeSession",
        "summary": "Close an agent session",
        "description": "Ends a session this principal opened, when the application's task is done (CR-048). Present SL-Session-Version, the version last read, as every turn does: a session is not ended over a turn its caller has not read. Refused while a tool round is open - the model's tool calls are outstanding, and their results are owed to it. Closing a session that is already closed answers the closure that stands and changes nothing. A closed session is never continued. A session also closes itself after the deployment's idle period with no turn (24 hours unless the deployment says otherwise), and an administrator may close one.",
        "parameters": [
          {
            "name": "session_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "minLength": 1
            },
            "description": "The session, as openSession returned it."
          },
          {
            "name": "SL-Session-Version",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[0-9]+$"
            },
            "description": "The session's version as last read. A version the session has moved past is refused with STATE_CONFLICT, and nothing is closed."
          }
        ],
        "responses": {
          "200": {
            "description": "The session is closed - by this call, or already, in which case the closure that stands is the answer.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SessionClosed"
                }
              }
            }
          },
          "400": {
            "description": "No session of that identity is available to this caller, or no version was presented.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Authentication is required.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "This principal may not execute, so it has no session to close.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "A tool round is open on the session, or the session moved on since the version presented. Not retried blindly: read the session again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "A mandatory governance dependency is unavailable, so nothing was closed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "CompletionRequest": {
        "type": "object",
        "additionalProperties": false,
        "description": "Unknown properties are refused rather than ignored, because an ignored misspelling silently becomes a different request than the caller wrote.",
        "required": [
          "tenant_id",
          "application_id",
          "purpose",
          "messages"
        ],
        "properties": {
          "tenant_id": {
            "type": "string",
            "minLength": 1
          },
          "application_id": {
            "type": "string",
            "minLength": 1
          },
          "purpose": {
            "type": "string",
            "enum": [
              "SERVE",
              "EVALUATE"
            ],
            "description": "Declared purpose. A purpose grant is checked separately; declaring one does not confer it."
          },
          "messages": {
            "type": "array",
            "minItems": 1,
            "items": {
              "type": "object"
            },
            "description": "Modalities are derived from what the messages actually carry, not from a caller declaration, so a caller cannot understate content to reach a model that may not receive it."
          },
          "temperature": {
            "type": [
              "number",
              "null"
            ],
            "description": "Omit to express no preference. Omission is carried as absent, never as a number."
          },
          "top_p": {
            "type": [
              "number",
              "null"
            ]
          },
          "max_output_tokens": {
            "type": [
              "integer",
              "null"
            ],
            "description": "The most output this request may produce. Omitted or null, it is the application's approved maximum (ADR-34), which the router sends to the provider as the cap - an execution always has one. A value above the application's approved maximum is refused REQUEST_TOO_LARGE, never clamped to fit, and a value below it is sent as given."
          },
          "stream": {
            "type": [
              "boolean",
              "null"
            ]
          },
          "tools": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "$ref": "#/components/schemas/FunctionTool"
            },
            "description": "Tools this request declares. Each entry is validated as the stream pump will read it and forwarded to the provider unchanged."
          },
          "response_format": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/ResponseFormat"
              },
              {
                "type": "null"
              }
            ],
            "description": "The structured output this request demands (MP-022). Omitted or null asks for nothing beyond text. It decides which configurations may take the request: json_object needs a server that guarantees valid JSON, json_schema a server that enforces the schema. Forwarded to the provider as sent."
          },
          "stop": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "type": "string"
            }
          },
          "correlation_id": {
            "type": [
              "string",
              "null"
            ]
          },
          "idempotency_key": {
            "type": [
              "string",
              "null"
            ],
            "minLength": 1,
            "maxLength": 255,
            "description": "Optional. Send one to make a request safe to send again after a lost response (EXE-04, CR-046). A key is scoped to your tenant, your application and the key itself, and is kept for 24 hours, then free again. The first request under a key claims it before anything is sent. The same key with the same request never reaches a provider again: while the first is still being answered it is refused STATE_CONFLICT (retryable); once it has finished, the answer is that operation's record - its decision id, the configuration that served it, its state and its usage - and NO output, because the router keeps no answer (CompletionRecord, answer_kept false). A request that was refused before anything was sent is refused the same way again, and one whose outcome was never established is EXECUTION_UNCERTAIN again; a new attempt takes a new key. A refusal marked retryable (for example ADMISSION_PAUSED) is the exception: it releases the key, so the same key may be sent again once the condition clears. The same key with a different request is refused IDEMPOTENCY_CONFLICT; different work takes a new key. A key that is empty, blank, longer than 255 characters or holds a control character is refused INVALID_REQUEST. Absent or null means no key: the request is sent every time. createSelection reads no key, and createChatCompletion has no such field."
          },
          "model": {
            "type": [
              "string",
              "null"
            ],
            "description": "Naming a model bypasses qualification. Refused with UNSUPPORTED_MODEL_OVERRIDE unless the deployment explicitly permits overrides, and even then it is a preference that carries no grant."
          }
        }
      },
      "CompletionResponse": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "request_id",
          "selected_configuration_id",
          "state",
          "output"
        ],
        "properties": {
          "request_id": {
            "type": "string"
          },
          "selected_configuration_id": {
            "type": "string"
          },
          "state": {
            "$ref": "#/components/schemas/RequestState"
          },
          "output": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "text",
              "finish"
            ],
            "properties": {
              "text": {
                "type": "string",
                "description": "What the model wrote. An empty string means it wrote nothing, which is different from not having answered - `finish` says which."
              },
              "finish": {
                "$ref": "#/components/schemas/StopReason"
              },
              "tool_calls": {
                "type": "array",
                "description": "Absent when the model called no tool.",
                "items": {
                  "$ref": "#/components/schemas/ToolCall"
                }
              },
              "usage": {
                "$ref": "#/components/schemas/CompletionUsage"
              }
            },
            "description": "What the model produced. `tool_calls` and `usage` are ABSENT rather than empty or zero where there are none: an empty array is a value a client must distinguish from 'not applicable', and a zeroed usage would say a model that reported nothing generated nothing. Absent when the provider reported no usage frame."
          },
          "policy_generation": {
            "type": "string"
          },
          "receipt_id": {
            "type": "string"
          },
          "charge": {
            "$ref": "#/components/schemas/Charge",
            "description": "Absent when no charge was resolved. An unresolved charge is reported as unresolved and never as zero."
          },
          "decision_id": {
            "type": "string",
            "description": "The decision this execution was authorized by. Quote it to correlate this response with what was recorded. Absent where the deployment minted none; never a fabricated value, which would name nothing (CR-028, CR-031)."
          },
          "served_configuration_id": {
            "type": "string",
            "description": "The configuration that actually produced this output. The same as `selected_configuration_id` on every request that succeeded first time, and DIFFERENT after an authorized fallback - which is the case you most need to see. Absent where nothing was dispatched."
          }
        }
      },
      "CompletionRecord": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "request_id",
          "state",
          "answer_kept"
        ],
        "description": "The router's record of a completion that already finished, answered to a repeat of its request under the same idempotency_key (CR-046). It is what the router's own decision and attempt records say - the decision, the configuration that served it, its state and its usage - and it carries NO output: the router keeps no answer, so a repeat says the answer is not kept rather than inventing one or sending the request again. Nothing was selected, recorded, admitted or sent for it. Every optional key is OMITTED where the records do not say, never null.",
        "properties": {
          "request_id": {
            "type": "string",
            "description": "This request's own id. The operation it repeats is named by decision_id; the id of the request that first made it is not kept."
          },
          "state": {
            "$ref": "#/components/schemas/RequestState",
            "description": "Where the operation ended: COMPLETED, or INTERRUPTED where output had begun and the stream broke. A refusal and an unknown outcome are not records: they are answered as the error the first request was given."
          },
          "answer_kept": {
            "type": "boolean",
            "enum": [
              false
            ],
            "description": "Always false. The router stores no answer, so there is none to return; capture (CR-007) is the only store of content and is off unless two administrators approved it."
          },
          "decision_id": {
            "type": "string",
            "description": "The decision the operation was authorized by - the one its first answer carried. Quote it to correlate this record with what was recorded."
          },
          "selected_configuration_id": {
            "type": "string"
          },
          "served_configuration_id": {
            "type": "string",
            "description": "The configuration that actually produced the output: the last attempt a provider accepted, which after an authorized fallback is not the selected one. Absent where the records do not establish it."
          },
          "policy_generation": {
            "type": "string"
          },
          "usage": {
            "$ref": "#/components/schemas/CompletionUsage",
            "description": "What the router currently records the provider as having reported for the answer. Absent where nothing was reported, and each count is independently nullable: a null is unknown, never zero."
          }
        }
      },
      "SelectionResponse": {
        "type": "object",
        "additionalProperties": false,
        "description": "What a recommendation may say. No charge and no request state: nothing happened, so there is nothing to bill or settle, and the absence of those fields is the separation made structural rather than promised in prose. A receipt only when the caller asked for one (X-SL-Receipt: issue): a signed statement of the decision, which grants nothing - dispatch revalidates authority, the model, the budget and the deadline live, and the revocation wins.",
        "required": [
          "selected_configuration_id",
          "policy_generation"
        ],
        "properties": {
          "selected_configuration_id": {
            "type": "string",
            "minLength": 1
          },
          "policy_generation": {
            "type": "string",
            "minLength": 1
          },
          "considered": {
            "type": "integer",
            "description": "How many candidates were evaluated."
          },
          "estimate": {
            "$ref": "#/components/schemas/Charge",
            "description": "An estimate is not a charge. Absent when no price could be bounded."
          },
          "decision_id": {
            "type": "string",
            "minLength": 1,
            "description": "This decision's identity in the router's records. Quote it back to correlate a recommendation with what was recorded; two identical requests are two decisions and have two of these. Absent only where the deployment recorded no decision."
          },
          "router_version": {
            "type": "string",
            "minLength": 1,
            "description": "The router bundle whose estimates this decision was made on. The first question after a rollback. Absent where no bundle was consulted, which is the state of a tenant that has activated none."
          },
          "reason_codes": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "uniqueItems": true,
            "description": "The controls that removed the candidates that were not selected, de-duplicated and sorted. Never says which control fired for which candidate: that is a reviewer's question and stays in the decision trace. Empty means the set was computed and nothing was removed; absent means no exclusion set was available."
          },
          "receipt": {
            "type": "string",
            "minLength": 1,
            "description": "The signed receipt, a JSON Web Token in compact form (EdDSA or ES256, typ sl-llmrouter-receipt+jwt) whose payload is an ExecutionReceipt. Check it with the keys listReceiptKeys publishes; no call back to the router is needed. Present only when X-SL-Receipt: issue was sent."
          },
          "receipt_id": {
            "type": "string",
            "minLength": 1,
            "description": "The receipt's jti: the value /routing/v1/dispatch takes. Present only with receipt."
          },
          "receipt_expires_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the receipt stops being accepted: the earliest of the deployment's receipt lifetime, the request's own deadline and five minutes. An expired receipt is not renewable; ask for a new decision. Present only with receipt."
          }
        }
      },
      "Charge": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "resolved"
        ],
        "description": "Money is exact integer micro-units. A float would let rounding decide an allowance.",
        "properties": {
          "resolved": {
            "type": "boolean"
          },
          "micros": {
            "type": "integer",
            "description": "Present only when resolved is true. Absent means not established."
          },
          "currency": {
            "type": "string",
            "minLength": 3,
            "maxLength": 3
          }
        }
      },
      "RequestState": {
        "type": "string",
        "enum": [
          "RECEIVED",
          "SELECTED",
          "ADMITTED",
          "DISPATCHED",
          "COMMITTED",
          "COMPLETED",
          "INTERRUPTED",
          "UNCERTAIN",
          "DENIED"
        ]
      },
      "Error": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "code",
          "message",
          "correlation_id"
        ],
        "description": "Bounded and content-free. The message names classes of constraint and field names, never a field value, a candidate, a prompt or how close anything came to qualifying.",
        "properties": {
          "code": {
            "$ref": "#/components/schemas/ErrorCode"
          },
          "message": {
            "type": "string",
            "maxLength": 512
          },
          "correlation_id": {
            "type": "string",
            "minLength": 1
          },
          "retryable": {
            "type": "boolean",
            "description": "Explicit on every error. EXECUTION_UNCERTAIN, AUTHORIZATION_REVOKED, DESTINATION_FORBIDDEN and RECEIPT_INVALID are never retryable."
          }
        }
      },
      "ErrorCode": {
        "type": "string",
        "enum": [
          "INVALID_REQUEST",
          "UNSUPPORTED_FIELD",
          "UNQUALIFIED_FIELD",
          "REQUEST_TOO_LARGE",
          "UNSUPPORTED_MODEL_OVERRIDE",
          "AUTHENTICATION_REQUIRED",
          "AUTHORIZATION_REVOKED",
          "DESTINATION_FORBIDDEN",
          "RECEIPT_INVALID",
          "SEPARATION_OF_DUTIES",
          "PROMOTION_FORBIDDEN",
          "PROFILE_FORBIDDEN",
          "CAPABILITY_NOT_LICENSED",
          "IDEMPOTENCY_CONFLICT",
          "STATE_CONFLICT",
          "INVALID_TRANSITION",
          "POLICY_INVALID",
          "NO_POLICY_ELIGIBLE_MODEL",
          "NO_CAPABILITY_ELIGIBLE_MODEL",
          "NO_QUALITY_QUALIFIED_MODEL",
          "NO_APPROVED_CAPACITY",
          "BUDGET_LIMIT_EXCEEDED",
          "BUDGET_NOT_BOUNDABLE",
          "ADMISSION_LIMIT_EXCEEDED",
          "ADMISSION_PAUSED",
          "DEADLINE_EXCEEDED",
          "AUDIT_UNAVAILABLE",
          "ROUTER_BUNDLE_UNAVAILABLE",
          "STREAM_INTERRUPTED",
          "EXECUTION_UNCERTAIN",
          "INTERNAL_FAILURE"
        ]
      },
      "DispatchRequest": {
        "type": "object",
        "description": "The original request, re-presented, together with the receipt issued for the decision taken over it. Both halves are required: the receipt names a decision and the request is what that decision was taken about, and the server re-derives the request digest and refuses when it does not match the receipt's. A receipt presented with a different request is a substitution attempt, not a retry.\n\nDeliberately a separate schema rather than CompletionRequest with an optional receipt: an optional receipt would make 'absent' and 'invalid' one code path, which ADR-08 forbids.",
        "required": [
          "request",
          "receipt_id"
        ],
        "additionalProperties": false,
        "properties": {
          "request": {
            "$ref": "#/components/schemas/CompletionRequest",
            "description": "The request exactly as it was presented for selection."
          },
          "receipt_id": {
            "type": "string",
            "minLength": 1,
            "description": "The jti of the receipt issued by /routing/v1/selections with X-SL-Receipt: issue. Evidence of a decision, never an authorization: current authority, the model and the allowance are revalidated at dispatch, and a bound, intact, unexpired receipt is still refused when authority was revoked (AUTHORIZATION_REVOKED) or the model changed (STATE_CONFLICT)."
          }
        }
      },
      "ChatCompletionRequest": {
        "type": "object",
        "description": "OpenAI's chat-completions request shape, accepted as sent.\n\n**No tenancy fields.** tenant_id, application_id and purpose are absent by design, not by omission: no OpenAI client sends them, so requiring them in the body made the compatibility claim unsatisfiable. They are resolved from the authenticated principal and the configuration bound to the API credential (C02). ADR-29.\n\n**No `n`.** More than one completion is not expressible, so it is refused as an unknown field rather than reduced to 1 (ADR-10). The upstream provider refuses n>1 only under greedy sampling and happily returns two choices under sampling, so this refusal is the router's own and cannot be delegated to the provider.\n\nUnknown properties are refused by name and never by value, so a caller sending an OpenAI field the router does not support is told which field rather than having it silently dropped.",
        "required": [
          "model",
          "messages"
        ],
        "additionalProperties": false,
        "properties": {
          "model": {
            "type": "string",
            "minLength": 1,
            "description": "A logical routing profile, not a provider model. A provider model id is refused with UNSUPPORTED_MODEL_OVERRIDE rather than honoured: serving a named model the policy did not authorize is the failure this product exists to prevent. This field is required here, unlike on the native operation, because every OpenAI client sends it and a silent choice of profile would be a silent default."
          },
          "messages": {
            "type": "array",
            "minItems": 1,
            "items": {
              "type": "object"
            },
            "description": "Modalities are derived from what the messages actually carry, not from a caller declaration, so a caller cannot understate content to reach a model that may not receive it."
          },
          "temperature": {
            "type": [
              "number",
              "null"
            ],
            "description": "Omit to express no preference. Omission is carried as absent, never as a number."
          },
          "top_p": {
            "type": [
              "number",
              "null"
            ]
          },
          "max_tokens": {
            "type": [
              "integer",
              "null"
            ],
            "description": "OpenAI's name for the output bound, accepted here because compatibility means accepting the caller's spelling. Carried to max_output_tokens internally. Omit to express no preference; an omitted value is absent, never zero."
          },
          "stream": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "true answers as OpenAI's chunk stream on text/event-stream (CR-044); false or absent in one body. A deployment that serves no stream refuses true with UNSUPPORTED_FIELD naming `stream`."
          },
          "tools": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "$ref": "#/components/schemas/FunctionTool"
            },
            "description": "Tools this request declares. Each entry is validated as the stream pump will read it and forwarded to the provider unchanged."
          },
          "stop": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "type": "string"
            }
          },
          "max_completion_tokens": {
            "type": [
              "integer",
              "null"
            ],
            "description": "OpenAI's newer name for the output bound, accepted beside max_tokens. Send one of the two: both is refused with INVALID_REQUEST rather than one chosen. Carried to max_output_tokens internally."
          },
          "response_format": {
            "$ref": "#/components/schemas/ResponseFormat"
          }
        }
      },
      "StopReason": {
        "type": "string",
        "description": "Why the provider's stream stopped. Never guessed: every value here is one the router observed. `undeclared_tool` and `tool_call_truncated` are deliberately not `protocol_violation` - the second says the request was well formed and a retry with more room may succeed, and the first says nothing of the kind. The provider's terminal reason, never inferred from whether `text` is empty. A caller that has to guess whether a short answer was complete or truncated will guess wrong, and the truncation then arrives as a quality complaint rather than as a limit it can act on. The provider's OWN finish is kept apart from the fact that it finished: `provider_length` is an answer cut at the token limit, `provider_content_filter` a refusal on content, and `provider_finish_unrecognised` a finish this version does not publish. All three are answers rather than failures - the provider did what it was asked - and none is retried, because the same request would meet the same cap. Before these existed all three arrived as `provider_done`, so a truncated answer read as a complete one.",
        "enum": [
          "provider_done",
          "provider_error",
          "client_cancelled",
          "buffer_limit",
          "idle_limit",
          "protocol_violation",
          "stream_ended",
          "caller_abandoned",
          "undeclared_tool",
          "tool_call_truncated",
          "provider_length",
          "provider_content_filter",
          "provider_finish_unrecognised"
        ]
      },
      "FunctionDefinition": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "name"
        ],
        "description": "The function a tool declares. Only two things here are READ by the router: `name`, which is the only name a provider is permitted to call, and `parameters.required`, which the stream pump checks a completed call against - an absent required property is how a truncated generation arrives and is indistinguishable from a deliberate omission once it reaches the caller (ADR-10). The router does no type or value validation of arguments: that is the caller's schema to enforce. The whole entry is forwarded to the provider unchanged. `additionalProperties` is false and the four fields above are the whole OpenAI function-tool surface, so no key a real client sends is forbidden. A key outside that surface is not part of this contract; the router neither reads nor refuses it.",
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "description": "The action. A provider naming anything else stops the stream with `undeclared_tool`."
          },
          "description": {
            "type": "string",
            "description": "Passed to the provider. The router does not read it."
          },
          "parameters": {
            "type": [
              "object",
              "null"
            ],
            "description": "A JSON Schema for the arguments. Absent or null means the tool requires no property, which is a statement rather than a value substituted for a missing one."
          },
          "strict": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "Forwarded to the provider unchanged and NOT interpreted by this router. It is declared because OpenAI-compatible clients commonly send it and the adapter maps `tools` onto the wire verbatim, so it reaches the provider whatever this document said - a contract that forbade it would forbid what the router does. This router implements none of the guarantees the flag names: whether arguments conform to the schema is the provider's behaviour, and the stream pump still checks only that a completed call names a declared tool and carries its required properties."
          }
        }
      },
      "FunctionTool": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "type",
          "function"
        ],
        "description": "One tool the request declares, in the OpenAI-compatible function-tool shape. Pinned by MP-174: this was published as a bare `object`, and the two ends of the serving path had drifted to different shapes behind it - the request edge demanded a top-level `name` and the stream pump reads `function.name` - so no ordinary tool definition could pass through /routing/v1/completions at all. A shape the pump cannot read is now refused at the edge with INVALID_REQUEST rather than raising behind it. A name may not be declared twice: two schemas under one name leave it undecided which properties are required.",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "function"
            ],
            "description": "Only function tools are accepted, because only function tools are enforced. `ToolDeclaration.from_wire` reads `function` whatever this says, so another kind would be silently checked against a function schema - a missing property reported against a promise the caller never made. A second kind therefore arrives with a contract change rather than by being misread."
          },
          "function": {
            "$ref": "#/components/schemas/FunctionDefinition"
          }
        }
      },
      "ToolCall": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "call_id",
          "name",
          "arguments_json",
          "ordinal"
        ],
        "description": "A complete tool call the router RECORDS and forwards. The router never invokes it - execution ownership stays with the caller (EXE-04) - which is why there is no result field here and never will be. An INCOMPLETE call is not delivered at all; it is recorded and the stream stops with `tool_call_truncated`.",
        "properties": {
          "call_id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "arguments_json": {
            "type": "string",
            "description": "The arguments exactly as the provider emitted them, unparsed. Parsing here would make this router decide what a malformed argument meant."
          },
          "ordinal": {
            "type": "integer",
            "minimum": 0,
            "description": "Position in the stream, so several calls keep their order."
          }
        }
      },
      "CompletionUsage": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "input_tokens",
          "output_tokens"
        ],
        "description": "Tokens as the PROVIDER reported them, not as this router estimated them. Each count is independently nullable, because a provider may report one and not the other and a null is UNKNOWN rather than zero. The whole object is absent where the provider reported no usage frame at all.",
        "properties": {
          "input_tokens": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 0,
            "description": "Null means the provider did not report this count. Never read as zero."
          },
          "output_tokens": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 0,
            "description": "Null means the provider did not report this count. Never read as zero."
          }
        }
      },
      "SessionOpened": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "session_id",
          "version"
        ],
        "properties": {
          "session_id": {
            "type": "string",
            "minLength": 1,
            "description": "The session's id, made by the router. Present it as SL-Session-Id."
          },
          "version": {
            "type": "integer",
            "minimum": 0,
            "description": "Always 0 for a session just opened. Present it as SL-Session-Version."
          }
        }
      },
      "ResponseFormat": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "type"
        ],
        "description": "OpenAI's structured output request. `text` asks for nothing; `json_object` for syntactically valid JSON; `json_schema` for output the server holds to `json_schema`, which only that type may carry.",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "text",
              "json_object",
              "json_schema"
            ]
          },
          "json_schema": {
            "$ref": "#/components/schemas/JsonSchemaFormat"
          }
        }
      },
      "JsonSchemaFormat": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "name",
          "schema"
        ],
        "description": "The schema a json_schema response must satisfy. The router routes it only to a configuration that enforces schemas, whatever `strict` says, and forwards `strict` as sent.",
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1
          },
          "schema": {
            "type": "object"
          },
          "strict": {
            "type": "boolean"
          },
          "description": {
            "type": "string"
          }
        }
      },
      "ChatCompletion": {
        "type": "object",
        "additionalProperties": false,
        "description": "OpenAI's chat.completion, for one executed request (CR-043). `model` echoes the routing profile the caller named; which configuration was selected and which served are in the X-SL-* headers. `usage` is ABSENT where the provider did not report both counts - never zero.",
        "required": [
          "id",
          "object",
          "created",
          "model",
          "choices"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "chatcmpl- followed by the router's request id."
          },
          "object": {
            "type": "string",
            "enum": [
              "chat.completion"
            ]
          },
          "created": {
            "type": "integer",
            "description": "Unix seconds."
          },
          "model": {
            "type": "string"
          },
          "choices": {
            "type": "array",
            "minItems": 1,
            "maxItems": 1,
            "items": {
              "$ref": "#/components/schemas/ChatCompletionChoice"
            }
          },
          "usage": {
            "$ref": "#/components/schemas/ChatCompletionUsage"
          }
        }
      },
      "ChatCompletionChoice": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "index",
          "message",
          "finish_reason"
        ],
        "properties": {
          "index": {
            "type": "integer",
            "enum": [
              0
            ]
          },
          "message": {
            "$ref": "#/components/schemas/ChatCompletionMessage"
          },
          "finish_reason": {
            "type": "string",
            "enum": [
              "stop",
              "length",
              "tool_calls",
              "content_filter"
            ],
            "description": "OpenAI's vocabulary. The router's own stop reason is in X-SL-Finish, so a provider finish OpenAI has no word for is not lost."
          }
        }
      },
      "ChatCompletionMessage": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "role",
          "content"
        ],
        "properties": {
          "role": {
            "type": "string",
            "enum": [
              "assistant"
            ]
          },
          "content": {
            "type": "string"
          },
          "tool_calls": {
            "type": "array",
            "description": "Absent when the model called no tool.",
            "items": {
              "$ref": "#/components/schemas/ChatCompletionToolCall"
            }
          }
        }
      },
      "ChatCompletionToolCall": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "id",
          "type",
          "function"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "enum": [
              "function"
            ]
          },
          "function": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "name",
              "arguments"
            ],
            "properties": {
              "name": {
                "type": "string"
              },
              "arguments": {
                "type": "string",
                "description": "The JSON string the provider produced, not re-serialized."
              }
            }
          }
        }
      },
      "ChatCompletionUsage": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "prompt_tokens",
          "completion_tokens",
          "total_tokens"
        ],
        "properties": {
          "prompt_tokens": {
            "type": "integer",
            "minimum": 0
          },
          "completion_tokens": {
            "type": "integer",
            "minimum": 0
          },
          "total_tokens": {
            "type": "integer",
            "minimum": 0
          }
        }
      },
      "OpenAIError": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "error"
        ],
        "description": "OpenAI's error object (CR-043). `code` is the router's published ErrorCode and the HTTP status is the one that code always has. The correlation id and any decision the refusal followed are in X-SL-Correlation-Id and X-SL-Decision-Id.",
        "properties": {
          "error": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "message",
              "type",
              "param",
              "code"
            ],
            "properties": {
              "message": {
                "type": "string",
                "maxLength": 512
              },
              "type": {
                "type": "string",
                "enum": [
                  "invalid_request_error",
                  "authentication_error",
                  "permission_error",
                  "not_found_error",
                  "timeout_error",
                  "rate_limit_error",
                  "api_error"
                ]
              },
              "param": {
                "type": "null"
              },
              "code": {
                "$ref": "#/components/schemas/ErrorCode"
              }
            }
          }
        }
      },
      "ChatCompletionChunk": {
        "type": "object",
        "additionalProperties": false,
        "description": "One frame of a streamed chat completion (CR-044), sent as `data: <json>` and ended by `data: [DONE]`. The first frame carries the assistant's role; the last carries finish_reason, usage where the provider reported both counts, and the router's evidence under x_sl.",
        "required": [
          "id",
          "object",
          "created",
          "model",
          "choices"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "chatcmpl- followed by the router's request id."
          },
          "object": {
            "type": "string",
            "enum": [
              "chat.completion.chunk"
            ]
          },
          "created": {
            "type": "integer",
            "description": "Unix seconds."
          },
          "model": {
            "type": "string"
          },
          "choices": {
            "type": "array",
            "minItems": 1,
            "maxItems": 1,
            "items": {
              "$ref": "#/components/schemas/ChatCompletionChunkChoice"
            }
          },
          "usage": {
            "$ref": "#/components/schemas/ChatCompletionUsage"
          },
          "x_sl": {
            "$ref": "#/components/schemas/StreamEvidence"
          }
        }
      },
      "ChatCompletionChunkChoice": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "index",
          "delta",
          "finish_reason"
        ],
        "properties": {
          "index": {
            "type": "integer",
            "enum": [
              0
            ]
          },
          "delta": {
            "$ref": "#/components/schemas/ChatCompletionDelta"
          },
          "finish_reason": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "stop",
              "length",
              "tool_calls",
              "content_filter",
              null
            ],
            "description": "Null on every frame but the last."
          }
        }
      },
      "ChatCompletionDelta": {
        "type": "object",
        "additionalProperties": false,
        "description": "What this frame adds. Empty on the last frame.",
        "properties": {
          "role": {
            "type": "string",
            "enum": [
              "assistant"
            ]
          },
          "content": {
            "type": "string"
          },
          "tool_calls": {
            "type": "array",
            "description": "One COMPLETE tool call per frame, arguments whole: the router never hands a caller part of an action (EXE-04).",
            "items": {
              "$ref": "#/components/schemas/ChatCompletionChunkToolCall"
            }
          }
        }
      },
      "ChatCompletionChunkToolCall": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "index",
          "id",
          "type",
          "function"
        ],
        "properties": {
          "index": {
            "type": "integer",
            "minimum": 0
          },
          "id": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "enum": [
              "function"
            ]
          },
          "function": {
            "type": "object",
            "required": [
              "name",
              "arguments"
            ],
            "properties": {
              "name": {
                "type": "string"
              },
              "arguments": {
                "type": "string"
              }
            },
            "additionalProperties": false
          }
        }
      },
      "StreamEvidence": {
        "type": "object",
        "additionalProperties": false,
        "description": "The router's evidence on a streamed answer's last frame (CR-044): what the X-SL-* headers carry on a buffered one. Absent keys are unknown, never empty.",
        "required": [
          "request_id",
          "selected_configuration"
        ],
        "properties": {
          "request_id": {
            "type": "string"
          },
          "selected_configuration": {
            "type": "string"
          },
          "served_configuration": {
            "type": "string"
          },
          "policy_generation": {
            "type": "string"
          },
          "decision_id": {
            "type": "string"
          },
          "finish": {
            "type": "string",
            "description": "The router's own stop reason."
          }
        }
      },
      "ReceiptKeySet": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "keys"
        ],
        "description": "A JSON Web Key Set of public receipt keys. Public material only.",
        "properties": {
          "keys": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ReceiptKey"
            }
          }
        }
      },
      "ReceiptKey": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "kty",
          "kid",
          "alg",
          "use",
          "crv",
          "x"
        ],
        "description": "One public JSON Web Key: Ed25519 as OKP (RFC 8037) with x, or P-256 as EC with x and y.",
        "properties": {
          "kty": {
            "type": "string",
            "enum": [
              "OKP",
              "EC"
            ]
          },
          "kid": {
            "type": "string",
            "minLength": 1
          },
          "alg": {
            "type": "string",
            "enum": [
              "EdDSA",
              "ES256"
            ]
          },
          "use": {
            "type": "string",
            "enum": [
              "sig"
            ]
          },
          "crv": {
            "type": "string",
            "enum": [
              "Ed25519",
              "P-256"
            ]
          },
          "x": {
            "type": "string",
            "minLength": 1
          },
          "y": {
            "type": "string",
            "minLength": 1
          }
        }
      },
      "SessionClosed": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "session_id",
          "state",
          "closed_at",
          "reason"
        ],
        "properties": {
          "session_id": {
            "type": "string",
            "minLength": 1,
            "description": "The session closed."
          },
          "state": {
            "type": "string",
            "enum": [
              "CLOSED"
            ],
            "description": "Always CLOSED."
          },
          "closed_at": {
            "type": "string",
            "format": "date-time",
            "description": "When it closed."
          },
          "reason": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "APPLICATION",
              "ADMINISTRATOR",
              "IDLE",
              null
            ],
            "description": "Why it closed: the application ended it, an administrator did, or it took no turn for the deployment's idle period. Null only for a session closed before the reason was recorded."
          }
        }
      }
    },
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer"
      }
    }
  },
  "security": [
    {
      "bearerAuth": []
    }
  ]
}
