iop/agent-contract/outer/openai-compatible-api.md
toki 0ffcb88db0 feat: provider-resource-admission-ownership alignment
- Archive provider-resource-admission-ownership milestone/SDD
- Align contract: CP-edge wire, runtime refresh, node runtime, OpenAI surface
- Update roadmap: phase state, priority queue
- Update specs: control-plane ops, OpenAI surface, edge execution, provider pool refresh
- Add node runtime supervisor bootstrapping and unit tests
- Fix control-plane edge registry handler and http_views
- Fix edge model queue admission and long context queue tests
2026-07-22 20:45:04 +09:00

32 KiB

OpenAI-Compatible API Contract

계약 메타

  • id: iop.openai-compatible-api
  • boundary: outer
  • status: active
  • 원본 경로:
    • apps/edge/internal/openai/routes.go
    • apps/edge/internal/openai/chat_handler.go
    • apps/edge/internal/openai/responses_handler.go
    • apps/edge/internal/openai/common_types.go
    • apps/edge/internal/openai/chat_types.go
    • apps/edge/internal/openai/responses_types.go
    • apps/node/internal/adapters/openai_compat/openai_compat.go
    • packages/go/config/config.go
    • configs/edge.yaml
  • human docs: docs/openai-compatible-api-contract.md

범위

이 문서는 외부 프로젝트가 IOP Edge의 OpenAI-compatible HTTP 표면을 호출할 때 확인할 계약 원문이다. IOP 내부 실행은 adapter + target 기준이며, OpenAI-compatible 경계에서는 호환성을 위해 model을 사용한다. IOP 고유 실행 문맥은 별도 iop wrapper field를 만들지 않고 OpenAI request의 metadata에 둔다. 기본 설계 기준은 OpenAI-compatible request/response surface 보존이다. OpenAI-compatible provider로 raw passthrough 되는 경로는 선택된 provider가 지원하는 표준 field와 provider extension field를 IOP allowlist로 제한하지 않는다. IOP 고유 field나 추상화 field는 OpenAI-compatible 기본 surface 위에 더하는 확장으로만 사용하며, provider-native OpenAI-compatible field를 대체하거나 금지하지 않는다. 라우팅의 1차 기준은 request model이 가리키는 route/provider capability다. 선택된 provider가 OpenAI-compatible provider이면 Edge는 provider tunnel passthrough를 사용하고, 그 외 CLI/Ollama/native 실행은 normalized path를 사용한다. 라우팅과 응답 형태를 caller metadata selector로 고르지 않는다. 2차 처리는 OpenAI metadata container에서 IOP가 아는 key만 발췌해 workspace, task, principal, usage/observability 같은 내부 문맥으로 쓰는 방식이다. 서로 다른 외부 model key가 같은 nodes[].providers[].id를 참조하면 일반·long-context capacity는 model group별이 아니라 해당 provider resource 하나에서 공유된다. Edge provider-pool queue의 전체 pending 상한과 timeout도 model group 공통 root policy를 사용한다.

Auth

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

Authorization: Bearer <token>

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

Principal token hash mapping auth

Edge 설정에 openai.principal_tokens[]가 설정된 경우, caller는 기존과 동일한 Authorization: Bearer <token> 헤더를 보낸다. Edge는 요청된 raw token의 SHA-256 hash를 계산하여 token_hash_sha256과 매칭한다. 매칭 성공 시 내부 dispatch metadata 후보로 iop_principal_ref, iop_principal_alias, iop_token_ref, iop_principal_source가 채워진다. 매칭할 entry가 없으면 401 unauthorized를 반환한다.

Metadata 정책

  • caller-provided metadata.user는 identity source가 아니며 사용되지 않는다.
  • caller가 metadata.iop_principal_*를 보내도 authenticated context 값이 overwrite한다.
  • metadata는 route/response mode selector가 아니다. Edge는 model 기반 route 선택 뒤 IOP가 아는 metadata key만 실행 문맥과 관측용으로 발췌한다.

Legacy fallback

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

Provider auth forwarding

