---
spec_doc_type: spec
spec_id: input/openai-compatible-surface
status: 부분
source_evidence:
  - type: contract
    path: agent-contract/outer/openai-compatible-api.md
    notes: OpenAI-compatible 외부 HTTP 계약
  - type: contract
    path: agent-contract/outer/anthropic-compatible-api.md
    notes: Anthropic-compatible Messages 외부 HTTP 계약
  - type: code
    path: apps/edge/internal/openai/routes.go
    notes: OpenAI-compatible route와 bearer auth 처리
  - type: code
    path: apps/edge/internal/openai/chat_handler.go
    notes: Chat Completions request validation, route dispatch, tool/reasoning 정책
  - type: code
    path: apps/edge/internal/openai/hot_path_light.go
    notes: unmarked light preset의 caller-workspace PLAN→Work REVIEW handoff→Review inspection/repair 상태
  - type: code
    path: apps/edge/internal/openai/provider_model_rewrite.go
    notes: selected-profile Chat token-limit alias normalization과 unmarked selector frontier에 따른 prepare-only/pair-write instruction 주입 경계
  - type: test
    path: apps/edge/internal/openai/hot_path_light_test.go
    notes: OpenAI/Anthropic caller-workspace handoff, reviewer inspection, repair, terminal, cleanup 회귀 검증
  - type: test
    path: apps/edge/internal/openai/hot_path_direct_test.go
    notes: Chat/Messages selector body의 prepare-only/pair-write 상태 결속과 swapped-operation 거부 회귀 검증
  - type: code
    path: apps/edge/internal/openai/gemini_handler.go
    notes: Gemini-native request validation과 official agy tool continuation의 Chat bridge 변환
  - type: test
    path: apps/edge/internal/openai/gemini_handler_test.go
    notes: official agy model-role functionResponse와 thought signature/tool-call 매칭 회귀 검증
  - type: code
    path: apps/edge/internal/openai/route_resolution.go
    notes: model catalog attribution policy와 direct provider id 해석
  - type: code
    path: apps/edge/internal/openai/principal_routes.go
    notes: Managed projected route resolution and no-fallback candidate predicate
  - type: code
    path: apps/edge/internal/service/provider_tunnel.go
    notes: Credential binding validation, lease attachment, pre-send fence, safe dispatch attribution
  - type: code
    path: apps/edge/internal/openai/stream_gate_ingress.go
    notes: body 첫 read 전 ingress 상한과 request-local snapshot
  - type: code
    path: apps/edge/internal/openai/stream_gate_runtime.go
    notes: runtime-enabled Chat과 provider tunnel response lifecycle
  - type: code
    path: apps/edge/internal/openai/chat_decode.go
    notes: Chat role/content/reasoning-alias repeat history decoder
  - type: code
    path: apps/edge/internal/openai/responses_decode.go
    notes: Responses message/reasoning/function-call repeat history decoder
  - type: code
    path: apps/edge/internal/openai/normalized_sse.go
    notes: normalized Chat Completions SSE stream과 terminal event 처리
  - type: code
    path: apps/edge/internal/openai/provider_tunnel.go
    notes: provider raw tunnel status/header/body passthrough 처리
  - type: code
    path: apps/edge/internal/openai/responses_handler.go
    notes: Responses API request validation, bounded metadata 처리, non-stream completion
  - type: code
    path: apps/edge/internal/openai/anthropic_handler.go
    notes: Anthropic Messages/CountTokens handler, protocol profile capability admission, native/bridge routing
  - type: code
    path: apps/edge/internal/openai/single_request_metrics.go
    notes: Unlabeled runtime counter for accepted marked Anthropic single-request ingress
  - type: test
    path: apps/edge/internal/openai/single_request_handler_test.go
    notes: Non-streaming real HTTP POST, multiple private Node tool round trips, exact ingress count, terminal acknowledgement, privacy, failure, cancellation, and count-tokens compatibility; linked ingress/lifecycle/privacy observation evidence with unlabeled metric and raw-free correlation assertion
  - type: code
    path: apps/edge/internal/service/single_request_tool_types.go
    notes: Closed internal workspace call/result schemas, strict operation decoding, and raw-free result projection
  - type: code
    path: apps/edge/internal/service/single_request_tool_loop.go
    notes: Ordered exact-generation workspace continuation with correlation, immutable budgets, and cancellation
  - type: test
    path: apps/edge/internal/service/single_request_tool_loop_test.go
    notes: Multi-tool continuation, identity/capability rejection, stale result, budget, deadline, and typed cancellation evidence
  - type: code
    path: apps/edge/internal/openai/single_request_anthropic_stream.go
    notes: Privacy-closed marked SSE progress, liveness, content-index, terminal, and ticker lifetime ownership
  - type: test
    path: apps/edge/internal/openai/single_request_anthropic_stream_test.go
    notes: Exact-wire progress/repair/ping/privacy tests, terminal races and failures, disconnect, acknowledgement order, and one streaming POST
  - type: code
    path: apps/edge/internal/openai/anthropic_native.go
    notes: Anthropic native tunnel response relay with header allowlist
  - type: code
    path: apps/edge/internal/openai/anthropic_bridge.go
    notes: Anthropic Messages ↔ Chat Completions request/response bridge
  - type: code
    path: apps/edge/internal/openai/provider_normalization.go
    notes: Caller-neutral provider operation selection, effort fallback, Messages/Chat ↔ Responses conversion for public bridges and private single-request stages
  - type: code
    path: apps/edge/internal/openai/single_request_provider_stage.go
    notes: Frozen stage dispatch verification with explicit-provider fencing and default-selector provider-pool ownership
  - type: test
    path: apps/edge/internal/openai/single_request_provider_stage_test.go
    notes: Private stage Chat/Responses selection, max-to-xhigh fallback, response conversion, and default-selector dispatch regression
  - type: code
    path: apps/edge/internal/openai/anthropic_stream.go
    notes: Chat/Responses provider output을 Anthropic Messages JSON/SSE로 변환
  - type: code
    path: apps/edge/internal/openai/anthropic_types.go
    notes: Anthropic request/response types, header validation, content block decode
  - type: test
    path: apps/edge/internal/openai/anthropic_bridge_test.go
    notes: Messages compatibility mapping, Responses operation selection, effort fallback, Gemini thought signature 왕복 검증
  - type: test
    path: apps/edge/internal/openai/responses_protocol_profile_test.go
    notes: Responses effort nearest-lower mapping과 나머지 reasoning field 보존 검증
  - type: code
    path: apps/edge/internal/openai/principal.go
    notes: Shared principal token hash auth for both OpenAI and Anthropic surfaces
  - type: code
    path: apps/edge/internal/openai/provider_tunnel.go
    notes: Shared provider tunnel auth headers and passthrough
  - type: code
    path: apps/edge/internal/openai/provider_model_rewrite.go
    notes: Selected profile의 operation별 tool-call wire에 따른 Gemini thought-signature 왕복 정규화
  - type: test
    path: apps/edge/internal/openai/provider_model_rewrite_test.go
    notes: Gemini-only non-stream/stream/HTTP tool continuation과 non-Gemini 무변경 회귀 검증
  - type: code
    path: packages/go/config/protocol_profile.go
    notes: ConcreteProtocolProfile, operation capability admission, model/effort normalization
  - type: code
    path: apps/edge/internal/openai/run_result.go
    notes: RunEvent stream을 OpenAI-compatible result로 수집
  - type: code
    path: apps/edge/internal/openai/principal.go
    notes: principal token hash auth와 authenticated principal metadata 구성
  - type: code
    path: apps/edge/internal/openai/usage_metrics.go
    notes: Request-local terminal and actual-provider attempt usage recording
  - type: code
    path: apps/edge/internal/openai/stream_gate_dispatcher.go
    notes: Attempt ownership and exactly-once usage finalization on close or abort
  - type: test
    path: apps/edge/internal/openai/chat_handler_test.go
    notes: Chat Completions route와 target dispatch 검증
  - type: test
    path: apps/edge/internal/openai/provider_tunnel_test.go
    notes: provider-pool raw tunnel passthrough 검증
  - type: test
    path: apps/edge/internal/openai/provider_dispatch_test.go
    notes: provider/model-group attribution route binding과 strict provider identity 검증
  - type: test
    path: apps/edge/internal/service/model_queue_admission_test.go
    notes: 공유 provider cross-model capacity와 no-candidate unavailable 검증
  - type: test
    path: apps/edge/internal/openai/usage_metrics_test.go
    notes: Canonical provider series, request-terminal deduplication, and provider-switch attribution
  - type: test
    path: apps/edge/internal/openai/stream_gate_stall_recovery_test.go
    notes: Always-owned Chat/Responses normalized/tunnel S05 recovery and disabled-semantic compatibility matrix
  - type: test
    path: apps/edge/internal/openai/liveness_recovery_observability_test.go
    notes: Chat/Responses normalized/tunnel liveness metric labels and default log-safety matrix
  - type: docs
    path: docs/openai-usage-grafana.md
    notes: Grafana query, daily/monthly rollup, usage origin, cloud-equivalent cost, avoided-cost ROI 조회 가이드
  - type: code
    path: apps/edge/internal/input/manager.go
    notes: Edge input manager composite construction; SetSingleRequestExecutor wires the production SingleRequestExecutor into the service at manager New
  - type: test
    path: apps/edge/internal/input/manager_test.go
    notes: Manager installation regression covering the composite single-request executor wiring
  - type: code
    path: apps/edge/internal/openai/single_request_executor.go
    notes: Production composite SingleRequestExecutor with private plan/work/review stage drivers and the correlated continuation bridge
  - type: code
    path: apps/edge/internal/service/single_request.go
    notes: Validated closed terminal disposition, frozen terminal winner, cleanup conversion, and acknowledgement stability
  - type: code
    path: apps/edge/internal/openai/single_request_quality_gate.go
    notes: Request-local S11 stage classification and fixed-hash repetition/no-progress detection
  - type: test
    path: apps/edge/internal/openai/single_request_quality_gate_test.go
    notes: S11 timeout, budget, repetition, malformed, context, length, cancel, and tool terminal evidence
  - type: test
    path: scripts/e2e-single-request-claude.sh
    notes: Supervised external Claude qualification harness with retry and session-title generation disabled
  - type: complete-log
    path: agent-task/archive/2026/08/m-iop-owned-single-request-agent-execution/25+23,24_claude_smoke_qualification/complete.log
    notes: S12 one-ingress Claude qualification with expected stage order, terminal, workspace output, timing, cleanup, and redacted evidence
