319 lines
30 KiB
Markdown
319 lines
30 KiB
Markdown
# 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 표면은 다음 헤더를 요구한다.
|
|
|
|
```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한다.
|
|
|
|
### 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에게 전달할 사용자 요청이다. 현재 구현은 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/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` 호출은 `metadata.iop_response_mode`를 명시하면 값과 무관하게 `400 invalid_request_error`로 거부한다. 생략 시 raw passthrough로 provider `POST /v1/responses`에 전달한다. caller body는 `model` field만 served target으로 rewrite하고, unknown/Codex field(`max_output_tokens`, `tools`, `store`, ...)는 보존하며, `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다.
|
|
- direct legacy provider route(`openai.model_routes[]`의 `openai_compat`/`vllm` adapter)에서 명시적 `metadata.iop_response_mode="passthrough+sideband"`는 opt-in extension surface다. non-streaming provider JSON object 응답은 top-level `metadata` object를 만들거나 병합해 IOP sideband metadata를 삽입한다. streaming 응답은 provider SSE event stream 사이에 `event: iop.sideband`를 삽입한다. sideband 내용은 `metadata` object 아래 확장 가능하며, 현재 최소 marker는 `iop_response_mode="passthrough+sideband"`다. `"transformed"`는 provider tunnel route에서 `400 invalid_request_error`로 거부한다.
|
|
- Responses provider passthrough success usage metric label은 endpoint=`responses`, response_mode=`passthrough` 또는 direct sideband route의 `passthrough+sideband`, model_group=request alias를 사용한다.
|
|
- `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`에 둔다. Chat Completions의 provider sampling option은 해당 endpoint의 OpenAI-compatible top-level request field를 따르며, `/v1/responses`와 마찬가지로 별도 `options` wrapper를 두지 않는다.
|
|
|
|
```json
|
|
{
|
|
"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`
|
|
|
|
### Chat Completions response mode
|
|
|
|
Provider route의 응답 경로는 route 종류에 따라 다르다.
|
|
|
|
- provider-pool model group route(`models[]`)는 `metadata.iop_response_mode`를 caller 선택자로 받지 않는다. 생략 시 provider HTTP status/header/body를 Node가 열어 기존 Edge-Node tunnel로 relay하고, Edge가 caller에게 쓴다. Chat Completions 성공 응답의 top-level `model` echo가 provider-served model이면 caller가 요청한 IOP model alias로 정규화한다. reasoning/content/tool_calls 같은 provider payload field는 보존한다. pure passthrough 응답 body에는 IOP sideband field/event를 섞지 않고, `X-IOP-Response-Mode` header도 붙이지 않는다.
|
|
- provider-pool model group route에서 `metadata.iop_response_mode`를 명시하면 `passthrough`, `passthrough+sideband`, `transformed`, 알 수 없는 값 모두 `400 invalid_request_error`로 거부한다. 이 제한은 provider model group이 client response-mode selector나 normalized `SubmitRun` path로 회귀하지 않게 하기 위한 것이다.
|
|
- direct legacy provider route(`openai.model_routes[]`의 `openai_compat`/`vllm` adapter)는 `metadata.iop_response_mode` 생략 또는 `passthrough`를 raw provider tunnel로 처리한다. Chat Completions의 `metadata.iop_response_mode="passthrough+sideband"`는 provider body와 IOP route/usage/assembled observation을 명시적 IOP extension surface로 함께 노출한다. streaming 응답은 provider SSE event 경계 사이에 `event: iop.sideband`를 추가하고, non-streaming 응답은 `iop.chat.passthrough_sideband` envelope로 provider body와 `iop_sideband`를 함께 반환한다. `/v1/responses`의 같은 모드는 위 Responses API 섹션처럼 응답 `metadata` 또는 SSE `event: iop.sideband`를 사용한다. 이 모드는 provider-original byte-identical response로 표시하지 않는다.
|
|
- direct legacy provider route에서 `metadata.iop_response_mode="transformed"`는 지원하지 않으며 `400 invalid_request_error`로 거부한다. non-provider normalized route에서는 raw tunnel을 쓰지 않고 normalized IOP output path를 사용하며 `X-IOP-Response-Mode: transformed`로 라벨링한다.
|
|
|
|
알 수 없는 `metadata.iop_response_mode` 값은 silent fallback 없이 `400 invalid_request_error`로 거부한다. provider-pool model group route는 값 검증 전에 explicit selector 자체를 거부한다.
|
|
|
|
Think 제어 field:
|
|
|
|
- `think` (bool, optional): thinking/reasoning 생성 활성화 여부. 생략하면 provider 기본값을 유지한다. `false`는 thinking 생성을 끄도록 요청하고, `true`는 provider가 지원하면 thinking 생성을 명시 활성화한다.
|
|
- `reasoning_effort` (string, optional): `none`, `low`, `medium`, `high` 중 하나. `none`은 `think=false`와 같은 disable 의미로 처리한다. `low`/`medium`/`high`는 provider가 지원하는 경우에만 전달한다.
|
|
- `thinking_token_budget` (int, optional): 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를 제거한다고 보장하지 않는다.
|
|
|
|
### dev-corp `gemma4:26b` provider-pool passthrough 파라미터 범위
|
|
|
|
이 표는 dev-corp의 `gemma4:26b` provider-pool route에서, 현재 IOP 설정과 provider/vLLM 최적화 값을 변경하지 않고 standard OpenAI-compatible caller가 요청 파라미터만 바꿔 측정할 때의 현재 계약 범위다. 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` 기본값을 바꾸는 측정으로만 사용한다. 일반 표준 안정 호출에서는 생략한다. |
|
|
| `metadata.iop_response_mode="passthrough+sideband"` | provider-pool model group route에서는 `400 invalid_request_error`로 거부한다. | dev-corp `gemma4:26b` provider-pool에서는 request selector로 사용하지 않는다. route/usage 관측은 metric/log surface를 기준으로 둔다. |
|
|
| `metadata.iop_response_mode="transformed"` | provider model group route에서는 `400 invalid_request_error`로 거부한다. | dev-corp `gemma4:26b` provider-pool에서는 사용하지 않는다. |
|
|
| `/v1/responses` 호출 | provider-pool model group route에서 raw `passthrough`로 provider `POST /v1/responses`에 전달한다. `model`만 rewrite하고 unknown/Codex field는 보존하며 `stream:true`는 raw SSE로 relay한다. 명시적 `metadata.iop_response_mode`는 값과 무관하게 `400 invalid_request_error`로 거부한다. usage metric은 endpoint=`responses`로 측정한다. | Codex 스타일 `/v1/responses` 호출을 provider-pool로 그대로 넘겨 측정할 수 있다. 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 기준이다. Chat Completions provider-pool pure `passthrough`는 Node adapter의 normalized `Execute`/request-body builder를 거치지 않고 provider HTTP body를 tunnel로 전달하므로, dev-corp `gemma4:26b` 측정 범위는 위 표를 우선한다.
|
|
|
|
- `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로 넘기기 전에 다음 값을 보정한다.
|
|
|
|
- `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`을 생략했을 때 내부 실행 입력 또는 Chat Completions provider tunnel body에 주입한다. strict output가 함께 활성화된 provider-pool Chat Completions 요청에서는 Edge가 `think=true`도 함께 주입해 vLLM/vLLM-MLX 계열 adapter가 `chat_template_kwargs.enable_thinking=true`로 전달하도록 한다.
|
|
|
|
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를 `transformed` normalized path로 전환하지 않는다. provider model group omitted mode는 계속 raw `passthrough`다.
|
|
- provider-pool `models[]` entry의 `default_thinking_token_budget`가 적용되는 모델은 catalog의 thinking policy가 우선한다. 이 경우 요청이 `think=false` 또는 `reasoning_effort=none`을 명시하지 않았다면 strict output에서도 `think=true`와 `thinking_token_budget`을 내부 실행 입력 또는 Chat Completions provider tunnel body에 넣는다.
|
|
|
|
`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`
|
|
- provider-pool model group route에서 `metadata.iop_response_mode`를 명시하는 방식
|
|
- `metadata.iop_response_mode`에 `passthrough`, `passthrough+sideband`, `transformed` 외 값을 넣는 방식
|
|
- `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로 매핑하는 방식을 우선한다.
|
|
|
|
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를 결정한다.
|