openai.provider_auth.enabled=true이면 caller는 provider별 raw user token을 openai.provider_auth.from_header에 담아 보낸다. 기본 header는 X-IOP-Provider-Authorization이다. Edge는 이 값을 provider tunnel request의 openai.provider_auth.target_header로 전달한다. 기본 target header는 Authorization, 기본 scheme은 Bearer다.

이 provider token은 IOP inbound auth인 Authorization: Bearer <token>과 분리된다. openai.bearer_token 또는 openai.principal_tokens[]가 쓰는 IOP auth token을 외부 provider credential로 재사용하지 않는다.

금지:

  • raw provider token을 Edge config, tracked docs, roadmap, task artifact, metric label에 저장하지 않는다.
  • host-local ~/.claude/anthropic_key.sh, ~/.codex/config.toml, env helper 파일을 OpenAI-compatible provider token source of truth로 읽지 않는다.
  • missing required provider auth error body나 log에 raw header 값을 echo하지 않는다.

Responses API

Endpoint:

POST /v1/responses
Content-Type: application/json

CLI agent 실행으로 라우팅되는 요청의 최소 형태:

{
  "model": "codex",
  "input": "현재 워크스페이스의 테스트 상태를 확인해줘.",
  "metadata": {
    "workspace": "/config/workspace/iop"
  }
}

현재 /v1/responses에서 허용하는 표준형 요청 예시:

{
  "model": "codex",
  "instructions": "응답은 짧게 작성해.",
  "input": "현재 워크스페이스의 테스트 상태를 확인해줘.",
  "stream": false,
  "background": false,
  "max_output_tokens": 4096,
  "temperature": 0,
  "top_p": 1,
  "metadata": {
    "workspace": "/config/workspace/iop",
    "request_id": "req-001",
    "task_id": "task-123"
  }
}

필드 의미:

  • model: Edge가 내부 adapter + target으로 해석할 외부 route 이름이다. IOP Edge에서는 라우팅을 위해 필수다.
  • instructions: OpenAI Responses API의 top-level instruction field다. 있으면 input 앞에 배치해 agent 실행 prompt를 만든다.
  • input: agent에게 전달할 사용자 요청이다. normalized(non-provider) route는 현재 string input만 지원한다.
  • stream: normalized(non-provider) route는 현재 false 또는 생략만 지원한다. Provider-pool passthrough는 provider가 지원하는 stream 값을 보존한다.
  • background: normalized(non-provider) route는 현재 false 또는 생략만 지원한다. Provider-pool passthrough는 provider가 지원하는 값을 보존한다.
  • metadata.workspace: CLI process를 실행할 작업 디렉터리다. CLI agent route에서는 필수 실행 문맥이다.
  • metadata: OpenAI 표준 metadata container다. string key/value를 허용하고, IOP는 workspace만 실행 문맥으로 해석한다. 나머지 key는 caller-defined metadata로 보존하되 source는 지원하지 않는다.
  • metadata.request_id, metadata.task_id: caller-defined metadata 예시다. 특별한 wrapper나 제품 전용 field가 아니다.
  • max_output_tokens: 출력 길이 상한이다. 내부 provider option의 max_tokens로 전달된다.
  • temperature: 생성 다양성 option이다. 대상 adapter가 지원하지 않으면 무시될 수 있다.
  • top_p: nucleus sampling option이다. 대상 adapter가 지원하지 않으면 무시될 수 있다.

Normalized route 금지:

  • metadata.cli 같은 CLI 전용 wrapper를 추가하지 않는다.
  • metadata.inference처럼 model route와 겹치는 target wrapper를 추가하지 않는다.
  • metadata.nomadcode처럼 특정 소비자 제품명에 묶인 wrapper를 추가하지 않는다.
  • metadata.source처럼 의미가 불명확한 호출 출처 field를 추가하지 않는다.
  • root-level iop 같은 별도 wrapper field를 추가하지 않는다.
  • normalized(non-provider) /v1/responsesoptions wrapper를 추가하지 않는다. Responses API option은 OpenAI 표준 top-level field를 따른다.
  • session_id, timeout_sec 같은 IOP 실행 제어 field를 request body 계약에 추가하지 않는다. logical session과 timeout은 route/config 기본값을 따른다.
  • workspace를 prompt 본문에 섞어 전달하지 않는다.