---

# 스펙: OpenAI-Compatible 입력 표면

## 목적

Edge가 OpenAI-compatible HTTP 요청을 받아 내부 `adapter + target` 실행으로 넘기는 현재 동작을 설명한다.

## 기능 목록

| 기능 | 설명 |
|------|------|
| OpenAI-compatible HTTP server | `openai.enabled=true`이면 Edge input manager가 `/healthz`, `/v1/models`, `/v1/chat/completions`, `/v1/responses`, `/api/` route를 제공한다. |
| bearer auth | `openai.bearer_token`이 있으면 matching bearer authorization header를 요구한다. |
| principal token auth | `openai.principal_tokens[]`가 설정된 경우 raw token의 SHA-256 hash를 `token_hash_sha256`과 매칭하고, 매칭 시 `iop_principal_ref`, `iop_principal_alias`, `iop_token_ref`, `iop_principal_source` metadata를 채운다. |
| multi-token principal | 같은 `principal_ref`에 여러 `token_ref`를 연결할 수 있으며, 사용량 metric은 사용자 합산과 token/app별 breakdown을 모두 가능하게 한다. |
| managed projection auth | `credential_plane.enabled=true` uses the fresh Control Plane projection for inbound token auth and principal route discovery. Static principal/bearer fallback is disabled. |
| managed slot route | Public model id/alias resolves to one projected route, exact slot/profile/upstream model/resource selector, and immutable revisions/generation. Unknown, cross-principal, stale, revoked, or ambiguous bindings fail closed. |
| marked preset single-request admission | An authorized fixed single-request preset compiles one service-owned admission value at request start: requested public model, canonical plan/work/review bindings resolved through managed authorization, opaque workspace capability, and absolute resource caps. Later refresh cannot mutate the admitted shape. No private binding is echoed to the caller. Compiled only after every canonical reference is verified through its catalog binding for the authenticated principal; missing, duplicate, unauthorized, dynamically selected, or option-inconsistent inputs are rejected without fallback. |
| unmarked caller-workspace light route | An unmarked `light` preset binds only admitted caller `workspace_tools`; it never accepts a raw caller path. Edge derives a phase-specific selector instruction from the locked artifact frontier. A non-parent-creating write binding first receives `prepare-only` for exactly one admitted `.iop/job/<request_id>` prepare call with PLAN/REVIEW writes prohibited; its successful receipt resumes the same selector with `pair-write` for the accepted artifact grammar. A parent-creating binding receives `pair-write` immediately. The selector provider sees only canonical `prepare(path)` or one atomic `iop_write_artifact_pair(plan_content,review_content)` schema. The selector must write every PLAN field in English ASCII; Edge rejects non-ASCII PLAN content. Edge validates and expands the pair into two independently receipted writes encoded in the admitted caller tool schema such as `bash(command)`; the selector is not required to produce parallel tool calls. Continuation issue identity covers semantic tool-call ID/name/arguments rather than SDK-decorated assistant content, reasoning, or provider metadata, while request prefix and result IDs stay independently validated. The selector instruction is the final leading Chat `system` message or final Anthropic top-level `system` text block and does not mutate the retained caller snapshot or add a public field. Work receives exactly the relative PLAN path plus `Read it and complete the task.` and only ordinary caller tools. After a successful PLAN read and a non-empty terminal worker report, Edge renders and validates the completed `P1..Pn` REVIEW handoff and projects its single caller write; Work does not manage that protocol. Selector/Work/Review Chat bodies normalize `max_tokens`/`max_completion_tokens` to the selected profile wire; target-native values win when both aliases exist. The bounded selector PLAN/REVIEW seed is capped at 4096 output tokens independently of a caller SDK's larger completion default. When tools plus reasoning effort are not representable on the selected OpenAI Chat wire, the internal stage alone uses that profile's lossless buffered Responses operation with body/tunnel `stream=false`, omits incompatible generic SDK `temperature`/`top_p` defaults, and restores the common hot-path stage shape by selected operation rather than caller protocol. A selector upstream HTTP 400 becomes one non-retryable caller HTTP 400 `invalid_request_error`, while provider details remain private. Review reads both artifacts, successfully inspects an ordinary caller result, repairs/re-verifies in the same binding when needed, and owns the non-empty final output without rewriting REVIEW. Cleanup removes only the request `.iop/job/<request_id>` directory. Marked `single_request` presets bypass this route and retain the operator-owned Node workspace. |
| marked single-request provider normalization | Plan/Work/Review derive caller-neutral effort/tool/structured-output requirements and let the selected protocol profile choose Chat Completions or Responses. Effort exact misses fall only to the nearest declared lower grade (`max` → `xhigh` when `max` is absent). Explicit resource selectors keep exact provider-ID verification; a `default` selector leaves provider choice to the pool while model group, profile, upstream model, credential slot/revision, and tunnel path remain frozen. Both Chat and Responses results are converted into the private common Chat-shaped stage codec before Plan/Work/Review validation. Chat conversion discards only bounded standard/provider bookkeeping (`service_tier`, `system_fingerprint`, provider `timings`, choice `logprobs`, message `annotations`, null `refusal`) and rejects a non-null refusal or unknown/duplicate fields. |
| marked single-request internal templates | The admission also freezes the operator-configured effective Plan/Review Markdown templates. They are internal artifact shapes only: the Plan stage first performs a bounded read/list-only workspace inspection and requires one successful result in the same request-local conversation, then obtains a strict one-line `goal` plus bounded one-line `steps`/`verification` arrays. Edge owns the bullet formatting and renders `plan.md`; no separate analysis artifact is created. The Review template shapes the private `review.md` artifact. Callers cannot supply, name, or select a template, and template paths, contents, and digests never appear in a response, error, log, or metric label. The caller-visible Messages request/response schema is unchanged and the final text stays the model's `decision.output`. |
| marked single-request ingress | One validated and authorized Messages POST enters the separate service coordinator capability before legacy provider/caller continuation and increments `iop_anthropic_single_request_ingress_total` once. Non-streaming returns one buffered final-only message. Streaming keeps one envelope across the coordinator lifetime, exposes only fixed plan/work/review/repair text blocks plus `event: ping`, and commits one final text/error terminal. Internal reasoning/tool wire never becomes caller `tool_use`; success is acknowledged only after the complete terminal write succeeds. |
| marked single-request S11 terminal policy | The service freezes one closed `end_turn`, `length`, `error`, or `cancelled` disposition. `error` classes are provider, validation, timeout, budget, repetition, malformed, context, internal-tool, and workspace-cleanup. Buffered and SSE share one projection: `end_turn`; `max_tokens` with no private partial output; `400 invalid_request_error` for validation/context; `502 api_error` for other failures; and silent cancellation after caller disconnect. No terminal classification retries, falls back, opens a second request, or later writes success. |
| marked single-request observation evidence | A single real POST links ingress=1, request-total=1, terminal=1, stage/tool/cleanup counts, and raw-free correlation without public tool protocol. `iop_anthropic_single_request_ingress_total` is unlabeled (no request_id, stage_id, provider identity, or content). Internal tool names, raw arguments, private results, and workspace references are absent from the public terminal and from log projections. Stage-pure timing, cardinality-bounded labels, and privacy semantics are documented here. SDD S12 qualified an external Claude task on an approved IOP Node with ingress `0→1`, Gemini → ornith-fast → Gemini, one `end_turn`, the exact expected workspace result, timing, cleanup, and redacted evidence. |
| marked internal workspace tool loop | The service accepts only closed read/list/write/delete/command calls from the saved internal stage, opens the admitted Node workspace once, executes calls sequentially on the frozen connection generation, correlates one result to one unique request/stage/tool identity, and resumes only through the emitting executor's optional continuation. Strict decoding, capability checks, cumulative per-stage iteration/output/deadline limits, request wall-clock budget, and typed cancellation fail closed without fallback or another Messages request. |
| managed provider credential | After candidate selection, Edge obtains a short-lived Node-targeted lease on the authenticated CP connection, fences it immediately before send, and never accepts caller provider credentials or same-model slot fallback. |
| legacy provider auth forwarding | Only when managed mode is disabled, `openai.provider_auth` can read a raw provider token from the configured caller header and forward it to the selected provider. |
| model catalog | `/v1/models`는 provider-pool `models[]`, legacy `openai.model_routes[]`, `openai.models` 또는 `openai.target` 순서로 노출 모델을 만든다. |
| model dispatch | request `model`은 provider-pool catalog, legacy model route, single target fallback 순서로 해석된다. |
| attribution route binding | provider-pool model은 `models[].usage_attribution`의 effective policy와 선택된 actual provider를 보존한다. direct route는 route-level `provider_id`를 top-level fallback보다 우선하며 adapter/node text를 provider identity로 대체하지 않는다. |
| provider-pool handoff | provider-pool catalog에 model이 있으면 service 요청은 `ProviderPool=true`로 전달되고 adapter/target은 provider selection 이후 확정된다. |
| cross-model provider admission | 서로 다른 외부 model key가 같은 provider id를 참조하면 Edge의 provider resource lease 하나에서 일반·long capacity를 합산한다. |
| provider-pool queue/unavailable | root provider-pool queue policy를 모든 model group에 공통 적용하고, pending request의 live candidate가 모두 사라지면 timeout을 기다리지 않고 기존 `502 node_dispatch_error` envelope로 종료한다. |
| mixed provider dispatch | model group 안에 OpenAI-compatible provider와 Ollama/native provider가 함께 있어도 request field가 아니라 selected provider capability가 passthrough 또는 normalized 실행 경로를 결정한다. |
| legacy route 변환 | legacy route는 외부 `model`을 route entry의 `adapter`, `target`, `node`, queue policy로 변환한다. |
| bounded metadata 처리 | metadata는 최대 16개 string key/value만 허용하며 실행 디렉터리나 runtime/session 소유권을 선택하지 않는다. |
| Chat Completions | `/v1/chat/completions`는 non-streaming과 streaming SSE를 지원한다. |
| Anthropic ingress | `POST /v1/messages` and `POST /anthropic/v1/messages` share one handler; the corresponding count-tokens paths share another. `/anthropic/v1/models`, and `/v1/models` with `anthropic-version`, return the Anthropic model-list shape. Wrong methods return `405 invalid_request_error`. |
| Anthropic caller auth | Anthropic ingress accepts `Authorization: Bearer <token>` or `X-Api-Key: <token>`. If both are present they must match; shared principal-token and legacy bearer fallback apply after this validation. |
| Anthropic provider-pool dispatch | Messages and count-tokens require a provider-pool model route. Native Messages requires `messages` capability and operation, while the Chat bridge requires `chat` capability and `chat_completions` operation; streaming and tools add their own capability checks. |
| provider-normalized Messages bridge | Supported Messages compatibility headers are consumed at the bridge. Edge derives caller-neutral tool/effort/token-budget/stream requirements, selects a profile operation that preserves them, and maps effort to exact or nearest lower provider grade. Chat-compatible providers may therefore use Chat or Responses without caller-name branches. Mid-conversation `system` messages retain their ordered privileged role as Chat `role=system` or Responses system input messages; they are not downgraded to user text. Placement, text-only content, and pending-tool fences fail closed. JSON schema and tool shapes are converted for the selected wire; Gemini tool thought signatures still round-trip through opaque tool-use ids. |
| Gemini-native agy tool continuation | Official agy 1.1.12의 Gemini-native 요청을 Chat 실행 경로로 변환한다. tool 실행 뒤 독립 `role:model` content로 전달되는 `functionResponse`는 앞선 function call과 매칭해 Chat `tool` message로 변환하며, assistant content와 response가 한 model content에 섞인 모호한 요청은 거부한다. |
| bounded ingress and StreamGate ownership | Chat/Responses bodies are limited to 16 MiB before the first read. Every supported path delegates response-start staging, applicable filter arbitration, bounded liveness recovery, and the single terminal to `runtime/stream-evidence-gate`; `enabled` controls configured semantic policy only. |
| typed stall terminal | Supported Chat/Responses normalized and tunnel attempts always translate only Edge-confirmed `response_stalled` terminals into a raw-free liveness recovery candidate; post-commit, cancelled, tool-bearing, missing-snapshot, exhausted, unsupported, unconfirmed, generic, and no-owner paths stay terminal. |
| liveness operational evidence | Each private liveness cycle emits one closed eligibility counter and at most one closed final-result counter. Constructor-owned generic logs use a safe projection without identifiers or payloads, while application-installed observation sinks retain the original immutable events. |
| repeat-resume request shape | A selected continuation uses only request-local assistant content/reasoning plus a fixed English directive. Chat emits assistant provenance followed by the directive; Responses emits assistant output/reasoning items and places the directive in `instructions`. Caller messages, `input`, and original `instructions` are excluded. |
| repeat history boundary | Chat and Responses use separate endpoint decoders to create a bounded raw-free role/channel/action snapshot from the current request only. User occurrences exclude assistant anchors; missing reasoning does not infer lineage or TTL state. |
| model-driven response path | request `model`이 가리키는 provider capability가 provider raw tunnel 또는 normalized RunEvent path를 결정한다. caller metadata는 route나 response shape를 선택하지 않는다. OpenAI와 Anthropic ingress는 같은 model catalog와 provider-pool dispatch를 공유한다. |
| provider raw passthrough | `passthrough`는 provider status/header/body bytes를 기존 Edge-Node tunnel로 relay하고 pure response body에 IOP 확장 envelope를 섞지 않는다. Chat request의 출력 상한 alias만 selected profile wire로 bounded normalization한다. |
| provider-native field 보존 | provider raw tunnel route는 `model` served target rewrite, auth/header 처리, selected-profile Chat token-limit alias normalization 외에 selected provider가 지원하는 표준 field와 provider extension field를 보존한다. OpenAI route는 OpenAI-compatible field를, Anthropic native route는 Anthropic field를 보존한다. |
| Gemini Chat tool-call normalization | Selected profile의 operation별 tool-call wire가 `gemini_openai_chat`이면 Gemini `thought_signature`를 opaque 표준 tool-call id로 캡슐화해 caller가 보존할 수 있게 하고, 다음 tool result 요청에서 원래 id/signature를 복원한다. Effort mapping과 독립적이며 다른 provider와 caller identity에는 적용하지 않는다. |
| OpenAI usage metering | OpenAI handlers emit one request terminal and canonical token/reasoning series for each actual provider attempt that reports usage. Anthropic handlers do not currently emit this metric series; native tunnel `USAGE` frames are ignored. |
| safe credential attribution | Managed OpenAI attempt metrics include only stable `credential_slot_ref` and immutable `credential_revision`; request terminals omit them, and slot alias, lease id, raw credential/key, target URL, request IDs, and payload content are forbidden labels. |
| reasoning observation metric | provider가 reasoning token을 보고하지 않고 reasoning text만 관측되면 관측 횟수와 character count 보조 metric을 emit하고, 별도 estimated-token counter(`iop_openai_reasoning_estimated_tokens_total`)로 `estimation_method="chars_div_4"` 추정을 제공한다. |
| Grafana usage surface | 1차 조회 표면은 Prometheus/Grafana query guide이며 actual `provider_id`·`served_model` 기준 daily/monthly rollup과 `usage_attribution=model_group`으로 승인된 `route_model` query-time rollup, usage origin breakdown, operator-managed cloud price baseline, cloud-equivalent cost, avoided-cost ROI 기준을 문서로 제공한다. Control Plane/Client dashboard와 request-level ledger는 후속 범위다. |
| Responses API | normalized(non-provider) `/v1/responses` supports only non-streaming string input. A provider model-group route chooses native Responses, Anthropic Messages, or OpenAI Chat wire from the selected concrete protocol profile and representable request requirements. |
| Responses provider bridge | Native Responses keeps raw tunnel bytes. Anthropic Messages and OpenAI Chat candidates bridge representable Responses requests and restore canonical Responses JSON/SSE; unsupported controls fail closed before dispatch. Gemini Chat thought signatures are carried in opaque Responses `call_id` values and restored for function-result replay. Usage remains attributed to endpoint=`responses`, route alias, and selected actual provider/served model. |
| strict output | strict output이 켜져 있으면 XML completion contract 기반 instruction 또는 prompt prefix를 추가할 수 있다. |
| tool call 처리 | Chat Completions `tools`는 provider native metadata 복원 또는 text tool-call synthesis/validation 경로를 사용한다. Anthropic Messages `tools`는 Chat bridge를 통해 OpenAI `tools`로 변환되거나, native Anthropic tunnel로 직접 전달된다. |
| cancel 전파 | HTTP caller timeout/cancel이 cancel-worthy error이면 Node `CancelRun`으로 전파한다. |

