{
  "openapi": "3.1.0",
  "info": {
    "title": "llm-gateway",
    "description": "OpenAI-compatible HTTP gateway over rig. Supports streaming chat completions and a static model catalog. Auth: Hydra-issued OAuth2 access token (RS256, RFC 9068, aud=agent-api).",
    "license": {
      "name": ""
    },
    "version": "0.1.0"
  },
  "paths": {
    "/v1/chat": {
      "post": {
        "tags": [
          "Chat"
        ],
        "summary": "AI SDK UI Message Stream chat (streaming-only)",
        "description": "Vercel AI SDK–native chat endpoint, parallel to `/v1/chat/completions`. Accepts a `UIMessage[]` body and streams the AI SDK **UI Message Stream** protocol (typed `data: {\"type\":…}` chunks under `x-vercel-ai-ui-message-stream: v1`, terminated by `data: [DONE]`). Point `useChat` / `DefaultChatTransport` at it.\n\nStreaming-only: `stream: false` returns 400. Top-level fields (`model`, `reasoning_effort`, `tools`, `tool_choice`, `stop`, `temperature`, `top_p`, `max_tokens`, `stream_options`) are identical to `/v1/chat/completions`; only `messages` takes the AI SDK shape.\n\n**Client-side tools only** in v1 — the gateway emits `tool-input-*` (the model's call); clients execute tools and replay results as `tool-<name>` parts with `state: \"output-available\"`. Reasoning replays via `reasoning` parts; `providerMetadata` round-trips the replay payload. See `docs/llm-gateway/AISDK.md` for the chunk union and multi-turn replay contract.",
        "operationId": "handler",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AiSdkChatRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "`text/event-stream` of AI SDK UI Message Stream chunks. Each `data:` line is an `AiSdkChunk`; the stream ends with `data: [DONE]`. A mid-stream upstream failure surfaces as an in-band `error` chunk under HTTP 200 (it replaces `finish`); a 502 is returned only for a pre-stream failure. See `docs/llm-gateway/AISDK.md` for the full union.",
            "content": {
              "text/event-stream": {
                "schema": {
                  "$ref": "#/components/schemas/AiSdkChunk"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request (bad part type, `stream: false`, `max_tokens: 0`, inherited validation 400)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            }
          },
          "502": {
            "description": "Upstream provider error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/v1/chat/completions": {
      "post": {
        "tags": [
          "Chat"
        ],
        "summary": "Chat completions (streaming + non-streaming, with reasoning)",
        "description": "OpenAI-compatible chat completions over an upstream provider chosen by `model`. Set `\"stream\": true` for SSE; otherwise the response is a single JSON body.\n\n### Reasoning\n\nOpt into model thinking with top-level `reasoning_effort` (`minimal | low | medium | high | xhigh | max`). Allowed values are per-model — query `/v1/models` for `reasoning_levels`. Reasoning is **streaming-only**: non-streaming responses drop it entirely.\n\nStreaming reasoning surfaces two channels per `delta`:\n\n- `reasoning_content` — display text for live UI (DeepSeek-style extension; concatenate across chunks like `content`).\n- `reasoning_meta` — structured snapshot at block close, carrying the `signature` (Anthropic) or `encrypted_content` (OpenAI) clients need to replay reasoning on subsequent turns.\n\n### Multi-turn replay\n\nPersist `reasoning_meta` between turns and send it back as a `{\"type\": \"reasoning\", ...}` block on the assistant message — before text and tool calls (Anthropic enforces this order). Required on Anthropic for tool-using turns (the API 400s without the matching thinking block); recommended on OpenAI so the model doesn't re-derive its plan and re-bill reasoning tokens.\n\nOpenAI's Responses API requires `id` on every replayed reasoning block — the gateway 400s pre-flight if missing. Anthropic ignores `id` (signatures live per-block instead).\n\nSignatures and encrypted blobs are **model-bound**: persist `model` alongside the reasoning meta and refuse to replay across models.\n\nSee `docs/llm-gateway/REASONING.md` for the full contract, JSONL persistence shape, and end-to-end curl examples.",
        "operationId": "chat_completions",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ChatRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "`application/json` when `stream: false`; `text/event-stream` when `stream: true` — server emits `data:` events with `ChatChunk` payloads, terminated by `data: [DONE]`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ChatResponse"
                }
              },
              "text/event-stream": {
                "schema": {
                  "$ref": "#/components/schemas/ChatChunk"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            }
          },
          "502": {
            "description": "Upstream provider error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/v1/models": {
      "get": {
        "tags": [
          "Models"
        ],
        "operationId": "list_models",
        "responses": {
          "200": {
            "description": "Supported models",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ModelsResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid Bearer token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            }
          },
          "502": {
            "description": "Supported model missing from Catwalk registry",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    }
  },
  "components": {
    "schemas": {
      "AiSdkChatRequest": {
        "type": "object",
        "description": "Request body for `POST /v1/chat`.\n\nTop-level fields mirror `ChatRequest` (and translate identically); only\n`messages` takes the AI SDK `UIMessage[]` shape instead of `OpenAI` messages.",
        "required": [
          "model",
          "messages"
        ],
        "properties": {
          "max_tokens": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64",
            "minimum": 0
          },
          "messages": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/UiMessage"
            }
          },
          "model": {
            "type": "string",
            "example": "claude-opus-4-7"
          },
          "reasoning_effort": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/ReasoningEffort"
              }
            ]
          },
          "stop": {
            "type": "object"
          },
          "stream": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "Rejected with 400 when `false` — the UI Message Stream is streaming by\ndefinition. Usually absent (`useChat` doesn't send it)."
          },
          "stream_options": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/StreamOptions"
              }
            ]
          },
          "temperature": {
            "type": [
              "number",
              "null"
            ],
            "format": "double"
          },
          "tool_choice": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/ToolChoiceWire"
              }
            ]
          },
          "tools": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "$ref": "#/components/schemas/OpenAiTool"
            }
          },
          "top_p": {
            "type": [
              "number",
              "null"
            ],
            "format": "double"
          }
        }
      },
      "AiSdkChunk": {
        "oneOf": [
          {
            "type": "object",
            "description": "First chunk. `messageId` is `msg_<uuid>`.",
            "required": [
              "messageId",
              "type"
            ],
            "properties": {
              "messageId": {
                "type": "string"
              },
              "type": {
                "type": "string",
                "enum": [
                  "start"
                ]
              }
            }
          },
          {
            "type": "object",
            "description": "Opens the (single, in v1) model step.",
            "required": [
              "type"
            ],
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "start-step"
                ]
              }
            }
          },
          {
            "type": "object",
            "required": [
              "id",
              "type"
            ],
            "properties": {
              "id": {
                "type": "string"
              },
              "type": {
                "type": "string",
                "enum": [
                  "text-start"
                ]
              }
            }
          },
          {
            "type": "object",
            "required": [
              "id",
              "delta",
              "type"
            ],
            "properties": {
              "delta": {
                "type": "string"
              },
              "id": {
                "type": "string"
              },
              "type": {
                "type": "string",
                "enum": [
                  "text-delta"
                ]
              }
            }
          },
          {
            "type": "object",
            "required": [
              "id",
              "type"
            ],
            "properties": {
              "id": {
                "type": "string"
              },
              "type": {
                "type": "string",
                "enum": [
                  "text-end"
                ]
              }
            }
          },
          {
            "type": "object",
            "required": [
              "id",
              "type"
            ],
            "properties": {
              "id": {
                "type": "string"
              },
              "type": {
                "type": "string",
                "enum": [
                  "reasoning-start"
                ]
              }
            }
          },
          {
            "type": "object",
            "required": [
              "id",
              "delta",
              "type"
            ],
            "properties": {
              "delta": {
                "type": "string"
              },
              "id": {
                "type": "string"
              },
              "type": {
                "type": "string",
                "enum": [
                  "reasoning-delta"
                ]
              }
            }
          },
          {
            "type": "object",
            "description": "Carries the replay payload at block close. `providerMetadata` is namespaced\n`{anthropic: {signature?, redactedData?}}` or\n`{openai: {itemId?, reasoningEncryptedContent?}}` — the same keys the\nadapter parses back inbound.",
            "required": [
              "id",
              "type"
            ],
            "properties": {
              "id": {
                "type": "string"
              },
              "providerMetadata": {
                "type": "object"
              },
              "type": {
                "type": "string",
                "enum": [
                  "reasoning-end"
                ]
              }
            }
          },
          {
            "type": "object",
            "description": "`providerExecuted: true` on a server-executed tool's **input** chunks is\nload-bearing, not cosmetic: the AI SDK client decides whether to\ndispatch `onToolCall` (local execution) when it processes\n`tool-input-available` — before any output chunk arrives — gated on\n`!chunk.providerExecuted`. A gateway-run tool whose inputs omit the flag\ngets executed a second time by a contract-following client. Client-local\nsurfaces omit it (`None`); `tool-input-delta` has no such field in the\nSDK schema.",
            "required": [
              "toolCallId",
              "toolName",
              "type"
            ],
            "properties": {
              "providerExecuted": {
                "type": [
                  "boolean",
                  "null"
                ]
              },
              "toolCallId": {
                "type": "string"
              },
              "toolName": {
                "type": "string"
              },
              "type": {
                "type": "string",
                "enum": [
                  "tool-input-start"
                ]
              }
            }
          },
          {
            "type": "object",
            "required": [
              "toolCallId",
              "inputTextDelta",
              "type"
            ],
            "properties": {
              "inputTextDelta": {
                "type": "string"
              },
              "toolCallId": {
                "type": "string"
              },
              "type": {
                "type": "string",
                "enum": [
                  "tool-input-delta"
                ]
              }
            }
          },
          {
            "type": "object",
            "required": [
              "toolCallId",
              "toolName",
              "input",
              "type"
            ],
            "properties": {
              "input": {
                "type": "object"
              },
              "providerExecuted": {
                "type": [
                  "boolean",
                  "null"
                ]
              },
              "toolCallId": {
                "type": "string"
              },
              "toolName": {
                "type": "string"
              },
              "type": {
                "type": "string",
                "enum": [
                  "tool-input-available"
                ]
              }
            }
          },
          {
            "type": "object",
            "description": "Server-executed tool result (the #107 server-side loop). Pairs with a\nprior `tool-input-available` of the same `toolCallId`. `providerExecuted`\nis `true` for gateway-run tools so `useChat` renders the result rather than\ndispatching the call back to the client to run.",
            "required": [
              "toolCallId",
              "output",
              "type"
            ],
            "properties": {
              "output": {
                "type": "object"
              },
              "providerExecuted": {
                "type": [
                  "boolean",
                  "null"
                ]
              },
              "toolCallId": {
                "type": "string"
              },
              "type": {
                "type": "string",
                "enum": [
                  "tool-output-available"
                ]
              }
            }
          },
          {
            "type": "object",
            "description": "Server-executed tool failure — the error-shaped sibling of\n`tool-output-available` (same `toolCallId` pairing, `errorText` in place of\n`output`).",
            "required": [
              "toolCallId",
              "errorText",
              "type"
            ],
            "properties": {
              "errorText": {
                "type": "string"
              },
              "providerExecuted": {
                "type": [
                  "boolean",
                  "null"
                ]
              },
              "toolCallId": {
                "type": "string"
              },
              "type": {
                "type": "string",
                "enum": [
                  "tool-output-error"
                ]
              }
            }
          },
          {
            "type": "object",
            "description": "Closes the step.",
            "required": [
              "type"
            ],
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "finish-step"
                ]
              }
            }
          },
          {
            "type": "object",
            "description": "App-defined metadata; carries `{usage: {inputTokens, outputTokens, totalTokens}}`\nwhen `stream_options.include_usage` is set.",
            "required": [
              "messageMetadata",
              "type"
            ],
            "properties": {
              "messageMetadata": {
                "type": "object"
              },
              "type": {
                "type": "string",
                "enum": [
                  "message-metadata"
                ]
              }
            }
          },
          {
            "type": "object",
            "description": "Terminal logical chunk (followed by `data: [DONE]`).",
            "required": [
              "type"
            ],
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "finish"
                ]
              }
            }
          },
          {
            "type": "object",
            "description": "Mid-stream upstream error; replaces `finish`. `[DONE]` still follows.",
            "required": [
              "errorText",
              "type"
            ],
            "properties": {
              "errorText": {
                "type": "string"
              },
              "type": {
                "type": "string",
                "enum": [
                  "error"
                ]
              }
            }
          }
        ],
        "description": "A single chunk on the UI Message Stream, serialized as\n`{\"type\": \"<kebab-case>\", ...}`.\n\nVariant set + field names are the subset of the AI SDK `UIMessageChunk`\nunion this gateway emits. The `tool-output-*` pair is protocol vocabulary for\nthe #107 server-side tool loop (which executes a tool and streams its result\nback as `providerExecuted: true`); llm-gateway's v1 `/v1/chat` is single-step,\nclient-side tools only, so it emits `tool-input-*` but never these. Client-side\nvalidation is a zod `strictObject`: do not add fields here without confirming\nthey exist on the corresponding chunk."
      },
      "AssistantMessage": {
        "type": "object",
        "required": [
          "role",
          "content"
        ],
        "properties": {
          "content": {
            "type": "string"
          },
          "role": {
            "type": "string",
            "example": "assistant"
          }
        }
      },
      "ChatChunk": {
        "type": "object",
        "required": [
          "id",
          "object",
          "created",
          "model",
          "choices"
        ],
        "properties": {
          "choices": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ChunkChoice"
            },
            "description": "Per `OpenAI`'s spec: normally a one-element array carrying the delta;\non the final usage chunk (`stream_options.include_usage`) it's `[]`\nand the `usage` field is populated instead."
          },
          "created": {
            "type": "integer",
            "format": "int64"
          },
          "id": {
            "type": "string"
          },
          "model": {
            "type": "string"
          },
          "object": {
            "type": "string",
            "example": "chat.completion.chunk"
          },
          "usage": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/Usage",
                "description": "Populated only on the final `include_usage` chunk; omitted otherwise."
              }
            ]
          }
        }
      },
      "ChatMessage": {
        "type": "object",
        "required": [
          "role"
        ],
        "properties": {
          "content": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/MessageContent"
              }
            ]
          },
          "role": {
            "type": "string",
            "example": "user"
          },
          "tool_call_id": {
            "type": [
              "string",
              "null"
            ]
          },
          "tool_calls": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "$ref": "#/components/schemas/ToolCallWire"
            }
          }
        }
      },
      "ChatRequest": {
        "type": "object",
        "description": "OpenAI-compatible chat completions request.\n\n`model` picks the upstream provider (see `/v1/models`). Set\n`\"stream\": true` to receive `text/event-stream` of `ChatChunk` deltas\ninstead of a single `ChatResponse` body. Tool-calling and reasoning are\nboth **streaming-only** in v1.",
        "required": [
          "model",
          "messages",
          "stop"
        ],
        "properties": {
          "max_tokens": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64",
            "example": 1024,
            "minimum": 0
          },
          "messages": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ChatMessage"
            }
          },
          "model": {
            "type": "string",
            "example": "gpt-5.5"
          },
          "reasoning_effort": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/ReasoningEffort"
              }
            ]
          },
          "stop": {
            "type": "object"
          },
          "stream": {
            "type": [
              "boolean",
              "null"
            ],
            "example": false
          },
          "stream_options": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/StreamOptions"
              }
            ]
          },
          "temperature": {
            "type": [
              "number",
              "null"
            ],
            "format": "double",
            "example": 0.7
          },
          "tool_choice": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/ToolChoiceWire"
              }
            ]
          },
          "tools": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "$ref": "#/components/schemas/OpenAiTool"
            }
          },
          "top_p": {
            "type": [
              "number",
              "null"
            ],
            "format": "double"
          }
        }
      },
      "ChatResponse": {
        "type": "object",
        "required": [
          "id",
          "object",
          "created",
          "model",
          "choices",
          "usage"
        ],
        "properties": {
          "choices": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Choice"
            }
          },
          "created": {
            "type": "integer",
            "format": "int64"
          },
          "id": {
            "type": "string"
          },
          "model": {
            "type": "string",
            "example": "gpt-5.5"
          },
          "object": {
            "type": "string",
            "example": "chat.completion"
          },
          "usage": {
            "$ref": "#/components/schemas/Usage"
          }
        }
      },
      "Choice": {
        "type": "object",
        "required": [
          "index",
          "message"
        ],
        "properties": {
          "finish_reason": {
            "type": [
              "string",
              "null"
            ],
            "example": "stop"
          },
          "index": {
            "type": "integer",
            "format": "int32",
            "minimum": 0
          },
          "message": {
            "$ref": "#/components/schemas/AssistantMessage"
          }
        }
      },
      "ChunkChoice": {
        "type": "object",
        "required": [
          "index",
          "delta"
        ],
        "properties": {
          "delta": {
            "$ref": "#/components/schemas/Delta"
          },
          "finish_reason": {
            "type": [
              "string",
              "null"
            ],
            "example": "stop"
          },
          "index": {
            "type": "integer",
            "format": "int32",
            "minimum": 0
          }
        }
      },
      "ContentPart": {
        "oneOf": [
          {
            "type": "object",
            "required": [
              "text",
              "type"
            ],
            "properties": {
              "text": {
                "type": "string"
              },
              "type": {
                "type": "string",
                "enum": [
                  "text"
                ]
              }
            }
          },
          {
            "type": "object",
            "description": "Replayed reasoning block.\n\nSend back on assistant turns to preserve the model's internal state\nacross multi-turn conversations. Required by Anthropic on any assistant\nturn that includes a tool call (the API 400s without the original\nthinking block); recommended on `OpenAI` so the model doesn't re-derive\nits plan and re-bill reasoning tokens.\n\n`id` is the provider-issued reasoning identifier. `OpenAI`'s Responses\nAPI requires it on replay (the gateway 400s pre-flight if missing).\nAnthropic ignores it — its integrity check lives on each block's\n`signature`.\n\nOn the assistant `content` array, reasoning blocks must precede text\nand tool calls — Anthropic enforces this ordering.\n\nSee `docs/llm-gateway/REASONING.md` for the full replay contract, JSONL\npersistence shape, and per-provider examples.",
            "required": [
              "content",
              "type"
            ],
            "properties": {
              "content": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/ReasoningPart"
                }
              },
              "id": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "type": {
                "type": "string",
                "enum": [
                  "reasoning"
                ]
              }
            }
          },
          {
            "type": "object",
            "description": "Inline image attachment on a user turn.\n\n`image_url.url` is either an `https://...` URL (forwarded verbatim;\nthe provider fetches it) or a `data:image/{mime};base64,...` data URL\n(decoded at the gateway and forwarded as base64 + media type).\n`image_url.detail` is `OpenAI`'s rendering hint; Anthropic ignores it.",
            "required": [
              "image_url",
              "type"
            ],
            "properties": {
              "image_url": {
                "$ref": "#/components/schemas/ImageUrlPart"
              },
              "type": {
                "type": "string",
                "enum": [
                  "image_url"
                ]
              }
            }
          }
        ],
        "description": "One part of a structured message body.\n\n`MessageContent` may be either a plain string or an array of these parts.\nThe `text` variant is the common case; `reasoning` is used on assistant\nturns to replay the model's prior thinking — see [reasoning replay\ndocs](https://github.com/Peteskiis/agents/blob/main/docs/llm-gateway/REASONING.md)\nfor the full contract."
      },
      "Delta": {
        "type": "object",
        "description": "Per-chunk delta payload in a streaming completion (`ChatChunk.choices[].delta`).\n\nFields are sparse — only the ones that changed in this chunk are present.\nClients accumulate `content` across chunks for the assistant message text,\n`reasoning_content` for live thinking display, and merge `tool_calls[]` by\n`index` for tool-calling flows. `reasoning_meta` is the structured replay\nsnapshot emitted once per reasoning block close — persist it verbatim.",
        "properties": {
          "content": {
            "type": [
              "string",
              "null"
            ]
          },
          "reasoning_content": {
            "type": [
              "string",
              "null"
            ]
          },
          "reasoning_meta": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/ReasoningMeta",
                "description": "Structured reasoning snapshot emitted at block close.\n\nOne chunk per reasoning block from the upstream provider. On `OpenAI`\na single logical reasoning item streams as multiple consecutive metas\nthat all share the same `id` (one per summary part + one for the\nencrypted blob) — clients can either store them separately and let the\ngateway merge on replay, or merge by `id` client-side.\n\nPersist verbatim and replay on the next turn as a\n`ContentPart::Reasoning` on the assistant message. The structured\npayload (signature/encrypted/redacted) cannot be reconstructed from\n`reasoning_content` (display text) alone.\n\nModel-bound: refuse to replay across models — signatures and encrypted\nblobs are tied to the model that generated them. Persist `model`\nalongside.\n\nSee `docs/llm-gateway/REASONING.md` for the full replay contract."
              }
            ]
          },
          "role": {
            "type": [
              "string",
              "null"
            ]
          },
          "tool_calls": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "$ref": "#/components/schemas/ToolCallChunkWire"
            }
          }
        }
      },
      "ErrorBody": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "$ref": "#/components/schemas/ErrorDetail"
          }
        }
      },
      "ErrorDetail": {
        "type": "object",
        "required": [
          "message",
          "type"
        ],
        "properties": {
          "message": {
            "type": "string",
            "example": "model not supported: foo"
          },
          "type": {
            "type": "string",
            "example": "invalid_request_error"
          }
        }
      },
      "ImageDetailWire": {
        "type": "string",
        "description": "Per-image rendering hint (`OpenAI`-only; Anthropic ignores it).\n\nMirrors rig's `ImageDetail` but stays wire-side so `utoipa::ToSchema` and\n`serde::Deserialize` are owned by this crate, not rig.",
        "enum": [
          "low",
          "high",
          "auto"
        ]
      },
      "ImageUrlPart": {
        "type": "object",
        "description": "OpenAI-shaped image part payload.\n\nMatches the Chat Completions `{\"type\": \"image_url\", \"image_url\": {...}}` wire\nshape. `url` may be an `https://...` URL or a `data:image/...;base64,...`\nURL. `detail` is optional and defaults (at the gateway) to the rig\n`ImageDetail::Auto` semantics.",
        "required": [
          "url"
        ],
        "properties": {
          "detail": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/ImageDetailWire"
              }
            ]
          },
          "url": {
            "type": "string"
          }
        }
      },
      "MessageContent": {
        "oneOf": [
          {
            "type": "string"
          },
          {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ContentPart"
            }
          }
        ]
      },
      "ModelInfo": {
        "type": "object",
        "required": [
          "id",
          "object",
          "created",
          "owned_by",
          "context_window",
          "default_max_tokens",
          "supports_attachments",
          "can_reason",
          "pricing"
        ],
        "properties": {
          "can_reason": {
            "type": "boolean"
          },
          "context_window": {
            "type": "integer",
            "format": "int64",
            "example": 1000000,
            "minimum": 0
          },
          "created": {
            "type": "integer",
            "format": "int64"
          },
          "default_max_tokens": {
            "type": "integer",
            "format": "int64",
            "example": 126000,
            "minimum": 0
          },
          "default_reasoning_effort": {
            "type": [
              "string",
              "null"
            ]
          },
          "id": {
            "type": "string",
            "example": "claude-opus-4-7"
          },
          "object": {
            "type": "string",
            "example": "model"
          },
          "owned_by": {
            "type": "string",
            "example": "anthropic"
          },
          "pricing": {
            "$ref": "#/components/schemas/Pricing"
          },
          "reasoning_levels": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "supports_attachments": {
            "type": "boolean"
          }
        }
      },
      "ModelsResponse": {
        "type": "object",
        "required": [
          "object",
          "data"
        ],
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ModelInfo"
            }
          },
          "object": {
            "type": "string",
            "example": "list"
          }
        }
      },
      "OpenAiTool": {
        "type": "object",
        "required": [
          "type",
          "function"
        ],
        "properties": {
          "function": {
            "$ref": "#/components/schemas/OpenAiToolFunction"
          },
          "type": {
            "$ref": "#/components/schemas/ToolKind"
          }
        }
      },
      "OpenAiToolFunction": {
        "type": "object",
        "required": [
          "name"
        ],
        "properties": {
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "name": {
            "type": "string"
          },
          "parameters": {
            "type": "object"
          }
        }
      },
      "Pricing": {
        "type": "object",
        "description": "Per-mtok pricing for one model.\n\nCache rates (#157) are provider-reconciled in `agent_core::catwalk` (Catwalk's\nraw `*_cached` fields mean different things per provider).\n`cache_write_per_mtok` is `None` where the provider charges no cache-write\npremium (`OpenAI`) or a model has no cache pricing.",
        "required": [
          "input_per_mtok",
          "output_per_mtok",
          "currency"
        ],
        "properties": {
          "cache_read_per_mtok": {
            "type": [
              "number",
              "null"
            ],
            "format": "double",
            "description": "Cache-read rate (≈0.1× input). Omitted when the model has no cache pricing.",
            "example": 0.5
          },
          "cache_write_per_mtok": {
            "type": [
              "number",
              "null"
            ],
            "format": "double",
            "description": "Cache-write rate (Anthropic cache creation ≈1.25× input). Omitted when\nthe provider has no cache-write charge.",
            "example": 6.25
          },
          "currency": {
            "type": "string",
            "example": "USD"
          },
          "input_per_mtok": {
            "type": "number",
            "format": "double",
            "example": 5
          },
          "output_per_mtok": {
            "type": "number",
            "format": "double",
            "example": 25
          }
        }
      },
      "ReasoningEffort": {
        "type": "string",
        "description": "Per-turn reasoning depth selector. Catwalk-validated per model.\n\nWire string forms (the serde `rename_all = \"lowercase\"` form): `minimal`,\n`low`, `medium`, `high`, `xhigh`, `max`. Provider-specific allowed lists\nare enforced by [`validate_effort_for_model`].",
        "enum": [
          "minimal",
          "low",
          "medium",
          "high",
          "xhigh",
          "max"
        ]
      },
      "ReasoningMeta": {
        "type": "object",
        "description": "Structured reasoning snapshot emitted at block close.\n\nMirrors the inbound `ContentPart::Reasoning` shape so clients can persist\nthis verbatim and round-trip it on the next turn's assistant message.\n`id` carries the provider's reasoning identifier (required by `OpenAI` on\nreplay; ignored by Anthropic).",
        "required": [
          "content"
        ],
        "properties": {
          "content": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ReasoningPart"
            }
          },
          "id": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "ReasoningPart": {
        "oneOf": [
          {
            "type": "object",
            "required": [
              "text",
              "type"
            ],
            "properties": {
              "signature": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "text": {
                "type": "string"
              },
              "type": {
                "type": "string",
                "enum": [
                  "text"
                ]
              }
            }
          },
          {
            "type": "object",
            "required": [
              "data",
              "type"
            ],
            "properties": {
              "data": {
                "type": "string"
              },
              "type": {
                "type": "string",
                "enum": [
                  "encrypted"
                ]
              }
            }
          },
          {
            "type": "object",
            "required": [
              "text",
              "type"
            ],
            "properties": {
              "text": {
                "type": "string"
              },
              "type": {
                "type": "string",
                "enum": [
                  "summary"
                ]
              }
            }
          },
          {
            "type": "object",
            "required": [
              "data",
              "type"
            ],
            "properties": {
              "data": {
                "type": "string"
              },
              "type": {
                "type": "string",
                "enum": [
                  "redacted"
                ]
              }
            }
          }
        ],
        "description": "One element of a replayed reasoning block.\n\nMirrors what providers emit:\n\n- `text` — a thinking-text block (Anthropic carries an integrity\n  `signature`; `OpenAI` summary parts use `summary` instead).\n- `summary` — `OpenAI` Responses-API summary text.\n- `encrypted` — opaque blob `OpenAI` emits under\n  `reasoning.encrypted_content` so clients can replay reasoning items\n  when the upstream is stateless (`store: false`).\n- `redacted` — opaque data preserved when Anthropic filters thinking\n  content; replay verbatim to keep the block sequence intact.\n\nOn replay, send every part the gateway emitted for a given reasoning\nitem back unmodified — providers reject tampered signatures and 404\nreasoning ids whose content is missing under `store: false`."
      },
      "SpecificFunction": {
        "type": "object",
        "required": [
          "name"
        ],
        "properties": {
          "name": {
            "type": "string"
          }
        }
      },
      "SpecificKind": {
        "type": "string",
        "enum": [
          "function"
        ]
      },
      "StreamOptions": {
        "type": "object",
        "description": "Streaming-only options. Mirrors `OpenAI`'s `stream_options` field.\n\nSet `include_usage: true` to receive one final SSE chunk before\n`data: [DONE]` carrying `usage` token counts (`choices: []` on that\nchunk per spec). Silently ignored when `stream: false`.",
        "properties": {
          "include_usage": {
            "type": [
              "boolean",
              "null"
            ]
          }
        }
      },
      "ToolCallChunkWire": {
        "type": "object",
        "required": [
          "index"
        ],
        "properties": {
          "function": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/ToolCallFunctionWire"
              }
            ]
          },
          "id": {
            "type": [
              "string",
              "null"
            ]
          },
          "index": {
            "type": "integer",
            "format": "int32",
            "minimum": 0
          },
          "type": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "ToolCallFunctionInWire": {
        "type": "object",
        "required": [
          "name",
          "arguments"
        ],
        "properties": {
          "arguments": {
            "type": "string"
          },
          "name": {
            "type": "string"
          }
        }
      },
      "ToolCallFunctionWire": {
        "type": "object",
        "properties": {
          "arguments": {
            "type": [
              "string",
              "null"
            ]
          },
          "name": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "ToolCallWire": {
        "type": "object",
        "required": [
          "id",
          "function"
        ],
        "properties": {
          "function": {
            "$ref": "#/components/schemas/ToolCallFunctionInWire"
          },
          "id": {
            "type": "string"
          }
        }
      },
      "ToolChoiceMode": {
        "type": "string",
        "enum": [
          "auto",
          "none",
          "required"
        ]
      },
      "ToolChoiceWire": {
        "oneOf": [
          {
            "$ref": "#/components/schemas/ToolChoiceMode"
          },
          {
            "type": "object",
            "required": [
              "type",
              "function"
            ],
            "properties": {
              "function": {
                "$ref": "#/components/schemas/SpecificFunction"
              },
              "type": {
                "$ref": "#/components/schemas/SpecificKind"
              }
            }
          }
        ]
      },
      "ToolKind": {
        "type": "string",
        "enum": [
          "function"
        ]
      },
      "UiMessage": {
        "type": "object",
        "description": "One `UIMessage` (a turn). `parts` carries the typed content.",
        "required": [
          "role"
        ],
        "properties": {
          "id": {
            "type": [
              "string",
              "null"
            ]
          },
          "parts": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/UiPart"
            }
          },
          "role": {
            "type": "string",
            "example": "user"
          }
        }
      },
      "UiPart": {
        "type": "object",
        "description": "One `UIMessage` part.\n\nDeserialized as a flat permissive struct rather than a tagged enum: the AI\nSDK `tool-<name>` part type embeds the tool name in the discriminant, which\nserde's `#[serde(tag)]` can't match. Adapters dispatch on `kind` and read\nonly the fields relevant to that type. Unknown fields are tolerated on\ninput; on output, `None` fields are omitted entirely.",
        "required": [
          "type"
        ],
        "properties": {
          "errorText": {
            "type": [
              "string",
              "null"
            ]
          },
          "input": {
            "type": "object"
          },
          "mediaType": {
            "type": [
              "string",
              "null"
            ]
          },
          "output": {
            "type": "object"
          },
          "providerExecuted": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "`true` on server-executed tool parts (this workspace's chat surface\nemits it; the persistence writer must round-trip it so reloaded\nhistory renders as server-run, not client-dispatchable)."
          },
          "providerMetadata": {
            "type": "object"
          },
          "state": {
            "type": [
              "string",
              "null"
            ],
            "description": "Tool-part lifecycle: `input-available` | `output-available` | `output-error`."
          },
          "text": {
            "type": [
              "string",
              "null"
            ]
          },
          "toolCallId": {
            "type": [
              "string",
              "null"
            ]
          },
          "type": {
            "type": "string",
            "description": "`text` | `file` | `reasoning` | `tool-<name>` | `step-start` | …\n(each service decides which it accepts).",
            "example": "text"
          },
          "url": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "Usage": {
        "type": "object",
        "required": [
          "prompt_tokens",
          "completion_tokens",
          "total_tokens"
        ],
        "properties": {
          "cache_creation_input_tokens": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64",
            "description": "Tokens written to Anthropic's prompt cache on this request. Present\nonly on Anthropic-bound traffic when caching actually wrote bytes\n(typically the first request in a session, including `max_tokens: 0`\nwarming requests). Omitted on OpenAI-bound traffic.",
            "minimum": 0
          },
          "cache_read_input_tokens": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64",
            "description": "Tokens served from Anthropic's prompt cache on this request — i.e.\nthe cache hit. Present only when the request's prefix matched a live\ncached prefix. Omitted on cold reads and on OpenAI-bound traffic.",
            "minimum": 0
          },
          "completion_tokens": {
            "type": "integer",
            "format": "int64",
            "minimum": 0
          },
          "prompt_tokens": {
            "type": "integer",
            "format": "int64",
            "minimum": 0
          },
          "total_tokens": {
            "type": "integer",
            "format": "int64",
            "minimum": 0
          }
        }
      }
    },
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "JWT"
      }
    }
  },
  "tags": [
    {
      "name": "Chat",
      "description": "Chat completions (streaming and non-streaming)"
    },
    {
      "name": "Models",
      "description": "Supported model catalog"
    }
  ]
}
