iop/agent-contract/outer/openai-compatible-api.md
toki 367e7f3dc4 feat(openai): 모델 생성 정책을 적용한다
provider pool 모델 catalog의 출력 토큰 기본값과 thinking budget을 OpenAI-compatible 요청에 반영하고, Ornith dev-runtime 기준과 관련 계약 문서를 함께 맞춘다.
2026-07-04 09:20:56 +09:00

15 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/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에 둔다.

Auth

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

Authorization: Bearer <token>

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

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에게 전달할 사용자 요청이다. 현재 구현은 string input만 지원한다.
  • stream: 현재 구현은 false 또는 생략만 지원한다.
  • background: 현재 구현은 false 또는 생략만 지원한다.
  • 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가 지원하지 않으면 무시될 수 있다.

금지:

  • metadata.cli 같은 CLI 전용 wrapper를 추가하지 않는다.
  • metadata.inference처럼 model route와 겹치는 target wrapper를 추가하지 않는다.
  • metadata.nomadcode처럼 특정 소비자 제품명에 묶인 wrapper를 추가하지 않는다.
  • metadata.source처럼 의미가 불명확한 호출 출처 field를 추가하지 않는다.
  • root-level iop 같은 별도 wrapper field를 추가하지 않는다.
  • /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 본문에 섞어 전달하지 않는다.

현재 구현 메모:

  • /v1/responses는 non-streaming 요청만 지원한다.
  • 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에 둔다. Chat Completions의 provider sampling option은 해당 endpoint의 OpenAI-compatible top-level request field를 따르며, /v1/responses와 마찬가지로 별도 options wrapper를 두지 않는다.

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

현재 지원하는 Chat Completions 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

Think 제어 field:

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

Provider별 think-control 정책:

  • 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는 요청을 내부 실행으로 넘기기 전에 다음 값을 보정한다.

  • 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을 생략했고 strict output이 reasoning을 금지하지 않을 때 내부 실행 입력에 주입한다.

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가 활성화되면 think=true가 명시되지 않은 요청은 내부 실행 입력에서 think=false로 낮춘다.

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으로 직렬화한다. provider route에서 assistant content에 <tool_call>, {{function(...)}}, [Calling tool: ...] 같은 텍스트가 들어와도 Edge는 기본적으로 이를 파싱하거나 OpenAI tool_calls로 합성하지 않는다. 해당 텍스트는 backend가 반환한 content로 그대로 둔다. 예외적으로 Node adapter가 native tool API unsupported fallback을 수행해 openai_text_tool_fallback: "true" metadata를 붙인 실행에 한해서만 Edge가 text tool-call을 OpenAI tool_calls로 복원한다. CLI route(adapter: "cli")는 native backend tool calling이 없으므로, 호환 fallback으로만 assistant content의 Cline-style 텍스트 tool call 블록을 OpenAI-compatible tool_calls로 합성할 수 있다. 이 fallback이 지원하는 텍스트 블록의 최소 형태는 <tool_call><function=<name>><parameter=<key>>JSON-or-text</parameter></function></tool_call> 또는 {{function_name(key=Python/JSON-like-literal)}}이다. CLI fallback에서 텍스트 tool call을 구조화할 때만 Edge는 요청의 tools[].function.parameters schema를 기준으로 arguments를 정규화한다. 예를 들어 tool schema가 commands: string[]만 허용하면 command 객체 입력도 shell string 배열로 접고, commands: {command,args}[]를 허용하면 shell 문법이 없는 명령을 structured argv로 만든다. CLI fallback에서 schema에 없는 UI 설명용 description이나 실행 위치 힌트용 runInTerminal은 command 객체와 최상위 args에서 제거한다. 단, cd, command -v, &&, pipe, redirect, quote 등 shell 해석이 필요한 명령은 schema가 허용할 때 commands: ["cd /work && git status"] 같은 shell string으로 유지한다. CLI route에서는 backend auto tool-calling 요구 조건으로 요청이 실패하지 않도록 내부 실행 입력의 tool_choice"none"으로 낮춘다. parallel_tool_calls, stream_options, store는 클라이언트 호환성을 위해 수신하지만 현재 Edge 실행 의미에는 반영하지 않는다.

금지:

  • metadata.source, metadata.cli, metadata.inference, metadata.nomadcode
  • options, chat_template_kwargs, format, keep_alive 같은 provider/Ollama 전용 request 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로 매핑하는 방식을 우선한다.