## 범위

- 포함: OpenAI-compatible HTTP auth, bounded ingress, request validation, route resolution, bounded metadata 처리, chat/responses 변환, provider-pool dispatch handoff, tool/reasoning/strict output 처리.
- 제외: OpenAI 원문 API 전체 호환, legacy `/v1/completions`, A2A JSON-RPC, Node adapter별 provider HTTP 세부, Control Plane 운영 API.

## 주요 흐름

```mermaid
sequenceDiagram
  participant Caller
  participant OpenAI as OpenAI handler
  participant Service as Edge service
  participant Runtime as Edge-Node runtime

  Caller->>OpenAI: chat/responses request(model)
  OpenAI->>OpenAI: auth, immutable projection route/binding validation
  alt selected provider supports OpenAI-compatible passthrough
    OpenAI->>Service: SubmitProviderTunnel(ProviderPool/direct, binding)
    Service->>Service: candidate selection, lease acquire, pre-send fence
    Service->>Runtime: ProviderTunnelRequest(binding, sealed lease)
    Runtime-->>Service: ProviderTunnelFrame stream
    Service-->>OpenAI: tunnel frames
    OpenAI-->>Caller: provider status/header/body bytes
  else selected provider uses normalized execution
    OpenAI->>Service: SubmitRun(adapter/target or ProviderPool)
    Service->>Runtime: RunRequest
    Runtime-->>Service: RunEvent stream
    Service-->>OpenAI: run stream
    OpenAI-->>Caller: OpenAI-compatible response or SSE
  end
```

