iop/agent-spec/input/openai-compatible-surface.md
toki f98d1b434b fix(openai): usage 귀속 마일스톤을 종료한다
Stream Gate 준비 실패에서도 요청 terminal 관측 계약을 지키고, direct provider identity 마이그레이션 누락으로 기존 검증과 운영 설정이 깨지지 않게 한다.
2026-07-31 21:20:19 +09:00

22 KiB

spec_doc_type spec_id status source_evidence
spec input/openai-compatible-surface 부분
type path notes
contract agent-contract/outer/openai-compatible-api.md OpenAI-compatible 외부 HTTP 계약
type path notes
code apps/edge/internal/openai/routes.go OpenAI-compatible route와 bearer auth 처리
type path notes
code apps/edge/internal/openai/chat_handler.go Chat Completions request validation, route dispatch, tool/reasoning 정책
type path notes
code apps/edge/internal/openai/route_resolution.go model catalog attribution policy와 direct provider id 해석
type path notes
code apps/edge/internal/openai/stream_gate_ingress.go body 첫 read 전 ingress 상한과 request-local snapshot
type path notes
code apps/edge/internal/openai/stream_gate_runtime.go runtime-enabled Chat과 provider tunnel response lifecycle
type path notes
code apps/edge/internal/openai/chat_decode.go Chat role/content/reasoning-alias repeat history decoder
type path notes
code apps/edge/internal/openai/responses_decode.go Responses message/reasoning/function-call repeat history decoder
type path notes
code apps/edge/internal/openai/normalized_sse.go normalized Chat Completions SSE stream과 terminal event 처리
type path notes
code apps/edge/internal/openai/provider_tunnel.go provider raw tunnel status/header/body passthrough 처리
type path notes
code apps/edge/internal/openai/responses_handler.go Responses API request validation, metadata/workspace 처리, non-stream completion
type path notes
code apps/edge/internal/openai/run_result.go RunEvent stream을 OpenAI-compatible result로 수집
type path notes
code apps/edge/internal/openai/principal.go principal token hash auth와 authenticated principal metadata 구성
type path notes
code apps/edge/internal/openai/usage_metrics.go Request-local terminal and actual-provider attempt usage recording
type path notes
code apps/edge/internal/openai/stream_gate_dispatcher.go Attempt ownership and exactly-once usage finalization on close or abort
type path notes
test apps/edge/internal/openai/chat_handler_test.go Chat Completions route와 target dispatch 검증
type path notes
test apps/edge/internal/openai/provider_tunnel_test.go provider-pool raw tunnel passthrough 검증
type path notes
test apps/edge/internal/openai/provider_dispatch_test.go provider/model-group attribution route binding과 strict provider identity 검증
type path notes
test apps/edge/internal/service/model_queue_admission_test.go 공유 provider cross-model capacity와 no-candidate unavailable 검증
type path notes
test apps/edge/internal/openai/workspace_metadata_test.go workspace와 metadata 전달 검증
type path notes
test apps/edge/internal/openai/usage_metrics_test.go Canonical provider series, request-terminal deduplication, and provider-switch attribution
type path notes
docs docs/openai-usage-grafana.md Grafana query, daily/monthly rollup, usage origin, cloud-equivalent cost, avoided-cost ROI 조회 가이드