현재 구현 메모:

  • normalized(non-provider) /v1/responses route는 strict field validation을 유지하며 non-streaming string input만 지원한다.
  • provider-pool model group route(models[])의 /v1/responses 호출은 selected provider가 OpenAI-compatible provider이면 raw passthrough로 provider POST /v1/responses에 전달한다. caller body는 model field만 served target으로 rewrite하고, selected provider가 지원하는 OpenAI-compatible 표준 field와 provider extension field(max_output_tokens, tools, store, provider-specific knobs 등)는 보존한다. stream:true는 provider raw SSE로 relay한다. provider auth forwarding이 적용되고, response model echo rewrite는 적용하지 않는다. 이 경로는 normalized SubmitRun으로 fallback하지 않는다.
  • provider-pool model group route는 provider candidate를 먼저 선택한다. 선택된 provider가 OpenAI-compatible 호출 방식을 지원하면 ProviderTunnelRequest passthrough를 사용하고, Ollama/CLI/native provider이면 normalized RunRequest를 사용한다. provider type만으로 Ollama를 candidate set에서 제거하지 않으며, OpenAI-compatible provider의 tunnel 구현이 없으면 normalized fallback이 아니라 unsupported/implementation error다.
  • provider-pool pending request는 lease 반환, config refresh, provider disable, Node disconnect/reconnect 때 live config와 dispatch-ready registry에서 candidate를 다시 계산한다. 후보가 full인 상태는 queue policy에 따라 계속 대기하지만 live candidate가 모두 사라지면 원래 queue timeout까지 기다리지 않고 terminal unavailable로 끝난다.
  • provider-pool admission/unavailable 실패는 현재 외부 error envelope를 유지해 HTTP 502type="node_dispatch_error"로 반환한다. 별도 public status code나 response field를 추가하지 않으며 error message에는 raw token이나 private endpoint를 포함하지 않는다.
  • direct legacy provider route(openai.model_routes[]openai_compat/vllm adapter)도 OpenAI-compatible provider이면 raw provider tunnel을 사용한다. Non-provider normalized route는 raw tunnel을 쓰지 않고 normalized IOP output path를 사용한다.
  • Responses provider passthrough success usage metric label은 endpoint와 model_group=request alias를 기준으로 집계한다. 관측/usage 정보는 provider body에 섞지 않는다.
  • metadata는 최대 16개 string key/value를 허용한다. key는 64자 이하, value는 512자 이하를 기준으로 한다.
  • CLI route의 metadata.workspace는 이 문서의 계약 기준이다. 구현은 이 값을 Edge service의 run workspace와 Node CLI adapter의 process working directory로 전달해야 한다.
  • metadata.workspaceRunRequest.Workspace로 전달하고 generic run metadata에는 복사하지 않는다.
  • 다른 Responses API 표준 field는 구현 필요가 생길 때 계약을 갱신한 뒤 추가한다.

Generic Authoring Handoff

외부 caller가 IOP Edge HTTP 표면으로 workspace authoring 작업을 넘길 때의 최소 요청 형태:

{
  "model": "codex",
  "input": "Todo 항목에 필요한 산출물을 현재 checkout에 작성해줘.",
  "metadata": {
    "workspace": "/config/workspace/work-slot-123",
    "task_id": "todo-123"
  }
}

이 handoff는 model, input, metadata.workspace, 필요한 caller-defined metadata만으로 충분해야 한다. 호출자는 metadata.cli, 소비자 전용 metadata wrapper, root-level iop wrapper, IOP CLI 직접 실행, prompt 본문 workspace 주입을 요구받지 않는다.

Workspace-bound route는 workspace가 없거나 상대 경로이면 OpenAI-compatible error로 거부한다. 존재하지 않는 경로, 권한 오류, agent process exit failure는 기본 cwd fallback으로 숨기지 않고 호출자가 실패로 구분할 수 있어야 한다.

Chat Completions