## 계약

- `iop.openai-compatible-api`: `agent-contract/outer/openai-compatible-api.md`
- 내부 실행 wire: `agent-contract/inner/edge-node-runtime-wire.md`
- config/provider pool: `agent-contract/inner/edge-config-runtime-refresh.md`

## 설정/데이터/이벤트

- `configs/edge.yaml`의 `openai` 섹션이 listener, bearer token, legacy adapter/target, model routes, strict output을 제공한다.
- `credential_plane.enabled` is the startup-only managed/legacy switch. Managed mode requires TLS on OpenAI ingress, CP-Edge, and Edge-Node hops; config validation rejects legacy principal/provider-auth and static provider credential sources.
- Managed authentication and model resolution use one immutable projection view per request. Trusted principal/route/slot/revision metadata overwrites caller spoofing and remains bound across recovery admission.
- `openai.stream_evidence_gate.enabled` defaults to false and activates configured semantic policy only. Supported OpenAI response/liveness ownership remains in the request runtime in both states; the same config also supplies the 0..3 recovery cap and up-to-16-MiB ingress snapshot bound. Changes remain restart-required.
- A typed stall recovery re-enters provider-pool admission with the failed provider avoided. No health classification allows the stalled provider to be selected again for the same request; without an alternate, recovery terminates once.
- `iop_edge_liveness_recovery_eligibility_total` labels are `execution_path`, `provider_health`, `commit_state`, and `eligibility`; `iop_edge_liveness_recovery_results_total` labels are `execution_path`, `provider_health`, and `recovery_result`. All are closed vocabularies and exclude request/attempt/provider/model identifiers and content.
- When `repeat_guard` is configured, Chat accepts plain `content`, `reasoning_content`, `reasoning`, and `reasoning_text` provenance for fingerprinting; Responses accepts its own text/reasoning/function-call item provenance. Signed, encrypted, and unknown values are canonical-only and never sanitation or observation payloads.
- Completed action/result fingerprints provide the only request-history progress boundary. An identical consecutive action/result is no-progress; a changed completed result is progress, while a different action alone is insufficient. No caller product, session metadata, inferred TTL, or cross-request cache participates.
- top-level `models[]`가 있으면 OpenAI model list와 provider-pool dispatch에서 legacy route보다 우선한다.
- provider-pool model의 `usage_attribution`은 생략 시 `provider`이고 `model_group`은 명시적 opt-in이다. direct dispatch는 `openai.model_routes[].provider_id`를 우선하고 없으면 `openai.provider_id`를 사용한다.
- normalized run과 provider tunnel의 성공 dispatch는 actual `provider_id`, served target, resolved node id, effective attribution policy를 Edge-local result에 보존한다. strict attempt binding은 `provider_id`만 actual provider로 인정하고 adapter 또는 node id로 대체하지 않는다.
- provider-pool model group은 capacity + priority + availability 기준으로 provider candidate를 먼저 선택하고, 선택된 provider가 OpenAI-compatible 호출 방식을 지원하면 raw tunnel passthrough로 dispatch한다. Ollama/native provider가 선택되면 normalized `RunRequest` path로 dispatch한다.
- Anthropic Messages and count-tokens do not use legacy direct-route or single-target fallback. Native responses preserve provider status, allowed headers, and body/SSE bytes; bridge responses are converted between Anthropic Messages and Chat Completions shapes.
- A marked single-request Messages dispatch requires the narrow service coordinator capability and never falls back to the generic provider pool. The handler copies the immutable binding and request input and counts the accepted HTTP admission once with no labels. The service projects exactly one frozen terminal candidate through both response modes: buffered/SSE `end_turn`; buffered/SSE `max_tokens` without private partial content; `invalid_request_error` for validation/context; `api_error` for provider, timeout, budget, repetition, malformed, internal-tool, and workspace-cleanup failures; or silent cancellation after caller disconnect. The streaming path maps only fixed plan/work/review/repair summaries, serializes pings and monotonic text-block indices with one terminal owner, stops and joins liveness before terminal/return, and acknowledges completion only after `message_stop`. Arbitrary progress, reasoning, tool/provider/credential/workspace data, raw failures, and internal stage terminals stay private. No classified terminal triggers retry, fallback, partial success, a second request, or a later success terminal. Count-tokens does not enter or increment this path.
- Marked single-request Plan/Review templates are Edge-owned internal artifact shapes, not part of this input surface. Admission freezes the effective pair; Edge renders deterministic PLAN `P1..Pn` IDs, Work writes one strict REVIEW handoff (item status, changes, verification, deviations), and Review rereads both artifacts without a memory worker payload or a final REVIEW write. No caller field, header, or metadata value can supply, name, select, or override a template, and no template path, content, or digest appears in a response, an error message, a log projection, or a metric label. Changing a template changes neither the Messages request schema nor the response schema: the buffered/SSE terminal projection is unchanged and the final caller-visible text is exactly the reviewer `decision.output` after any repair/re-verification.
- Marked single-request observation evidence links ingress=1, request-total=1, terminal=1, stage/tool/cleanup counts, and raw-free correlation for one real POST. `iop_anthropic_single_request_ingress_total` is strictly unlabeled: no request_id, stage_id, provider identity, content, or workspace reference appears as a metric label. Internal tool names (`workspace_read`, `workspace_write`, etc.), raw arguments, private results, and workspace references are absent from the public terminal JSON and from log projections. Stage-pure timing, cardinality-bounded labels, and privacy semantics are documented here. SDD S12 qualifies the external Claude path on an approved IOP Node with one accepted ingress, the expected stage sequence, one terminal, exact output, timing, cleanup, and redacted evidence.
- Internal workspace calls use a service-owned schema independent of caller-facing tool codecs. The five closed operation names decode into typed Node requests only after request/stage/tool identity, canonical relative path, approved operation/command/environment capability, and immutable budget checks. The loop opens once, preserves the admitted connection generation, executes one pending call at a time, accepts only correlated typed results, and returns a deep-copied raw-free result to the same executor continuation. Repeated IDs, stale responses, malformed or denied input, timeout, output/iteration exhaustion, and cancellation never become public Anthropic tool protocol or trigger a second ingress.
- Claude Code Messages requests may use adaptive thinking, `output_config.effort`, structured output, cache-control annotations, and supported beta headers, including the compatibility-only `advisor-tool-2026-03-01` marker emitted by the pinned official caller. The Chat bridge consumes rather than forwards those headers, maps supported fields, and requires callers to replay opaque `tool_use.id` values unchanged so Gemini thought signatures can be restored on tool-result turns.
- provider capacity와 long-context slot은 model alias별이 아니라 `node_id + provider_id`별로 공유한다. queue pending 상한과 timeout은 Edge root `provider_pool` policy이며, lease 반환·refresh·disconnect/reconnect가 모든 model group waiter를 global enqueue 순서로 재평가한다.
- provider가 full이면 queue policy에 따라 대기하지만 live candidate가 모두 사라지면 즉시 unavailable로 수렴한다. Chat Completions와 Responses provider-pool 표면은 새 public status/field 없이 HTTP 502 `node_dispatch_error`를 유지한다.
- Unmarked caller-workspace selector의 upstream HTTP 400은 validation으로 분류되어 재시도 없이 caller HTTP 400 `invalid_request_error` 한 번으로 끝난다. Upstream body/endpoint는 노출하지 않으며 upstream 5xx와 transport failure는 sanitized 502를 유지한다.
- In legacy mode, `openai.provider_auth` stores only a forwarding rule and reads raw provider material from its request-time header; inbound IOP authorization is never reused. Managed mode rejects that rule and the caller header and uses only the sealed slot lease.
- OpenAI request metadata is bounded caller context. Workspace, runtime, and session ownership are outside this input surface.
- Chat Completions와 Responses request는 caller metadata로 provider raw tunnel과 normalized response shape를 선택하지 않는다. route/provider capability만 실행 경로를 결정한다.
- Provider operation selection never branches on caller/agent identity. It evaluates the selected profile against normalized request requirements. Effort uses `none < low < medium < high < xhigh < max`; an unsupported grade may fall only to the nearest declared lower grade, and no lower grade means fail-closed admission.
- run metadata에는 `openai_model`, `openai_stream`, `strict_output`, `estimated_input_tokens`, `context_class`가 들어갈 수 있다.
- provider tunnel metadata에는 routing context와 관측 후보가 들어갈 수 있으며, provider body에는 합쳐지지 않는다.
- Node complete event metadata의 `openai_tool_calls`와 `openai_text_tool_fallback`은 response tool call 복원에 쓰인다.
- OpenAI handlers emit `iop_openai_requests_total`, `iop_openai_usage_tokens_total`, `iop_openai_reasoning_observed_total`, `iop_openai_reasoning_chars_total`, and `iop_openai_reasoning_estimated_tokens_total`. Anthropic handlers do not emit these series. The marked single-request boundary emits only the unlabeled `iop_anthropic_single_request_ingress_total` admission counter.
- The request terminal uses `route_model`, `endpoint`, final `response_mode`, `status`, and `usage_source` with the stable caller labels. Provider token/reasoning series additionally use `usage_attribution`, strict actual `provider_id`, and actual `served_model` for each attempt.
- A request terminal is emitted exactly once. Each actual attempt is finalized exactly once by the attempt owner on graceful close or abort, so a provider switch records both the replaced and final providers without duplicating the request count.
- `usage_attribution="model_group"` is a query-time rollup instruction over canonical provider series grouped by `route_model`; it does not emit a duplicate model-group token counter.
- `usage_source="provider_reported"` requires provider token fields from at least one actual attempt. Reasoning characters alone may advance reasoning observation/estimate counters but leave the request source unavailable.
- `principal_ref`는 사용자/테넌트 참조값이고 `token_ref`는 앱/통합/용도별 token 참조값이다. 같은 principal에 여러 token이 있으면 `principal_ref` 기준 합산과 `token_ref` 기준 분해를 함께 사용할 수 있다.
- `node_id`, attempt/run/request/session ids, raw bearer token, provider token, and raw prompt/response content are not public metric labels. The node id remains internal attempt evidence only.
- Managed attempt labels may include `credential_slot_ref` and `credential_revision` from the verified lease scope. Slot alias, lease id, ciphertext, keys, auth header values, recipient data, and target URL are never public metric labels.
- For OpenAI passthrough, provider body usage takes precedence over tunnel `USAGE` values and proto-only reasoning/cached input may supplement it. The Anthropic native relay ignores tunnel `USAGE` frames.

