# Anthropic-Compatible Messages API Contract

## 계약 메타

- id: `iop.anthropic-compatible-api`
- boundary: `outer`
- status: active
- 원본 경로:
  - `apps/edge/internal/openai/anthropic_handler.go`
  - `apps/edge/internal/openai/anthropic_native.go`
  - `apps/edge/internal/openai/anthropic_bridge.go`
  - `apps/edge/internal/openai/anthropic_stream.go`
  - `apps/edge/internal/openai/single_request_anthropic_stream.go`
  - `apps/edge/internal/openai/single_request_quality_gate.go`
  - `apps/edge/internal/service/single_request.go`
  - `apps/edge/internal/service/single_request_tool_types.go`
  - `apps/edge/internal/service/single_request_tool_loop.go`
  - `apps/edge/internal/service/single_request_types.go`
  - `apps/edge/internal/openai/single_request_preset_binding.go`
  - `apps/edge/internal/openai/single_request_provider_stage.go`
  - `apps/edge/internal/openai/single_request_plan_stage.go`
  - `apps/edge/internal/openai/single_request_work_stage.go`
  - `apps/edge/internal/openai/single_request_review_stage.go`
  - `apps/edge/internal/openai/provider_normalization.go`
  - `apps/edge/internal/openai/artifact_pair.go`
  - `apps/edge/internal/openai/hot_path_light.go`
  - `apps/edge/internal/openai/hot_path_review.go`
  - `apps/edge/internal/openai/hot_path_stage_input.go`
  - `packages/go/singlerequesttemplate/template.go`
  - `apps/edge/internal/openai/anthropic_types.go`
  - `apps/edge/internal/openai/routes.go`
  - `apps/edge/internal/openai/principal.go`
  - `apps/edge/internal/authprojection/cache.go`
  - `apps/edge/internal/openai/provider_tunnel.go`
  - `apps/edge/internal/openai/provider_model_rewrite.go`
  - `packages/go/config/protocol_profile.go`
- human docs: (none yet)

## 범위

이 문서는 외부 프로젝트가 IOP Edge의 Anthropic-compatible HTTP 표면을 호출할 때 확인할 계약 원문이다.
IOP 내부 실행은 `adapter + target` 기준이며, Anthropic-compatible 경계에서는 `model`과 `messages`를 사용한다.
Anthropic-compatible provider로 raw passthrough 되는 경로는 선택된 provider가 지원하는 표준 field와 provider extension field를 IOP allowlist로 제한하지 않는다.
Routing first resolves the request `model` through the provider pool. An `anthropic_messages` candidate uses a native provider tunnel, while an `openai_chat` candidate uses the Messages-to-Chat bridge over its provider tunnel.

## Auth

Edge 설정의 `openai.bearer_token`이 비어 있지 않으면 Anthropic-compatible HTTP 표면은 다음 헤더를 요구한다.

```http
Authorization: Bearer <token>
```

`X-Api-Key: <token>` is an equivalent caller-auth form. When both headers are supplied, the bearer token and API key must be equal; a non-Bearer `Authorization` value is rejected.

토큰이 없거나 일치하지 않으면 `401 authentication_error` Anthropic-compatible error response를 반환한다. `openai.bearer_token`이 빈 값이면 auth를 적용하지 않는다.

### Shared principal token auth

When `openai.principal_tokens[]` is configured, either supported caller-auth form is hashed and matched against `token_hash_sha256`. A match supplies `iop_principal_ref`, `iop_principal_alias`, `iop_token_ref`, and `iop_principal_source` to internal dispatch metadata; no match returns `401 authentication_error` unless the legacy fallback applies.

### Managed projection auth

`credential_plane.enabled=true` selects managed mode at startup. The Control Plane supplies the initial secret-free projection in the authenticated mTLS hello and pushes newer generations after durable credential mutations. Edge shares one bounded immutable cache across OpenAI and Anthropic-compatible ingress and fails closed when a managed hello or refresh is missing, stale, invalid, or expired.

Bearer and `X-Api-Key` remain equivalent inbound IOP token forms, and when both are present they must contain the same token. The token digest must exist in the fresh projection. Mismatch, unknown or removed digest, malformed Authorization, and projection expiry return `401 authentication_error` before provider dispatch. Static principal mappings and legacy bearer fallback are prohibited in managed mode.

In managed mode, model discovery (`GET /anthropic/v1/models` and `GET /v1/models`
with anthropic-version) lists active ordinary projected `route_id`s and any authorized
virtual preset model IDs for the authenticated principal. Ordinary request model
selection binds strictly to one projected route's `slot_id`, `profile_id`, and
`upstream_model`. A catalog execution preset is discoverable and admissible only when
its selector and every referenced stage model resolve through their canonical catalog
bindings to exactly one active route for that principal. Missing or ambiguous
selector/stage bindings fail closed and never fall back to the global catalog, legacy
defaults, or a different route.

