# 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 표면은 다음 헤더를 요구한다.

```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_token`과 `openai.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:

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

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

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

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

```json
{
  "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/responses`에 `options` 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 `502`와 `type="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.workspace`는 `RunRequest.Workspace`로 전달하고 generic run metadata에는 복사하지 않는다.
- 다른 Responses API 표준 field는 구현 필요가 생길 때 계약을 갱신한 뒤 추가한다.

## Generic Authoring Handoff

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

```json
{
  "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를 보존한다.

```json
{
  "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다. `none`은 `think=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 `content`와 `tool_calls` 없이 완료하면 Edge는 성공 응답을 빈 content로 끝내지 않는다.
- `include_reasoning` 생략 또는 `true`인 요청은 기존 reasoning 본문을 `reasoning_content`에 유지하고, `content`가 비어 있으면 reasoning 본문을 fallback content로도 반환한다. `finish_reason`이 `stop`이 아니면 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=false`와 `reasoning_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=true`나 `thinking_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를 결정한다.