/v1/chat/completions도 같은 metadata 원칙을 따른다. CLI route의 workspace는 metadata.workspace에 둔다. Normalized route에서 Chat Completions의 sampling option은 해당 endpoint의 OpenAI-compatible top-level request field를 따르며, /v1/responses와 마찬가지로 별도 options wrapper를 두지 않는다. Provider-pool passthrough route에서는 selected provider가 지원하는 OpenAI-compatible field와 provider extension field를 보존한다.

{
  "model": "codex",
  "messages": [
    {
      "role": "user",
      "content": "현재 워크스페이스의 테스트 상태를 확인해줘."
    }
  ],
  "metadata": {
    "workspace": "/config/workspace/iop"
  }
}

Normalized(non-provider) Chat Completions route가 해석하는 request field:

  • model
  • messages
  • stream
  • metadata
  • max_tokens
  • max_completion_tokens
  • temperature
  • top_p
  • presence_penalty
  • frequency_penalty
  • seed
  • stop
  • response_format
  • tools
  • tool_choice
  • parallel_tool_calls
  • stream_options
  • store
  • think
  • reasoning_effort
  • thinking_token_budget
  • include_reasoning

Provider-pool raw passthrough route는 위 목록을 provider request allowlist로 사용하지 않는다. 이 경로의 기본은 selected OpenAI-compatible provider가 지원하는 요청 surface 보존이며, chat_template_kwargs, provider별 extra_body/template option, 새 OpenAI-compatible field처럼 IOP가 아직 해석하지 않는 top-level field도 model rewrite 후 provider로 전달되어야 한다. 해당 field의 성공/실패 의미는 provider가 결정하고, IOP는 provider HTTP status/header/body를 relay한다.

Chat Completions routing and response

Chat Completions의 실행 경로는 caller가 보낸 model의 route/provider capability로 결정한다.

  • provider-pool model group route(models[])는 candidate를 선택한 뒤 selected provider가 OpenAI-compatible 호출 방식을 지원하면 provider HTTP status/header/body를 Node가 열어 기존 Edge-Node tunnel로 relay하고, Edge가 caller에게 쓴다. 요청 body는 라우팅에 필요한 envelope만 읽고 model alias를 selected provider의 served target으로 rewrite하는 것을 기본으로 하며, provider가 지원하는 OpenAI-compatible field와 provider extension field를 보존한다.
  • selected provider가 Ollama/CLI/native provider처럼 normalized execution을 요구하면 Edge는 normalized RunRequest path를 사용한다. 이 경로는 OpenAI-compatible 표면을 입력/출력 compatibility layer로 제공하되, backend 호출은 normalized adapter 계약을 따른다.
  • metadata는 경로 선택자가 아니다. Edge는 route 결정 뒤 workspace, task_id, 인증 principal, usage/observability 등 IOP가 아는 metadata key만 발췌한다. 이 발췌 정보는 provider body를 바꾸는 selector가 아니며, passthrough 응답 body에 IOP marker/event/envelope를 섞지 않는다.
  • Chat Completions 성공 응답의 top-level model echo가 provider-served model이면 caller가 요청한 IOP model alias로 정규화할 수 있다. reasoning/content/tool_calls 같은 provider payload field는 보존한다.

IOP 확장 think 제어 field:

  • think (bool, optional): thinking/reasoning 생성 활성화 여부를 표현하는 IOP 확장 field다. 생략하면 provider 기본값을 유지한다. false는 thinking 생성을 끄도록 요청하고, true는 provider가 지원하면 thinking 생성을 명시 활성화한다.
  • reasoning_effort (string, optional): none, low, medium, high 중 하나인 IOP 확장 field다. nonethink=false와 같은 disable 의미로 처리한다. low/medium/high는 provider 또는 normalized backend가 지원하는 경우에만 전달한다.
  • thinking_token_budget (int, optional): IOP 확장 thinking token budget. 0 이상이어야 한다.
  • include_reasoning (bool, optional): OpenAI-compatible 응답에서 reasoning_content 노출 여부. non-provider normalized route에서는 생략하거나 true이면 provider reasoning delta/message를 노출할 수 있고, false이면 provider가 reasoning을 생성해도 response의 reasoning_content를 제거한다. provider-pool pure passthrough는 provider body 보존이 우선이며, 현재 IOP가 이 field만으로 reasoning field를 제거한다고 보장하지 않는다.