Authentication and route resolution retain one immutable projection generation for a
request. A public `route_id` resolves only inside the verified managed gate to one
internal model group and selector-compatible provider resource set; it is distinct from
the provider resource and from `credential_slot_ref`. For a virtual preset, the
selector's real projected route and revisions remain the credential and lease authority;
the virtual ID is never synthesized as a route or credential binding. The credential
slot is trusted attribution/lease scope, not a provider ID. Edge overwrites caller
metadata with trusted route/slot revisions and preserves the internal model group and
binding through recovery; missing or ambiguous bindings are rejected with no fallback.
An authorized virtual preset retains its requested virtual ID in successful responses
across the native Messages tunnel and Chat bridge. Ordinary native routes preserve the
provider response model and body bytes; the Chat bridge emits its converted Anthropic
response model semantics.

### Unmarked light preset: caller-workspace continuation

An unmarked `light` preset with `workspace_tools` uses only the tool schemas
admitted by the caller request. Edge never receives or configures a raw caller
workspace path. Before issuing caller writes, Edge validates the selector's
default lightweight PLAN and pending REVIEW seed and retains the PLAN `P1..Pn`
inventory request-locally. Selector instructions require every PLAN field to be
English ASCII, and Edge rejects a non-ASCII PLAN before issuing caller writes.
Work receives only the relative PLAN path followed by the fixed English sentence
`Read it and complete the task.` and sees only ordinary caller tools. It must read
PLAN and perform and verify caller-workspace work. After a successful PLAN read and
a non-empty terminal worker report, Edge renders and validates the completed REVIEW
handoff and projects its single caller write; Work does not own that protocol. Review
must read both artifacts and successfully inspect an ordinary caller result before
it can return a non-empty caller-visible terminal. It never rewrites REVIEW; repair
and re-verification remain in the same Review binding. Cleanup targets only the
request's `.iop/job/<request_id>` directory.

After Edge issues the request-local artifact paths, it adds one bounded Edge-owned,
phase-specific selector instruction to the actual provider body. A binding whose
write operation cannot create parents receives a `prepare-only` initial instruction:
the provider must emit exactly one admitted prepare call for
`.iop/job/<request_id>` and must not write PLAN or REVIEW. After the successful
prepare receipt, the resumed selector receives `pair-write` and must emit exactly
one atomic artifact-pair call containing the complete PLAN and REVIEW content with
the accepted default PLAN grammar and deterministic pending REVIEW grammar. Edge
expands that decision into two independently receipted admitted caller writes. A parent-creating write binding receives
`pair-write` immediately on its initial selector turn. In Chat Completions this is
the final leading `system` message; in native Messages it is the final top-level
`system` text block. The instruction is not caller input, does not alter the retained
caller request snapshot, and adds no public request/config field or raw workspace
path. Work, Review, cleanup, and marked `single_request` provider bodies do not
receive it.

Continuation issue correlation hashes only the emitted tool-use identity, name, and
arguments/input. SDK-added text, thinking, and provider metadata decorations are not
part of that issue identity; the retained request prefix and exact result IDs remain
independently validated.

For a command-mode caller binding, Work/Review provider deltas are collected through
the bounded internal stage before caller projection. Edge validates and maps canonical
operations first, so an internal tool name is never released early on a Messages stream;
directly projectable structured bindings retain progressive streaming.

A preset marked with `single_request` is excluded from this continuation before
caller tool binding. Config validation rejects a preset that declares both
`single_request` and `workspace_tools`, so the operator-owned Node workspace and
caller-owned workspace modes cannot cross or fall back into one another.

### Marked preset: single-request admission

An authorized fixed single-request preset compiles one service-owned admission value
at request start. The admission freezes the requested public model, the canonical
plan/work/review stage bindings resolved through the principal's managed authorization,
an opaque workspace capability reference, and absolute resource caps (wall-clock,
stage-timeout, tool-iterations, output-bytes). The admission is compiled only after
every canonical reference has been verified through its catalog binding for the
authenticated principal; missing, duplicate, unauthorized, dynamically selected, or
option-inconsistent inputs are rejected without generic fallback. Later runtime
refresh or config mutation cannot alter an admitted request's frozen shape. No private
binding (route ID, credential slot, provider ID, endpoint, or raw workspace data) is
echoed to the caller. The admission is owned by the service package; the OpenAI and
Anthropic surfaces read only the public model identity and the frozen limits.

Each private Plan/Work/Review call derives caller-neutral requirements from the
frozen stage options, tool presence, and structured-output contract. The selected
protocol profile then chooses Chat Completions or Responses and maps effort to the
exact or nearest supported lower grade; the caller product or SDK name never enters
that choice. A managed route with an explicit resource selector still freezes and
verifies the exact provider ID. A `default` resource selector intentionally leaves
the provider ID open for the provider pool while continuing to freeze and verify the
model group, profile, upstream model, credential slot/revision, and tunnel execution
path. The concrete provider selected by that pool is therefore valid dispatch
evidence, not a stage-binding mismatch.

The admission also freezes the effective Plan and Review Markdown templates resolved
from the operator's `execution_presets[].single_request.templates` configuration (see
`agent-contract/inner/edge-config-runtime-refresh.md`). These templates are
Edge-owned internal stage inputs only:

- The Plan stage first exposes only the admitted `workspace_read` and
  `workspace_list` operations and requires at least one successful inspection
  result before Plan completion is eligible. The bounded tool result remains in
  the request-local Plan conversation; no separate analysis artifact is created.
  The stage then requests a stage-owned strict JSON object with a one-line `goal`
  string, 2-6 non-empty one-line `steps` strings, and 1-3 non-empty one-line
  `verification` strings. Edge renders deterministic `P1..Pn` step IDs into the
  frozen PLAN and Work must parse that stored document before provider dispatch.
  Work returns strict worker item status, changes, verification, and deviations;
  Edge renders and validates the one `review.md` handoff. Review reads both stored
  artifacts, never accepts a memory work payload, and never rewrites `review.md`.
  Provider output never controls artifact headings or static template text.
- The caller-visible request and response schemas are unchanged. A configured
  template never adds, removes, or renames a Messages request field, a content block,
  an SSE event, a `stop_reason`, or an error shape, and the final Messages text stays
  the model's `decision.output`.
- Callers cannot supply, name, select, or override a template. Template file paths,
  template contents, and template digests are never echoed in a response, an error
  message, or a metric label.
- An already admitted request keeps its frozen template pair for its whole lifetime.
  A config refresh that changes a template reaches only requests admitted after it,
  and a template that no longer satisfies the documented grammar fails admission
  closed rather than degrading to a default.

### Marked preset: one-ingress runtime boundary

After request validation, principal authorization, and immutable preset resolution, a
marked Messages request requires the service's separate `StartSingleRequest`
capability. The handler never widens the generic run service or falls back to the
ordinary provider-pool/caller-continuation path when this capability is missing.
Missing capability returns a sanitized `503 api_error`; a coordinator start or runtime
failure returns a sanitized `502 api_error` on the same request.

An accepted marked Messages POST increments
`iop_anthropic_single_request_ingress_total` exactly once. The counter has no labels and
is not incremented for internal stages, tools, retries, progress events, terminals,
count-tokens requests, or a marked request rejected before capability admission.
Request, principal, route, provider, credential, workspace, and stage identities are
forbidden metric dimensions.

The handler gives the service an immutable copy of the admitted binding and request
input. Arbitrary internal progress messages, reasoning, tool protocol, and execution
identities remain private. The service freezes exactly one validated terminal
disposition before it crosses the endpoint boundary. Its closed kinds are `end_turn`,
`length`, `error`, and `cancelled`; error classes are `provider`, `validation`,
`timeout`, `budget`, `repetition`, `malformed`, `context`, `internal_tool`, and
`workspace_cleanup`. A legacy result without a disposition normalizes to `end_turn`.
Raw provider, tool, workspace, and decoder errors are never retained in this public
value.

Buffered and streaming projectors use the same closed mapping:

| Service disposition | Buffered Messages terminal | Streaming Messages terminal |
|---|---|---|
| `end_turn` | `200`, one caller-safe text block, `stop_reason="end_turn"` | one caller-safe final text block, `message_delta(end_turn)`, then `message_stop` |
| `length` | `200`, empty content, `stop_reason="max_tokens"` | no private partial final block, `message_delta(max_tokens)`, then `message_stop` |
| `error/validation`, `error/context` | `400 invalid_request_error` with a fixed safe message | one `error` event of type `invalid_request_error` |
| every other `error/*` | `502 api_error` with a fixed safe message | one `error` event of type `api_error` |
| `cancelled` | no response body after caller disconnect | no later event after caller disconnect |

For either buffered or streaming `error/*`, Edge emits exactly one
`edge_single_request_terminal_rejection` operational event with only the fixed
`surface=messages`, `terminal_kind`, `terminal_error_class`, and `http_status`
fields. This preserves the closed distinction between `malformed` and `validation`
without logging request content, provider output, credentials, workspace data, or an
unbounded identifier. Success, length, and cancelled terminals do not emit this event.

Private Plan/Work/Review Chat Completions responses may contain the standard bounded
`usage` bookkeeping object (`prompt_tokens`, `completion_tokens`, `total_tokens`, and
their standard detail objects), top-level `service_tier`/`system_fingerprint`/provider
`timings`, choice
`logprobs`, message `annotations`, a null `refusal`, and an optional string
`message.reasoning_content`. The shared provider-normalization layer removes the
bookkeeping fields and emits the same canonical private Chat-shaped envelope used for
Responses results before the stage decoder runs. These private values are discarded;
they do not enter a stage result or artifact and do not select a route, credential,
workspace, tool, or terminal. A non-null refusal, non-string reasoning value, and
unknown or duplicate response members still fail closed. The external Claude
qualification harness also disables SDK retry and automatic session-title generation
only in its supervised child so the single observed Messages ingress is the actual task.

Gemini Plan and Review additionally admit only the exact OpenAI-compatible thought
signature shape `extra_content.google.thought_signature`, with a non-empty string and
no sibling extension members. A terminal text signature is discarded. When Review
or Plan receives a workspace tool call, its tool-call signature is retained only in
request-local memory and replayed unchanged in the immediately resumed Gemini
assistant tool-call message; it is absent from Work, artifacts, caller output, logs,
and durable evidence.

