사용자별 credential 저장, lease, projection, runtime 전달과 OpenAI-compatible 계약 및 검증 근거를 함께 반영한다.
313 lines
16 KiB
Markdown
313 lines
16 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/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 only active projected `route_id`s for the authenticated
|
|
principal. Request model selection binds strictly to one projected route's `slot_id`,
|
|
`profile_id`, and `upstream_model`. Unknown, inactive, or cross-principal routes never
|
|
fall back to global catalog or legacy defaults.
|
|
|
|
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`. 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.
|
|
|
|
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` 값:
|
|
|
|
- `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
|
|
|
|
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 or alias for the authenticated principal. It resolves to exactly one internal model group and selector-compatible provider; failure never falls back to a legacy model or another credential slot.
|
|
|
|
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.
|
|
|
|
## 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.
|
|
|
|
## 관련 계약
|
|
|
|
- `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)
|