이 field들은 provider-native field의 대체물이 아니다. Provider-pool passthrough caller는 선택된 provider가 지원하는 native field(예: vLLM/Qwen 계열의 chat_template_kwargs.enable_thinking=false)를 그대로 보낼 수 있어야 하며, IOP 확장 field는 provider-native field가 없거나 normalized backend를 호출할 때의 추가 호환 표면이다.

dev-corp gemma4:26b provider-pool passthrough 파라미터 범위

이 표는 dev-corp의 gemma4:26b provider-pool route에서, 현재 provider/vLLM 최적화 값을 변경하지 않고 standard OpenAI-compatible caller가 요청 파라미터만 바꿔 측정할 때의 계약상 기대 범위다. Provider-pool passthrough는 이 표에 없는 provider-native OpenAI-compatible field도 금지하지 않는다. Pi TUI의 고정 호출 방식이나 다른 model/provider route의 동작으로 일반화하지 않는다.

요청 파라미터 현재 기대 동작 권장 판정
stream=false, think 생략 provider에 non-stream Chat Completions 요청으로 전달되고 Edge는 provider JSON body를 relay한다. reasoning field가 있으면 보존될 수 있다. standard OpenAI-compatible caller에서 non-stream 동작 측정 가능. reasoning을 숨기려면 client에서 choices[].message.reasoning_content, reasoning 등 provider reasoning field를 제거한다.
stream=true, think 생략 provider SSE body를 relay한다. reasoning delta가 있으면 보존될 수 있다. streaming 동작 측정 가능. client는 choices[].delta.reasoning_content, reasoning, reasoning_text 같은 reasoning delta를 선택적으로 무시한다.
include_reasoning=false field는 수신/전달될 수 있지만 pure passthrough에서 IOP-side filtering을 보장하지 않는다. provider가 무시하면 reasoning field가 그대로 올 수 있다. 현재 IOP 설정을 바꾸지 않는 조건에서는 hide-only 스위치로 보지 않는다. client-side filtering을 기준으로 둔다.
think=false Edge validation conflict가 없으면 요청은 통과하고 model catalog의 default thinking budget 주입은 억제된다. 이후 provider body에 think:false가 반영될 수 있으나, 현재 gemma4:26b provider 최적화 값과 충돌하거나 backend별로 무시/실패/품질 저하가 날 수 있다. “숨기기만” 하는 옵션이 아니다. dev-corp gemma4:26b 기본 안정 호출에서는 생략한다.
reasoning_effort="none" think=false와 같은 disable 의도로 해석되어 default thinking budget 주입을 억제한다. provider tunnel에서는 runtime/provider 지원 여부에 의존한다. think=false와 같은 이유로 기본 안정 호출에서는 생략한다.
명시적 thinking_token_budget 0 이상이면 conflict validation 후 provider tunnel body에 반영될 수 있다. catalog 기본 budget 대신 caller 값으로 provider thinking budget을 바꾸는 요청이다. 최적화된 gemma4:26b 기본값을 바꾸는 측정으로만 사용한다. 일반 표준 안정 호출에서는 생략한다.
provider-native field 예: chat_template_kwargs selected provider가 해당 OpenAI-compatible extension을 지원하면 IOP provider-pool passthrough는 이를 보존하고 provider로 전달해야 한다. 이 field를 IOP 추상 field로 치환하지 않는다. provider가 거부하면 provider error를 relay한다.
/v1/responses 호출 provider-pool model group route에서 selected provider가 OpenAI-compatible provider이면 raw passthrough로 provider POST /v1/responses에 전달한다. model만 rewrite하고 selected provider가 지원하는 field는 보존하며 stream:true는 raw SSE로 relay한다. usage metric은 endpoint=responses로 측정한다. Provider가 /v1/responses를 지원하면 그대로 측정할 수 있다. Provider가 지원하지 않으면 provider error를 relay한다. Chat 기반 호출은 /v1/chat/completions를 쓴다.

