{
  "openapi": "3.1.0",
  "info": {
    "title": "AI Crucible API",
    "version": "1.0.0",
    "description": "OpenAI-compatible REST API for running AI Crucible ensemble strategies (multiple LLMs orchestrated through a strategy and judged or synthesized by an arbiter model), evaluating model responses with AI judges, and reading usage.\n\n**API access is Pro-only.** API keys can only be created, listed and rotated by users on the Pro subscription tier (`requireProTier` in the key-management functions), and every API request re-checks that the key owner still has an active Pro subscription; otherwise it is rejected with 403. Keys are managed in the dashboard under Settings > API Keys; they are not created through this API.\n\nAuthentication: send the key as `Authorization: Bearer <key>`. Keys have the form `sk-aicruc_<random>`.\n\nErrors from authenticated endpoints are JSON of the form `{\"error\": {\"message\": string, \"type\": string}}`. There is no `code` field.\n\nLong-running multi-model strategies can take several minutes; the server timeout is 540 seconds.",
    "contact": {
      "name": "AI Crucible Support",
      "email": "support@ai-crucible.com",
      "url": "https://ai-crucible.com"
    }
  },
  "servers": [
    {
      "url": "https://ai-crucible.com",
      "description": "Production. Firebase Hosting rewrites `/v1/**` to the `api` Cloud Function. Paths below include the `/v1` prefix, so OpenAI SDK clients should use `https://ai-crucible.com/v1` as their base URL."
    }
  ],
  "security": [
    {
      "ApiKeyAuth": []
    }
  ],
  "tags": [
    {
      "name": "Chat",
      "description": "OpenAI-compatible chat completions with ensemble orchestration."
    },
    {
      "name": "Responses",
      "description": "OpenAI Responses-style endpoint with single-string input."
    },
    {
      "name": "Evaluations",
      "description": "Score model responses with AI judge models."
    },
    {
      "name": "Usage",
      "description": "Token and cost usage for the authenticated user."
    }
  ],
  "paths": {
    "/v1/chat/completions": {
      "post": {
        "tags": ["Chat"],
        "operationId": "createChatCompletion",
        "summary": "Create a chat completion using an ensemble strategy",
        "description": "Runs the selected ensemble strategy. The root `model` is the arbiter/judge; `ai_crucible.models` are the participants. The last message is the prompt; earlier non-system messages become conversation history; the first system message is used as the arbiter system prompt unless `ai_crucible.systemPrompt` is set. With `stream: true` the response is Server-Sent Events.\n\nSet `model` to `crucible/auto` and Autopilot reads the prompt, then picks the route (one model or an ensemble), strategy, models, rounds and judge within the Speed and Cost you set. The response reports the plan in `ai_crucible.plan`.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ChatCompletionRequest"
              },
              "examples": {
                "autopilot": {
                  "summary": "Autopilot chooses the strategy and models",
                  "value": {
                    "model": "crucible/auto",
                    "messages": [
                      {
                        "role": "user",
                        "content": "Should a five-person startup move from Heroku to AWS this year?"
                      }
                    ],
                    "ai_crucible": {
                      "speed": "balanced",
                      "cost": "standard"
                    }
                  }
                },
                "ensemble": {
                  "summary": "Choose the strategy and models yourself",
                  "value": {
                    "model": "claude-sonnet-5",
                    "messages": [
                      {
                        "role": "system",
                        "content": "You are a precise technical editor. Pick the most accurate answer."
                      },
                      {
                        "role": "user",
                        "content": "Explain the difference between optimistic and pessimistic locking in two paragraphs."
                      }
                    ],
                    "temperature": 0.7,
                    "stream": false,
                    "ai_crucible": {
                      "strategy": "competitive_refinement",
                      "rounds": 2,
                      "models": ["gemini-3.8-flash", "gpt-5.4-mini"],
                      "includeCandidates": true,
                      "includeReasoning": true
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Completion. JSON when `stream` is false or omitted; an SSE stream of `ChatCompletionChunk` objects terminated by `data: [DONE]` when `stream` is true.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ChatCompletionResponse"
                }
              },
              "text/event-stream": {
                "schema": {
                  "type": "string",
                  "description": "Server-Sent Events. Each event is `data: <JSON>` followed by a blank line. Payloads are `ChatCompletionChunk` objects (or `StreamError`), and the stream ends with `data: [DONE]`."
                },
                "x-itemSchema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/ChatCompletionChunk"
                    },
                    {
                      "$ref": "#/components/schemas/StreamError"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Invalid request (empty/missing `messages`, `temperature` outside 0-2, non-positive `max_tokens`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "message": "Invalid messages array",
                    "type": "invalid_request_error"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "parameters": [
          {
            "name": "x-crucible-speed",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "enum": ["fast", "balanced", "thorough"]
            },
            "description": "`crucible/auto` only. Same as `ai_crucible.speed`; the body field wins."
          },
          {
            "name": "x-crucible-cost",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "enum": ["lean", "standard", "max"]
            },
            "description": "`crucible/auto` only. Same as `ai_crucible.cost`; the body field wins."
          }
        ]
      }
    },
    "/v1/responses": {
      "post": {
        "tags": ["Responses"],
        "operationId": "createResponse",
        "summary": "Create a response from a single input string",
        "description": "Responses-style endpoint. `input` must be a string. Pass a previous `id` as `previous_response_id` to continue a conversation. If `ai_crucible.strategy` is omitted, the strategy is taken from `model` when it contains \"ensemble\" or \"panel\", otherwise a single-model run is requested. With `stream: true` the response is SSE using the same chat.completion.chunk format as /v1/chat/completions.\n\nSet `model` to `crucible/auto` and Autopilot reads the prompt, then picks the route (one model or an ensemble), strategy, models, rounds and judge within the Speed and Cost you set. The response reports the plan in `ai_crucible.plan`.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ResponsesRequest"
              },
              "examples": {
                "autopilot": {
                  "summary": "Autopilot chooses the strategy and models",
                  "value": {
                    "model": "crucible/auto",
                    "input": "Should a five-person startup move from Heroku to AWS this year?",
                    "ai_crucible": {
                      "speed": "balanced",
                      "cost": "standard"
                    }
                  }
                },
                "ensemble": {
                  "summary": "Choose the strategy and models yourself",
                  "value": {
                    "model": "gemini-3.8-flash",
                    "input": "Summarize the trade-offs between SQLite and PostgreSQL for a small SaaS app.",
                    "stream": false,
                    "ai_crucible": {
                      "strategy": "collaborative_synthesis"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Response object, or an SSE stream when `stream` is true.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ResponsesResponse"
                }
              },
              "text/event-stream": {
                "schema": {
                  "type": "string",
                  "description": "Server-Sent Events carrying `ChatCompletionChunk` objects (not Responses-API event types), ending with `data: [DONE]`."
                },
                "x-itemSchema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/ChatCompletionChunk"
                    },
                    {
                      "$ref": "#/components/schemas/StreamError"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "`input` missing or not a string.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "message": "Invalid input string",
                    "type": "invalid_request_error"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "parameters": [
          {
            "name": "x-crucible-speed",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "enum": ["fast", "balanced", "thorough"]
            },
            "description": "`crucible/auto` only. Same as `ai_crucible.speed`; the body field wins."
          },
          {
            "name": "x-crucible-cost",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "enum": ["lean", "standard", "max"]
            },
            "description": "`crucible/auto` only. Same as `ai_crucible.cost`; the body field wins."
          }
        ]
      }
    },
    "/v1/evaluations": {
      "post": {
        "tags": ["Evaluations"],
        "operationId": "createEvaluation",
        "summary": "Evaluate model responses with AI judges",
        "description": "Scores each response on accuracy, creativity, clarity, completeness and usefulness using one or more judge models. Multiple judges produce consensus scores.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EvaluationRequest"
              },
              "example": {
                "prompt": "What is the capital of France?",
                "responses": [
                  {
                    "modelId": "gemini-3.8-flash",
                    "modelName": "Gemini 3.8 Flash",
                    "response": "The capital of France is Paris, home of the Eiffel Tower."
                  },
                  {
                    "modelId": "gpt-5.4-mini",
                    "modelName": "GPT-5.4 Mini",
                    "response": "Paris is the capital of France."
                  }
                ],
                "judge_models": [
                  {
                    "id": "claude-sonnet-5",
                    "name": "Claude Sonnet 5"
                  }
                ],
                "evaluation_mode": "standard",
                "weighted": false
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Evaluation results.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EvaluationResponse"
                }
              }
            }
          },
          "400": {
            "description": "Validation failed (missing prompt, empty responses or judge_models, missing fields, unsupported strategy).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "message": "Invalid request: judge_models is required and must be a non-empty array",
                    "type": "invalid_request_error"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/usage": {
      "get": {
        "tags": ["Usage"],
        "operationId": "getUsage",
        "summary": "Get usage statistics",
        "description": "Aggregates monthly usage documents covering the requested date range (at most 24 months). Defaults to the start of the current month through now.",
        "parameters": [
          {
            "name": "start_date",
            "in": "query",
            "required": false,
            "description": "Start of range. Either a Unix timestamp in seconds (digits only) or any date string parseable by JavaScript `Date` (e.g. ISO 8601). Default: first day of the current month.",
            "schema": {
              "type": "string"
            },
            "example": "2026-09-01"
          },
          {
            "name": "end_date",
            "in": "query",
            "required": false,
            "description": "End of range, same formats as `start_date`. Default: now.",
            "schema": {
              "type": "string"
            },
            "example": "1790000000"
          }
        ],
        "responses": {
          "200": {
            "description": "Usage list.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UsageResponse"
                }
              }
            }
          },
          "400": {
            "description": "Invalid date format.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "message": "Invalid date format",
                    "type": "invalid_request_error"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "ApiKeyAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "sk-aicruc_<base62>",
        "description": "API key sent as `Authorization: Bearer sk-aicruc_...`. Keys are created in the dashboard (Pro tier only) and are stored hashed; revoked or inactive keys are rejected with 401."
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "Missing, malformed, invalid, revoked or inactive API key.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "message": "Unauthorized: Invalid API key",
                "type": "invalid_request_error"
              }
            }
          }
        }
      },
      "Forbidden": {
        "description": "The API key is valid but its owner no longer has an active Pro subscription (tier `pro` with status `active` or `trialing`). The subscription is re-checked on every request, so keys stop working after a downgrade, cancellation or failed payment; changes take effect within about a minute. Other Firebase `permission-denied` errors also map to 403.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "message": "API access requires an active Pro subscription. Please upgrade or renew your plan.",
                "type": "permission_error"
              }
            }
          }
        }
      },
      "RateLimited": {
        "description": "Rate limit exceeded. Limits are per user over a one-minute window; Pro allows 30 requests per minute.",
        "headers": {
          "Retry-After": {
            "description": "Seconds to wait before retrying.",
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "message": "Rate limit exceeded",
                "type": "rate_limit_error"
              }
            }
          }
        }
      },
      "InternalError": {
        "description": "Internal error, including orchestration failures.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": ["error"],
        "properties": {
          "error": {
            "type": "object",
            "required": ["message", "type"],
            "properties": {
              "message": {
                "type": "string",
                "description": "Human-readable error message."
              },
              "type": {
                "type": "string",
                "description": "Error category. `invalid_request_error` unless the thrown error carries its own type (e.g. `rate_limit_error`).",
                "examples": ["invalid_request_error", "permission_error", "rate_limit_error"]
              }
            }
          }
        }
      },
      "StreamError": {
        "type": "object",
        "description": "Emitted inside an SSE stream when orchestration fails after headers were sent. The stream then ends (no `[DONE]` for orchestration error events).",
        "required": ["error"],
        "properties": {
          "error": {
            "type": "string",
            "description": "Error message (a plain string, not an object)."
          }
        }
      },
      "EnsembleStrategy": {
        "type": "string",
        "enum": [
          "competitive_refinement",
          "collaborative_synthesis",
          "expert_panel",
          "debate_tournament",
          "hierarchical",
          "chain_of_thought",
          "red_team_blue_team"
        ],
        "description": "Ensemble strategy ID. On /v1/chat/completions an unknown value silently falls back to `competitive_refinement` (the default)."
      },
      "ModelId": {
        "type": "string",
        "description": "Model identifier: an AI Crucible model id (lowercase, hyphen/dot separated, e.g. `gemini-3.8-flash`, `claude-sonnet-5`) or a provider model name from the model catalog. Unknown values are passed through as-is. The listed examples are the currently non-deprecated ids; the default participant model is `gemini-3.8-flash`. Use `crucible/auto` to let Autopilot choose the strategy, models and judge (see `AiCrucibleOptions.speed` and `cost`).",
        "examples": [
          "gemini-3.8-flash",
          "gemini-3.5-flash",
          "gemini-3.1-pro",
          "gemini-3.5-flash-lite",
          "gemini-3.1-flash-lite",
          "gemini-3-flash",
          "gpt",
          "gpt-4-mini",
          "gpt-5.4",
          "gpt-5.4-mini",
          "gpt-5.4-nano",
          "gpt-6-astra",
          "gpt-5.6-sol",
          "gpt-5.6-terra",
          "gpt-5.6-luna",
          "gpt-5.5",
          "gpt-5.5-pro",
          "claude-fable-5-1",
          "claude-opus-5",
          "claude-sonnet-5",
          "claude-haiku-4-5",
          "grok-4.6",
          "grok-4.5",
          "grok-4.3",
          "grok-4.20-beta",
          "mistral-large-3",
          "mistral-medium-3-5",
          "mistral-small-4",
          "ministral-3-14b",
          "ministral-3-8b",
          "ministral-3-3b",
          "glm-5-3",
          "glm-5-3-flash",
          "glm-5-2",
          "qwen-3.8-max",
          "qwen-3.8-flash",
          "qwen-3.7-max",
          "qwen-3.7-plus",
          "qwen-3.6-flash",
          "qwen-3.5",
          "qwen-flash",
          "kimi-k3",
          "kimi-k2.6",
          "deepseek-v4-pro",
          "deepseek-v4-flash"
        ]
      },
      "ChatMessage": {
        "type": "object",
        "required": ["role", "content"],
        "properties": {
          "role": {
            "type": "string",
            "enum": ["system", "user", "assistant", "tool"]
          },
          "content": {
            "type": "string",
            "description": "Message text."
          },
          "name": {
            "type": "string"
          },
          "tool_calls": {
            "type": "array",
            "items": {
              "type": "object"
            },
            "description": "Accepted for compatibility; not used."
          }
        }
      },
      "AiCrucibleOptions": {
        "type": "object",
        "description": "AI Crucible extension object. Controls ensemble orchestration. Additional unknown properties are accepted and ignored.",
        "additionalProperties": true,
        "properties": {
          "strategy": {
            "$ref": "#/components/schemas/EnsembleStrategy"
          },
          "rounds": {
            "type": "integer",
            "minimum": 1,
            "maximum": 10,
            "default": 1,
            "description": "Number of refinement rounds. Non-integers are floored; values outside 1-10 are clamped; non-numbers fall back to 1."
          },
          "iterations": {
            "type": "integer",
            "minimum": 1,
            "maximum": 10,
            "default": 1,
            "description": "Alias for `rounds`. If both are sent, `iterations` wins."
          },
          "models": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ModelId"
            },
            "description": "Participant models that generate competing responses. Model identifiers may be passed as strings (or `{id}`/`{name}` objects, which are also resolved)."
          },
          "includeFollowUpPrompts": {
            "type": "boolean",
            "default": false,
            "description": "Generate follow-up prompt suggestions."
          },
          "includeCandidates": {
            "type": "boolean",
            "default": false,
            "description": "Include individual participant responses in `choices[].candidates`."
          },
          "includeReasoning": {
            "type": "boolean",
            "default": false,
            "description": "Include the arbiter's explanation. If omitted, it is enabled whenever `reasoning.effort` is set."
          },
          "systemPrompt": {
            "type": "string",
            "maxLength": 100000,
            "description": "Instructions for the arbiter/synthesis model. Truncated to 100,000 characters; empty string is treated as absent."
          },
          "speed": {
            "type": "string",
            "enum": ["fast", "balanced", "thorough"],
            "default": "balanced",
            "description": "`crucible/auto` only. How long Autopilot may take: up to 1 round at `fast`, 2 at `balanced` and 3 at `thorough`; at Fast with Lean cost, low-stakes questions go to one model. Overrides the `x-crucible-speed` header. Other values return 400."
          },
          "cost": {
            "type": "string",
            "enum": ["lean", "standard", "max"],
            "default": "standard",
            "description": "`crucible/auto` only. The cost level Autopilot plans at: panel size and model tier (economy, standard or premium models). When Autopilot is unsure how to read a question it may step `standard` up to `max` (never `lean`), and it steps down when the balance cannot cover the plan, reported in `ai_crucible.plan.downgraded_for_balance`; `ai_crucible.plan.speed_cost.cost` is the level used. Overrides the `x-crucible-cost` header. Other values return 400."
          }
        }
      },
      "Reasoning": {
        "type": "object",
        "description": "OpenAI-style reasoning control. Setting `effort` (any value) enables the explanation field unless `ai_crucible.includeReasoning` is set explicitly. The effort level itself is not used.",
        "additionalProperties": true,
        "properties": {
          "effort": {
            "type": "string",
            "enum": ["low", "medium", "high"]
          }
        }
      },
      "ChatCompletionRequest": {
        "type": "object",
        "required": ["model", "messages"],
        "properties": {
          "model": {
            "$ref": "#/components/schemas/ModelId",
            "description": "Arbiter/judge model. If `ai_crucible.models` is omitted, this model is also the sole participant. `crucible/auto` hands the strategy, models and arbiter to Autopilot, and `ai_crucible.models`, `strategy` and `rounds` are ignored."
          },
          "messages": {
            "type": "array",
            "minItems": 1,
            "items": {
              "$ref": "#/components/schemas/ChatMessage"
            },
            "description": "Conversation. The last message is the prompt."
          },
          "stream": {
            "type": "boolean",
            "default": false,
            "description": "Return Server-Sent Events."
          },
          "temperature": {
            "type": "number",
            "minimum": 0,
            "maximum": 2,
            "description": "Applied to the final arbiter/synthesis step."
          },
          "top_p": {
            "type": "number",
            "description": "Applied to the final arbiter/synthesis step."
          },
          "max_tokens": {
            "type": "integer",
            "exclusiveMinimum": 0,
            "description": "Validated (must be > 0) but not forwarded to models."
          },
          "n": {
            "type": "integer",
            "description": "Accepted for OpenAI compatibility; ignored (always one choice)."
          },
          "stop": {
            "oneOf": [
              {
                "type": "string"
              },
              {
                "type": "array",
                "items": {
                  "type": "string"
                }
              }
            ],
            "description": "Accepted for compatibility; ignored."
          },
          "presence_penalty": {
            "type": "number",
            "description": "Accepted for compatibility; ignored."
          },
          "frequency_penalty": {
            "type": "number",
            "description": "Accepted for compatibility; ignored."
          },
          "logit_bias": {
            "type": "object",
            "additionalProperties": {
              "type": "number"
            },
            "description": "Accepted for compatibility; ignored."
          },
          "user": {
            "type": "string",
            "description": "Accepted for compatibility; ignored."
          },
          "reasoning": {
            "$ref": "#/components/schemas/Reasoning"
          },
          "ai_crucible": {
            "$ref": "#/components/schemas/AiCrucibleOptions"
          }
        }
      },
      "Candidate": {
        "type": "object",
        "required": ["modelId", "modelName", "response"],
        "properties": {
          "modelId": {
            "type": "string"
          },
          "modelName": {
            "type": "string"
          },
          "response": {
            "type": "string"
          },
          "tokenUsage": {
            "type": "object",
            "properties": {
              "promptTokens": {
                "type": "integer"
              },
              "completionTokens": {
                "type": "integer"
              },
              "totalTokens": {
                "type": "integer"
              }
            }
          },
          "executionTimeMs": {
            "type": "number"
          },
          "cost": {
            "type": "number",
            "description": "Cost in USD."
          }
        }
      },
      "ChatCompletionResponse": {
        "type": "object",
        "required": ["id", "object", "created", "model", "choices", "usage"],
        "properties": {
          "id": {
            "type": "string",
            "description": "`chatcmpl-<ms timestamp>`."
          },
          "object": {
            "type": "string",
            "const": "chat.completion"
          },
          "created": {
            "type": "integer",
            "description": "Unix seconds."
          },
          "model": {
            "type": "string",
            "description": "Echo of the request `model`."
          },
          "choices": {
            "type": "array",
            "items": {
              "type": "object",
              "required": ["index", "message", "finish_reason"],
              "properties": {
                "index": {
                  "type": "integer"
                },
                "message": {
                  "type": "object",
                  "required": ["role", "content"],
                  "properties": {
                    "role": {
                      "type": "string",
                      "const": "assistant"
                    },
                    "content": {
                      "type": "string",
                      "description": "Best answer or synthesis."
                    },
                    "explanation": {
                      "type": "string",
                      "description": "Arbiter's explanation (when reasoning is enabled and the strategy produces one)."
                    }
                  }
                },
                "finish_reason": {
                  "type": "string",
                  "const": "stop"
                },
                "candidates": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Candidate"
                  },
                  "description": "Participant responses, when available from the strategy."
                }
              }
            }
          },
          "usage": {
            "type": "object",
            "required": ["prompt_tokens", "completion_tokens", "total_tokens"],
            "properties": {
              "prompt_tokens": {
                "type": "integer"
              },
              "completion_tokens": {
                "type": "integer"
              },
              "total_tokens": {
                "type": "integer"
              },
              "input_tokens": {
                "type": "integer",
                "description": "Same as prompt_tokens."
              },
              "output_tokens": {
                "type": "integer",
                "description": "Same as completion_tokens."
              },
              "input_token_details": {
                "type": "object",
                "properties": {
                  "text_tokens": {
                    "type": "integer"
                  },
                  "audio_tokens": {
                    "type": "integer"
                  },
                  "image_tokens": {
                    "type": "integer"
                  },
                  "cached_tokens": {
                    "type": "integer"
                  },
                  "cached_tokens_details": {
                    "type": "object",
                    "properties": {
                      "text_tokens": {
                        "type": "integer"
                      },
                      "audio_tokens": {
                        "type": "integer"
                      },
                      "image_tokens": {
                        "type": "integer"
                      }
                    }
                  }
                }
              },
              "output_token_details": {
                "type": "object",
                "properties": {
                  "text_tokens": {
                    "type": "integer"
                  },
                  "audio_tokens": {
                    "type": "integer"
                  }
                }
              }
            }
          },
          "ai_crucible": {
            "type": "object",
            "description": "`crucible/auto` only.",
            "properties": {
              "plan": {
                "$ref": "#/components/schemas/AutopilotPlan"
              }
            }
          }
        }
      },
      "ChatCompletionChunk": {
        "type": "object",
        "required": ["id", "object", "created", "model", "choices"],
        "description": "SSE payload. Content deltas carry the final synthesized/best answer. Progress chunks carry an empty delta plus `status`. A `candidates` chunk may be sent before the end. A usage chunk (empty `choices`) is sent when tokens were consumed, followed by a final chunk with `finish_reason: \"stop\"`, then `data: [DONE]`. `stream_options` is not read. For `crucible/auto`, a chunk with an empty delta and `ai_crucible.plan` comes first.",
        "properties": {
          "id": {
            "type": "string"
          },
          "object": {
            "type": "string",
            "const": "chat.completion.chunk"
          },
          "created": {
            "type": "integer"
          },
          "model": {
            "type": "string"
          },
          "choices": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "index": {
                  "type": "integer"
                },
                "delta": {
                  "type": "object",
                  "properties": {
                    "content": {
                      "type": "string"
                    }
                  }
                },
                "finish_reason": {
                  "type": ["string", "null"],
                  "enum": ["stop", null]
                },
                "explanation": {
                  "type": "string",
                  "description": "Present on the final chunk when the arbiter produced an explanation."
                }
              }
            }
          },
          "status": {
            "type": "string",
            "description": "Progress message (progress chunks only)."
          },
          "candidates": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "model": {
                  "type": "string"
                },
                "text": {
                  "type": "string"
                },
                "reasoning": {
                  "type": "string"
                }
              }
            }
          },
          "usage": {
            "type": "object",
            "properties": {
              "prompt_tokens": {
                "type": "integer"
              },
              "completion_tokens": {
                "type": "integer"
              },
              "total_tokens": {
                "type": "integer"
              },
              "unifiedTokens": {
                "type": "number",
                "description": "AI Crucible billing units."
              },
              "input_tokens": {
                "type": "integer"
              },
              "output_tokens": {
                "type": "integer"
              },
              "input_token_details": {
                "type": "object"
              },
              "output_token_details": {
                "type": "object"
              }
            }
          },
          "ai_crucible": {
            "type": "object",
            "description": "`crucible/auto` only.",
            "properties": {
              "plan": {
                "$ref": "#/components/schemas/AutopilotPlan"
              }
            }
          }
        }
      },
      "ResponsesRequest": {
        "type": "object",
        "required": ["model", "input"],
        "properties": {
          "model": {
            "$ref": "#/components/schemas/ModelId",
            "description": "Model to run; also the arbiter. Required in practice (strategy detection reads it). `crucible/auto` hands the strategy, models and arbiter to Autopilot."
          },
          "input": {
            "type": "string",
            "minLength": 1,
            "description": "Prompt text. Arrays are rejected."
          },
          "previous_response_id": {
            "type": "string",
            "description": "Accepted for compatibility but has no effect: /v1/responses does not store conversations, so each call runs without earlier history."
          },
          "stream": {
            "type": "boolean",
            "default": false
          },
          "temperature": {
            "type": "number"
          },
          "top_p": {
            "type": "number"
          },
          "reasoning": {
            "$ref": "#/components/schemas/Reasoning"
          },
          "ai_crucible": {
            "$ref": "#/components/schemas/AiCrucibleOptions"
          }
        }
      },
      "ResponsesResponse": {
        "type": "object",
        "required": ["id", "object", "created", "model", "output", "usage"],
        "properties": {
          "id": {
            "type": "string",
            "description": "`resp_<chatId>` when a chat id is available, otherwise `resp_<ms timestamp>`."
          },
          "object": {
            "type": "string",
            "const": "response"
          },
          "created": {
            "type": "integer"
          },
          "model": {
            "type": "string"
          },
          "output": {
            "type": "array",
            "items": {
              "type": "object",
              "required": ["type", "id"],
              "properties": {
                "type": {
                  "type": "string",
                  "enum": ["message", "reasoning", "function_call", "tool_call"],
                  "description": "Currently always `message`."
                },
                "id": {
                  "type": "string"
                },
                "role": {
                  "type": "string",
                  "const": "assistant"
                },
                "content": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "required": ["type", "text"],
                    "properties": {
                      "type": {
                        "type": "string",
                        "const": "text"
                      },
                      "text": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          },
          "usage": {
            "type": "object",
            "required": ["input_tokens", "output_tokens", "total_tokens"],
            "properties": {
              "input_tokens": {
                "type": "integer"
              },
              "output_tokens": {
                "type": "integer"
              },
              "total_tokens": {
                "type": "integer"
              }
            }
          },
          "ai_crucible": {
            "type": "object",
            "description": "`crucible/auto` only.",
            "properties": {
              "plan": {
                "$ref": "#/components/schemas/AutopilotPlan"
              }
            }
          }
        }
      },
      "EvaluationRequest": {
        "type": "object",
        "required": ["prompt", "responses", "judge_models"],
        "properties": {
          "prompt": {
            "type": "string",
            "description": "The original prompt the responses answer."
          },
          "responses": {
            "type": "array",
            "minItems": 1,
            "items": {
              "type": "object",
              "required": ["modelId", "modelName", "response"],
              "properties": {
                "modelId": {
                  "$ref": "#/components/schemas/ModelId"
                },
                "modelName": {
                  "type": "string"
                },
                "response": {
                  "type": "string",
                  "minLength": 1
                },
                "role": {
                  "type": "string",
                  "enum": [
                    "blueTeam",
                    "redTeam",
                    "whiteTeam",
                    "proposition",
                    "opposition",
                    "judge",
                    "strategist",
                    "implementer",
                    "reviewer"
                  ],
                  "description": "Team role for role-aware evaluation."
                }
              }
            }
          },
          "judge_models": {
            "type": "array",
            "minItems": 1,
            "items": {
              "type": "object",
              "required": ["id", "name"],
              "properties": {
                "id": {
                  "$ref": "#/components/schemas/ModelId"
                },
                "name": {
                  "type": "string"
                }
              }
            }
          },
          "strategy": {
            "type": "string",
            "enum": ["standard", "speed_pair", "round_robin", "competitive_refinement"],
            "description": "Strategy context. Other values are rejected with 400."
          },
          "evaluation_mode": {
            "type": "string",
            "enum": ["standard", "pointwise"],
            "default": "standard",
            "description": "Any value other than `pointwise` is treated as `standard`."
          },
          "weighted": {
            "type": "boolean",
            "default": false
          }
        }
      },
      "EvaluationCriteria": {
        "type": "object",
        "description": "Scores 0-10. Role-aware evaluations may carry additional criteria.",
        "required": ["accuracy", "creativity", "clarity", "completeness", "usefulness"],
        "additionalProperties": {
          "type": "number"
        },
        "properties": {
          "accuracy": {
            "type": "number"
          },
          "creativity": {
            "type": "number"
          },
          "clarity": {
            "type": "number"
          },
          "completeness": {
            "type": "number"
          },
          "usefulness": {
            "type": "number"
          }
        }
      },
      "ModelEvaluation": {
        "type": "object",
        "required": ["modelId", "modelName", "response", "criteria", "overallScore", "reasoning"],
        "properties": {
          "modelId": {
            "type": "string"
          },
          "modelName": {
            "type": "string"
          },
          "response": {
            "type": "string"
          },
          "criteria": {
            "$ref": "#/components/schemas/EvaluationCriteria"
          },
          "overallScore": {
            "type": "number"
          },
          "reasoning": {
            "type": "string"
          },
          "role": {
            "type": "string"
          },
          "roleSpecificCriteria": {
            "type": "object",
            "additionalProperties": {
              "type": "number"
            }
          },
          "individualScores": {
            "type": "array",
            "items": {
              "type": "object",
              "required": ["judgeId", "judgeName", "criteria", "overallScore", "reasoning"],
              "properties": {
                "judgeId": {
                  "type": "string"
                },
                "judgeName": {
                  "type": "string"
                },
                "criteria": {
                  "$ref": "#/components/schemas/EvaluationCriteria"
                },
                "overallScore": {
                  "type": "number"
                },
                "reasoning": {
                  "type": "string"
                }
              }
            }
          },
          "overallScoreVariance": {
            "type": "number"
          },
          "scoredByJudges": {
            "type": "integer"
          },
          "totalJudges": {
            "type": "integer"
          },
          "notScored": {
            "type": "boolean",
            "description": "True when no judge produced a usable score; scores are 0 placeholders."
          },
          "divergencePercent": {
            "type": "number",
            "minimum": 0,
            "maximum": 100
          },
          "weighted": {
            "type": "boolean"
          },
          "evaluationMode": {
            "type": "string",
            "enum": ["standard", "pointwise"]
          }
        }
      },
      "EvaluationResponse": {
        "type": "object",
        "required": ["evaluations", "usage"],
        "properties": {
          "evaluations": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ModelEvaluation"
            }
          },
          "usage": {
            "type": "object",
            "required": ["prompt_tokens", "completion_tokens", "total_tokens", "total_cost"],
            "properties": {
              "prompt_tokens": {
                "type": "integer"
              },
              "completion_tokens": {
                "type": "integer"
              },
              "total_tokens": {
                "type": "integer"
              },
              "total_cost": {
                "type": "number",
                "description": "USD."
              }
            }
          }
        }
      },
      "UsageResponse": {
        "type": "object",
        "required": ["object", "data", "has_more"],
        "properties": {
          "object": {
            "type": "string",
            "const": "list"
          },
          "has_more": {
            "type": "boolean",
            "const": false
          },
          "data": {
            "type": "array",
            "maxItems": 1,
            "items": {
              "type": "object",
              "required": [
                "aggregation_timestamp",
                "n_requests",
                "n_context_tokens_total",
                "n_generated_tokens_total",
                "total_tokens",
                "total_cost",
                "period",
                "breakdown",
                "apiKeyBreakdown"
              ],
              "properties": {
                "aggregation_timestamp": {
                  "type": "integer",
                  "description": "Unix seconds."
                },
                "n_requests": {
                  "type": "integer"
                },
                "n_context_tokens_total": {
                  "type": "integer"
                },
                "n_generated_tokens_total": {
                  "type": "integer"
                },
                "total_tokens": {
                  "type": "integer"
                },
                "total_cost": {
                  "type": "number",
                  "description": "USD."
                },
                "period": {
                  "type": "object",
                  "required": ["start", "end"],
                  "properties": {
                    "start": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "end": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                },
                "breakdown": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "model_id": {
                        "type": "string"
                      },
                      "model_name": {
                        "type": "string"
                      },
                      "n_requests": {
                        "type": "integer"
                      },
                      "cost": {
                        "type": "number"
                      },
                      "n_context_tokens": {
                        "type": "integer"
                      },
                      "n_generated_tokens": {
                        "type": "integer"
                      }
                    }
                  }
                },
                "apiKeyBreakdown": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "keyName": {
                        "type": "string"
                      },
                      "requests": {
                        "type": "integer"
                      },
                      "totalTokens": {
                        "type": "integer"
                      },
                      "cost": {
                        "type": "number"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "AutopilotPlan": {
        "type": "object",
        "description": "The plan Autopilot ran for a `crucible/auto` request.",
        "required": [
          "route",
          "strategy",
          "team_id",
          "models",
          "arbiter_model",
          "rounds",
          "speed_cost",
          "downgraded_for_balance",
          "estimate",
          "explanation"
        ],
        "properties": {
          "route": {
            "type": "string",
            "enum": ["single", "ensemble"],
            "description": "One model, or an ensemble."
          },
          "strategy": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/EnsembleStrategy"
              },
              {
                "type": "null"
              }
            ],
            "description": "Null for the single route."
          },
          "team_id": {
            "type": ["string", "null"],
            "description": "The team, when the plan uses one."
          },
          "models": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ModelId"
            }
          },
          "arbiter_model": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/ModelId"
              },
              {
                "type": "null"
              }
            ]
          },
          "rounds": {
            "type": "integer"
          },
          "speed_cost": {
            "type": "object",
            "properties": {
              "speed": {
                "type": "string",
                "enum": ["fast", "balanced", "thorough"]
              },
              "cost": {
                "type": "string",
                "enum": ["lean", "standard", "max"],
                "description": "The cost level actually used, after any step-down for balance."
              }
            }
          },
          "downgraded_for_balance": {
            "type": "boolean"
          },
          "estimate": {
            "type": "object",
            "properties": {
              "min_credits": {
                "type": "number"
              },
              "max_credits": {
                "type": "number"
              },
              "seconds": {
                "type": "number"
              }
            },
            "description": "Estimate made before the run, in credits (unified tokens)."
          },
          "explanation": {
            "type": "string",
            "description": "One sentence on why this plan."
          }
        }
      }
    }
  }
}