## 검증

- `go test ./apps/edge/internal/openai`
- `go test ./apps/edge/internal/openai -run 'Test(AnthropicSingleRequest|PresetRequestIdentityAcrossAnthropicTurns|PresetRequestIdentityAnthropicCountTokensBypassesCoordinator)' -count=1`
- `go test -race ./apps/edge/internal/service -run 'Test(InternalWorkspaceTool|SingleRequestInternalToolLoop)' -count=1`
- `go test ./apps/edge/internal/openai -run 'TestAnthropicSingleRequest(UsesOnePost|InternalToolsStayPrivate|Observation)' -count=1`
- `go test -race ./apps/edge/internal/openai -run 'TestSingleRequestAnthropicStream' -count=1`
- `go test -count=1 ./apps/edge/internal/openai -run 'TestSingleRequest(PresetBinding|ReviewStage)'` — admitted effective-template snapshot, refresh isolation, and the custom-template internal REVIEW artifact with unchanged caller-visible output.
- `go test -race -count=1 ./packages/go/streamgate ./apps/edge/internal/openai ./packages/go/config`
- `go test ./apps/edge/internal/service`
- `go test ./apps/edge/internal/openai -run 'Tunnel|UsageMetrics|ToolValidation|Dispatch|Reasoning|Retry'`
- `rg --fixed-strings "cloud_equivalent_cost" docs/openai-usage-grafana.md`
- `make test-openai-ollama`
- provider별 실제 runtime smoke는 환경별 test profile을 따른다.