현재 구현에서 think=false를 “provider에는 기본 think를 유지하되 IOP가 응답에서 reasoning만 감추는 hide-only 모드”로 해석하지 않는다. 그런 동작이 필요하면 provider/vLLM 설정 변경이 아니라 Edge provider-pool passthrough 응답 filtering 정책을 별도 구현/계약 갱신해야 한다.

Reasoning-only 완료 처리 (non-provider normalized route):

  • provider가 reasoning은 생성했지만 최종 assistant contenttool_calls 없이 완료하면 Edge는 성공 응답을 빈 content로 끝내지 않는다.
  • include_reasoning 생략 또는 true인 요청은 기존 reasoning 본문을 reasoning_content에 유지하고, content가 비어 있으면 reasoning 본문을 fallback content로도 반환한다. finish_reasonstop이 아니면 fallback content 뒤에 IOP notice를 붙인다.
  • include_reasoning=false인 요청은 reasoning 본문을 노출하지 않는다. 대신 content에 IOP notice를 넣어 "reasoning was hidden" 상태와 finish_reason을 알린다.
  • streaming 응답도 같은 정책을 따른다. reasoning-only 완료 시 최종 finish chunk와 [DONE] 전에 fallback 또는 hidden-reasoning notice를 content delta로 한 번 전송한다.
  • Chat Completions provider-pool pure passthrough 응답 body에는 이 normalized fallback/filtering 정책을 적용하지 않는다.

Provider별 think-control 정책:

아래 정책은 normalized adapter execution path 또는 IOP 확장 field를 provider-specific request로 변환해야 하는 경로의 기준이다. Chat Completions provider-pool pure passthrough는 provider-native OpenAI-compatible field를 우선 보존한다. 따라서 caller가 이미 chat_template_kwargs 같은 provider-native field를 보냈다면 IOP 확장 field 변환은 이를 대체하거나 삭제하지 않는다.

  • vLLM:
    • think=false 또는 reasoning_effort=none -> 내부 chat_template_kwargs.enable_thinking=false
    • think=true 또는 budget-only -> 내부 chat_template_kwargs.enable_thinking=true
    • thinking_token_budget -> 내부 chat_template_kwargs.thinking_token_budget
    • reasoning_effort=low|medium|high -> unsupported think control 오류 반환
  • vLLM-MLX:
    • think=true 또는 budget-only -> 내부 chat_template_kwargs.enable_thinking=true
    • thinking_token_budget -> 내부 chat_template_kwargs.thinking_token_budget
    • think=false 또는 reasoning_effort=none -> unsupported think control 오류 반환. vLLM-MLX 런타임은 스트리밍 응답 전체를 reasoning_content로 표기하고 enable_thinking=false로도 reasoning 생성을 멈추지 않으므로, think disable을 조용한 성공(200 reasoning stream)으로 처리하지 않고 명시 오류를 반환한다.
    • reasoning_effort=low|medium|high -> unsupported think control 오류 반환
  • Lemonade:
    • think=false 또는 reasoning_effort=none -> 내부 chat_template_kwargs.enable_thinking=false. 이 런타임은 top-level think field를 무시하므로 top-level think를 사용하지 않는다.
    • think=true 또는 budget-only -> 내부 chat_template_kwargs.enable_thinking=true
    • thinking_token_budget -> 내부 chat_template_kwargs.thinking_token_budget
    • reasoning_effort=low|medium|high -> unsupported think control 오류 반환
  • Unknown / 기타 provider: 요청 field를 그대로 전달하되, provider가 지원하지 않는 값은 backend 또는 adapter error가 될 수 있다.

Provider pool model catalog의 models[] entry가 generation policy를 제공하면 Edge는 요청을 내부 실행 또는 Chat Completions provider tunnel로 넘기기 전에 다음 값을 보정할 수 있다. 단, provider-pool raw passthrough에서는 caller가 명시한 provider-native OpenAI-compatible field를 삭제하거나 IOP 추상 field로 대체하지 않는다.

  • default_max_tokens: caller가 출력 token limit을 생략했을 때 max_tokens 또는 max_output_tokens로 주입한다.
  • min_max_tokens: caller가 너무 작은 출력 token limit을 보냈을 때 해당 값까지 올린다. caller 값이 더 크면 보존한다.
  • default_thinking_token_budget: caller가 thinking_token_budget과 provider-native thinking budget field를 모두 생략했을 때 내부 실행 입력 또는 Chat Completions provider tunnel body에 주입할 수 있다. strict output가 함께 활성화된 normalized 경로에서는 Edge가 think=true도 함께 주입해 provider adapter가 지원하는 request shape로 전달할 수 있다. Provider-pool passthrough에서는 provider-native field 보존이 우선이다.

