{
  "openapi": "3.1.0",
  "info": {
    "title": "Palette API",
    "version": "2026-10-20",
    "summary": "OpenAI-compatible chat completions with reasoning_effort, served by Palette.",
    "description": "Base URL https://llm.pltt.dev/v1. Seven routes: GET /v1/health (no auth); GET /v1/models, POST /v1/chat/completions, POST /v1/decision/systemone, GET /v1/decision/models, POST /v1/ocr, GET /v1/ocr/models (Bearer key). Chat is the OpenAI chat-completions shape plus reasoning_effort (low | medium | high | xhigh); reasoning is returned in reasoning_content separate from content. palette-island answers tool calls only (send `tools`). Decision and OCR are Palette services proxied behind the same key. Every field, status code and string here is what the gateway does on 2026-10-20.",
    "contact": {
      "name": "Palette",
      "url": "https://www.pltt.xyz"
    },
    "license": {
      "name": "Apache 2.0",
      "identifier": "Apache-2.0"
    }
  },
  "servers": [
    {
      "url": "https://llm.pltt.dev",
      "description": "Palette gateway. Routes are under /v1."
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "tags": [
    {
      "name": "health",
      "description": "Upstream status per seat. No key needed."
    },
    {
      "name": "chat",
      "description": "Chat completions with reasoning_effort, non-stream or Server-Sent Events."
    },
    {
      "name": "models",
      "description": "The five seats, one alias and the two sibling services behind the model field."
    },
    {
      "name": "decision",
      "description": "Palette Decision: typed questions with calibrated probabilities."
    },
    {
      "name": "ocr",
      "description": "Palette Light OCR: Korean line OCR on one image."
    }
  ],
  "paths": {
    "/v1/health": {
      "get": {
        "tags": [
          "health"
        ],
        "operationId": "getHealth",
        "summary": "Health of every seat",
        "description": "Probes each seat's upstream /v1/models with a 20 s timeout. Always returns 200; a failing seat changes the body, not the status code. ok is true only when every configured seat returned 200.",
        "security": [],
        "responses": {
          "200": {
            "description": "Per-seat upstream status.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Health"
                },
                "examples": {
                  "allUp": {
                    "summary": "Every seat answers 200",
                    "value": {
                      "ok": true,
                      "seats": {
                        "palette-llm": 200,
                        "palette-llm-max": 200,
                        "palette-vlm": 200,
                        "palette-llm-ko": 200
                      }
                    }
                  },
                  "oneDown": {
                    "summary": "One seat timed out",
                    "value": {
                      "ok": false,
                      "seats": {
                        "palette-llm": 200,
                        "palette-llm-max": 200,
                        "palette-vlm": 200,
                        "palette-llm-ko": "ConnectTimeout"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/models": {
      "get": {
        "tags": [
          "models"
        ],
        "operationId": "listModels",
        "summary": "List seats and aliases",
        "description": "Lists the five seats (palette-llm, palette-llm-max, palette-vlm, palette-llm-ko, palette-island) with tier, configured flag and modalities; chat seats carry the accepted reasoning_effort values, palette-island carries requires: tools. Then the alias palette-llm-fast and two service entries, palette-decision (/v1/decision/systemone) and palette-light-ocr (/v1/ocr), reachable with the same key.",
        "responses": {
          "200": {
            "description": "Model list.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ModelList"
                },
                "example": {
                  "object": "list",
                  "data": [
                    {
                      "id": "palette-llm",
                      "object": "model",
                      "owned_by": "palette",
                      "backing": "Palette LLM",
                      "configured": true,
                      "modalities": [
                        "text"
                      ],
                      "reasoning_effort": [
                        "low",
                        "medium",
                        "high",
                        "xhigh"
                      ]
                    },
                    {
                      "id": "palette-llm-max",
                      "object": "model",
                      "owned_by": "palette",
                      "backing": "Palette LLM Max",
                      "configured": true,
                      "modalities": [
                        "text",
                        "image"
                      ],
                      "reasoning_effort": [
                        "low",
                        "medium",
                        "high",
                        "xhigh"
                      ]
                    },
                    {
                      "id": "palette-vlm",
                      "object": "model",
                      "owned_by": "palette",
                      "backing": "Palette Vision",
                      "configured": true,
                      "modalities": [
                        "text",
                        "image"
                      ],
                      "reasoning_effort": [
                        "low",
                        "medium",
                        "high",
                        "xhigh"
                      ]
                    },
                    {
                      "id": "palette-llm-ko",
                      "object": "model",
                      "owned_by": "palette",
                      "backing": "palette-llm",
                      "configured": true,
                      "modalities": [
                        "text",
                        "image"
                      ],
                      "reasoning_effort": [
                        "low",
                        "medium",
                        "high",
                        "xhigh"
                      ]
                    },
                    {
                      "id": "palette-llm-fast",
                      "object": "model",
                      "owned_by": "palette",
                      "alias_of": "palette-vlm"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/v1/chat/completions": {
      "post": {
        "tags": [
          "chat"
        ],
        "operationId": "createChatCompletion",
        "summary": "Create a chat completion",
        "description": "OpenAI chat-completions shape plus reasoning_effort. model omitted resolves to palette-llm; palette-llm-fast resolves to palette-vlm; any other unknown model silently falls back to palette-llm (no 404). reasoning_effort defaults to medium; unknown values silently become medium. When max_tokens is omitted the gateway sets the per-effort default 1024 / 2048 / 8192 / 16384. Fields the gateway does not know (temperature, top_p, stop, n, seed, tools, tool_choice, response_format, stream_options) are forwarded to the backing server untouched. On palette-llm-ko the gateway appends one Korean system message at the end of messages. With stream: true the response is text/event-stream: one line 'data: {json}' per event, a blank line between events, and a final 'data: [DONE]'. On the shared seats stream chunks are forwarded unchanged, so model in a chunk is the backing id and there is no top-level reasoning_effort; do not key on it. xhigh runs two passes (draft, then a self-review with max_tokens 4096); both count against the 900 s request timeout.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ChatCompletionRequest"
              },
              "examples": {
                "low": {
                  "summary": "Non-stream, low effort",
                  "value": {
                    "model": "palette-llm",
                    "reasoning_effort": "low",
                    "messages": [
                      {
                        "role": "user",
                        "content": "공급사 A(단가 낮음, 납기 2주 지연)와 B(단가 8% 높음, 납기 준수) 중 하나를 고르고 이유를 두 문장으로 답하세요."
                      }
                    ]
                  }
                },
                "streamHigh": {
                  "summary": "Stream, high effort, explicit budget",
                  "value": {
                    "model": "palette-llm",
                    "reasoning_effort": "high",
                    "max_tokens": 4000,
                    "stream": true,
                    "messages": [
                      {
                        "role": "user",
                        "content": "공급사 A(단가 낮음, 납기 2주 지연)와 B(단가 8% 높음, 납기 준수) 중 하나를 고르고 이유를 두 문장으로 답하세요."
                      }
                    ]
                  }
                },
                "image": {
                  "summary": "Image content part on a vision seat",
                  "value": {
                    "model": "palette-vlm",
                    "reasoning_effort": "low",
                    "messages": [
                      {
                        "role": "user",
                        "content": [
                          {
                            "type": "text",
                            "text": "이 세금계산서의 합계 금액을 읽고, 품목 금액의 합과 일치하는지 확인해 주세요."
                          },
                          {
                            "type": "image_url",
                            "image_url": {
                              "url": "data:image/png;base64,iVBORw0KGgo..."
                            }
                          }
                        ]
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "A chat completion. application/json when stream is false or omitted; text/event-stream when stream is true.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ChatCompletion"
                },
                "example": {
                  "id": "chatcmpl-4f1d0b6c9a2e",
                  "object": "chat.completion",
                  "created": 1790035200,
                  "model": "palette-llm",
                  "reasoning_effort": "low",
                  "choices": [
                    {
                      "index": 0,
                      "message": {
                        "role": "assistant",
                        "reasoning_content": "A는 단가가 낮지만 납기가 2주 늦어 생산 일정에 영향을 준다. B는 단가가 8% 높지만 납기를 지킨다.",
                        "content": "공급사 B를 선택합니다. 8%의 단가 차이보다 2주 납기 지연으로 인한 생산 중단 비용이 더 크기 때문입니다."
                      },
                      "finish_reason": "stop"
                    }
                  ],
                  "usage": {
                    "prompt_tokens": 96,
                    "completion_tokens": 88,
                    "total_tokens": 184
                  }
                }
              },
              "text/event-stream": {
                "schema": {
                  "type": "string",
                  "description": "Server-Sent Events. Each event is one line 'data: <ChatCompletionChunk JSON>' followed by a blank line; the stream ends with 'data: [DONE]'. Reasoning arrives in choices[0].delta.reasoning_content, the answer in choices[0].delta.content. Some upstreams send a final chunk with empty choices carrying usage. Chunks the gateway synthesizes may omit created or usage; always read choices[0].delta.",
                  "x-event-schema": {
                    "$ref": "#/components/schemas/ChatCompletionChunk"
                  }
                },
                "example": "data: {\"id\":\"chatcmpl-9b7e2c1a\",\"object\":\"chat.completion.chunk\",\"created\":1790035201,\"model\":\"palette-llm-ko\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"reasoning_content\":\"공급사 A는 단가가 낮지만 \"},\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-9b7e2c1a\",\"object\":\"chat.completion.chunk\",\"created\":1790035201,\"model\":\"palette-llm-ko\",\"choices\":[{\"index\":0,\"delta\":{\"reasoning_content\":\"납기가 2주 늦다. B는 8% 비싸지만 납기를 지킨다.\"},\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-9b7e2c1a\",\"object\":\"chat.completion.chunk\",\"created\":1790035201,\"model\":\"palette-llm-ko\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"공급사 B를 선택합니다.\"},\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-9b7e2c1a\",\"object\":\"chat.completion.chunk\",\"created\":1790035201,\"model\":\"palette-llm-ko\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" 납기 지연 비용이 단가 차이보다 큽니다.\"},\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-9b7e2c1a\",\"object\":\"chat.completion.chunk\",\"created\":1790035201,\"model\":\"palette-llm-ko\",\"choices\":[{\"index\":0,\"delta\":{},\"finish_reason\":\"stop\"}]}\n\ndata: [DONE]\n"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "503": {
            "description": "The seat has no upstream URL configured, or the Palette cloud proxy token is missing (shared seats).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "noUpstream": {
                    "value": {
                      "detail": "seat has no upstream configured yet"
                    }
                  },
                  "noProxyToken": {
                    "value": {
                      "detail": "Palette cloud proxy token not configured"
                    }
                  }
                }
              }
            }
          },
          "default": {
            "description": "Upstream 4xx or 5xx passed through with the same status code; detail is the first 500 characters of the upstream body. Separately, Cloudflare returns 403 'error code: 1010' (HTML or text, not JSON) to Python's stock urllib User-Agent only; the OpenAI SDK, requests, httpx, curl and fetch all pass.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              },
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/v1/decision/systemone": {
      "post": {
        "tags": [
          "decision"
        ],
        "operationId": "decide",
        "summary": "Typed decision over one state",
        "description": "Forwards the body untouched to Palette Decision and returns its answer. Up to 32 questions, scored independently. Models: palette-decision-27b (K-DECIDE 800 items 0.944, training-harness eval 2026-10-15) and the default palette-decision-latest (served 7.3B, 0.873 on the same items, faster). Measured through the gateway: two-question 27B request 1.1 s.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DecisionRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Answers per question.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DecisionResponse"
                }
              }
            }
          },
          "401": {
            "description": "Error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/decision/models": {
      "get": {
        "tags": [
          "decision"
        ],
        "operationId": "listDecisionModels",
        "summary": "Decision model ids",
        "responses": {
          "200": {
            "description": "Model list.",
            "content": {
              "application/json": {
                "example": {
                  "data": [
                    {
                      "id": "palette-decision-latest",
                      "object": "model",
                      "owned_by": "palette"
                    },
                    {
                      "id": "palette-decision-27b",
                      "object": "model",
                      "owned_by": "palette"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/ocr": {
      "post": {
        "tags": [
          "ocr"
        ],
        "operationId": "ocr",
        "summary": "Korean line OCR on one image",
        "description": "Send the image as multipart `file`, as JSON `image_b64` (data-URL prefix accepted), or as raw bytes with an image Content-Type. 20 MB limit; long side downscaled to 4000 px; EXIF rotation applied. Lines come back in reading order. Measured: 48 lines on an A4 test page in 1.6 s server-side; recogniser CER 0.08% on 600 held-out synthetic Korean lines; real scans and photos not yet measured.",
        "parameters": [
          {
            "name": "lang",
            "in": "query",
            "schema": {
              "type": "string",
              "default": "ko"
            }
          },
          {
            "name": "min_conf",
            "in": "query",
            "schema": {
              "type": "number",
              "default": 0
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary"
                  }
                }
              }
            },
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OcrJsonRequest"
              }
            },
            "image/*": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Lines and joined text.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OcrResponse"
                }
              }
            }
          },
          "400": {
            "description": "Error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "Error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/ocr/models": {
      "get": {
        "tags": [
          "ocr"
        ],
        "operationId": "listOcrModels",
        "summary": "OCR model, version and languages",
        "responses": {
          "200": {
            "description": "Model list.",
            "content": {
              "application/json": {
                "example": {
                  "object": "list",
                  "data": [
                    {
                      "id": "palette-light-ocr",
                      "object": "model",
                      "owned_by": "palette",
                      "version": "palette-light-ocr-tiny-v0",
                      "languages": [
                        "ko",
                        "en",
                        "digits",
                        "symbols"
                      ]
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "pk_ followed by 48 URL-safe characters",
        "description": "Authorization: Bearer <palette key> on every route except GET /v1/health. Keys are minted by the operator and stored server-side as a SHA-256 hash. A revoked key can keep working for up to 60 seconds (per-container cache TTL). No per-key quotas are enforced today."
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "Missing Authorization header, or an unknown or revoked key.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "missingHeader": {
                "summary": "No Authorization: Bearer header",
                "value": {
                  "detail": "Authorization: Bearer <palette key> 헤더가 필요합니다."
                }
              },
              "badKey": {
                "summary": "Unknown or revoked key",
                "value": {
                  "detail": "유효하지 않거나 폐기된 키입니다."
                }
              }
            }
          }
        }
      }
    },
    "schemas": {
      "Seat": {
        "type": "string",
        "description": "Seat id or alias. palette-llm: Palette LLM (Palette LLM), text only, 1M context, default. palette-llm-max: Palette LLM Max (Palette LLM Max), vision, 1M. palette-vlm: Palette Vision (Palette Vision), vision, 1M, alias palette-llm-fast. palette-llm-ko: Palette LLM Korean on Palette's own GPU, vision (up to 6 images per request), 256K. Any other string silently falls back to palette-llm. palette-island answers tool calls only: send an OpenAI `tools` array or the gateway returns 400.",
        "enum": [
          "palette-llm",
          "palette-llm-max",
          "palette-vlm",
          "palette-llm-ko",
          "palette-llm-fast",
          "palette-island"
        ],
        "default": "palette-llm"
      },
      "ReasoningEffort": {
        "type": "string",
        "description": "Case-insensitive; '-' and '_' are stripped. minimal becomes low; extra-high, extrahigh and max become xhigh; anything unrecognized silently becomes medium. Shared seats map low and medium to native low, high to native high, xhigh to native max plus a self-review pass. palette-llm-ko maps them to a Korean reasoning protocol. Default max_tokens when omitted: low 1024, medium 2048, high 8192, xhigh 16384 (draft) with the review pass fixed at 4096.",
        "enum": [
          "low",
          "medium",
          "high",
          "xhigh"
        ],
        "default": "medium"
      },
      "TextPart": {
        "type": "object",
        "required": [
          "type",
          "text"
        ],
        "properties": {
          "type": {
            "const": "text"
          },
          "text": {
            "type": "string"
          }
        }
      },
      "ImageUrlPart": {
        "type": "object",
        "description": "Accepted on palette-vlm (alias palette-llm-fast), palette-llm-max and palette-llm-ko. palette-llm is text only. palette-llm-ko accepts up to 6 images per request.",
        "required": [
          "type",
          "image_url"
        ],
        "properties": {
          "type": {
            "const": "image_url"
          },
          "image_url": {
            "type": "object",
            "required": [
              "url"
            ],
            "properties": {
              "url": {
                "type": "string",
                "description": "data:image/png;base64,... or an https:// URL."
              }
            }
          }
        }
      },
      "ContentPart": {
        "oneOf": [
          {
            "$ref": "#/components/schemas/TextPart"
          },
          {
            "$ref": "#/components/schemas/ImageUrlPart"
          }
        ],
        "discriminator": {
          "propertyName": "type",
          "mapping": {
            "text": "#/components/schemas/TextPart",
            "image_url": "#/components/schemas/ImageUrlPart"
          }
        }
      },
      "Message": {
        "type": "object",
        "required": [
          "role",
          "content"
        ],
        "properties": {
          "role": {
            "type": "string",
            "enum": [
              "system",
              "user",
              "assistant"
            ]
          },
          "content": {
            "description": "A string, or an array of parts (text and, on vision seats, image_url).",
            "oneOf": [
              {
                "type": "string"
              },
              {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/ContentPart"
                }
              }
            ]
          }
        }
      },
      "ChatCompletionRequest": {
        "type": "object",
        "required": [
          "messages"
        ],
        "additionalProperties": true,
        "description": "OpenAI chat-completions body plus reasoning_effort. Unknown fields are forwarded to the backing server untouched; the gateway neither validates nor guarantees them.",
        "properties": {
          "model": {
            "$ref": "#/components/schemas/Seat"
          },
          "messages": {
            "type": "array",
            "minItems": 1,
            "items": {
              "$ref": "#/components/schemas/Message"
            },
            "description": "OpenAI messages. On palette-llm-ko the gateway appends one Korean system message (the effort instruction) at the end."
          },
          "reasoning_effort": {
            "$ref": "#/components/schemas/ReasoningEffort"
          },
          "reasoning": {
            "type": "object",
            "description": "Alternative spelling. Wins over reasoning_effort when both are present. Consumed by the gateway, never forwarded verbatim.",
            "properties": {
              "effort": {
                "$ref": "#/components/schemas/ReasoningEffort"
              }
            }
          },
          "max_tokens": {
            "type": "integer",
            "minimum": 1,
            "description": "Forwarded. When omitted the gateway sets the per-effort default: 1024 / 2048 / 8192 / 16384. Set at least 4000 for high and xhigh; a small budget can be consumed by thinking and leave content empty."
          },
          "stream": {
            "type": "boolean",
            "default": false,
            "description": "true returns Server-Sent Events (text/event-stream)."
          },
          "temperature": {
            "type": "number",
            "description": "Forwarded untouched."
          },
          "top_p": {
            "type": "number",
            "description": "Forwarded untouched."
          },
          "stop": {
            "description": "Forwarded untouched. Support depends on the backing model's server.",
            "oneOf": [
              {
                "type": "string"
              },
              {
                "type": "array",
                "items": {
                  "type": "string"
                }
              }
            ]
          }
        }
      },
      "AssistantMessage": {
        "type": "object",
        "required": [
          "role",
          "content"
        ],
        "properties": {
          "role": {
            "const": "assistant"
          },
          "content": {
            "type": [
              "string",
              "null"
            ],
            "description": "The answer. Can be empty when a small max_tokens was consumed by thinking; render reasoning_content in that case."
          },
          "reasoning_content": {
            "type": "string",
            "description": "Thinking, separate from the answer. Added by the gateway. Empty string when the model produced none. At xhigh it also carries '[초안 답] <draft answer>' and '[검토] <review>'."
          }
        }
      },
      "Choice": {
        "type": "object",
        "required": [
          "index",
          "message"
        ],
        "properties": {
          "index": {
            "type": "integer"
          },
          "message": {
            "$ref": "#/components/schemas/AssistantMessage"
          },
          "finish_reason": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "Usage": {
        "type": "object",
        "properties": {
          "prompt_tokens": {
            "type": "integer"
          },
          "completion_tokens": {
            "type": "integer",
            "description": "Includes thinking tokens."
          },
          "total_tokens": {
            "type": "integer"
          }
        }
      },
      "ChatCompletion": {
        "type": "object",
        "required": [
          "id",
          "object",
          "model",
          "choices"
        ],
        "description": "Non-stream response. model (seat id), reasoning_effort (normalized) and choices[0].message.reasoning_content are set by the gateway; every other field comes from the upstream.",
        "properties": {
          "id": {
            "type": "string"
          },
          "object": {
            "const": "chat.completion"
          },
          "created": {
            "type": "integer",
            "description": "Unix seconds."
          },
          "model": {
            "$ref": "#/components/schemas/Seat"
          },
          "reasoning_effort": {
            "$ref": "#/components/schemas/ReasoningEffort"
          },
          "choices": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Choice"
            }
          },
          "usage": {
            "$ref": "#/components/schemas/Usage"
          }
        }
      },
      "Delta": {
        "type": "object",
        "properties": {
          "role": {
            "const": "assistant",
            "description": "First chunk only."
          },
          "content": {
            "type": "string",
            "description": "A piece of the answer."
          },
          "reasoning_content": {
            "type": "string",
            "description": "A piece of the thinking. Arrives before content."
          }
        }
      },
      "ChunkChoice": {
        "type": "object",
        "required": [
          "index",
          "delta"
        ],
        "properties": {
          "index": {
            "type": "integer"
          },
          "delta": {
            "$ref": "#/components/schemas/Delta"
          },
          "finish_reason": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "ChatCompletionChunk": {
        "type": "object",
        "required": [
          "id",
          "object",
          "choices"
        ],
        "description": "One 'data:' event of a stream. On the shared seats chunks are the upstream's bytes: model is the backing id (for example Palette LLM) and there is no top-level reasoning_effort. On palette-llm-ko at high and xhigh the gateway re-emits chunks with model set to the seat id and routes text around the [답변] marker. Synthesized chunks may omit created or usage. Some upstreams send a final chunk with empty choices carrying usage; guard for it.",
        "properties": {
          "id": {
            "type": "string"
          },
          "object": {
            "const": "chat.completion.chunk"
          },
          "created": {
            "type": "integer"
          },
          "model": {
            "type": "string",
            "description": "Backing id on the shared seats, seat id on palette-llm-ko high and xhigh. Do not key on it."
          },
          "choices": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ChunkChoice"
            }
          },
          "usage": {
            "$ref": "#/components/schemas/Usage"
          }
        }
      },
      "Model": {
        "type": "object",
        "required": [
          "id",
          "object",
          "owned_by"
        ],
        "description": "A seat entry carries backing, configured, modalities and reasoning_effort. An alias entry carries alias_of and no other capability fields.",
        "properties": {
          "id": {
            "$ref": "#/components/schemas/Seat"
          },
          "object": {
            "const": "model"
          },
          "owned_by": {
            "const": "palette"
          },
          "backing": {
            "type": "string",
            "description": "Model id the gateway sends upstream. For palette-llm-ko this is the Palette inference server served-model name (palette-llm by default), not the Hugging Face id."
          },
          "configured": {
            "type": "boolean",
            "description": "An upstream URL is set."
          },
          "modalities": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "text",
                "image"
              ]
            }
          },
          "reasoning_effort": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ReasoningEffort"
            }
          },
          "alias_of": {
            "$ref": "#/components/schemas/Seat"
          }
        }
      },
      "ModelList": {
        "type": "object",
        "required": [
          "object",
          "data"
        ],
        "properties": {
          "object": {
            "const": "list"
          },
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Model"
            }
          }
        }
      },
      "Health": {
        "type": "object",
        "required": [
          "ok",
          "seats"
        ],
        "properties": {
          "ok": {
            "type": "boolean",
            "description": "true only when every configured seat returned 200."
          },
          "seats": {
            "type": "object",
            "description": "One entry per seat: the upstream HTTP status as an integer, the string 'unconfigured' when no upstream URL is set, or an exception class name such as 'ConnectTimeout'.",
            "additionalProperties": {
              "oneOf": [
                {
                  "type": "integer"
                },
                {
                  "type": "string"
                }
              ]
            }
          }
        }
      },
      "Error": {
        "type": "object",
        "required": [
          "detail"
        ],
        "properties": {
          "detail": {
            "type": "string",
            "description": "FastAPI-shaped message. Korean for the two 401 cases."
          }
        }
      },
      "DecisionQuestion": {
        "type": "object",
        "required": [
          "type",
          "instructions"
        ],
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "choice",
              "noul",
              "score"
            ]
          },
          "instructions": {
            "type": "string",
            "description": "The question, usually Korean."
          },
          "criteria": {
            "description": "choice: object option -> description (or null). score: ordered list of level descriptions, low to high. noul: optional {\"true\": ..., \"false\": ...}.",
            "oneOf": [
              {
                "type": "object",
                "additionalProperties": {
                  "type": [
                    "string",
                    "null"
                  ]
                }
              },
              {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "minItems": 2,
                "maxItems": 10
              }
            ]
          }
        }
      },
      "DecisionRequest": {
        "type": "object",
        "required": [
          "state",
          "questions"
        ],
        "properties": {
          "model": {
            "type": "string",
            "default": "palette-decision-latest",
            "enum": [
              "palette-decision-latest",
              "palette-decision-27b"
            ]
          },
          "state": {
            "description": "Text or JSON object describing the situation.",
            "oneOf": [
              {
                "type": "string"
              },
              {
                "type": "object"
              }
            ]
          },
          "questions": {
            "type": "object",
            "minProperties": 1,
            "maxProperties": 32,
            "additionalProperties": {
              "$ref": "#/components/schemas/DecisionQuestion"
            }
          }
        },
        "example": {
          "model": "palette-decision-27b",
          "state": "배송이 3일째 물류센터에 멈춰 있어요. 오늘 안에 출발 안 되면 취소하고 환불받을게요.",
          "questions": {
            "team": {
              "type": "choice",
              "instructions": "이 문의를 처리할 팀은?",
              "criteria": {
                "배송": "배송 지연·추적",
                "환불": "취소·환불 처리",
                "기타": "그 외"
              }
            },
            "cancel": {
              "type": "noul",
              "instructions": "고객이 취소 의사를 밝혔는가?"
            },
            "urgency": {
              "type": "score",
              "instructions": "긴급도를 평가하라.",
              "criteria": [
                "기한 없음",
                "며칠 내",
                "명확한 기한 있음",
                "오늘 안에 처리 필요"
              ]
            }
          }
        }
      },
      "DecisionAnswer": {
        "type": "object",
        "required": [
          "type",
          "probabilities",
          "confidence"
        ],
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "choice",
              "noul",
              "score"
            ]
          },
          "choice": {
            "type": "string"
          },
          "noul": {
            "type": "number"
          },
          "score": {
            "type": "number"
          },
          "probabilities": {
            "type": "object",
            "additionalProperties": {
              "type": "number"
            }
          },
          "confidence": {
            "type": "number"
          },
          "legend": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            }
          }
        }
      },
      "DecisionResponse": {
        "type": "object",
        "required": [
          "model",
          "answers"
        ],
        "properties": {
          "model": {
            "type": "string"
          },
          "answers": {
            "type": "object",
            "additionalProperties": {
              "$ref": "#/components/schemas/DecisionAnswer"
            }
          },
          "usage": {
            "type": "object",
            "properties": {
              "input_tokens": {
                "type": "integer"
              },
              "shared_prefix_tokens": {
                "type": "integer"
              },
              "output_tokens": {
                "type": "integer"
              }
            }
          }
        }
      },
      "OcrLine": {
        "type": "object",
        "required": [
          "text",
          "confidence",
          "box"
        ],
        "properties": {
          "text": {
            "type": "string"
          },
          "confidence": {
            "type": "number"
          },
          "box": {
            "type": "array",
            "minItems": 4,
            "maxItems": 4,
            "items": {
              "type": "array",
              "minItems": 2,
              "maxItems": 2,
              "items": {
                "type": "integer"
              }
            }
          }
        }
      },
      "OcrResponse": {
        "type": "object",
        "required": [
          "model",
          "text",
          "lines"
        ],
        "properties": {
          "model": {
            "type": "string",
            "example": "palette-light-ocr"
          },
          "lang": {
            "type": "string"
          },
          "text": {
            "type": "string"
          },
          "lines": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/OcrLine"
            }
          },
          "width": {
            "type": "integer"
          },
          "height": {
            "type": "integer"
          },
          "ms": {
            "type": "object",
            "properties": {
              "detect": {
                "type": "number"
              },
              "recognise": {
                "type": "number"
              },
              "total": {
                "type": "number"
              }
            }
          }
        }
      },
      "OcrJsonRequest": {
        "type": "object",
        "properties": {
          "image_b64": {
            "type": "string",
            "description": "Base64 image, plain or data URL (alias: image)."
          }
        }
      }
    }
  }
}