## 한계와 주의사항

- normalized(non-provider) `/v1/responses`는 non-streaming string input만 지원한다. provider model group route의 `/v1/responses`는 raw passthrough로 streaming과 Codex/unknown field를 그대로 provider에 전달한다.
- Always-on StreamGate ownership does not automatically activate repeat, missing-tool-call, schema, or other semantic policy. Those mechanics and supported paths follow `agent-spec/runtime/stream-evidence-gate.md`.
- A repeat-resume rebuild requires the request-start model catalog context window. Unknown or insufficient context fails before a replacement dispatch, preserving the recovery budget; it does not use a translator, local model, or `RecoveryPlanPreparer`.
- `/v1/completions`는 제공하지 않는다.
- OpenAI-compatible request에 provider/Ollama 전용 root field를 추가하지 않는다.
- workspace와 session 실행 제어를 request metadata 또는 prompt에 추가하지 않는다.
- pure `passthrough` body는 provider-original byte stream이며 IOP 확장 envelope나 normalized label을 포함하지 않는다.
- provider route와 non-provider normalized route의 차이는 selected provider capability에서 파생되며 caller metadata selector로 고르지 않는다.
- Grafana guide는 actual provider 기준 canonical query와 승인된 model-group rollup을 분리한다. request ledger, billing, chargeback은 이 구현 범위 밖이다.
- text tool-call synthesis는 요청 `tools[]` schema를 기준으로만 수행한다. 자연어 추론으로 tool call을 만들지 않는다.
- private token이나 endpoint 원문은 tracked spec/docs에 남기지 않는다.
- `metadata.user`는 identity source가 아니며 사용되지 않는다.
- caller가 `metadata.iop_principal_*`를 보내도 authenticated context 값이 overwrite한다.
- `openai.principal_tokens[]` 변경은 restart-required로 분류된다.
- principal token auth가 실패하면 legacy `openai.bearer_token`이 unmapped fallback으로 동작한다.
- The preceding legacy fallback applies only when the credential plane is disabled. Managed mode never returns to it after projection expiry or route/lease failure.
- provider가 별도 reasoning token을 보고하지 않으면 provider-reported `token_type="reasoning"`은 증가하지 않고, 별도 estimated token counter(`iop_openai_reasoning_estimated_tokens_total`, `estimation_method="chars_div_4"`)로 ceil(chars/4) 추정을 제공하되 billing-grade 확정값이 아니다.
- Grafana guide는 metric 조회와 operator-managed price baseline 예시이며 live cloud pricing, billing, chargeback, long-term ledger, 사용자별 제한 enforcement의 source of truth가 아니다.
- Seulgivibe Claude/OpenAI proxy는 별도 OpenAI-compatible provider family label로 보존될 수 있지만, HTTP body shape는 provider tunnel passthrough 경계를 따른다.
- Anthropic metrics are not inferred from native responses or tunnel frames; adding them requires a separate runtime change.
- The marked single-request SSE projector and private workspace continuation do not add event kinds, filters, release rules, or recovery behavior to the generic Stream Evidence Gate. The active Plan -> Work -> Review composite and request-artifact cleanup use the closed S11 `error-cancel`/length policy with deterministic local evidence. S11 is implemented, and S12 (`claude-smoke`) qualifies the external Claude path on an approved IOP Node.
- Managed API-key profiles qualify end to end: the Control Plane canonicalizes the resolved auth header (for example lowercase `x-api-key` to `X-Api-Key`) before signing the lease scope, so lease issuance and consumption succeed and the Node injects only that exact header upstream. A lease failure fails closed with a sanitized provider-dispatch error and no Node/upstream call, never a fallback to a bearer slot or caller auth. This outbound provider-auth canonicalization is separate from inbound IOP `X-Api-Key`/Bearer caller-auth equivalence.