Conflict 정책:

  • reasoning_effort가 비어 있거나 none|low|medium|high 외 값이면 400 에러.
  • thinking_token_budget가 음수이면 400 에러.
  • think=falsereasoning_effort=low|medium|high가 함께 있으면 400 에러.
  • think=false일 때 thinking_token_budget를 설정하면 400 에러.
  • reasoning_effort=none일 때 thinking_token_budget를 설정하면 400 에러.

Strict output 모드:

  • strict output가 활성화되면 non-provider normalized route에서 think=true가 명시되지 않은 요청은 내부 실행 입력에서 think=false로 낮춘다.
  • strict output만으로 OpenAI-compatible provider model group route를 normalized path로 전환하지 않는다. provider model group에서 selected provider가 OpenAI-compatible provider이면 계속 raw passthrough다.
  • provider-pool models[] entry의 default_thinking_token_budget가 적용되는 모델은 catalog의 thinking policy를 기본값으로 사용할 수 있다. 이 경우에도 caller가 provider-native thinking field를 명시했다면 해당 field가 우선하며, IOP가 think=truethinking_token_budget으로 대체하지 않는다.

tools가 있는 Chat Completions 요청에서 provider route(openai_compat, vllm, ollama, provider pool)는 forced tool 선택 객체와 "none" 같은 명시적 tool_choice를 backend에 전달한다. 단, "auto"는 OpenAI-compatible 기본값과 같으므로 provider request에서는 생략한다. 일부 vLLM 계열 backend는 explicit/default "auto"--enable-auto-tool-choice/--tool-call-parser 없이 400으로 거부한다. 이 400이 발생하고 요청 tool이 정확히 1개이면 Node adapter는 해당 tool에 대한 forced tool_choice로 1회 재시도한다. forced tool도 --tool-call-parser 요구로 거부되거나 여러 tool이라 forced를 고를 수 없으면, Node adapter는 tools/tool_choice를 제거하고 text tool-call system instruction을 leading system message에 병합해 1회 재시도하며 완료 metadata에 openai_text_tool_fallback: "true"를 싣는다. provider가 native OpenAI-compatible tool_calls를 반환하면 Node는 내부 RunEvent.metadata["openai_tool_calls"] JSON으로 보존하고, Edge는 이를 OpenAI-compatible message.tool_calls 또는 stream delta.tool_calls로 반환하며 finish_reason: "tool_calls"를 사용한다. provider native tool_calls[].function.arguments는 OpenAI 계약에 맞는 JSON string으로 반환한다. 단, provider가 요청 tools[].function.parameters schema상 배열/객체여야 하는 값을 JSON 문자열로 이중 인코딩한 경우 Edge는 해당 arguments JSON만 schema 기준으로 복원해 다시 JSON string으로 직렬화한다. 요청에 tools[]가 있고 provider가 native tool_calls 없이 assistant content에 raw text tool-call 블록을 담아 응답하면, provider route(openai_compat, vllm, ollama, provider pool)와 CLI route(adapter: "cli") 모두에서 Edge는 그 블록을 요청 tool schema 기준으로 구조화하거나 차단한다. 이 정규화가 인식하는 텍스트 블록의 최소 형태는 <tool_call><function=<name>><parameter=<key>>JSON-or-text</parameter></function></tool_call> XML 형식과 {{function_name(key=Python/JSON-like-literal)}} mustache 형식이다. 후보 tool 이름이 요청 tools[]에 있고 arguments가 파싱되어 해당 tool의 function.parameters schema를 만족하면, Edge는 이를 OpenAI tool_calls로 정규화하고 raw 블록을 content에서 제거한 뒤 finish_reason: "tool_calls"로 반환한다. 요청 tools[]에 없는 tool 이름, unclosed/function 정의 누락 같은 malformed 블록, schema를 위반하는 arguments는 성공 content로 반환하지 않고 tool validation 실패로 처리한다. non-stream과 strict buffered stream 응답은 bounded tool-validation attempt 한도까지 run을 재시도하고, 그래도 실패하면 tool_validation_error로 응답한다. live SSE 스트림은 raw 블록을 content delta로 flush하지 않고 tool_validation_error 이벤트로 스트림을 종료한다. 요청에 tools[]가 없으면 assistant content의 tool-call 유사 텍스트는 파싱하거나 합성하지 않고 backend content 원문으로 그대로 둔다. 자연어 추론은 어떤 경우에도 tool_calls로 변환하지 않는다. raw <tool_call>/{{...}} 블록과 <|mask_end|> 같은 알려진 chat-template sentinel은 성공 응답의 content나 SSE delta에 노출하지 않는다. sentinel은 content와 reasoning 양쪽에서, streaming chunk 경계에 걸쳐 분할되더라도 sanitize한다. text tool-call을 구조화할 때 Edge는 route와 무관하게 요청의 tools[].function.parameters schema를 기준으로 arguments를 정규화한다. 예를 들어 tool schema가 commands: string[]만 허용하면 command 객체 입력도 shell string 배열로 접고, commands: {command,args}[]를 허용하면 shell 문법이 없는 명령을 structured argv로 만든다. schema에 없는 UI 설명용 description이나 실행 위치 힌트용 runInTerminal은 command 객체와 최상위 args에서 제거하되, cd, command -v, &&, pipe, redirect, quote 등 shell 해석이 필요한 명령은 schema가 허용할 때 commands: ["cd /work && git status"] 같은 shell string으로 유지한다. CLI route(adapter: "cli")는 native backend tool calling이 없어 이 text tool-call 구조화가 유일한 tool_calls 경로이며, backend auto tool-calling 요구 조건으로 요청이 실패하지 않도록 내부 실행 입력의 tool_choice"none"으로 낮춘다. parallel_tool_calls, stream_options, store는 클라이언트 호환성을 위해 수신하지만 현재 Edge 실행 의미에는 반영하지 않는다.