스펙: 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을 모두 가능하게 한다.
provider auth forwarding openai.provider_auth가 활성화된 provider tunnel route는 caller의 configured request header에서 raw provider token을 읽어 provider request header로 전달하고, required header가 없으면 dispatch 전에 거부한다.
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/CLI/native provider가 함께 있어도 request field가 아니라 selected provider capability가 passthrough 또는 normalized 실행 경로를 결정한다.
legacy route 변환 legacy route는 외부 model을 route entry의 adapter, target, node, session_id, queue policy로 변환한다.
metadata/workspace 처리 metadata.workspaceRunRequest.workspace로 분리하고, 일반 metadata는 최대 16개 string key/value만 허용한다.
Chat Completions /v1/chat/completions는 non-streaming과 streaming SSE를 지원한다.
bounded ingress와 Stream Evidence Gate Chat/Responses body를 첫 read 전에 최대 16 MiB로 제한한다. openai.stream_evidence_gate.enabled=true인 지원 경로는 response-start staging, filter arbitration, bounded recovery와 단일 terminal을 runtime/stream-evidence-gate에 위임한다.
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를 선택하지 않는다.
provider raw passthrough passthrough는 provider status/header/body bytes를 기존 Edge-Node tunnel로 relay하고 pure response body에 IOP 확장 envelope를 섞지 않는다.
provider-native field 보존 provider raw tunnel route는 model served target rewrite와 auth/header 처리 외에 selected provider가 지원하는 OpenAI-compatible 표준 field와 provider extension field를 보존한다.
OpenAI usage metering Edge emits one request terminal and one canonical token/reasoning series for each actual provider attempt that reports usage. Rejected, aborted, and replacement attempts remain attributable to their own actual provider and served model.
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는 string input의 non-streaming 요청만 지원한다. provider model group route는 /v1/responses를 raw passthrough로 provider POST /v1/responses에 전달한다.
Responses provider passthrough provider-pool model group route와 direct OpenAI-compatible provider route의 /v1/responses는 provider raw tunnel을 사용한다. Edge는 model만 served target으로 rewrite하고 unknown/Codex field와 stream:true raw SSE를 provider로 relay한다. Usage is recorded with endpoint=responses, response_mode=passthrough, route_model=request alias, and the 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 경로를 사용한다.
cancel 전파 HTTP caller timeout/cancel이 cancel-worthy error이면 Node CancelRun으로 전파한다.

범위

  • 포함: OpenAI-compatible HTTP auth, bounded ingress, request validation, route resolution, metadata/workspace 처리, 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.

주요 흐름

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, metadata, route 검증
  alt selected provider supports OpenAI-compatible passthrough
    OpenAI->>Service: SubmitProviderTunnel(ProviderPool/direct)
    Service->>Runtime: ProviderTunnelRequest
    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.yamlopenai 섹션이 listener, bearer token, legacy adapter/target, model routes, strict output을 제공한다.
  • openai.stream_evidence_gate는 기본 비활성이고, recovery cap 0..3과 16 MiB 이하 ingress snapshot 상한을 설정한다. 변경은 현재 restart-required다.
  • 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/CLI/native provider가 선택되면 normalized RunRequest path로 dispatch한다.
  • 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를 유지한다.
  • openai.provider_auth는 provider tunnel forwarding rule만 저장하고 raw provider token 값은 request-time header에서만 읽는다. inbound IOP Authorization header를 provider token source로 재사용하지 않는다.
  • OpenAI request의 metadata.workspace는 absolute path가 필요한 route에서만 필수 검증된다.
  • Chat Completions와 Responses request는 caller metadata로 provider raw tunnel과 normalized response shape를 선택하지 않는다. route/provider capability만 실행 경로를 결정한다.
  • 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_callsopenai_text_tool_fallback은 response tool call 복원에 쓰인다.
  • usage metric은 iop_openai_requests_total, iop_openai_usage_tokens_total, iop_openai_reasoning_observed_total, iop_openai_reasoning_chars_total, iop_openai_reasoning_estimated_tokens_total로 emit된다.
  • 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.
  • provider body usage와 provider tunnel USAGE frame이 모두 있으면 body input/output을 우선하고 proto-only reasoning/cached input을 보조로 병합해 중복 집계를 피한다.

검증

  • go test ./apps/edge/internal/openai
  • 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는 환경별 agent-test/dev 또는 dev-corp profile을 따른다.

한계와 주의사항

  • normalized(non-provider) /v1/responses는 non-streaming string input만 지원한다. provider model group route의 /v1/responses는 raw passthrough로 streaming과 Codex/unknown field를 그대로 provider에 전달한다.
  • Stream Evidence Gate 활성화만으로 반복, missing tool-call, schema 같은 semantic filter가 자동 활성화되지는 않는다. 해당 mechanics와 현재 지원 경로는 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는 prompt 본문에 섞지 않고 metadata에서 분리한다.
  • 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으로 동작한다.
  • 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 경계를 따른다.

변경 기록

  • 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 완료 상태를 반영했다.