Provider/tool timeouts, exhausted stage/request budgets, first proven repeated
action/result no-progress, malformed calls/results, provider context/output limits,
internal-tool failure, and cleanup failure stop the active composite without retry,
fallback, partial success, or a second request. One accepted marked POST therefore
remains one ingress and produces at most one frozen caller terminal. Cleanup may
replace a pending success or length candidate with `error/workspace_cleanup` before
publication; after publication, negative endpoint acknowledgement changes internal
completion only and cannot write a second terminal. This is the implemented S11
`error-cancel` boundary. S12 external Claude qualification passed on an approved IOP
Node with SDK retry and session-title generation disabled: one supervised Claude task
produced ingress `0→1`, Gemini → ornith-fast → Gemini stage order, one `end_turn`, and
the exact expected workspace result without exposing private tool protocol.

A streaming marked request uses a separate privacy-closed projector for the same
coordinator execution. The projector opens exactly one `message_start` envelope and
may expose each of the following fixed summaries at most once, each as a complete text
content block with a monotonically increasing index:

- planning: `Planning the requested work.`
- work: `Executing the requested work.`
- review: `Reviewing the completed work.`
- repair: `Repairing issues found during review.`

Accepted, internal-tool, finalizing, completed, and cleanup details do not create
public progress blocks. `event: ping` may occur between `message_start` and the
exclusive terminal, does not open or consume a content-block index, and is stopped and
joined before terminal output or handler return. An `end_turn` terminal writes the
final caller-safe text block, one `message_delta` with `stop_reason="end_turn"`, and one
`message_stop`. A `length` terminal writes no private partial stage block and closes
with `stop_reason="max_tokens"`. A classified failure writes one sanitized `error`
event and never writes a success terminal. Caller disconnect owns `cancelled`, cancels
execution, and suppresses all later wire output.

One serialized writer owns envelope state, content indices, pings, flushes, and the
terminal decision. The endpoint acknowledges success only after `message_stop` is
written successfully; a partial or failed terminal write is negatively acknowledged
and cannot be retried as another success or error terminal. Calls arriving after a
terminal decision are no-ops that return the established write result. Private
provider reasoning, `tool_use`/tool arguments/results, route/provider/credential
identifiers, workspace paths, raw commands, internal stage terminal data, and
caller-supplied arbitrary progress strings are forbidden from the marked stream.

### Marked preset: private internal workspace continuation

An executor may emit exactly one service-owned `InternalWorkspaceToolCall` while its
active stage is saved in `internal_tool`. The closed names are `workspace_read`,
`workspace_list`, `workspace_write`, `workspace_delete`, and `workspace_command`.
Each operation has a distinct strict JSON object schema: unknown fields, duplicate
keys, trailing values, malformed identities, non-canonical paths, private `.iop`
paths, unapproved operations or command IDs, and unapproved environment names are
rejected before any Node wire effect. Command input contains only an approved command
ID and approved environment values; executable paths and argv are never model input.

The service opens the admitted workspace lifecycle once on the exact frozen Node
connection generation, then executes one tool call at a time. Every result must echo
the immutable request, canonical stage (`plan`, `work`, or `review`), and unique tool
call ID. Only bounded typed content, entries, stdout, stderr, exit status, truncation,
duration, and closed status/error code reach the emitting executor's optional
`ContinueInternalTool` port. Raw arguments and raw Node error text are excluded. A
result permits only the saved stage to resume; repeated IDs, stale identities,
malformed or denied calls, unavailable continuation, and exhausted per-stage
iteration/output/deadline or request wall-clock budgets fail closed without
reselection, fallback, or caller continuation. Caller cancellation cancels the
request context and an in-flight Node tool receives the typed request/stage/tool
cancel through the admitted connection.

The continuation does not create an HTTP request or an Anthropic content block. The
deterministic real-POST evidence performs multiple private Node tool round trips while
observing exactly one `/v1/messages` ingress, one caller-safe terminal, and no public
`tool_use` or `tool_result` protocol.

This projector is a service-to-endpoint boundary and does not widen the generic Stream
Evidence Gate event/filter/recovery contract. Edge startup installs the composite
single-request executor driving the active Plan -> Work -> Review stage pipeline with
generic failure behavior on private stage errors; local deterministic evidence is test-covered.
S12 (`claude-smoke`) also qualifies the external Claude path on an approved IOP Node
with exactly one accepted Messages ingress, the expected Plan → Work → Review stage order,
one `end_turn`, and the exact expected workspace result.
Ordinary unmarked Messages routing, Chat behavior, and both count-tokens routes remain
unchanged.

After provider selection, Edge validates the projected slot/profile/model/revision/generation binding, acquires a short-lived signed lease over the authenticated Control Plane connection, and revalidates immediately before sending it to the selected Node. The Node opens the recipient-sealed lease only immediately before provider execution. Rotation, disable, revoke, expiry, or a stale binding fails closed without legacy, route, provider, or same-model slot fallback.

### Legacy fallback

`openai.principal_tokens[]`가 설정되어 있더라도, raw token이 어떤 `principal_tokens` entry에도 매칭되지 않으면 `openai.bearer_token`이 설정된 경우 legacy 단일 bearer auth가 unmapped fallback으로 동작한다. `openai.bearer_token`과 `openai.principal_tokens[]`가 모두 설정된 경우, principal token 매칭이 실패하면 legacy fallback을 시도하고, 그래도 실패하면 `401 authentication_error`를 반환한다.