금지:

  • metadata.source, metadata.cli, metadata.inference, metadata.nomadcode
  • normalized(non-provider) route에서 options, format, keep_alive 같은 backend/provider 전용 request wrapper를 OpenAI-compatible 표준 field처럼 요구하는 방식. 이 금지는 provider-pool raw passthrough에서 selected provider가 지원하는 OpenAI-compatible extension field 보존에는 적용하지 않는다.
  • session_id, timeout_sec 같은 IOP 실행 제어 field

Legacy Completions

POST /v1/completions는 현재 IOP Edge OpenAI-compatible 표면에서 제공하지 않는다. text completion 형태의 신규 호출은 /v1/responses를 사용하고, message 기반 호출은 /v1/chat/completions를 사용한다.

Routing

Edge 설정이 openai.model_routes[]를 제공하면 model은 먼저 route catalog에서 해석된다. 매칭 route가 없으면 기존 fallback 규칙에 따라 openai.target 또는 요청의 model을 내부 target으로 사용한다.

CLI agent를 OpenAI-compatible API로 노출할 때는 route catalog에서 해당 model을 명시적으로 adapter: "cli"와 target profile로 매핑하는 방식을 우선한다.

Top-level models[]가 있으면 IOP /v1/models와 provider-pool dispatch의 static catalog source of truth다. Seulgivibe provider는 runtime adapter type을 openai_compat로 정규화하되 provider family label로 seulgivibe_claude 또는 seulgivibe_openai를 보존할 수 있다. Tracked catalog 예시는 model/provider mapping만 담고 실제 endpoint credential이나 raw user token은 담지 않는다. models[] provider mapping은 OpenAI-compatible provider와 normalized-only provider를 같은 model group 안에 둘 수 있다. dispatch는 기존 capacity + priority + availability 기준으로 provider를 한 번 선택하고, client request field가 아니라 selected provider capability로 passthrough 또는 normalized execution path를 결정한다.