## 변경 기록

- 2026-08-15: Normalized Chat output-token aliases by selected protocol profile across provider-pool and unmarked caller-workspace selector/Work/Review calls. OpenAI Chat uses `max_completion_tokens`, Gemini Chat uses `max_tokens`, target-native values win, and unmarked internal stages use the selected OpenAI profile's Responses operation when tools plus effort cannot be represented by Chat. The reasoning Responses conversion omits incompatible generic SDK `temperature`/`top_p` defaults. Selector upstream HTTP 400 now terminates once as caller `invalid_request_error` instead of retryable 502. Command-mode workspace execution also carries the admitted containment guard in the actual outgoing command.
- 2026-08-14: Split the unmarked selector instruction into frontier-derived `prepare-only` and `pair-write` operations. Non-parent-creating bindings prepare the request job directory first and receive the exact artifact-pair grammar only after the successful receipt; parent-creating bindings receive the pair operation immediately.
- 2026-08-15: Replaced probabilistic parallel PLAN/REVIEW selector calls with one canonical atomic artifact-pair call that Edge deterministically expands into the same two admitted caller writes.
- 2026-08-15: Bound continuation issue identity to semantic tool calls so compatible SDK decoration of assistant messages does not break an otherwise exact caller receipt.
- 2026-08-15: Normalized Work/Review canonical reserved-artifact operation names through the selected caller binding before encoding them into structured or command-mode caller tools.
- 2026-08-15: Buffered command-mode Work/Review provider stages until canonical operation validation and caller-binding projection complete, preventing internal tool deltas from escaping before normalization while retaining structured-binding progressive streams.
- 2026-08-15: Removed the redundant outer identity projection from caller-stage-only cleanup so its issued semantic hash and caller-visible call ID remain identical.
- 2026-08-15: Canonicalized reserved Work/Review path aliases such as caller `filePath` back to the issued path field before command-mode binding encode.
- 2026-08-15: Added one Work-only canonical REVIEW handoff tool so completed control content is normalized into the caller binding instead of depending on an arbitrary caller edit or shell schema.
- 2026-08-14: Added the Edge-owned provider-side selector instruction for unmarked caller-workspace initial/resume turns. Actual Chat/Messages bodies now carry the exact request-local relative paths and accepted PLAN/pending REVIEW grammar without changing caller snapshots or public schemas.
- 2026-08-14: Added the restored unmarked caller-workspace light route, including template-validated PLAN/pending REVIEW creation, worker-owned completed REVIEW handoff, reviewer reads/result inspection/repair, reviewer-owned non-empty terminal, and strict separation from marked Node-owned `single_request` presets.
- 2026-08-14: Added bounded read/list-first workspace analysis inside the marked Plan stage. Successful inspection is required before structured Plan completion, while mutation tools, a separate analysis artifact, and caller-visible schema changes remain absent.
- 2026-08-14: Synchronized marked single-request artifact-only PLAN→Work→REVIEW→Review handoff, reviewer-owned repair/re-verification, reviewer zero-write, and strict terminal output provenance.
- 2026-08-14: Added operation-scoped `normalization.tool_calls` and Gemini-only Chat thought-signature round trips across standard OpenAI-compatible callers, including non-stream, SSE, and recovery-selected dispatches. Effort mapping and caller identity remain independent (`packages/go/config/protocol_profile.go`, `apps/edge/internal/openai/provider_model_rewrite.go`).
- 2026-08-13: Added official agy 1.1.12 model-role `functionResponse` continuation support while retaining fail-closed rejection for mixed assistant/tool-response content (`apps/edge/internal/openai/gemini_handler.go`).
- 2026-08-12: Admitted Claude Code's `advisor-tool-2026-03-01` beta as a consumed compatibility marker for both direct and marked-preset Messages ingress. It grants no internal capability and is not forwarded through the Chat bridge (`apps/edge/internal/openai/anthropic_types.go`).
- 2026-08-12: Replaced free-form PlanMD generation with a stage-owned strict `goal`/`steps`/`verification` JSON response and deterministic Edge rendering of the frozen Plan template. Internal artifact customization and all caller-visible Messages schemas remain unchanged (`apps/edge/internal/openai/single_request_plan_stage.go`, `packages/go/singlerequesttemplate/template.go`).
- 2026-08-12: Made the private Plan response provider-independent by representing steps and verification as bounded one-line string arrays; Edge now owns Markdown bullet and newline formatting (`apps/edge/internal/openai/single_request_plan_stage.go`, `packages/go/singlerequesttemplate/template.go`).
- 2026-08-09: Separated Edge-owned internal Plan/Review stage templates from caller-visible I/O. Operator-configured `execution_presets[].single_request.templates` is frozen at admission, supplies the private Plan prompt/PlanMD grammar and the private `review.md` artifact shape, and is not selectable, supplyable, or observable by callers. The Messages request/response schema, terminal projection, and final `decision.output` text are unchanged (`apps/edge/internal/openai/single_request_preset_binding.go`, `apps/edge/internal/openai/single_request_review_stage.go`, `agent-contract/outer/anthropic-compatible-api.md`).
- 2026-08-09: Qualified S12 `claude-smoke` with one supervised external Claude task on an approved IOP Node: ingress `0→1`, Gemini → ornith-fast → Gemini, one `end_turn`, exact 42-byte workspace output, timing, cleanup, and redacted evidence.
- 2026-08-07: Implemented and documented S11 `error-cancel`: one closed service terminal disposition, request-local typed failure/no-progress classification, shared buffered/SSE `end_turn`/`max_tokens`/`invalid_request_error`/`api_error` mapping, silent disconnect, private-partial suppression, and deterministic one-ingress/one-terminal/no-second-request evidence. S12 external qualification remains pending.
- 2026-07-07: 현재 코드와 OpenAI-compatible 계약 기준으로 bootstrap spec 작성.
- 2026-07-07: 기능 목록 중심으로 축소하고 주요 흐름을 Mermaid sequence diagram으로 정리.
- 2026-07-08: Chat Completions provider raw tunnel과 normalized execution semantics를 현재 코드와 계약 기준으로 반영.
- 2026-07-10: principal token 기반 usage metering, Prometheus metric, Grafana query guide, reasoning/cached token breakdown을 Milestone completion evidence 기준으로 반영.
- 2026-07-10: 일별 Usage 비용/ROI 리포트 MVP 종료 검토에서 daily/monthly rollup, usage origin breakdown, cloud-equivalent cost, avoided-cost ROI 문서 표면을 반영.
- 2026-07-11: provider model group `/v1/responses` raw passthrough 동작(모델 rewrite, unknown/Codex field 보존, streaming relay, provider auth forwarding)과 endpoint=`responses` usage metric label을 현재 코드 기준으로 반영.
- 2026-07-11: Responses provider route의 provider raw tunnel 동작을 반영했다.
- 2026-07-11: provider auth forwarding과 Seulgivibe OpenAI-compatible provider family surface를 종료 검토 기준으로 보강.
- 2026-07-12: Model Group Mixed Provider Dispatch 종료 검토 기준으로 selected provider capability 기반 passthrough/normalized 실행 경로를 반영.
- 2026-07-14: OpenAI-compatible Provider Passthrough 계약 동기화 종료 검토 기준으로 caller-facing response selector 설명을 제거하고, `model` 기반 route와 provider-native field 보존 기준을 반영.
- 2026-07-14: provider 미보고 reasoning text에 estimated-token counter(`iop_openai_reasoning_estimated_tokens_total`, `estimation_method="chars_div_4"`) 추가와 provider-reported reasoning 우선 기준을 반영.
- 2026-07-18: 저장소 구조 분해 뒤 streaming, provider tunnel, split test의 `source_evidence`를 현재 경로로 동기화.
- 2026-07-22: cross-model provider resource admission, provider-pool 공통 queue policy와 live candidate 소진 시 502 unavailable 의미를 현재 service/OpenAI 구현과 계약 기준으로 동기화.
- 2026-07-28: bounded ingress와 Stream Evidence Gate 활성 경로·한계·검증 포인터를 현재 구현 기준으로 반영.
- 2026-07-31: provider-default/model-group opt-in attribution policy, direct provider id precedence, actual Edge-local dispatch binding을 반영했다.
- 2026-07-31: Added request-local exactly-once terminal emission and actual-provider usage emission for every observed attempt, including recovery replacement and legacy tool-validation retry paths.
- 2026-07-31: Grafana query guide의 actual provider 집계와 승인된 model-group query-time rollup migration 완료 상태를 반영했다.
- 2026-08-01: Synchronized Anthropic ingress, provider-pool admission, usage boundaries, and Responses capability admission with the current handlers.
- 2026-08-02: Synchronized active managed projection auth, exact slot-route binding, lease acquisition/fencing, managed-versus-legacy credentials, safe slot/revision attribution, and the repaired managed API-key lease header canonicalization with source and deterministic two-profile qualification evidence.
- 2026-08-02: Removed IOP-owned workspace and Agent/CLI runtime semantics while preserving bounded metadata, managed projection, and credential lease behavior.
- 2026-08-05: Added Claude Code adaptive-effort/structured-output/cache-control bridge compatibility, stateless Gemini thought-signature tool round trips, and generic Chat replay handling for unsigned private thinking blocks.
- 2026-08-09: Extended `output_config.effort` to accept `low`, `medium`, `high`, `xhigh`, and `max` across Anthropic native and Chat bridge routes without substitution or normalization. Unknown effort values remain `400 invalid_request_error` before provider dispatch. Deterministic Go coverage added for exact bridge mapping, native `max` preservation, and invalid-value rejection. (`apps/edge/internal/openai/anthropic_types.go`, `apps/edge/internal/openai/anthropic_bridge_test.go`, `apps/edge/internal/openai/anthropic_native_test.go`)
- 2026-08-13: Added caller-neutral provider operation normalization for Messages/Responses routes. Tool-bearing adaptive effort can select Responses when Chat cannot preserve the combination, and unsupported effort grades fall only to the nearest declared lower grade (for example `max` to `xhigh`).
- 2026-08-14: Normalized Anthropic mid-conversation `system` messages across strict ingress, logical-request lineage, Chat, and Responses provider wires. Ordered system authority is preserved; first-position, invalid-placement, non-text, and pending-tool interruption shapes remain fail-closed.
- 2026-08-14: `/v1/responses` provider-pool bridge now selects native Responses, Anthropic Messages, or OpenAI Chat by protocol profile. Messages/Chat responses are rendered back to canonical Responses; unrepresentable controls fail closed and Gemini thought-signature tool continuations retain opaque `call_id` replay.
- 2026-08-14: Applied the same provider-normalization boundary to private single-request Plan/Work/Review stages. Removed pre-selection Chat operation pinning, added Chat↔Responses stage conversion and nearest-lower effort mapping, and corrected default-selector dispatch validation so the pool-selected provider is accepted without weakening explicit provider, profile, credential, target, or execution-path fences.
- 2026-08-14: Canonicalized private Chat Completions stage responses before strict Plan/Work/Review decoding. Standard OpenAI bookkeeping fields are stripped centrally, a non-null refusal remains fail-closed, and Responses/Chat now converge on the same private envelope instead of making stage codecs provider-specific.
- 2026-08-14: Added provider `timings` to the bounded Chat response bookkeeping stripped before private stage decoding, aligning the common normalizer with the existing Ornith Work codec without admitting unknown response fields.
- 2026-08-15: Reduced the unmarked light Work input to the relative PLAN path plus one fixed English sentence, limited Work to caller tools, moved completed REVIEW projection into Edge after the worker terminal report, and required English-ASCII PLAN content.
- 2026-08-13: Gemini-native path parsing now keeps route IDs strict while accepting the bounded URL-encoded official caller model label `Gemini 3.6 Flash`.
- 2026-08-06: Synchronized always-owned Chat/Responses typed-stall recovery, provider avoidance/fallback admission, and closed-label liveness operational evidence with the current runtime, contracts, and deterministic recovery tests.
- 2026-08-06: Added marked single-request Messages admission through the separate service coordinator capability, one unlabeled runtime ingress counter, buffered sanitized terminal acknowledgement, and deterministic real-POST compatibility evidence.
- 2026-08-06: Added the marked streaming subset with fixed plan/work/review/repair progress, liveness ping, serialized monotonic text blocks, private-wire exclusion, one success/error terminal, joined ticker shutdown, and post-`message_stop` completion acknowledgement.
- 2026-08-07: Added the private marked-request workspace tool continuation, strict closed schemas, ordered exact-generation Node round trips, immutable correlation/budgets/cancellation, and real one-POST multi-tool privacy evidence.
- 2026-08-08: Synchronized marked single-request observation evidence: one real POST links ingress=1, request-total=1, terminal=1, stage/tool/cleanup counts, and raw-free correlation without public tool protocol. The `iop_anthropic_single_request_ingress_total` counter remains unlabeled (no request_id, stage_id, or provider identity). External Claude/Mac timing evidence is explicitly deferred to `claude-smoke`. Deterministic internal tool privacy and lifecycle delta assertions cover the full single-request path.
- 2026-08-08: Repaired current-state contradiction: the active Plan -> Work -> Review composite, request-artifact cleanup via generic private-stage failure projection, and deterministic local evidence are now documented as active; only actual external Claude qualification remains explicitly deferred to S12 (`claude-smoke`). Added exact manager/executor/test source evidence paths.