### Managed versus legacy provider credentials

- Managed mode sources provider authentication only from the credential slot and Node-targeted lease. Config validation rejects `openai.provider_auth` and static provider credential sources, while ingress rejects caller-supplied legacy provider credential headers with `400 invalid_request_error`.
- Legacy mode may enable `openai.provider_auth`; the caller supplies the provider token through the configured header (default `X-IOP-Provider-Authorization`) and Edge applies the configured target header/scheme. This provider token is distinct from either supported inbound IOP token form.
- Both modes keep the Anthropic-compatible public request and response schema unchanged. Route and credential selection remain internal authenticated operations.

## Required Headers

Anthropic-compatible 요청은 다음 헤더를 필수로 포함해야 한다.

```http
anthropic-version: 2023-06-01
```

지원하는 `Anthropic-Beta` 값:

- `advanced-tool-use-2025-11-20`
- `advisor-tool-2026-03-01`
- `claude-code-20250219`
- `context-management-2025-06-27`
- `effort-2025-11-24`
- `fine-grained-tool-streaming-2025-05-14`
- `interleaved-thinking-2025-05-14`
- `mid-conversation-system-2026-04-07`
- `prompt-caching-2024-07-31`
- `prompt-caching-scope-2026-01-05`
- `redact-thinking-2026-02-12`
- `structured-outputs-2025-12-15`
- `thinking-token-count-2026-05-13`

지원하지 않는 beta 값을 보내면 `400 invalid_request_error`를 반환한다.
Native Messages 경로는 지원 beta 헤더를 upstream으로 전달한다. Chat bridge 경로는 지원 beta 헤더를 upstream으로 전달하지 않고, 아래에 명시한 대응 field만 Chat Completions 형식으로 변환한다.
`prompt-caching-scope-2026-01-05`, `advanced-tool-use-2025-11-20`, `advisor-tool-2026-03-01`, `redact-thinking-2026-02-12`, `thinking-token-count-2026-05-13` 수용은 Claude Code 호출 호환성만 제공한다. 이 beta들은 Chat bridge에서 cache, route, stage, provider, workspace 또는 authorization 권한을 만들지 않으며 normalized Chat provider 요청으로 전달되지 않는다.

## Routes

### `POST /v1/messages` 및 `POST /anthropic/v1/messages`

Anthropic Messages API 호환 chat 요청.

### `POST /v1/messages/count_tokens` 및 `POST /anthropic/v1/messages/count_tokens`

Anthropic count_tokens 호환 요청.

### `GET /v1/models` and `GET /anthropic/v1/models`

`/anthropic/v1/models` always returns the Anthropic model-list shape. `/v1/models` returns that shape when `anthropic-version` is present; otherwise it retains the OpenAI-compatible list shape.

### Method Not Allowed

Wrong methods on Anthropic-selected endpoints return `405 invalid_request_error`.

## Request/Response Contract

### Messages

```json
{
  "model": "claude-route",
  "max_tokens": 1024,
  "messages": [
    {
      "role": "user",
      "content": "Hello, world."
    }
  ],
  "stream": false,
  "temperature": 0.5,
  "top_p": 0.9,
  "stop_sequences": ["\\n\\nHuman:"],
  "tools": [
    {
      "name": "search",
      "description": "Search the web",
      "input_schema": { "type": "object", "properties": { "query": { "type": "string" } } }
    }
  ],
  "tool_choice": { "type": "auto" },
  "thinking": { "type": "adaptive" },
  "output_config": {
    "effort": "high",
    "format": {
      "type": "json_schema",
      "schema": { "type": "object" }
    }
  },
  "metadata": { "user_id": "user-123" },
  "context_management": {
    "edits": [{ "type": "clear_tool_uses_20250919" }]
  }
}
```

필드 의미:

