284 lines
12 KiB
Markdown
284 lines
12 KiB
Markdown
# 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/anthropic_types.go`
|
|
- `apps/edge/internal/openai/routes.go`
|
|
- `apps/edge/internal/openai/principal.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.
|
|
|
|
### 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`를 반환한다.
|
|
|
|
### Provider auth forwarding
|
|
|
|
`openai.provider_auth.enabled=true`이면 caller는 provider별 raw user token을 `openai.provider_auth.from_header`에 담아 보낸다. 기본 header는 `X-IOP-Provider-Authorization`이다.
|
|
Edge는 이 값을 provider tunnel request의 `openai.provider_auth.target_header`로 전달한다. 기본 target header는 `Authorization`, 기본 scheme은 `Bearer`다.
|
|
|
|
이 provider token은 IOP inbound auth인 `Authorization: Bearer <token>`과 분리된다. `openai.bearer_token` 또는 `openai.principal_tokens[]`가 쓰는 IOP auth token을 외부 provider credential로 재사용하지 않는다.
|
|
|
|
## Required Headers
|
|
|
|
Anthropic-compatible 요청은 다음 헤더를 필수로 포함해야 한다.
|
|
|
|
```http
|
|
anthropic-version: 2023-06-01
|
|
```
|
|
|
|
지원하는 `Anthropic-Beta` 값:
|
|
|
|
- `claude-code-20250219`
|
|
- `fine-grained-tool-streaming-2025-05-14`
|
|
- `interleaved-thinking-2025-05-14`
|
|
- `prompt-caching-2024-07-31`
|
|
|
|
지원하지 않는 beta 값을 보내면 `400 invalid_request_error`를 반환한다.
|
|
Chat bridge 경로는 `Anthropic-Beta`를 지원하지 않으며, bridge로 라우팅될 때 beta 값이 있으면 `400 invalid_request_error`를 반환한다.
|
|
|
|
## 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": "enabled", "budget_tokens": 1000 },
|
|
"metadata": { "user_id": "user-123" }
|
|
}
|
|
```
|
|
|
|
필드 의미:
|
|
|
|
- `model`: Edge가 내부 `adapter + target`으로 해석할 외부 route 이름이다. IOP Edge에서는 라우팅을 위해 필수다.
|
|
- `max_tokens`: 출력 토큰 상한이다. 필수 field다. 0 이하 값은 `400 invalid_request_error`를 반환한다.
|
|
- `messages`: `user` 또는 `assistant` role만 허용한다. content는 string 또는 content block array다.
|
|
- `system`: string 또는 text block array만 허용한다.
|
|
- `stream`: `true`이면 provider raw SSE를 relay한다. `false` 또는 생략이면 non-streaming JSON 응답을 반환한다.
|
|
- `temperature`: 0..1 범위. 범위를 벗어나면 `400 invalid_request_error`를 반환한다.
|
|
- `top_p`: 0..1 범위. 범위를 벗어나면 `400 invalid_request_error`를 반환한다.
|
|
- `top_k`: 양수여야 한다.
|
|
- `stop_sequences`: 빈 문자열은 허용되지 않는다.
|
|
- `tools`: 각 tool은 `name`, `input_schema`를 필수로 가진다.
|
|
- `tool_choice`: `auto`, `any`, `none`, `tool` 타입만 허용한다.
|
|
- `thinking`: `type="enabled"`와 양수 `budget_tokens`만 허용한다.
|
|
- `metadata`: caller-defined metadata로 보존하되 IOP identity source로 사용하지 않는다.
|
|
|
|
### 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`: 요청 model echo.
|
|
- `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
|
|
|
|
### 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
|
|
|
|
`openai.provider_auth.enabled=true`이고 required header가 없으면 `400 invalid_request_error` "provider auth token is required"를 반환한다.
|
|
|
|
## 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.
|
|
|
|
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한다.
|
|
`openai_chat`이면 Edge는 Anthropic Messages request를 Chat Completions request로 bridge하고, Chat bridge 응답을 다시 Anthropic Messages response로 변환한다.
|
|
그 외 driver는 `502 api_error` "selected provider returned an unsupported protocol driver"를 반환한다.
|
|
|
|
### Profile capability admission
|
|
|
|
Anthropic Messages 요청은 선택된 provider가 다음 capability를 가져야 한다:
|
|
|
|
- native: `messages` capability + `messages` operation
|
|
- Chat bridge: `chat` capability + `chat_completions` operation
|
|
- `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는 provider profile의 `extensions.thinking` 또는 `extensions.reasoning`이 `true`일 때만 `thinking` block을 지원한다.
|
|
thinking 미지원 profile로 bridge하면 `400 invalid_request_error` "selected Chat profile does not support thinking"를 반환한다.
|
|
|
|
## Usage Attribution
|
|
|
|
Anthropic handlers do not currently 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.
|
|
|
|
## 금지 사항
|
|
|
|
- `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하지 않는다.
|
|
|
|
## 관련 계약
|
|
|
|
- `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)
|