- `model`: Edge가 내부 `adapter + target`으로 해석할 외부 route 이름이다. IOP Edge에서는 라우팅을 위해 필수다.
- `max_tokens`: 출력 토큰 상한이다. 필수 field다. 0 이하 값은 `400 invalid_request_error`를 반환한다.
- `messages`: `user`, `assistant`, `system` role을 허용한다. content는 string 또는 content block array다. `system` role은 mid-conversation privileged instruction으로 보존하며 text content만 허용한다. 첫 message에는 올 수 없고 top-level `system`을 사용해야 한다. user message(연속 system message 포함) 직후에만 오며 assistant message 앞 또는 message array 끝에만 위치할 수 있고, 미완료 `tool_use`와 `tool_result` 사이에는 올 수 없다. Chat bridge는 같은 위치의 `role=system`으로, Responses bridge는 같은 위치의 system input message로 normalize한다.
- `system`: string 또는 text block array만 허용한다.
- `stream`: `true`이면 ordinary provider routes relay raw provider SSE. `false` 또는 생략이면 non-streaming JSON 응답을 반환한다. An admitted virtual-preset Hot Path is the narrow exception described in routing: it emits the caller-requested endpoint-native shape after structural classification. A marked single-request request with `stream=true` uses the closed progress/ping/terminal subset above; `stream=false` retains the buffered final-only response.
- `temperature`: 0..1 범위. 범위를 벗어나면 `400 invalid_request_error`를 반환한다.
- `top_p`: 0..1 범위. 범위를 벗어나면 `400 invalid_request_error`를 반환한다.
- `top_k`: 양수여야 한다.
- `stop_sequences`: 빈 문자열은 허용되지 않는다.
- `tools`: 각 tool은 `name`, `input_schema`를 필수로 가진다. 선택 boolean `defer_loading`은 Claude Code tool-search 호출 호환성 annotation으로만 수용한다. Native Messages raw tunnel은 원문을 보존하지만, decoded Chat bridge와 marked single-request 경로에서는 route, provider, workspace, tool policy 또는 authorization 권한으로 해석하지 않고 normalized Chat provider body에서 제거한다.
- `tool_choice`: `auto`, `any`, `none`, `tool` 타입만 허용한다.
- `thinking`: 양수 `budget_tokens`가 있는 `type="enabled"` 또는 budget 없는 `type="adaptive"`를 허용한다. 선택 `display`는 Claude Code thinking-redaction 호환성을 위해 `omitted` 또는 `summarized`만 수용한다. Native Messages raw tunnel은 원문을 보존하지만, decoded Chat bridge와 marked single-request 경로에서는 display를 route, stage, provider, workspace, tool policy 또는 authorization 권한으로 해석하지 않고 normalized Chat provider body에서 제거한다. Chat bridge의 `enabled`는 profile의 thinking/reasoning extension이 필요하고, `adaptive`는 `output_config.effort` 기반 provider 제어를 사용한다.
- `output_config.effort`: canonical IOP 등급 `low`, `medium`, `high`, `xhigh`, `max`를 허용한다. Edge는 caller identity가 아니라 요청의 tools/thinking/stream 요구와 selected protocol profile의 operation별 normalization을 사용해 provider wire 값을 정한다. exact 등급이 없으면 선언된 가장 가까운 하위 등급으로만 매핑하고 상향하지 않는다. 예를 들어 `max`가 없고 `xhigh`가 있으면 `xhigh`를 사용한다. 어떤 하위 등급도 없으면 dispatch 전에 `not_supported_error`로 거부한다.
- `output_config.format`: `type="json_schema"`와 object `schema`를 허용하며 Chat bridge에서 OpenAI-compatible `response_format.json_schema`로 변환한다.
- `cache_control`: text/image/tool/tool-result/thinking block과 tool declaration의 compatibility annotation을 수용하되 Chat bridge에서는 정책으로 해석하거나 provider body에 전달하지 않는다.
- `metadata`: caller-defined object이며 IOP identity source로 사용하지 않는다. Native Messages 경로는 원문을 보존하고, Chat bridge는 object 여부만 검증한 뒤 provider body에서는 제거한다.
- `context_management`: `null` 또는 object만 허용하는 Claude Code compatibility input이다. decoded Chat bridge와 marked single-request 경로에서는 IOP identity, route, credential, workspace, tool policy로 해석하지 않고 normalized Chat provider body에도 전달하지 않는다. Native Messages raw tunnel은 기존 raw-body 전달 계약을 유지한다.

### Response (non-streaming)

```json
{
  "id": "msg_iop_xxx",
  "type": "message",
  "role": "assistant",
  "model": "claude-route",
  "content": [
    { "type": "text", "text": "Hello!" },
    { "type": "thinking", "thinking": "...", "signature": "" },
    { "type": "tool_use", "id": "toolu_xxx", "name": "search", "input": { "query": "..." } }
  ],
  "stop_reason": "end_turn",
  "usage": {
    "input_tokens": 100,
    "output_tokens": 50,
    "cache_read_input_tokens": 0,
    "cache_creation_input_tokens": 0
  }
}
```

응답 필드:

- `id`: provider 응답 ID 또는 `"msg_iop"` prefix fallback.
- `type`: 항상 `"message"`.
- `role`: 항상 `"assistant"`.
- `model`: Authorized virtual presets echo the requested virtual model. Ordinary native responses preserve the provider response model, while Chat bridge responses use the converted Anthropic request model.
- `content`: text, thinking, tool_use block array.
- `stop_reason`: `end_turn`, `max_tokens`, `tool_use`, `stop_sequence` 중 하나.
- `usage`: provider-reported token count.

### Response (streaming, SSE)

```
event: message_start
data: {"type":"message_start","message":{"id":"msg_iop_xxx","role":"assistant","content":[],"stop_reason":null}}

event: content_block_start
data: {"type":"content_block_start","content_block":{"type":"text","text":""}}

event: content_block_delta
data: {"type":"content_block_delta","delta":{"type":"text_delta","text":"Hello"}}

event: content_block_delta
data: {"type":"content_block_delta","delta":{"type":"thinking_delta","thinking":"..."}}

event: content_block_stop
data: {"type":"content_block_stop","content_block":{"type":"text"}}

event: message_delta
data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null}}

event: message_stop
data: {"type":"message_stop"}
```

streaming 응답 header allowlist:

- `Cache-Control`, `Content-Length`, `Content-Type`, `Request-Id`, `Retry-After`, `X-Request-Id`, `X-Robots-Tag`
- `anthropic-ratelimit-*` prefix header
- `ratelimit-*` prefix header

#### Marked single-request SSE subset

The marked projector preserves the standard Anthropic event framing while narrowing
the allowed content. Its order is:

1. exactly one `message_start` containing the coordinator-derived `msg_iop_` id, the
   requested public model, an empty content array, and no stop reason;
2. zero or more complete fixed progress text blocks and zero or more `event: ping`
   frames, with pings consuming no block index;
3. on `end_turn`, one complete final text block, one `message_delta` with `end_turn`,
   and exactly one `message_stop`;
4. on `length`, no private partial final text block, one `message_delta` with
   `max_tokens`, and exactly one `message_stop`;
5. on classified service failure, one sanitized `invalid_request_error` or `api_error`
   event and no `message_delta`/`message_stop` success terminal; or
6. on caller disconnect, silent cancellation with no later event.

The subset never emits `thinking`, `thinking_delta`, `tool_use`, or
`input_json_delta`, and never forwards internal provider/stage terminal events. A
terminal or wire failure closes projector ownership: no ping, block, alternate
terminal, or other byte may follow it.

### Count Tokens

```json
{"input_tokens": 42}
```

## Error Contract

The Anthropic-compatible error body uses a top-level `type: "error"` containing a nested `error` object with `type` and `message`.

```json
{
  "type": "error",
  "error": {
    "type": "invalid_request_error",
    "message": "model is required"
  }
}
```

오류 타입:

- `invalid_request_error`: 요청 validation 실패 (missing field, bad value, unsupported header), request body가 ingress 상한 초과 (413)
- `authentication_error`: auth 실패 (401)
- `not_supported_error`: model이 protocol profile로 해석되지 않음, operation 미지원, unsupported driver (400)
- `api_error`: provider dispatch 실패, tunnel unavailable, timeout, upstream error (400/502)

### Typed operation rejection

provider-pool candidate가 요청 Messages operation을 지원하지 않으면 `400 not_supported_error` "no provider profile supports the requested Messages operation"으로 종료한다.

### Provider auth required

In legacy mode, `openai.provider_auth.enabled=true` with a missing required header returns `400 invalid_request_error` "provider auth token is required". Managed mode does not read that caller header.

## Routing

Messages requests require a `models[]` provider-pool route. A configured model-catalog TokenCounter returns a deterministic local count for count-tokens without provider selection. Only the native upstream count-tokens fallback requires an `anthropic_messages` provider-pool candidate. Legacy direct-route and single-target fallback are not admitted to this surface.

In managed mode, the public model must also be an active projected route ID/alias or an
authorized virtual preset ID for the authenticated principal. An ordinary route resolves
to exactly one internal model group and selector-compatible provider; a virtual preset
requires unique canonical projected-route bindings for its selector and every stage.
Failure never falls back to a legacy model, another route, or another credential slot.
An authorized virtual preset retains its requested virtual response model identity;
ordinary native routes and the Chat bridge retain their distinct response semantics.

Top-level `models[]` is the static catalog source for IOP model discovery and provider-pool dispatch.
`models[]` provider mapping은 OpenAI-compatible provider와 normalized-only provider를 같은 model group 안에 둘 수 있다. dispatch는 기존 capacity + priority + availability 기준으로 provider를 한 번 선택하고, client request field가 아니라 selected provider capability로 native Anthropic 또는 Chat bridge execution path를 결정한다.

### Native vs Bridge

선택된 provider의 `ConcreteProtocolProfile.Driver`가 `anthropic_messages`이면 Edge는 provider raw tunnel을 통해 Anthropic-native request/response를 relay한다. Ordinary native routes preserve provider response model/body bytes, while authorized virtual presets rewrite successful response identity to the requested virtual model.
`openai_chat`이면 Edge는 caller-neutral request requirements를 먼저 만들고, profile이 보존 가능한 operation을 선택한다. Chat Completions가 tools+effort 조합을 보존하지 못하지만 같은 profile의 Responses operation이 보존할 수 있으면 `POST /v1/responses`로 bridge하고 결과를 Anthropic Messages 형식으로 되돌린다. Chat operation이 요구사항을 모두 보존할 때만 기존 Chat bridge를 사용한다. Authorized virtual presets retain their requested virtual response model identity through that conversion; ordinary bridge responses use the selected bridge's converted response model semantics.
그 외 driver는 `502 api_error` "selected provider returned an unsupported protocol driver"를 반환한다.

Chat bridge는 Gemini OpenAI-compatible tool call의 `extra_content.google.thought_signature`를 opaque Anthropic `tool_use.id`에 담아 caller에게 전달한다. Caller는 해당 id를 tool result까지 변경 없이 replay해야 하며, 다음 요청에서 Edge는 원래 tool call id와 signature를 복원한다. Signature가 없는 provider의 tool id는 변경하지 않는다.

### Authorized virtual-preset Hot Path

Ordinary native Messages routes preserve selected-provider status, allowlisted headers,
body bytes, and SSE framing; the ordinary Chat bridge retains its documented converted
response semantics. The exception is an admitted catalog execution preset with an
authorized virtual public model and immutable selector provider, health, capability,
and credential-binding evidence.

For that virtual-preset Hot Path, Edge collects and structurally classifies selected
tunnel or normalized output before commitment, then emits the caller-requested
endpoint-native JSON or SSE shape. Successful output keeps the requested virtual model
and requires a provider-reported response ID (including `message_start.message.id` for
native SSE). It never promotes a run ID, frame timestamp, or another IOP transport value
into public provider metadata, and it does not apply the ordinary `msg_iop` fallback.
Missing provider identity, `BODY` or `END` before `RESPONSE_START`, malformed selected
output, or a failed selector gate returns one sanitized endpoint-standard `api_error`
before response commitment.

### Profile capability admission

Anthropic Messages 요청은 선택된 provider가 다음 capability를 가져야 한다:

- native: `messages` capability + `messages` operation
- Chat bridge: `chat` capability + `chat_completions` operation + 요청 control을 보존하는 operation별 normalization
- Responses bridge: `responses` capability + `responses` operation + 요청 control을 보존하는 operation별 normalization
- `streaming` capability (streaming 요청인 경우)
- `tool_calling` capability (tools가 있는 요청인 경우)
- `count_tokens` capability + `count_tokens` operation (count_tokens native fallback 요청인 경우; TokenCounter local count path는 provider selection 및 capability check가 필요 없다)

capability 불만족은 `400 not_supported_error`로 종료한다.

### Profile thinking support

Chat bridge의 explicit `thinking.type="enabled"`와 assistant thinking block 전달은 provider profile의 `extensions.thinking` 또는 `extensions.reasoning`이 `true`일 때만 지원한다. Claude Code가 이전 응답에서 받은 빈 signature의 thinking block을 generic Chat profile 요청에 replay하면 private reasoning block만 제거하고 visible text/tool history는 유지한다. Signed thinking block은 profile과 관계없이 Chat bridge에서 거부한다.
해당 profile extension 없이 explicit enabled thinking으로 bridge하면 `400 invalid_request_error` "selected Chat profile does not support thinking"를 반환한다. `thinking.type="adaptive"`는 별도 budget field를 만들지 않고 `output_config.effort`를 `reasoning_effort`로 변환한다.
OpenAI profile의 Responses operation은 adaptive effort와 caller tools를 함께 보존할 수 있지만 explicit `thinking.budget_tokens`를 보존하지 않으므로, 이 조합은 Responses로 조용히 변환하지 않고 fail closed한다.

## Usage Attribution

Anthropic handlers do not record the OpenAI canonical usage metric series. Native `USAGE` tunnel frames are ignored by the Anthropic relay; provider-reported usage remains in the native response body or is converted by the Chat bridge response path. The marked coordinator exception records only the unlabeled admission counter `iop_anthropic_single_request_ingress_total`; it does not infer provider usage or expose request-derived dimensions.

## Managed API-key lease issuance

Built-in API-key profiles such as `seulgi_messages` may declare their auth header case-insensitively (for example the lowercase `x-api-key`). The Control Plane canonicalizes the resolved header name to its HTTP-canonical spelling (`X-Api-Key`) before signing the lease scope, so the managed API-key lease is issued and consumed successfully and the Node injects only that exact signed lease instruction upstream, never the raw secret. A lease-issuance or consumption failure fails closed with a sanitized `502 api_error` and never falls back to caller auth or a bearer slot. This outbound provider-header canonicalization is distinct from inbound IOP caller auth. The deterministic credential-slot qualification exercises both managed profiles (Chat and Messages) end to end.

## 금지 사항

- `metadata.user`는 identity source가 아니며 사용되지 않는다.
- `metadata`는 route/response mode selector가 아니다.
- Anthropic request에 provider/Ollama 전용 root field를 추가하지 않는다.
- provider body에는 IOP 확장 envelope를 섞지 않는다.
- raw provider token을 Edge config, tracked docs, roadmap, task artifact, metric label에 저장하지 않는다.
- missing required provider auth error body나 log에 raw header 값을 echo하지 않는다.
- Never expose a slot alias, raw credential, lease id, ciphertext, private key, recipient material, or target URL through an Anthropic error, log, event, metric, or task artifact.
- Marked-preset Plan/Review template customization은 caller-visible 계약이 아니다. 요청 body, header, metadata로 템플릿을 선택하거나 전달할 수 없고, 템플릿 경로·본문·digest를 응답, 오류, log, metric label로 노출하지 않는다.

## 관련 계약

- `iop.openai-compatible-api`: `agent-contract/outer/openai-compatible-api.md` (공유 auth, metadata, ingress, usage metric, model catalog)
- `iop.edge-node-runtime-wire`: `agent-contract/inner/edge-node-runtime-wire.md` (provider tunnel, protocol profile wire)
- `iop.edge-config-runtime-refresh`: `agent-contract/inner/edge-config-runtime-refresh.md` (protocol profile config, overlay, alias)
