chore: sync OpenAI compatible API contract, routing policy, and dev-corp test updates

This commit is contained in:
leedongmyun 2026-07-13 19:18:07 +09:00
parent 10ede9381b
commit 9f5d15d91c
16 changed files with 482 additions and 69 deletions

View file

@ -12,7 +12,7 @@
| id | 읽는 조건 | 원본 경로 | path | | id | 읽는 조건 | 원본 경로 | path |
|----|-----------|-----------|------| |----|-----------|-----------|------|
| `iop.openai-compatible-api` | OpenAI-compatible API, Responses API, Chat Completions, legacy Completions, `model` route, Codex/CLI workspace, generic authoring metadata, `metadata.workspace`, `metadata.task_id`, `metadata.iop_response_mode`, provider raw passthrough/sideband/transformed response modes | `apps/edge/internal/openai/*`, `packages/go/config/config.go`, `configs/edge.yaml` | `agent-contract/outer/openai-compatible-api.md` | | `iop.openai-compatible-api` | OpenAI-compatible API, Responses API, Chat Completions, legacy Completions, `model` route, model-driven passthrough/normalized routing, Codex/CLI workspace, generic authoring metadata, `metadata.workspace`, `metadata.task_id`, provider-native OpenAI-compatible extension fields such as `chat_template_kwargs` | `apps/edge/internal/openai/*`, `packages/go/config/config.go`, `configs/edge.yaml` | `agent-contract/outer/openai-compatible-api.md` |
| `iop.a2a-json-rpc-api` | A2A JSON-RPC API, `message/send`, `tasks/get`, `tasks/cancel`, A2A task state, agent card, `a2a.bearer_token`, Edge A2A input surface | `apps/edge/internal/input/a2a/*`, `packages/go/config/config.go`, `configs/edge.yaml` | `agent-contract/outer/a2a-json-rpc-api.md` | | `iop.a2a-json-rpc-api` | A2A JSON-RPC API, `message/send`, `tasks/get`, `tasks/cancel`, A2A task state, agent card, `a2a.bearer_token`, Edge A2A input surface | `apps/edge/internal/input/a2a/*`, `packages/go/config/config.go`, `configs/edge.yaml` | `agent-contract/outer/a2a-json-rpc-api.md` |
## Inner Contracts ## Inner Contracts

View file

@ -33,7 +33,7 @@ Edge는 Node 연결을 수락하고, Node는 연결 직후 등록 요청을 보
- register: Node가 `RegisterRequest`를 보내고 Edge가 `RegisterResponse`로 수락 여부와 `NodeConfigPayload`를 돌려준다. - register: Node가 `RegisterRequest`를 보내고 Edge가 `RegisterResponse`로 수락 여부와 `NodeConfigPayload`를 돌려준다.
- execution: Edge가 `RunRequest`를 보내고 Node가 `RunEvent` stream으로 실행 상태를 보낸다. - execution: Edge가 `RunRequest`를 보내고 Node가 `RunEvent` stream으로 실행 상태를 보낸다.
- provider raw tunnel: Edge가 기존 Edge-Node socket으로 `ProviderTunnelRequest`를 보내고 Node가 provider HTTP/SSE 요청을 연 뒤 `ProviderTunnelFrame` stream으로 provider status/header/body/end/error/usage 후보를 sequence와 함께 돌려준다. 이 경로는 OpenAI-compatible provider `passthrough`와 `passthrough+sideband`용이며 `RunEvent` 실행 stream과 분리된다. - provider raw tunnel: Edge가 기존 Edge-Node socket으로 `ProviderTunnelRequest`를 보내고 Node가 provider HTTP/SSE 요청을 연 뒤 `ProviderTunnelFrame` stream으로 provider status/header/body/end/error/usage 후보를 sequence와 함께 돌려준다. 이 경로는 OpenAI-compatible provider passthrough용이며 `RunEvent` 실행 stream과 분리된다.
- provider-pool mixed dispatch: Edge service는 model group provider candidate를 선택한 뒤, 같은 selected provider/queue lease로 OpenAI-compatible provider에는 `ProviderTunnelRequest`, Ollama/CLI/native provider에는 normalized `RunRequest`를 보낸다. Edge-Node wire는 client-provided response path selector를 받지 않고, provider type만으로 후보를 제외하지 않는다. - provider-pool mixed dispatch: Edge service는 model group provider candidate를 선택한 뒤, 같은 selected provider/queue lease로 OpenAI-compatible provider에는 `ProviderTunnelRequest`, Ollama/CLI/native provider에는 normalized `RunRequest`를 보낸다. Edge-Node wire는 client-provided response path selector를 받지 않고, provider type만으로 후보를 제외하지 않는다.
- cancel: Edge가 `CancelRequest`를 보내며 `CANCEL_RUN``TERMINATE_SESSION`을 구분한다. - cancel: Edge가 `CancelRequest`를 보내며 `CANCEL_RUN``TERMINATE_SESSION`을 구분한다.
- command: Edge가 `NodeCommandRequest`를 보내고 Node가 `NodeCommandResponse`로 usage/capabilities/session/transport/provider 상태를 응답한다. - command: Edge가 `NodeCommandRequest`를 보내고 Node가 `NodeCommandResponse`로 usage/capabilities/session/transport/provider 상태를 응답한다.
@ -46,8 +46,8 @@ Edge는 Node 연결을 수락하고, Node는 연결 직후 등록 요청을 보
- `RunRequest.input`: adapter가 해석할 실행 입력이다. CLI 실행에서는 prompt 계열 입력으로 변환된다. - `RunRequest.input`: adapter가 해석할 실행 입력이다. CLI 실행에서는 prompt 계열 입력으로 변환된다.
- `RunRequest.metadata`: caller-defined 실행 metadata다. workspace 자체는 별도 `workspace` 필드로 전달한다. - `RunRequest.metadata`: caller-defined 실행 metadata다. workspace 자체는 별도 `workspace` 필드로 전달한다.
- `RunEvent.type`: `start`, `delta`, `complete`, `error`, `cancelled` 같은 실행 이벤트 종류다. - `RunEvent.type`: `start`, `delta`, `complete`, `error`, `cancelled` 같은 실행 이벤트 종류다.
- `ProviderTunnelRequest`: 기존 Edge-Node socket 위에서 provider HTTP request를 열기 위한 요청이다. `adapter`, `target`, `method`, `path`, `headers`, `body`, `stream`, `timeout_sec`, `metadata`, `session_id`를 싣되 normalized adapter execution인 `RunRequest`와 분리된다. Chat Completions response mode는 `metadata["iop_response_mode"]``passthrough` 또는 `passthrough+sideband`로 전달될 수 있다. - `ProviderTunnelRequest`: 기존 Edge-Node socket 위에서 provider HTTP request를 열기 위한 요청이다. `adapter`, `target`, `method`, `path`, `headers`, `body`, `stream`, `timeout_sec`, `metadata`, `session_id`를 싣되 normalized adapter execution인 `RunRequest`와 분리된다. 외부 caller의 response selector를 전달하지 않으며, 경로는 Edge가 `model`로 선택한 provider capability에 의해 결정된다.
- `ProviderTunnelFrame`: Node가 provider response를 Edge로 돌려주는 ordered frame이다. `kind`, `sequence`, `status_code`, `headers`, `body`, `end`, `error`, `usage`, `metadata`를 싣는다. `body`는 passthrough source of truth이며 `RunEvent.delta`나 Edge `events.Bus` fanout payload로 보내지 않는다. `usage``metadata`sideband observation 후보이고 pure passthrough body에 합쳐지지 않는다. - `ProviderTunnelFrame`: Node가 provider response를 Edge로 돌려주는 ordered frame이다. `kind`, `sequence`, `status_code`, `headers`, `body`, `end`, `error`, `usage`, `metadata`를 싣는다. `body`는 passthrough source of truth이며 `RunEvent.delta`나 Edge `events.Bus` fanout payload로 보내지 않는다. `usage``metadata`Edge의 metric/log/known-key 관측 후보이고 provider passthrough body에 합쳐지지 않는다.
- tunnel cancellation: HTTP caller disconnect, response wait timeout, 또는 Edge write failure가 발생하면 Edge는 같은 run id에 대한 `CancelRequest(CANCEL_RUN)`을 보내 upstream provider request 중단을 요청한다. Node adapter는 provider request context cancellation을 관측하고 ordered error/end semantics를 유지해야 한다. - tunnel cancellation: HTTP caller disconnect, response wait timeout, 또는 Edge write failure가 발생하면 Edge는 같은 run id에 대한 `CancelRequest(CANCEL_RUN)`을 보내 upstream provider request 중단을 요청한다. Node adapter는 provider request context cancellation을 관측하고 ordered error/end semantics를 유지해야 한다.
- `RunEvent.metadata["openai_tool_calls"]`: OpenAI-compatible provider adapter가 native `tool_calls`를 반환했을 때 완료 이벤트에 싣는 JSON 배열이다. Edge OpenAI-compatible 표면은 이 값을 `message.tool_calls` 또는 stream `delta.tool_calls`로 복원한다. provider assistant content 텍스트를 이 값으로 파싱/합성하지 않는다. - `RunEvent.metadata["openai_tool_calls"]`: OpenAI-compatible provider adapter가 native `tool_calls`를 반환했을 때 완료 이벤트에 싣는 JSON 배열이다. Edge OpenAI-compatible 표면은 이 값을 `message.tool_calls` 또는 stream `delta.tool_calls`로 복원한다. provider assistant content 텍스트를 이 값으로 파싱/합성하지 않는다.
- `RunEvent.metadata["openai_text_tool_fallback"]`: OpenAI-compatible provider adapter가 backend native tool API 거부 후 `tools`/`tool_choice`를 제거하고 text tool-call instruction으로 재시도했을 때 `"true"`를 싣는다. 이 instruction은 backend가 system role 위치를 거부하지 않도록 leading system message에 병합한다. Edge는 이 표시가 있는 실행에서만 assistant content의 text tool-call을 OpenAI-compatible `tool_calls`로 복원할 수 있다. - `RunEvent.metadata["openai_text_tool_fallback"]`: OpenAI-compatible provider adapter가 backend native tool API 거부 후 `tools`/`tool_choice`를 제거하고 text tool-call instruction으로 재시도했을 때 `"true"`를 싣는다. 이 instruction은 backend가 system role 위치를 거부하지 않도록 leading system message에 병합한다. Edge는 이 표시가 있는 실행에서만 assistant content의 text tool-call을 OpenAI-compatible `tool_calls`로 복원할 수 있다.

View file

@ -20,6 +20,8 @@
이 문서는 외부 프로젝트가 IOP Edge의 OpenAI-compatible HTTP 표면을 호출할 때 확인할 계약 원문이다. 이 문서는 외부 프로젝트가 IOP Edge의 OpenAI-compatible HTTP 표면을 호출할 때 확인할 계약 원문이다.
IOP 내부 실행은 `adapter + target` 기준이며, OpenAI-compatible 경계에서는 호환성을 위해 `model`을 사용한다. IOP 내부 실행은 `adapter + target` 기준이며, OpenAI-compatible 경계에서는 호환성을 위해 `model`을 사용한다.
IOP 고유 실행 문맥은 별도 `iop` wrapper field를 만들지 않고 OpenAI request의 `metadata`에 둔다. 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 같은 내부 문맥으로 쓰는 방식이다.
## Auth ## Auth
@ -39,6 +41,7 @@ Edge 설정에 `openai.principal_tokens[]`가 설정된 경우, caller는 기존
- caller-provided `metadata.user`는 identity source가 아니며 사용되지 않는다. - caller-provided `metadata.user`는 identity source가 아니며 사용되지 않는다.
- caller가 `metadata.iop_principal_*`를 보내도 authenticated context 값이 overwrite한다. - caller가 `metadata.iop_principal_*`를 보내도 authenticated context 값이 overwrite한다.
- `metadata`는 route/response mode selector가 아니다. Edge는 model 기반 route 선택 뒤 IOP가 아는 metadata key만 실행 문맥과 관측용으로 발췌한다.
### Legacy fallback ### Legacy fallback
@ -102,9 +105,9 @@ CLI agent 실행으로 라우팅되는 요청의 최소 형태:
- `model`: Edge가 내부 `adapter + target`으로 해석할 외부 route 이름이다. IOP Edge에서는 라우팅을 위해 필수다. - `model`: Edge가 내부 `adapter + target`으로 해석할 외부 route 이름이다. IOP Edge에서는 라우팅을 위해 필수다.
- `instructions`: OpenAI Responses API의 top-level instruction field다. 있으면 `input` 앞에 배치해 agent 실행 prompt를 만든다. - `instructions`: OpenAI Responses API의 top-level instruction field다. 있으면 `input` 앞에 배치해 agent 실행 prompt를 만든다.
- `input`: agent에게 전달할 사용자 요청이다. 현재 구현은 string input만 지원한다. - `input`: agent에게 전달할 사용자 요청이다. normalized(non-provider) route는 현재 string input만 지원한다.
- `stream`: 현재 구현은 `false` 또는 생략만 지원한다. - `stream`: normalized(non-provider) route는 현재 `false` 또는 생략만 지원한다. Provider-pool passthrough는 provider가 지원하는 stream 값을 보존한다.
- `background`: 현재 구현은 `false` 또는 생략만 지원한다. - `background`: normalized(non-provider) route는 현재 `false` 또는 생략만 지원한다. Provider-pool passthrough는 provider가 지원하는 값을 보존한다.
- `metadata.workspace`: CLI process를 실행할 작업 디렉터리다. CLI agent route에서는 필수 실행 문맥이다. - `metadata.workspace`: CLI process를 실행할 작업 디렉터리다. CLI agent route에서는 필수 실행 문맥이다.
- `metadata`: OpenAI 표준 metadata container다. string key/value를 허용하고, IOP는 `workspace`만 실행 문맥으로 해석한다. 나머지 key는 caller-defined metadata로 보존하되 `source`는 지원하지 않는다. - `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가 아니다. - `metadata.request_id`, `metadata.task_id`: caller-defined metadata 예시다. 특별한 wrapper나 제품 전용 field가 아니다.
@ -112,24 +115,24 @@ CLI agent 실행으로 라우팅되는 요청의 최소 형태:
- `temperature`: 생성 다양성 option이다. 대상 adapter가 지원하지 않으면 무시될 수 있다. - `temperature`: 생성 다양성 option이다. 대상 adapter가 지원하지 않으면 무시될 수 있다.
- `top_p`: nucleus sampling option이다. 대상 adapter가 지원하지 않으면 무시될 수 있다. - `top_p`: nucleus sampling option이다. 대상 adapter가 지원하지 않으면 무시될 수 있다.
금지: Normalized route 금지:
- `metadata.cli` 같은 CLI 전용 wrapper를 추가하지 않는다. - `metadata.cli` 같은 CLI 전용 wrapper를 추가하지 않는다.
- `metadata.inference`처럼 `model` route와 겹치는 target wrapper를 추가하지 않는다. - `metadata.inference`처럼 `model` route와 겹치는 target wrapper를 추가하지 않는다.
- `metadata.nomadcode`처럼 특정 소비자 제품명에 묶인 wrapper를 추가하지 않는다. - `metadata.nomadcode`처럼 특정 소비자 제품명에 묶인 wrapper를 추가하지 않는다.
- `metadata.source`처럼 의미가 불명확한 호출 출처 field를 추가하지 않는다. - `metadata.source`처럼 의미가 불명확한 호출 출처 field를 추가하지 않는다.
- root-level `iop` 같은 별도 wrapper field를 추가하지 않는다. - root-level `iop` 같은 별도 wrapper field를 추가하지 않는다.
- `/v1/responses``options` wrapper를 추가하지 않는다. Responses API option은 OpenAI 표준 top-level 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 기본값을 따른다. - `session_id`, `timeout_sec` 같은 IOP 실행 제어 field를 request body 계약에 추가하지 않는다. logical session과 timeout은 route/config 기본값을 따른다.
- workspace를 prompt 본문에 섞어 전달하지 않는다. - workspace를 prompt 본문에 섞어 전달하지 않는다.
현재 구현 메모: 현재 구현 메모:
- normalized(non-provider) `/v1/responses` route는 strict field validation을 유지하며 non-streaming string input만 지원한다. - 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(`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 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`로 거부한다. - 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=`responses`, response_mode=`passthrough` 또는 direct sideband route의 `passthrough+sideband`, model_group=request alias를 사용한다. - Responses provider passthrough success usage metric label은 endpoint와 model_group=request alias를 기준으로 집계한다. 관측/usage 정보는 provider body에 섞지 않는다.
- `metadata`는 최대 16개 string key/value를 허용한다. key는 64자 이하, value는 512자 이하를 기준으로 한다. - `metadata`는 최대 16개 string key/value를 허용한다. key는 64자 이하, value는 512자 이하를 기준으로 한다.
- CLI route의 `metadata.workspace`는 이 문서의 계약 기준이다. 구현은 이 값을 Edge service의 run workspace와 Node CLI adapter의 process working directory로 전달해야 한다. - CLI route의 `metadata.workspace`는 이 문서의 계약 기준이다. 구현은 이 값을 Edge service의 run workspace와 Node CLI adapter의 process working directory로 전달해야 한다.
- `metadata.workspace``RunRequest.Workspace`로 전달하고 generic run metadata에는 복사하지 않는다. - `metadata.workspace``RunRequest.Workspace`로 전달하고 generic run metadata에는 복사하지 않는다.
@ -158,7 +161,7 @@ Workspace-bound route는 workspace가 없거나 상대 경로이면 OpenAI-compa
## Chat Completions ## 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를 두지 않는다. `/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 ```json
{ {
@ -175,7 +178,7 @@ Workspace-bound route는 workspace가 없거나 상대 경로이면 OpenAI-compa
} }
``` ```
현재 지원하는 Chat Completions request field: Normalized(non-provider) Chat Completions route가 해석하는 request field:
- `model` - `model`
- `messages` - `messages`
@ -200,27 +203,29 @@ Workspace-bound route는 workspace가 없거나 상대 경로이면 OpenAI-compa
- `thinking_token_budget` - `thinking_token_budget`
- `include_reasoning` - `include_reasoning`
### Chat Completions response mode 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한다.
Provider route의 응답 경로는 route 종류에 따라 다르다. ### Chat Completions routing and response
- 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도 붙이지 않는다. Chat Completions의 실행 경로는 caller가 보낸 `model`의 route/provider capability로 결정한다.
- 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 자체를 거부한다. - 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는 보존한다.
Think 제어 field: IOP 확장 think 제어 field:
- `think` (bool, optional): thinking/reasoning 생성 활성화 여부. 생략하면 provider 기본값을 유지한다. `false`는 thinking 생성을 끄도록 요청하고, `true`는 provider가 지원하면 thinking 생성을 명시 활성화한다. - `think` (bool, optional): thinking/reasoning 생성 활성화 여부를 표현하는 IOP 확장 field다. 생략하면 provider 기본값을 유지한다. `false`는 thinking 생성을 끄도록 요청하고, `true`는 provider가 지원하면 thinking 생성을 명시 활성화한다.
- `reasoning_effort` (string, optional): `none`, `low`, `medium`, `high` 중 하나. `none``think=false`와 같은 disable 의미로 처리한다. `low`/`medium`/`high`는 provider가 지원하는 경우에만 전달한다. - `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): thinking token budget. 0 이상이어야 한다. - `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를 제거한다고 보장하지 않는다. - `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 passthrough 파라미터 범위
이 표는 dev-corp의 `gemma4:26b` provider-pool route에서, 현재 IOP 설정과 provider/vLLM 최적화 값을 변경하지 않고 standard OpenAI-compatible caller가 요청 파라미터만 바꿔 측정할 때의 현재 계약 범위다. Pi TUI의 고정 호출 방식이나 다른 model/provider route의 동작으로 일반화하지 않는다. 이 표는 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의 동작으로 일반화하지 않는다.
| 요청 파라미터 | 현재 기대 동작 | 권장 판정 | | 요청 파라미터 | 현재 기대 동작 | 권장 판정 |
| --- | --- | --- | | --- | --- | --- |
@ -230,9 +235,8 @@ Think 제어 field:
| `think=false` | Edge validation conflict가 없으면 요청은 통과하고 model catalog의 default thinking budget 주입은 억제된다. 이후 provider body에 `think:false`가 반영될 수 있으나, 현재 `gemma4:26b` provider 최적화 값과 충돌하거나 backend별로 무시/실패/품질 저하가 날 수 있다. | “숨기기만” 하는 옵션이 아니다. dev-corp `gemma4:26b` 기본 안정 호출에서는 생략한다. | | `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`와 같은 이유로 기본 안정 호출에서는 생략한다. | | `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` 기본값을 바꾸는 측정으로만 사용한다. 일반 표준 안정 호출에서는 생략한다. | | 명시적 `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를 기준으로 둔다. | | provider-native field 예: `chat_template_kwargs` | selected provider가 해당 OpenAI-compatible extension을 지원하면 IOP provider-pool passthrough는 이를 보존하고 provider로 전달해야 한다. | 이 field를 IOP 추상 field로 치환하지 않는다. provider가 거부하면 provider error를 relay한다. |
| `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에서 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`를 쓴다. |
| `/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 정책을 별도 구현/계약 갱신해야 한다. 현재 구현에서 `think=false`를 “provider에는 기본 think를 유지하되 IOP가 응답에서 reasoning만 감추는 hide-only 모드”로 해석하지 않는다. 그런 동작이 필요하면 provider/vLLM 설정 변경이 아니라 Edge provider-pool passthrough 응답 filtering 정책을 별도 구현/계약 갱신해야 한다.
@ -246,7 +250,7 @@ Reasoning-only 완료 처리 (non-provider normalized route):
Provider별 think-control 정책: 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` 측정 범위는 위 표를 우선한다. 아래 정책은 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`: - `vLLM`:
- `think=false` 또는 `reasoning_effort=none` -> 내부 `chat_template_kwargs.enable_thinking=false` - `think=false` 또는 `reasoning_effort=none` -> 내부 `chat_template_kwargs.enable_thinking=false`
@ -265,11 +269,11 @@ Provider별 think-control 정책:
- `reasoning_effort=low|medium|high` -> `unsupported think control` 오류 반환 - `reasoning_effort=low|medium|high` -> `unsupported think control` 오류 반환
- Unknown / 기타 provider: 요청 field를 그대로 전달하되, provider가 지원하지 않는 값은 backend 또는 adapter error가 될 수 있다. - Unknown / 기타 provider: 요청 field를 그대로 전달하되, provider가 지원하지 않는 값은 backend 또는 adapter error가 될 수 있다.
Provider pool model catalog의 `models[]` entry가 generation policy를 제공하면 Edge는 요청을 내부 실행 또는 Chat Completions provider tunnel로 넘기기 전에 다음 값을 보정한다. 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`로 주입한다. - `default_max_tokens`: caller가 출력 token limit을 생략했을 때 `max_tokens` 또는 `max_output_tokens`로 주입한다.
- `min_max_tokens`: caller가 너무 작은 출력 token limit을 보냈을 때 해당 값까지 올린다. caller 값이 더 크면 보존한다. - `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`로 전달하도록 한다. - `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 정책: Conflict 정책:
@ -282,8 +286,8 @@ Conflict 정책:
Strict output 모드: Strict output 모드:
- strict output가 활성화되면 non-provider normalized route에서 `think=true`가 명시되지 않은 요청은 내부 실행 입력에서 `think=false`로 낮춘다. - 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`다. - 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가 우선한다. 이 경우 요청이 `think=false` 또는 `reasoning_effort=none`을 명시하지 않았다면 strict output에서도 `think=true``thinking_token_budget`을 내부 실행 입력 또는 Chat Completions provider tunnel body에 넣는다. - 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"`를 싣는다. `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 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"`를 사용한다.
@ -298,9 +302,7 @@ text tool-call을 구조화할 때 Edge는 route와 무관하게 요청의 `tool
금지: 금지:
- `metadata.source`, `metadata.cli`, `metadata.inference`, `metadata.nomadcode` - `metadata.source`, `metadata.cli`, `metadata.inference`, `metadata.nomadcode`
- provider-pool model group route에서 `metadata.iop_response_mode`를 명시하는 방식 - 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 보존에는 적용하지 않는다.
- `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 - `session_id`, `timeout_sec` 같은 IOP 실행 제어 field
## Legacy Completions ## Legacy Completions

View file

@ -61,7 +61,7 @@ dev-corp provider pool을 현재 dev-runtime과 같은 구조로 배포한다.
- 2026-07-13 기준 Spark Gemma4 `8002`/`8004` runtime은 내린 상태로 보고, Spark 01/02는 Ornith `8003`/`8005`를 IOP provider로 둔다. Gemma4는 Mac Studio `192.168.2.3:8004` provider만 기본 대상으로 둔다. - 2026-07-13 기준 Spark Gemma4 `8002`/`8004` runtime은 내린 상태로 보고, Spark 01/02는 Ornith `8003`/`8005`를 IOP provider로 둔다. Gemma4는 Mac Studio `192.168.2.3:8004` provider만 기본 대상으로 둔다.
- 2026-07-13 재확인 기준 DGX Spark 01 Ornith는 Docker `iop-vllm-ornith35b-fp8`, host `8003`, `--gpu-memory-utilization 0.47`로 동작하며 startup log에서 GPU KV cache `783,347` tokens, full-context concurrency `2.99x`를 확인했다. DGX Spark 02 Ornith는 host `8005`, `--gpu-memory-utilization 0.50`으로 동작하며 GPU KV cache `1,074,276` tokens, full-context concurrency `4.10x`를 확인했다. - 2026-07-13 재확인 기준 DGX Spark 01 Ornith는 Docker `iop-vllm-ornith35b-fp8`, host `8003`, `--gpu-memory-utilization 0.47`로 동작하며 startup log에서 GPU KV cache `783,347` tokens, full-context concurrency `2.99x`를 확인했다. DGX Spark 02 Ornith는 host `8005`, `--gpu-memory-utilization 0.50`으로 동작하며 GPU KV cache `1,074,276` tokens, full-context concurrency `4.10x`를 확인했다.
- 2026-07-02 mac-mini native Control Plane `18002/19004/19005` 관측값은 historical evidence다. 2026-07-09 현재 dev-corp Edge source of truth는 public `iop.ai.kr`이고, current runner에서 public host SSH와 public Control Plane status 접근은 확인되지 않았다. - 2026-07-02 mac-mini native Control Plane `18002/19004/19005` 관측값은 historical evidence다. 2026-07-09 현재 dev-corp Edge source of truth는 public `iop.ai.kr`이고, current runner에서 public host SSH와 public Control Plane status 접근은 확인되지 않았다.
- 2026-07-13 기준 dev-corp provider-pool Edge OpenAI-compatible capacity smoke 표준은 public `https://digitalplatform.iop.ai.kr/v1/chat/completions`에서 `ornith:35b` 9/6, `gemma4:26b` 9/6 동시 요청이다. Direct Edge listener `http://digitalplatform.iop.ai.kr:18086/v1`도 같은 backend로 동작하지만 사용자-facing 기본값으로 쓰지 않는다. `/v1/responses`OpenAI-compatible provider model group raw passthrough parity 전까지 미지원이므로 성공 기준에서 제외한다. - 2026-07-13 기준 dev-corp provider-pool Edge OpenAI-compatible capacity smoke 표준은 public `https://digitalplatform.iop.ai.kr/v1/chat/completions`에서 `ornith:35b` 9/6, `gemma4:26b` 9/6 동시 요청이다. Direct Edge listener `http://digitalplatform.iop.ai.kr:18086/v1`도 같은 backend로 동작하지만 사용자-facing 기본값으로 쓰지 않는다. `/v1/responses`capacity 성공 기준이 아니라 selected provider 지원 여부와 IOP raw passthrough/relay 동작을 분리해 보고한다.
- 2026-07-13 live status 기준 DGX Spark 01/02와 Mac Studio node는 `iop.ai.kr:18087`로 연결되어 있으며, provider snapshot은 Spark01 Ornith capacity `4`, Spark02 Ornith capacity `4`, Mac Studio Gemma4 capacity `5`이다. 같은 날 model-specific smoke에서 `ornith:35b` 9/6, `gemma4:26b` 9/6 요청이 모두 성공했고 완료 후 `in_flight=0`, `queued=0`, `healthy`로 회복했다. - 2026-07-13 live status 기준 DGX Spark 01/02와 Mac Studio node는 `iop.ai.kr:18087`로 연결되어 있으며, provider snapshot은 Spark01 Ornith capacity `4`, Spark02 Ornith capacity `4`, Mac Studio Gemma4 capacity `5`이다. 같은 날 model-specific smoke에서 `ornith:35b` 9/6, `gemma4:26b` 9/6 요청이 모두 성공했고 완료 후 `in_flight=0`, `queued=0`, `healthy`로 회복했다.
## 실행 절차 ## 실행 절차
@ -118,7 +118,7 @@ dev-corp provider pool을 현재 dev-runtime과 같은 구조로 배포한다.
- `/v1/models`가 대상 model alias를 노출하는지 확인한다. - `/v1/models`가 대상 model alias를 노출하는지 확인한다.
9. **OpenAI-compatible capacity smoke** 9. **OpenAI-compatible capacity smoke**
- `/v1/chat/completions`를 검증한다. `/v1/responses`는 OpenAI-compatible provider model group raw passthrough parity 전까지 미지원이므로 provider-pool capacity 성공 기준에서 제외한다. legacy `/v1/completions`는 route가 구현되어 있지 않으면 실패로 보지 않는다. - `/v1/chat/completions`를 검증한다. Provider-pool passthrough 경계를 바꾼 배포라면 selected provider가 지원하는 OpenAI-compatible extension field 예: `chat_template_kwargs`가 IOP에서 거부되지 않고 provider로 전달되는지도 확인한다. `/v1/responses`는 capacity 성공 기준이 아니라 provider-dependent passthrough/relay 검증으로 분리한다. legacy `/v1/completions`는 route가 구현되어 있지 않으면 실패로 보지 않는다.
- 표준 부하 프롬프트는 700~1200 token 수준의 구조화된 답변을 유도해 요청이 동시에 관측될 시간을 만든다. - 표준 부하 프롬프트는 700~1200 token 수준의 구조화된 답변을 유도해 요청이 동시에 관측될 시간을 만든다.
- model-specific smoke를 실행한다. `ornith:35b`는 Spark01/02 합산 capacity `8` 기준 9개와 6개 동시 요청을 보내고, `gemma4:26b`는 Mac Studio capacity `5` 기준 9개와 6개 동시 요청을 보낸다. - model-specific smoke를 실행한다. `ornith:35b`는 Spark01/02 합산 capacity `8` 기준 9개와 6개 동시 요청을 보내고, `gemma4:26b`는 Mac Studio capacity `5` 기준 9개와 6개 동시 요청을 보낸다.
- Control Plane status가 접근 가능하면 요청 실행 중 반복 polling하여 대상 provider들의 `in_flight`가 각 model capacity에 도달하고 초과 요청이 queue에 들어가는지 증거로 남긴다. 짧은 요청에서는 polling이 queued 순간을 놓칠 수 있으므로 HTTP 성공, max in_flight, 최종 회복을 함께 판정한다. - Control Plane status가 접근 가능하면 요청 실행 중 반복 polling하여 대상 provider들의 `in_flight`가 각 model capacity에 도달하고 초과 요청이 queue에 들어가는지 증거로 남긴다. 짧은 요청에서는 polling이 queued 순간을 놓칠 수 있으므로 HTTP 성공, max in_flight, 최종 회복을 함께 판정한다.
@ -157,7 +157,7 @@ dev-corp runtime 배포 결과
- Ports: <port summary> - Ports: <port summary>
- Nodes: <node_id connected summary> - Nodes: <node_id connected summary>
- Providers: <provider_id capacity/in_flight/queued/health summary> - Providers: <provider_id capacity/in_flight/queued/health summary>
- OpenAI-compatible: models=<pass|fail>, chat-completions-capacity=<pass|fail>, responses-provider-pool=<unsupported|pass|fail> - OpenAI-compatible: models=<pass|fail>, chat-completions-capacity=<pass|fail>, provider-native-field-passthrough=<pass|fail|not-run>, responses-provider-pool=<pass|provider-unsupported|fail|not-run>
- Capacity evidence: <endpoint별 max in_flight/queued snapshot> - Capacity evidence: <endpoint별 max in_flight/queued snapshot>
- Blockers/Risk: <없음 또는 내용> - Blockers/Risk: <없음 또는 내용>
``` ```

View file

@ -37,7 +37,7 @@ OpenAI-compatible Chat Completions provider 경로에서 모델 출력 이상을
- 출력 검증 filter별 enable/disable 정책을 environment(`dev`, `dev-corp`), model group/model/provider, 기능 단위로 평가하는 config/registry 계층 - 출력 검증 filter별 enable/disable 정책을 environment(`dev`, `dev-corp`), model group/model/provider, 기능 단위로 평가하는 config/registry 계층
- 반복 출력 루프 감지용 rolling stream inspector, upstream abort, continuation repair, 1회 repair 제한 - 반복 출력 루프 감지용 rolling stream inspector, upstream abort, continuation repair, 1회 repair 제한
- `metadata.scheme` JSON schema 계약 수신, 마지막 user message prompt append, buffered validation, schema 위반 시 bounded retry - `metadata.scheme` JSON schema 계약 수신, 마지막 user message prompt append, buffered validation, schema 위반 시 bounded retry
- `passthrough`, `passthrough_guarded`, `contract_schema` 내부 response path 구분과 실행 로그/side observation. 이 이름들은 caller가 임의로 넣는 `metadata.iop_response_mode` 값이 아니라 IOP 내부 경로/로그 기준이다. - `passthrough`, `passthrough_guarded`, `contract_schema` 내부 response path 구분과 실행 로그/관측 기준. 이 이름들은 caller가 지정하는 공개 request field가 아니라 IOP 내부 경로/로그 기준이다.
- normalized 실행 경로와 CLI adapter 경로를 OpenAI-compatible provider 출력 검증 경로와 분리하는 책임 경계 - normalized 실행 경로와 CLI adapter 경로를 OpenAI-compatible provider 출력 검증 경로와 분리하는 책임 경계
## 기능 ## 기능
@ -84,6 +84,6 @@ OpenAI-compatible provider 응답을 사용자에게 노출하기 전에 필터
- 표준선(선택): optional online filter가 비활성화된 모델은 pure passthrough로 처리할 수 있지만, caller가 `metadata.scheme`처럼 필수 계약을 요청했는데 해당 filter가 비활성화된 모델은 silent passthrough가 아니라 unsupported/400으로 거부한다. - 표준선(선택): optional online filter가 비활성화된 모델은 pure passthrough로 처리할 수 있지만, caller가 `metadata.scheme`처럼 필수 계약을 요청했는데 해당 filter가 비활성화된 모델은 silent passthrough가 아니라 unsupported/400으로 거부한다.
- 표준선(선택): OpenAI-compatible provider 출력 검증은 normalized 경로로 전환하지 않는다. normalized는 CLI 전용으로 유지한다. - 표준선(선택): OpenAI-compatible provider 출력 검증은 normalized 경로로 전환하지 않는다. normalized는 CLI 전용으로 유지한다.
- 우선순위 순서: 현재 active 1순위다. 이 Milestone을 먼저 구현하고, 그 다음 [Seulgivibe OpenAI-compatible Provider 연동](../../routing-policy-model-orchestration/milestones/seulgivibe-openai-compatible-provider.md)을 진행한다. - 우선순위 순서: 현재 active 1순위다. 이 Milestone을 먼저 구현하고, 그 다음 [Seulgivibe OpenAI-compatible Provider 연동](../../routing-policy-model-orchestration/milestones/seulgivibe-openai-compatible-provider.md)을 진행한다.
- 선행 작업: [OpenAI-compatible Tool Call Boundary Hardening](../../../archive/phase/knowledge-tool-optimization-extension/milestones/openai-compatible-tool-call-boundary-hardening.md), [OpenAI-compatible Raw Tunnel과 Sideband Passthrough](../../../archive/phase/routing-policy-model-orchestration/milestones/openai-compatible-raw-tunnel-sideband-passthrough.md) - 선행 작업: [OpenAI-compatible Tool Call Boundary Hardening](../../../archive/phase/knowledge-tool-optimization-extension/milestones/openai-compatible-tool-call-boundary-hardening.md), [OpenAI-compatible Raw Tunnel 기반](../../../archive/phase/routing-policy-model-orchestration/milestones/openai-compatible-raw-tunnel-sideband-passthrough.md)
- 후속 작업: 단계 호출과 검증 최적화 MVP, Tool Call 판정 모델 Gate 리뷰 - 후속 작업: 단계 호출과 검증 최적화 MVP, Tool Call 판정 모델 Gate 리뷰
- 확인 필요: 없음 - 확인 필요: 없음

View file

@ -16,18 +16,22 @@ IOP의 OpenAI-compatible, A2A, IOP native 입력 표면에서 들어온 요청
완료, 검토중, 진행중, 계획, 스케치 순서로 두어 아래로 갈수록 미래 작업에 가까워지게 정렬한다. 완료, 검토중, 진행중, 계획, 스케치 순서로 두어 아래로 갈수록 미래 작업에 가까워지게 정렬한다.
스케치 Milestone은 아직 구현 가능한 계획이 아니므로 계획 Milestone보다 아래에 둔다. 스케치 Milestone은 아직 구현 가능한 계획이 아니므로 계획 Milestone보다 아래에 둔다.
- [완료] OpenAI-compatible Raw Tunnel과 Sideband Passthrough - [완료] OpenAI-compatible Raw Tunnel 기반
- 경로: [openai-compatible-raw-tunnel-sideband-passthrough](../../archive/phase/routing-policy-model-orchestration/milestones/openai-compatible-raw-tunnel-sideband-passthrough.md) - 경로: [openai-compatible-raw-tunnel-sideband-passthrough](../../archive/phase/routing-policy-model-orchestration/milestones/openai-compatible-raw-tunnel-sideband-passthrough.md)
- 요약: OpenAI-compatible provider 응답을 기존 Edge-Node proto-socket 위 lossless raw tunnel로 전달하고, 기본값은 provider-original `passthrough`로 두며, 요청이 명시한 경우에만 `passthrough+sideband` 또는 `transformed`를 사용한다. - 요약: OpenAI-compatible provider 응답을 기존 Edge-Node proto-socket 위 lossless raw tunnel로 전달하는 기반을 만들었다. 현재 계약 동기화 작업에서는 이 기반 위에서 public request selector를 제거하고 model/provider capability 기반 passthrough로 정렬한다.
- [완료] Seulgivibe OpenAI-compatible Provider 연동 - [완료] Seulgivibe OpenAI-compatible Provider 연동
- 경로: [seulgivibe-openai-compatible-provider](../../archive/phase/routing-policy-model-orchestration/milestones/seulgivibe-openai-compatible-provider.md) - 경로: [seulgivibe-openai-compatible-provider](../../archive/phase/routing-policy-model-orchestration/milestones/seulgivibe-openai-compatible-provider.md)
- 요약: Seulgivibe Claude/OpenAI 프록시를 OpenAI-compatible provider family로 관리하고, model group에서는 passthrough(+sideband) 계열 provider로 분류한다. 정적 catalog, 요청 시점 provider token forwarding, Codex Responses passthrough는 코드 감사와 spec sync까지 통과해 archive했다. - 요약: Seulgivibe Claude/OpenAI 프록시를 OpenAI-compatible provider family로 관리하고, model group에서는 OpenAI-compatible passthrough provider로 분류한다. 정적 catalog, 요청 시점 provider token forwarding, Codex Responses passthrough는 코드 감사와 spec sync까지 통과해 archive했다.
- [완료] Model Group Mixed Provider Dispatch - [완료] Model Group Mixed Provider Dispatch
- 경로: [model-group-mixed-provider-dispatch](../../archive/phase/routing-policy-model-orchestration/milestones/model-group-mixed-provider-dispatch.md) - 경로: [model-group-mixed-provider-dispatch](../../archive/phase/routing-policy-model-orchestration/milestones/model-group-mixed-provider-dispatch.md)
- 요약: model group provider pool에서 OpenAI-compatible provider와 Ollama/CLI 같은 normalized provider를 같은 후보군으로 두고, 기존 capacity+priority 선택 뒤 OpenAI-compatible 지원 provider는 모두 passthrough, native provider는 normalized 실행 경로로 자동 결정한다. - 요약: model group provider pool에서 OpenAI-compatible provider와 Ollama/CLI 같은 normalized provider를 같은 후보군으로 두고, 기존 capacity+priority 선택 뒤 OpenAI-compatible 지원 provider는 모두 passthrough, native provider는 normalized 실행 경로로 자동 결정한다.
- [계획] OpenAI-compatible Provider Passthrough 계약 동기화
- 경로: [openai-compatible-provider-passthrough-contract-sync](milestones/openai-compatible-provider-passthrough-contract-sync.md)
- 요약: 사용자 설계 의도에 맞춰 `model` 기반 provider capability routing을 source of truth로 두고, OpenAI-compatible provider의 provider-native payload를 Edge allowlist 없이 raw tunnel로 보존하며, metadata는 IOP known-key를 read-only로 발췌하는 container로만 정리한다.
- [스케치] OpenAI-compatible 하이브리드 라우팅과 컨텍스트 최적화 - [스케치] OpenAI-compatible 하이브리드 라우팅과 컨텍스트 최적화
- 경로: [openai-compatible-hybrid-routing-context-optimization](milestones/openai-compatible-hybrid-routing-context-optimization.md) - 경로: [openai-compatible-hybrid-routing-context-optimization](milestones/openai-compatible-hybrid-routing-context-optimization.md)
- 요약: skill/예약어 기반 lane/grade 라우팅을 고신뢰 경로로 유지하고, DiffusionGemma 같은 로컬 모델을 자동 triage, negative guard, cloud context 최적화 보조, 주기적 scoring policy 학습 루프로 선택적으로 사용하는 hybrid local/cloud routing 방향을 스케치한다. - 요약: skill/예약어 기반 lane/grade 라우팅을 고신뢰 경로로 유지하고, DiffusionGemma 같은 로컬 모델을 자동 triage, negative guard, cloud context 최적화 보조, 주기적 scoring policy 학습 루프로 선택적으로 사용하는 hybrid local/cloud routing 방향을 스케치한다.

View file

@ -0,0 +1,98 @@
# Milestone: OpenAI-compatible Provider Passthrough 계약 동기화
## 위치
- Roadmap: [ROADMAP.md](../../../ROADMAP.md)
- Phase: [PHASE.md](../PHASE.md)
## 목표
OpenAI-compatible provider 경로를 request `model`이 가리키는 provider capability 기준으로 결정하고, provider가 지원하는 OpenAI-compatible 표준 payload와 provider-native extension payload를 Edge allowlist 없이 raw tunnel로 전달한다.
`metadata`는 route/response selector가 아니라 IOP가 아는 key를 read-only로 발췌하는 실행/관측 문맥으로 정리하고, caller-facing response mode selector와 provider body sideband 주입 경로를 제거한다.
## 상태
[계획]
## 승격 조건
- 없음
## 구현 잠금
- 상태: 해제
- SDD: 필요
- SDD 문서: [SDD.md](../../../sdd/routing-policy-model-orchestration/openai-compatible-provider-passthrough-contract-sync/SDD.md)
- SDD 사유: OpenAI-compatible API 계약, Edge routing, provider tunnel body, dev-corp field smoke에 영향을 주는 Milestone이다.
- 잠금 해제 조건:
- [x] SDD 잠금이 해제되어 있다.
- [x] SDD 사용자 리뷰가 없거나 승인/해결되었다.
- [x] Acceptance Scenario가 Milestone 기능 Task와 연결되어 있다.
- [x] Evidence Map이 완료 시 Roadmap Completion과 최종 검증 evidence로 검증 가능하게 연결되어 있다.
- 결정 필요: 없음
## 범위
- Edge OpenAI-compatible Chat Completions와 Responses provider-pool passthrough request/response 경계
- direct legacy OpenAI-compatible provider route의 public response selector 제거와 model/provider capability 기준 정렬
- `metadata` known-key 발췌 책임 정리: workspace, task, principal, usage/log/observability
- provider-native payload preservation: `chat_template_kwargs`, 새 OpenAI-compatible field, provider-specific extension field와 중첩 값
- Node provider tunnel request body와 Edge-Node tunnel metadata의 selector 제거
- dev-corp Spark Ornith/vLLM 계열 field passthrough smoke
## 기능
### Epic: [route-contract] Model-driven Routing Contract
OpenAI-compatible 요청의 경로 선택을 caller metadata가 아니라 `model`이 가리키는 provider capability로만 결정하도록 계약과 코드를 동기화한다.
- [ ] [selector-remove] caller-facing response mode selector 파싱, 분기, header/envelope, 테스트 fixture를 제거한다. 내부 metric/log label이 필요하면 caller 입력과 연결되지 않는 관측명으로만 남긴다. 검증: archive를 제외한 코드/계약/문서 검색에서 public selector field와 selector mode 설명이 남지 않는다.
- [ ] [model-route] Chat Completions와 Responses에서 `model` -> route/provider capability -> passthrough 또는 normalized path가 결정된다. 검증: OpenAI-compatible provider는 raw tunnel, CLI/Ollama/native provider는 normalized path를 타는 Edge unit test가 통과한다.
- [ ] [metadata-known] `metadata`는 route selector가 아니라 IOP known-key를 read-only로 발췌하는 container로만 처리된다. 검증: workspace/task/principal/usage/log key는 내부 문맥으로 복사되고, 원본 `metadata` payload와 임의 metadata key는 provider passthrough body에서 제거/변형되지 않으며 route/response path를 바꾸지 않는 테스트가 통과한다.
### Epic: [provider-pass] Provider-native Field Passthrough
vLLM, vLLM-MLX, Lemonade, SGLang, Seulgivibe 같은 OpenAI-compatible provider가 지원하는 request surface를 Edge가 자체 allowlist로 제한하지 않게 한다.
- [ ] [field-preserve] provider-pool과 direct OpenAI-compatible provider tunnel body는 `model` served target rewrite와 auth/header 처리 외에 provider-native request payload를 보존한다. 검증: `chat_template_kwargs`, 새 임의 provider field, tools/stream_options/store와 중첩 값 fixture가 provider request body에 그대로 남는 Edge/Node 테스트가 통과한다.
- [ ] [provider-error] provider가 모르는 field는 Edge가 선판단하지 않고 provider HTTP status/body로 relay한다. 검증: fake provider가 extension field를 거부하는 fixture에서 Edge가 provider error를 변환 없이 전달한다.
- [ ] [policy-priority] catalog generation policy와 IOP 내부 기본값 처리는 caller가 명시한 provider-native thinking field를 삭제하거나 대체하지 않는다. 검증: `chat_template_kwargs.enable_thinking=false`가 있는 요청에서 `think`/budget 주입 또는 rewrite가 provider-native 값을 덮지 않는다.
### Epic: [verification] Verification and Deployment Evidence
계약, 테스트, dev-corp smoke가 같은 설계 의도를 증명하도록 완료 evidence를 남긴다.
- [ ] [contract-sync] agent-contract, inner wire contract, README/docs, roadmap/SDD 포인터가 model-driven passthrough와 metadata known-key 발췌 기준으로 정리된다. 검증: `rg`로 public selector 필드/모드 설명이 archive를 제외한 최신 문서에 남지 않는다.
- [ ] [go-tests] Edge OpenAI handler, service provider-pool tunnel, Node OpenAI-compatible adapter 관련 테스트가 새 계약 기준으로 통과한다. 검증: `go test ./apps/edge/internal/openai ./apps/edge/internal/service ./apps/node/internal/adapters/openai_compat` 또는 동등 범위가 통과한다.
- [ ] [devcorp-smoke] dev-corp IOP 경유 Ornith Spark 요청에서 `chat_template_kwargs.enable_thinking=false`가 provider까지 전달된다. 검증: direct Spark와 IOP 경유 단일 호출 모두 첫 content가 빠르게 오고 reasoning 생성이 없거나 provider-native think-off 결과와 일치한다.
## 완료 리뷰
- 상태: 없음
- 요청일: 없음
- 완료 근거: 기능 Task와 검증이 아직 충족되지 않았다.
- 검토 항목:
- [ ] 모든 기능 Task와 Task별 검증 evidence가 `Roadmap Completion`에 남아 있다.
- [ ] SDD Evidence Map이 최종 검증 evidence와 일치한다.
- [ ] dev-corp smoke 결과가 문서화되어 있다.
- agent-ui 상태 반영: 해당 없음
- 리뷰 코멘트: 없음
## 범위 제외
- 새 route scorer, hybrid local/cloud routing policy, score learning loop 구현
- provider runtime launch/restart option 변경
- output validation filter의 schema/repair policy 구현
- archive 문서의 과거 결정 기록 재작성
- billing/chargeback, 조직 IAM, 장기 retention 정책
## 작업 컨텍스트
- 관련 경로: `apps/edge/internal/openai`, `apps/edge/internal/service`, `apps/node/internal/adapters/openai_compat`, `agent-contract/outer/openai-compatible-api.md`, `agent-contract/inner/edge-node-runtime-wire.md`, `agent-test/dev-corp`
- 표준선(선택): OpenAI-compatible provider 경로는 provider-compatible proxy처럼 동작하고, Edge는 provider field를 자체 구현/해석하지 않고 provider가 판단하도록 전달한다.
- 표준선(선택): `model`이 1차 route source of truth이며, selected provider capability가 passthrough와 normalized path를 결정한다.
- 표준선(선택): `metadata`는 workspace/task/principal/usage/log 같은 IOP known-key를 read-only로 발췌하는 container이며 response mode selector가 아니다.
- 표준선(선택): IOP 확장 field는 OpenAI-compatible 기본 surface 위의 보완 surface이고 provider-native field를 대체하지 않는다.
- 선행 작업: 기존 raw tunnel과 model group mixed dispatch 구현
- 후속 작업: OpenAI-compatible hybrid routing/context optimization, output validation filters
- 확인 필요: 없음

View file

@ -72,8 +72,8 @@
- 반복루프 guard 같은 optional online filter가 비활성화되면 해당 filter만 skip하고 pure passthrough 또는 남은 filter path로 진행한다. - 반복루프 guard 같은 optional online filter가 비활성화되면 해당 filter만 skip하고 pure passthrough 또는 남은 filter path로 진행한다.
- `metadata.scheme`처럼 caller가 필수 출력 계약을 요청한 filter가 비활성화되면 silent passthrough로 낮추지 않고 `policy_rejected`로 종료한다. - `metadata.scheme`처럼 caller가 필수 출력 계약을 요청한 filter가 비활성화되면 silent passthrough로 낮추지 않고 `policy_rejected`로 종료한다.
- 내부 path 주의: - 내부 path 주의:
- `passthrough_guarded``contract_schema`는 caller가 임의로 지정하는 `metadata.iop_response_mode` 값이 아니라 IOP 내부 실행/로그 path 이름이다. - `passthrough_guarded``contract_schema`는 caller가 지정하는 공개 request field가 아니라 IOP 내부 실행/로그 path 이름이다.
- caller가 지정할 수 있는 공개 response mode 값은 [계약 원문](../../../../agent-contract/outer/openai-compatible-api.md)에서 별도로 명시한 값만 허용한다. - 공개 OpenAI-compatible 경로 선택은 [계약 원문](../../../../agent-contract/outer/openai-compatible-api.md)에 따라 request `model`이 가리키는 provider capability로 결정한다.
- 금지: - 금지:
- `metadata.scheme``iop_output_contract` 같은 wrapper로 감싸지 않는다. - `metadata.scheme``iop_output_contract` 같은 wrapper로 감싸지 않는다.
- schema 계약 요청을 normalized path로 보내지 않는다. - schema 계약 요청을 normalized path로 보내지 않는다.

View file

@ -0,0 +1,120 @@
# SDD: OpenAI-compatible Provider Passthrough 계약 동기화
## 위치
- Milestone: [Milestone 문서](../../../phase/routing-policy-model-orchestration/milestones/openai-compatible-provider-passthrough-contract-sync.md)
- Phase: [PHASE.md](../../../phase/routing-policy-model-orchestration/PHASE.md)
## 상태
[승인됨]
## SDD 잠금
- 상태: 해제
- 사용자 리뷰: 없음
- 잠금 항목:
- 없음
## 문제 / 비목표
- 문제: 현재 코드에는 caller metadata로 응답 경로를 선택하는 과거 설계와 provider-native OpenAI-compatible payload를 Edge가 제한하거나 IOP 확장 field로 치환하는 흐름이 남아 있다. 사용자 설계 의도는 `model`이 가리키는 provider capability가 passthrough/normalized 경로를 결정하고, OpenAI-compatible provider에서는 provider request surface를 그대로 보존하는 것이다.
- 비목표:
- 새 route scorer 또는 hybrid local/cloud routing policy 구현
- provider runtime launch/restart option 변경
- output validation filter의 schema/repair policy 구현
- archive 문서의 과거 결정 기록 재작성
## Source of Truth
| 영역 | 기준 | 메모 |
|------|------|------|
| Roadmap | [Milestone 문서](../../../phase/routing-policy-model-orchestration/milestones/openai-compatible-provider-passthrough-contract-sync.md) | 목표, 범위, 기능 Task, 완료 evidence 기준 |
| Outer Contract | [openai-compatible-api.md](../../../../agent-contract/outer/openai-compatible-api.md) | OpenAI-compatible public request/response 계약 |
| Inner Wire Contract | [edge-node-runtime-wire.md](../../../../agent-contract/inner/edge-node-runtime-wire.md) | Edge-Node provider tunnel body/frame 책임 |
| Code | `apps/edge/internal/openai`, `apps/edge/internal/service`, `apps/node/internal/adapters/openai_compat` | 구현 source of truth |
| External Provider | dev-corp Spark Ornith/vLLM provider | provider-native field passthrough smoke 대상 |
| User Decision | 현재 사용자 설계 의도 | `model` 기반 route, metadata known-key read-only 발췌, provider-native payload 보존 |
## State Machine
| 상태 | 진입 조건 | 다음 상태 | 근거 |
|------|-----------|-----------|------|
| request-received | Edge OpenAI-compatible endpoint가 JSON body를 수신한다 | route-resolved 또는 invalid-request | `model` envelope parse |
| route-resolved | request `model`이 route catalog 또는 provider-pool model group에 매칭된다 | metadata-observed | selected provider capability |
| metadata-observed | route 결정 후 metadata가 있으면 known-key를 내부 문맥으로 복사한다 | provider-passthrough 또는 normalized-dispatch | read-only extraction, route/response path 불변 |
| provider-passthrough | selected provider가 OpenAI-compatible 호출 방식을 지원한다 | provider-response-relayed 또는 provider-error-relayed | ProviderTunnelRequest/ProviderTunnelFrame |
| normalized-dispatch | selected provider/backend가 normalized adapter 실행을 요구한다 | normalized-response-built 또는 run-error | RunRequest/RunEvent |
| provider-response-relayed | provider가 HTTP/SSE 성공 응답을 반환한다 | terminal success | provider status/header/body relay |
| provider-error-relayed | provider가 HTTP error 또는 tunnel error를 반환한다 | terminal error | provider status/body 또는 tunnel error |
## Interface Contract
- 계약 원문: [openai-compatible-api.md](../../../../agent-contract/outer/openai-compatible-api.md)
- 입력:
- `model`: route/provider capability 선택의 1차 source of truth
- `messages` 또는 `input`: endpoint별 OpenAI-compatible request payload
- `metadata`: IOP known-key를 read-only로 발췌하는 container. route/response selector가 아니다.
- provider-native payload: selected provider가 지원하는 OpenAI-compatible 표준 field, extension field, 중첩 값. Edge passthrough 경로에서 보존한다.
- 출력:
- provider-passthrough: provider HTTP status/header/body를 relay한다. Chat Completions의 top-level model echo는 alias 보정을 할 수 있다.
- normalized-dispatch: normalized adapter 결과를 OpenAI-compatible response shape로 구성한다.
- metrics/log: provider usage 후보와 IOP known-key 관측은 internal metric/log로 남긴다.
- 금지:
- caller metadata로 passthrough/normalized/response shape를 선택하게 하지 않는다.
- OpenAI-compatible provider passthrough body에 IOP marker/event/envelope를 주입하지 않는다.
- Edge allowlist로 provider-native OpenAI-compatible extension field를 거부하지 않는다.
- IOP 확장 field가 caller의 provider-native field를 삭제하거나 대체하지 않는다.
- metadata known-key 발췌 과정에서 원본 provider payload를 strip/mutate하지 않는다.
## Acceptance Scenarios
| ID | Milestone Task | Given | When | Then |
|----|----------------|-------|------|------|
| S01 | `selector-remove` | caller가 metadata 안에 임의 response selector로 보일 수 있는 key를 넣는다 | Edge가 OpenAI-compatible request를 처리한다 | 해당 key는 route selector로 해석되지 않고, provider-passthrough body를 바꾸지 않는다 |
| S02 | `model-route` | request `model`이 OpenAI-compatible provider를 가리킨다 | Chat Completions 또는 Responses를 호출한다 | Edge는 provider tunnel passthrough를 사용한다 |
| S03 | `model-route` | request `model`이 CLI/Ollama/native normalized backend를 가리킨다 | Chat Completions 또는 Responses를 호출한다 | Edge는 normalized RunRequest path를 사용한다 |
| S04 | `metadata-known` | request metadata에 workspace/task/principal 관련 known key와 임의 key가 섞여 있다 | Edge가 route를 결정한 뒤 metadata를 처리한다 | known key는 내부 문맥으로 복사되고 원본 metadata payload는 제거/변형되지 않으며 임의 key는 route/response path를 바꾸지 않는다 |
| S05 | `field-preserve` | provider-pool 또는 direct OpenAI-compatible Chat request가 `chat_template_kwargs`와 임의 provider extension field를 포함한다 | Edge가 provider tunnel body를 만든다 | provider request body에는 `model` rewrite 외 field와 중첩 값이 보존된다 |
| S06 | `provider-error` | provider가 특정 extension field를 지원하지 않아 HTTP error를 반환한다 | Edge가 provider response를 relay한다 | caller는 provider status/body를 받으며 Edge가 자체 unsupported error로 선변환하지 않는다 |
| S07 | `policy-priority` | caller가 provider-native thinking field를 명시한다 | catalog generation policy 또는 IOP think 확장 처리가 실행된다 | caller provider-native field가 삭제되거나 대체되지 않는다 |
| S08 | `contract-sync` | 구현이 완료된다 | 계약/문서/로드맵 최신 문서를 검색한다 | archive를 제외한 최신 문서에는 public response selector field/모드 설명이 없다 |
| S09 | `go-tests` | 코드 변경이 완료된다 | 관련 Go 테스트를 실행한다 | Edge OpenAI handler, service provider-pool, Node OpenAI adapter 테스트가 통과한다 |
| S10 | `devcorp-smoke` | dev-corp IOP가 Spark Ornith provider를 라우팅한다 | `chat_template_kwargs.enable_thinking=false` 요청을 IOP 경유로 1회 호출한다 | provider-native think-off 동작과 일치하는 빠른 content-first 응답을 관찰한다 |
## Evidence Map
| Scenario | Required Evidence | `agent-task` 연결 | 완료 Evidence 기대 |
|----------|-------------------|------------------|---------------------------|
| S01 | selector metadata negative test, request body fixture | `agent-task/m-openai-compatible-provider-passthrough-contract-sync/...` | `Roadmap Completion``selector-remove`와 test name |
| S02 | OpenAI-compatible provider route unit test | `agent-task/m-openai-compatible-provider-passthrough-contract-sync/...` | `Roadmap Completion``model-route`와 provider tunnel dispatch evidence |
| S03 | normalized backend route unit test | `agent-task/m-openai-compatible-provider-passthrough-contract-sync/...` | `Roadmap Completion``model-route`와 normalized dispatch evidence |
| S04 | metadata known-key parsing and body immutability test | `agent-task/m-openai-compatible-provider-passthrough-contract-sync/...` | `Roadmap Completion``metadata-known`, route-stability, body immutability evidence |
| S05 | provider tunnel body preservation test | `agent-task/m-openai-compatible-provider-passthrough-contract-sync/...` | `Roadmap Completion``field-preserve`와 captured body fixture |
| S06 | fake provider error relay test | `agent-task/m-openai-compatible-provider-passthrough-contract-sync/...` | `Roadmap Completion``provider-error`와 status/body relay fixture |
| S07 | generation policy/provider-native priority test | `agent-task/m-openai-compatible-provider-passthrough-contract-sync/...` | `Roadmap Completion``policy-priority`와 body diff evidence |
| S08 | `rg` evidence over non-archive docs/contracts/roadmap | `agent-task/m-openai-compatible-provider-passthrough-contract-sync/...` | `Roadmap Completion``contract-sync`와 command output summary |
| S09 | Go test command output | `agent-task/m-openai-compatible-provider-passthrough-contract-sync/...` | `Roadmap Completion``go-tests`와 pass summary |
| S10 | dev-corp single-call smoke output summary | `agent-task/m-openai-compatible-provider-passthrough-contract-sync/...` | `Roadmap Completion``devcorp-smoke`, first content latency, reasoning/content observation |
## Cross-repo Dependencies
- 없음
## Drift Check
- [x] Milestone 기능 Task와 Acceptance Scenario가 일치한다.
- [x] Evidence Map이 code-review/complete.log에서 검증 가능하다.
- [x] agent-contract를 쓰는 경우 SDD에 계약 원문을 복제하지 않았다.
- [x] 사용자 리뷰가 필요한 항목은 `USER_REVIEW.md`에만 남겼다.
## 사용자 리뷰 이력
- 없음
## 작업 컨텍스트
- 표준선: OpenAI-compatible provider 경로는 provider-compatible proxy처럼 동작하고, provider-native field 판단은 provider에 맡긴다.
- 표준선: `model`이 route source of truth이며, metadata는 IOP known-key를 read-only로 발췌하는 container다.
- 표준선: provider tunnel body는 `model` served target rewrite와 auth/header 처리 외에는 caller/provider request surface와 중첩 값을 보존한다.
- 후속 SDD: 없음

View file

@ -110,11 +110,11 @@ Mac Studio의 `http://192.168.2.3:8005/v1` mlx-vlm DiffusionGemma endpoint는
- Active Edge runtime is `iop.ai.kr` (`115.21.224.82`) with public `18085` bootstrap, `18086` OpenAI-compatible, and `18087` Edge-Node TCP. - Active Edge runtime is `iop.ai.kr` (`115.21.224.82`) with public `18085` bootstrap, `18086` OpenAI-compatible, and `18087` Edge-Node TCP.
- mac-mini checkout/runtime is a runner and must not be used as Edge source of truth unless explicitly doing a local-runner comparison. - mac-mini checkout/runtime is a runner and must not be used as Edge source of truth unless explicitly doing a local-runner comparison.
- On 2026-07-09 the three provider nodes were moved from mac-mini/reverse-tunnel Edge addresses to `iop.ai.kr:18087`; the public `/v1/chat/completions` aggregate 15-concurrency smoke passed 15/15 and is retained as historical routing evidence. - On 2026-07-09 the three provider nodes were moved from mac-mini/reverse-tunnel Edge addresses to `iop.ai.kr:18087`; the public `/v1/chat/completions` aggregate 15-concurrency smoke passed 15/15 and is retained as historical routing evidence.
- On 2026-07-09 after rebuilding the public Edge from the current passthrough source, default streaming `/v1/chat/completions` preserved provider reasoning deltas for 15/15 historical aggregate concurrent requests; transformed `chatcmpl-manual` IDs were 0/15. Evidence: `build/dev-corp-runtime/logs/public_passthrough_chat_15_20260709_180115.json`. - On 2026-07-09 after rebuilding the public Edge from the current passthrough source, default streaming `/v1/chat/completions` preserved provider reasoning deltas for 15/15 historical aggregate concurrent requests; manual IOP-generated `chatcmpl-manual` IDs were 0/15. Evidence: `build/dev-corp-runtime/logs/public_passthrough_chat_15_20260709_180115.json`.
- On 2026-07-09 after deploying latest node binaries to DGX Spark 01/02 and Mac Studio, public streaming `/v1/chat/completions` again passed 15/15 historical aggregate concurrency with provider reasoning deltas preserved and transformed `chatcmpl-manual` IDs 0/15. Evidence: `build/dev-corp-runtime/logs/public_latest_edge_node_chat_15_20260709_181222.json`. - On 2026-07-09 after deploying latest node binaries to DGX Spark 01/02 and Mac Studio, public streaming `/v1/chat/completions` again passed 15/15 historical aggregate concurrency with provider reasoning deltas preserved and manual IOP-generated `chatcmpl-manual` IDs 0/15. Evidence: `build/dev-corp-runtime/logs/public_latest_edge_node_chat_15_20260709_181222.json`.
- Mac Studio provider catalog `type`은 최신 config validator 기준으로 `openai_compat`를 사용한다. 실제 provider runtime은 vLLM-MLX이고 `runtime_type: vllm-mlx`로 추적한다. - Mac Studio provider catalog `type`은 최신 config validator 기준으로 `openai_compat`를 사용한다. 실제 provider runtime은 vLLM-MLX이고 `runtime_type: vllm-mlx`로 추적한다.
- `/v1/chat/completions` capacity smoke 표준: public `https://digitalplatform.iop.ai.kr/v1`에서 `ornith:35b`는 9개/6개 동시 요청, `gemma4:26b`는 9개/6개 동시 요청을 확인한다. Direct Edge listener `http://digitalplatform.iop.ai.kr:18086/v1`도 같은 backend로 동작하지만 사용자-facing 기본값으로 쓰지 않는다. 2026-07-13 live smoke에서는 네 시나리오 모두 성공했고 완료 후 provider `in_flight=0`, `queued=0`, `healthy`로 회복했다. - `/v1/chat/completions` capacity smoke 표준: public `https://digitalplatform.iop.ai.kr/v1`에서 `ornith:35b`는 9개/6개 동시 요청, `gemma4:26b`는 9개/6개 동시 요청을 확인한다. Direct Edge listener `http://digitalplatform.iop.ai.kr:18086/v1`도 같은 backend로 동작하지만 사용자-facing 기본값으로 쓰지 않는다. 2026-07-13 live smoke에서는 네 시나리오 모두 성공했고 완료 후 provider `in_flight=0`, `queued=0`, `healthy`로 회복했다.
- `/v1/responses`는 OpenAI-compatible provider model group raw passthrough parity 전까지 미지원이므로 provider-pool capacity 성공 기준에서 제외한다. - Provider-pool passthrough 계약은 selected provider가 지원하는 OpenAI-compatible 표준 field와 provider extension field를 보존한다. `/v1/responses`는 capacity 성공 기준이 아니라 provider-dependent passthrough/relay 증거로 분리한다. Provider가 지원하면 raw passthrough 성공을 확인하고, provider가 미지원하면 provider status/body가 IOP 변환 없이 relay되는지 확인한다.
## 명령 ## 명령
@ -131,7 +131,7 @@ Mac Studio의 `http://192.168.2.3:8005/v1` mlx-vlm DiffusionGemma endpoint는
- 변경한 edge 패키지 또는 `go test ./apps/edge/...`를 실행한다. - 변경한 edge 패키지 또는 `go test ./apps/edge/...`를 실행한다.
- registry, service, transport, console, HTTP/A2A 입력 표면을 바꾼 경우 edge-node 메시지 2회 왕복과 command 응답을 확인한다. - registry, service, transport, console, HTTP/A2A 입력 표면을 바꾼 경우 edge-node 메시지 2회 왕복과 command 응답을 확인한다.
- OpenAI-compatible 경계를 바꾼 경우 dev-corp `18086` 기준 `iop-edge smoke openai` 또는 동등한 `/healthz`, `/v1/models`, `/v1/chat/completions` 확인으로 edge service와 node adapter 경로 수렴을 확인한다. provider-pool `/v1/responses` 미지원은 실패로 보지 않는다. - OpenAI-compatible 경계를 바꾼 경우 dev-corp `18086` 기준 `iop-edge smoke openai` 또는 동등한 `/healthz`, `/v1/models`, `/v1/chat/completions` 확인으로 edge service와 node adapter 경로 수렴을 확인한다. Provider-pool `/v1/responses`는 selected provider 지원 여부와 relay 동작을 분리해 판정한다.
- provider pool config 변경 전 mac-mini에서 세 기본 provider endpoint의 `/health``/v1/models`가 성공하는지 확인한다. - provider pool config 변경 전 mac-mini에서 세 기본 provider endpoint의 `/health``/v1/models`가 성공하는지 확인한다.
- DGX Spark 02가 provider port down과 SSH banner exchange timeout을 동시에 보이면 provider runtime 추가 조작을 보류하고, SSH 회복 후 process/log, `/health`, `/v1/models`, Edge OpenAI-compatible smoke를 순서대로 재검증한다. - DGX Spark 02가 provider port down과 SSH banner exchange timeout을 동시에 보이면 provider runtime 추가 조작을 보류하고, SSH 회복 후 process/log, `/health`, `/v1/models`, Edge OpenAI-compatible smoke를 순서대로 재검증한다.
- bootstrap/artifact 경계를 바꾼 경우 dev-corp artifact/base URL 후보 `18085`가 local/test/dev field baseline을 덮어쓰지 않는지 확인한다. - bootstrap/artifact 경계를 바꾼 경우 dev-corp artifact/base URL 후보 `18085`가 local/test/dev field baseline을 덮어쓰지 않는지 확인한다.
@ -146,8 +146,8 @@ Mac Studio의 `http://192.168.2.3:8005/v1` mlx-vlm DiffusionGemma endpoint는
- node 등록, `/nodes`, console 메시지 전송, 기대 payload를 포함한 `[node-*-message]` 출력, 같은 run의 complete event가 확인된다. - node 등록, `/nodes`, console 메시지 전송, 기대 payload를 포함한 `[node-*-message]` 출력, 같은 run의 complete event가 확인된다.
- node 로컬 `[node-message]` payload 라인 목록과 edge `[node-*-message]` payload 라인 목록이 run별로 내용/순서까지 동일해야 한다. - node 로컬 `[node-message]` payload 라인 목록과 edge `[node-*-message]` payload 라인 목록이 run별로 내용/순서까지 동일해야 한다.
- edge complete는 같은 run의 마지막 `[node-*-message]` 이후에만 정상이다. - edge complete는 같은 run의 마지막 `[node-*-message]` 이후에만 정상이다.
- OpenAI-compatible smoke에서 `/healthz`, `/v1/models`, `/v1/chat/completions`가 기대 상태로 응답한다. provider-pool `/v1/responses`는 현재 미지원이다. - OpenAI-compatible smoke에서 `/healthz`, `/v1/models`, `/v1/chat/completions`가 기대 상태로 응답한다. Provider-pool passthrough 경계 변경 시 provider-native field 예: `chat_template_kwargs`가 IOP에서 거부되지 않고 selected provider로 전달되는지 확인한다.
- dev-corp provider-pool capacity smoke는 현재 `/v1/chat/completions`에 model-specific 동시 요청을 보낸다. 표준 시나리오는 `ornith:35b` 9/6, `gemma4:26b` 9/6이다. `/v1/responses`OpenAI-compatible provider model group raw passthrough parity 전까지 미지원이므로 capacity 성공 기준에서 제외한다. - dev-corp provider-pool capacity smoke는 현재 `/v1/chat/completions`에 model-specific 동시 요청을 보낸다. 표준 시나리오는 `ornith:35b` 9/6, `gemma4:26b` 9/6이다. `/v1/responses`capacity 성공 기준이 아니라 provider-dependent passthrough/relay 검증으로 분리한다.
- capacity smoke 완료 후 대상 provider의 `in_flight=0`, `queued=0` 회복을 확인한다. - capacity smoke 완료 후 대상 provider의 `in_flight=0`, `queued=0` 회복을 확인한다.
- Gemma 계열 provider-pool smoke는 thinking enabled 기준이며 reasoning/tool-parser 관련 텍스트가 포함될 수 있다. exact-output match를 기본 판정으로 쓰지 않는다. - Gemma 계열 provider-pool smoke는 thinking enabled 기준이며 reasoning/tool-parser 관련 텍스트가 포함될 수 있다. exact-output match를 기본 판정으로 쓰지 않는다.

View file

@ -121,7 +121,7 @@ edge:
concurrent_requests: 15 concurrent_requests: 15
elapsed_sec: 5.678 elapsed_sec: 5.678
passthrough_reasoning_stream_seen: 15 passthrough_reasoning_stream_seen: 15
transformed_manual_id_count: 0 manual_iop_response_id_count: 0
remote_log: build/dev-corp-runtime/logs/public_latest_edge_node_chat_15_20260709_181222.json remote_log: build/dev-corp-runtime/logs/public_latest_edge_node_chat_15_20260709_181222.json
latest_node_binary_deploy: latest_node_binary_deploy:
observed_at: "2026-07-09T18:12:22+09:00" observed_at: "2026-07-09T18:12:22+09:00"
@ -138,7 +138,7 @@ edge:
corp-mac-studio-mlx-vllm-node: corp-mac-studio-mlx-vllm-node:
pid_after_restart: 87325 pid_after_restart: 87325
binary_sha256: 95c59ee0dacb134dc2c6d46fb34f2dac49c33d7da38abe96157583822364f57a binary_sha256: 95c59ee0dacb134dc2c6d46fb34f2dac49c33d7da38abe96157583822364f57a
note: public /v1/responses remains unsupported for OpenAI-compatible provider model groups until raw passthrough parity is implemented; use /v1/chat/completions for provider-pool capacity smoke. note: provider-pool passthrough contract preserves selected provider OpenAI-compatible fields; use /v1/chat/completions for capacity smoke, and treat /v1/responses as provider-dependent passthrough/relay evidence rather than an IOP-level unsupported route.
latest_bootstrap_domain_fix: latest_bootstrap_domain_fix:
observed_at: "2026-07-11" observed_at: "2026-07-11"
public_host: iop.ai.kr public_host: iop.ai.kr
@ -327,8 +327,8 @@ model:
capacity_smoke: capacity_smoke:
endpoints: endpoints:
- /v1/chat/completions - /v1/chat/completions
unsupported_endpoints: provider_dependent_endpoints:
/v1/responses: OpenAI-compatible provider model group raw passthrough parity 전까지 미지원 /v1/responses: selected provider가 지원하면 raw passthrough로 검증하고, provider가 미지원하면 provider status/body relay 여부를 증거로 남긴다
aggregate_provider_capacity: 13 aggregate_provider_capacity: 13
model_scenarios: model_scenarios:
ornith_9: ornith_9:
@ -445,8 +445,9 @@ model:
max_queued: 1 max_queued: 1
final_recovery: in_flight_0_queued_0_healthy final_recovery: in_flight_0_queued_0_healthy
responses: responses:
supported: false iop_passthrough_contract: true
reason: /v1/responses is not supported for OpenAI-compatible provider model groups until raw passthrough parity is implemented provider_support: provider_dependent
reason: selected provider가 /v1/responses를 지원하면 raw passthrough로 검증하고, provider가 미지원하면 provider error relay를 확인한다
legacy_capacity_verification_2026_07_08: legacy_capacity_verification_2026_07_08:
date: "2026-07-08" date: "2026-07-08"
source_ref: c2437aaedefbac4312d69dfd10aa017c2739e187 source_ref: c2437aaedefbac4312d69dfd10aa017c2739e187

View file

@ -0,0 +1,187 @@
# Mac Studio Ornith Think-On Baseline
측정 목적: Mac Studio Ornith runtime의 `think off` 실험 전 기준 성능을 보존한다.
## Runtime Identity
- 측정 시각: 2026-07-13 17:47:59 KST
- Host: `dc-devui-MacStudio.local`
- Endpoint: `http://127.0.0.1:8007/v1`
- Model alias: `ornith:35b`
- Current runtime path: `ornith35b_alias_proxy.py` on `0.0.0.0:8007` -> `mlx_lm.server` on `127.0.0.1:8008`
- Upstream model path: `/Users/dc_dev/.cache/huggingface/manual/mlx-community/Ornith-1.0-35B-8bit`
- Runtime note: the active `vllm_mlx.cli serve` process on this host is currently Gemma4 on `8004`; this Ornith baseline uses the current Mac Studio Ornith alias path above.
- Thinking mode: current default. Upstream `mlx_lm.server` was started with `--chat-template-args '{"enable_thinking":true}'`; benchmark requests did not pass `think=false`.
- API shape: OpenAI-compatible streaming `/v1/chat/completions`
- Token counting: `Qwen2Tokenizer` over concatenated streaming `reasoning`, `reasoning_content`, and `content` deltas.
## Request Parameters
- `max_tokens`: `256`
- `temperature`: `0.2`
- `top_p`: `0.95`
- Warmup: one short warmup request, excluded from the table.
- Prompt: `Benchmark generation task. Do not use tools. Produce a compact Korean report with exactly 24 numbered lines about reliable agent runtime operations. Keep each line short, but continue until the list is complete.`
## Metric Definitions
- `avg_ttft_s`: average seconds from request start to first non-empty streaming delta. This includes `reasoning` deltas and is not the user-visible content latency.
- `avg_per_call_tok_s`: average per-call decode throughput, computed as output tokens divided by time from first non-empty delta to completion.
- `total_tok_s`: aggregate group throughput, computed as total output tokens divided by wall-clock time from earliest request start to latest completion.
- `group_wall_s`: wall-clock duration for the concurrency group.
## Content-First Baseline
This corrected measurement records when the first non-empty `content` delta appears. It is the user-visible answer latency requested for the think-off comparison.
- 측정 시각: 2026-07-13 17:55:18 KST
- Endpoint: `http://127.0.0.1:8007/v1`
- Model alias: `ornith:35b`
- Thinking mode: current default. Upstream `mlx_lm.server` was started with `--chat-template-args '{"enable_thinking":true}'`; benchmark requests did not pass `think=false`.
- `max_tokens`: `1024`
- `temperature`: `0.2`
- `top_p`: `0.95`
- Prompt: `Benchmark generation task. Produce exactly 8 short numbered Korean lines about reliable agent runtime operations.`
| Concurrent calls | Success | Avg first reasoning/any delta (s) | Avg first content delta (s) | Avg reasoning tokens/call | Avg content tokens/call | Avg all tok/s per call | Avg content tok/s per call | Total all tok/s | Total content tok/s | Group wall (s) | Finish reason |
|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---|
| 1 | 1/1 | 0.227 | 17.830 | 919.0 | 104.0 | 52.29 | 53.00 | 51.69 | 5.25 | 19.792 | length |
| 2 | 2/2 | 0.339 | 22.193 | 978.0 | 45.0 | 44.74 | 44.44 | 88.17 | 3.88 | 23.206 | length |
| 3 | 3/3 | 0.462 | 26.260 | 978.0 | 45.0 | 37.93 | 38.26 | 111.85 | 4.92 | 27.439 | length |
| 4 | 4/4 | 0.598 | 30.638 | 978.0 | 45.0 | 32.55 | 32.34 | 127.74 | 5.62 | 32.035 | length |
| 5 | 5/5 | 0.728 | 38.788 | 978.0 | 45.0 | 25.69 | 25.64 | 126.13 | 5.55 | 40.553 | length |
### Content-First Per-Call Detail
| Concurrent calls | Call | First content delta (s) | Reasoning tokens | Content tokens | All tok/s | Content tok/s | Duration (s) |
|---:|---:|---:|---:|---:|---:|---:|---:|
| 1 | 1 | 17.830 | 919 | 104 | 52.29 | 53.00 | 19.792 |
| 2 | 1 | 22.195 | 978 | 45 | 44.74 | 44.49 | 23.206 |
| 2 | 2 | 22.191 | 978 | 45 | 44.73 | 44.38 | 23.205 |
| 3 | 1 | 26.258 | 978 | 45 | 37.93 | 38.33 | 27.432 |
| 3 | 2 | 26.263 | 978 | 45 | 37.93 | 38.29 | 27.439 |
| 3 | 3 | 26.258 | 978 | 45 | 37.92 | 38.15 | 27.437 |
| 4 | 1 | 30.637 | 978 | 45 | 32.55 | 32.47 | 32.023 |
| 4 | 2 | 30.637 | 978 | 45 | 32.54 | 32.22 | 32.034 |
| 4 | 3 | 30.642 | 978 | 45 | 32.55 | 32.31 | 32.035 |
| 4 | 4 | 30.637 | 978 | 45 | 32.55 | 32.34 | 32.028 |
| 5 | 1 | 38.787 | 978 | 45 | 25.69 | 25.50 | 40.551 |
| 5 | 2 | 38.787 | 978 | 45 | 25.70 | 25.70 | 40.538 |
| 5 | 3 | 38.787 | 978 | 45 | 25.69 | 25.61 | 40.544 |
| 5 | 4 | 38.793 | 978 | 45 | 25.69 | 25.58 | 40.552 |
| 5 | 5 | 38.788 | 978 | 45 | 25.70 | 25.81 | 40.531 |
### Content-First Observations
- The user-visible first content delta was much later than the first reasoning delta.
- At concurrency 1, first reasoning/any delta arrived at `0.227s`, but first content arrived at `17.830s`.
- At concurrency 5, first reasoning/any delta arrived at `0.728s`, but first content arrived at `38.788s`.
- The run consistently spent most of the `1024` token budget on reasoning before content. Most calls produced about `978` reasoning tokens and `45` content tokens before `finish_reason=length`.
- For think-off comparison, `Avg first content delta (s)` is the latency column to compare first.
## Think-Off Result
### Compatibility Note
The alias proxy currently maps request-level `think:false` to upstream `chat_template_args.enable_thinking=false`. The installed `mlx_lm.server` request parser reads `chat_template_kwargs`, not request-level `chat_template_args` (`mlx_lm/server.py` request parse path stores `self.chat_template_kwargs = self.body.get("chat_template_kwargs")`). Because of that mismatch, plain `think:false` did not produce a real think-off result in this runtime path.
Observed `think:false` compatibility probe:
| Mode | First content delta (s) | Reasoning chars | Content chars | Finish reason | Interpretation |
|---|---:|---:|---:|---|---|
| `think:false` via alias proxy | 17.975 | 3300 | 203 | length | Not effective; still generated reasoning first |
The effective think-off measurement below used request `chat_template_kwargs: {"enable_thinking": false}`.
### Effective Think-Off Content-First Table
- 측정 시각: 2026-07-13 18:05:09 KST
- Endpoint: `http://127.0.0.1:8007/v1`
- Model alias: `ornith:35b`
- Thinking mode: effective request-level think-off via `chat_template_kwargs.enable_thinking=false`
- `max_tokens`: `1024`
- `temperature`: `0.2`
- `top_p`: `0.95`
- Prompt: same as the Content-First Baseline.
| Concurrent calls | Success | Avg first content delta (s) | Avg reasoning tokens/call | Avg content tokens/call | Avg content tok/s per call | Total content tok/s | Group wall (s) | Finish reason |
|---:|---:|---:|---:|---:|---:|---:|---:|---|
| 1 | 1/1 | 0.208 | 0.0 | 104.0 | 52.77 | 47.74 | 2.179 | stop |
| 2 | 2/2 | 0.333 | 0.0 | 113.0 | 44.81 | 79.15 | 2.855 | stop |
| 3 | 3/3 | 0.457 | 0.0 | 113.0 | 37.79 | 98.27 | 3.450 | stop |
| 4 | 4/4 | 0.580 | 0.0 | 113.0 | 32.47 | 111.20 | 4.065 | stop |
| 5 | 5/5 | 0.703 | 0.0 | 113.0 | 25.82 | 111.03 | 5.089 | stop |
### Think-On vs Effective Think-Off
| Concurrent calls | Think-on first content (s) | Effective think-off first content (s) | Delta (s) | Speedup |
|---:|---:|---:|---:|---:|
| 1 | 17.830 | 0.208 | 17.622 | 85.9x |
| 2 | 22.193 | 0.333 | 21.860 | 66.7x |
| 3 | 26.260 | 0.457 | 25.803 | 57.5x |
| 4 | 30.638 | 0.580 | 30.058 | 52.9x |
| 5 | 38.788 | 0.703 | 38.085 | 55.2x |
### Effective Think-Off Observations
- Effective think-off eliminated reasoning output in this benchmark (`0` reasoning tokens for all calls).
- First user-visible content became the first non-empty stream delta.
- Total content throughput peaked around concurrency 4-5 at about `111 tok/s`.
- The largest improvement is first content latency, not single-stream decode tok/s.
- To make `think:false` work through the alias endpoint, update the proxy to map `think` to `chat_template_kwargs.enable_thinking`, not only `chat_template_args.enable_thinking`.
## First-Content Optimization Candidates
| Priority | Candidate | Expected effect | Risk / note |
|---:|---|---|---|
| 1 | Fix alias proxy `think` mapping to upstream `chat_template_kwargs.enable_thinking` | Makes simple `think:false` clients get the measured `0.2-0.7s` first-content path | Very small code change, but verify `/v1/chat/completions` streaming and existing `chat_template_kwargs` passthrough |
| 2 | Start a separate always-non-thinking Ornith endpoint with `mlx_lm.server --chat-template-args '{"enable_thinking":false}'` | Makes non-thinking the default without per-request kwargs | Keep on a separate port first; quality/tool-call behavior may differ |
| 3 | Use `vllm_mlx` Ornith path if stable, with `--default-chat-template-kwargs '{"enable_thinking": false}'` or request `enable_thinking=false` | Better OpenAI-compatible semantics, continuous batching, prefix cache, warm prompts, and request-level thinking controls | This changes runtime stack from current `mlx_lm.server`; test as a parallel endpoint before replacing `8007` |
| 4 | If reasoning must stay on, enforce a small thinking budget where supported | Bounds reasoning delay before content instead of fully disabling reasoning | Current `mlx_lm.server` path has no observed thinking budget control; `vllm_mlx`/vLLM-style serving has this concept |
| 5 | Warm repeated prompt prefixes / prompt cache | Reduces prefill/TTFT for repeated system prompts and agent templates | Helps prefill, not the long reasoning-before-content delay. Think-off gives the bigger win here |
| 6 | Tune `decode-concurrency`, `prompt-concurrency`, and `prefill-step-size` on `mlx_lm.server` | May improve batch scheduling and prefill behavior | Current content-first bottleneck was reasoning generation. Tune only after think-off route is correct |
References checked:
- Local `mlx_lm.server --help`: supports `--chat-template-args`, `--decode-concurrency`, `--prompt-concurrency`, `--prefill-step-size`, `--prompt-cache-size`, `--prompt-cache-bytes`, and draft-model options.
- Local installed `mlx_lm/server.py`: request body uses `chat_template_kwargs`; startup CLI uses `chat_template_args`.
- Qwen docs: previous Qwen3-style hybrid thinking can be controlled by `enable_thinking=False`; newer Qwen3 Instruct/Thinking variants may be mode-specific.
- vLLM docs: request-level `chat_template_kwargs` can override server defaults, and thinking budgets can bound reasoning tokens.
- `vllm-mlx` docs: continuous batching, prefix cache, SSD-tiered cache, and warm prompts are intended Apple Silicon serving optimizations.
## Reasoning-Inclusive First-Delta Table
| Concurrent calls | Success | Avg output tokens/call | Total output tokens | Avg TTFT (s) | Avg duration (s) | Avg decode duration (s) | Avg tok/s per call | Total tok/s | Group wall (s) | Finish reason |
|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---|
| 1 | 1/1 | 256.0 | 256 | 0.221 | 5.085 | 4.864 | 52.63 | 50.34 | 5.085 | length |
| 2 | 2/2 | 256.0 | 512 | 0.340 | 6.072 | 5.732 | 44.66 | 84.31 | 6.073 | length |
| 3 | 3/3 | 256.0 | 768 | 0.462 | 7.213 | 6.751 | 37.92 | 106.42 | 7.217 | length |
| 4 | 4/4 | 256.0 | 1024 | 0.570 | 8.445 | 7.875 | 32.51 | 121.17 | 8.451 | length |
| 5 | 5/5 | 256.0 | 1280 | 0.698 | 10.639 | 9.940 | 25.75 | 120.20 | 10.649 | length |
## Per-Call Detail
| Concurrent calls | Call | Output tokens | TTFT (s) | Duration (s) | Decode duration (s) | Tok/s |
|---:|---:|---:|---:|---:|---:|---:|
| 1 | 1 | 256 | 0.221 | 5.085 | 4.864 | 52.63 |
| 2 | 1 | 256 | 0.339 | 6.072 | 5.734 | 44.65 |
| 2 | 2 | 256 | 0.342 | 6.072 | 5.730 | 44.68 |
| 3 | 1 | 256 | 0.465 | 7.216 | 6.751 | 37.92 |
| 3 | 2 | 256 | 0.461 | 7.216 | 6.755 | 37.90 |
| 3 | 3 | 256 | 0.460 | 7.208 | 6.748 | 37.94 |
| 4 | 1 | 256 | 0.569 | 8.443 | 7.874 | 32.51 |
| 4 | 2 | 256 | 0.569 | 8.438 | 7.869 | 32.53 |
| 4 | 3 | 256 | 0.569 | 8.450 | 7.881 | 32.48 |
| 4 | 4 | 256 | 0.574 | 8.451 | 7.877 | 32.50 |
| 5 | 1 | 256 | 0.697 | 10.647 | 9.950 | 25.73 |
| 5 | 2 | 256 | 0.697 | 10.632 | 9.935 | 25.77 |
| 5 | 3 | 256 | 0.696 | 10.639 | 9.943 | 25.75 |
| 5 | 4 | 256 | 0.704 | 10.648 | 9.944 | 25.74 |
| 5 | 5 | 256 | 0.698 | 10.626 | 9.929 | 25.78 |
## Observations
- Every measured request completed successfully and stopped at `finish_reason=length`.
- Aggregate throughput increased from concurrency 1 to 4, then flattened at concurrency 5.
- In this baseline, total throughput was `121.17 tok/s` at concurrency 4 and `120.20 tok/s` at concurrency 5.
- Average TTFT rose with concurrency, from `0.221s` at concurrency 1 to `0.698s` at concurrency 5.

View file

@ -109,7 +109,7 @@ Mac Studio의 secondary `http://192.168.2.3:8005/v1` endpoint는 기본 Node/pro
- DGX Spark 01, DGX Spark 02, Mac Studio Node는 각 host의 `~/iop-dev-corp-field/node.yaml`에서 `edge_addr: "iop.ai.kr:18087"`로 실행된다. - DGX Spark 01, DGX Spark 02, Mac Studio Node는 각 host의 `~/iop-dev-corp-field/node.yaml`에서 `edge_addr: "iop.ai.kr:18087"`로 실행된다.
- 2026-07-13 live status 기준 세 Node 모두 connected이며 provider snapshot은 Spark01 Ornith capacity `4`, Spark02 Ornith capacity `4`, Mac Studio Gemma4 capacity `5`이다. Public OpenAI-compatible route에서 `ornith:35b` 9/6, `gemma4:26b` 9/6 동시 요청이 모두 성공했고 완료 후 `in_flight=0`, `queued=0`, `healthy`로 회복했다. 사용자-facing 기본 base URL은 `https://digitalplatform.iop.ai.kr/v1`이다. - 2026-07-13 live status 기준 세 Node 모두 connected이며 provider snapshot은 Spark01 Ornith capacity `4`, Spark02 Ornith capacity `4`, Mac Studio Gemma4 capacity `5`이다. Public OpenAI-compatible route에서 `ornith:35b` 9/6, `gemma4:26b` 9/6 동시 요청이 모두 성공했고 완료 후 `in_flight=0`, `queued=0`, `healthy`로 회복했다. 사용자-facing 기본 base URL은 `https://digitalplatform.iop.ai.kr/v1`이다.
- `/v1/responses`는 OpenAI-compatible provider model group raw passthrough parity 전까지 미지원이므로 provider-pool capacity 성공 기준에서 제외한다. - Provider-pool passthrough 계약은 selected provider가 지원하는 OpenAI-compatible 표준 field와 provider extension field를 보존한다. `/v1/responses`는 capacity 성공 기준이 아니라 provider-dependent passthrough/relay 검증으로 분리한다. Provider가 지원하면 raw passthrough 성공을 확인하고, provider가 미지원하면 provider status/body가 IOP 변환 없이 relay되는지 확인한다.
## 명령 ## 명령

View file

@ -235,7 +235,7 @@ openai:
target: "llama3:8b" target: "llama3:8b"
``` ```
`/v1/chat/completions`는 기본 non-streaming과 streaming SSE 응답을 모두 지원한다. `/v1/responses`는 현재 non-streaming 요청만 지원한다. 두 endpoint 모두 `metadata.workspace`를 run workspace로 전달한다. `metadata`는 OpenAI 표준의 caller-defined string metadata container로 보고, IOP가 특별히 해석하는 key는 `workspace`뿐이다. `/v1/responses``max_output_tokens`, `temperature`, `top_p`, `instructions`, `background``/v1/chat/completions``max_tokens`, `max_completion_tokens`, `temperature`, `top_p`, `think`, `reasoning_effort`, `thinking_token_budget`, `include_reasoning` 등은 OpenAI API처럼 top-level field에 둔다. `metadata.source`, `metadata.cli`, `metadata.inference`, 소비자 전용 metadata wrapper, `options`, `chat_template_kwargs`, `format`, `keep_alive` 같은 provider/Ollama 전용 request field는 지원하지 않는다. `/v1/chat/completions`는 기본 non-streaming과 streaming SSE 응답을 모두 지원한다. Normalized `/v1/responses`는 현재 non-streaming 요청을 지원하고, provider-pool `/v1/responses`는 selected OpenAI-compatible provider로 raw passthrough 된다. 두 endpoint 모두 `metadata.workspace`를 run workspace로 전달한다. `metadata`는 OpenAI 표준의 caller-defined string metadata container로 보고, IOP가 특별히 해석하는 key는 `workspace`뿐이다. `/v1/responses``max_output_tokens`, `temperature`, `top_p`, `instructions`, `background``/v1/chat/completions``max_tokens`, `max_completion_tokens`, `temperature`, `top_p` 등은 OpenAI API처럼 top-level field에 둔다. Provider-pool passthrough에서는 `chat_template_kwargs` 같은 provider-native OpenAI-compatible extension field도 보존한다. `think`, `reasoning_effort`, `thinking_token_budget`, `include_reasoning`은 OpenAI-compatible 기본 surface 위의 IOP 확장 field다. Normalized route에서는 `metadata.source`, `metadata.cli`, `metadata.inference`, 소비자 전용 metadata wrapper, `options`, `format`, `keep_alive` 같은 backend/provider 전용 wrapper를 OpenAI-compatible 표준 field처럼 요구하지 않는다.
```bash ```bash
curl -s http://127.0.0.1:18081/v1/chat/completions \ curl -s http://127.0.0.1:18081/v1/chat/completions \

View file

@ -133,15 +133,15 @@ http://<edge-host>:18081/v1
## 7. Thinking/Reasoning 제어 smoke ## 7. Thinking/Reasoning 제어 smoke
`/v1/chat/completions` 요청은 `think`, `reasoning_effort`, `thinking_token_budget`, `include_reasoning`으로 thinking/reasoning 동작과 응답 노출을 제어할 수 있다. 단, provider-pool pure `passthrough`는 provider body 보존이 우선이므로 `include_reasoning=false`만으로 IOP-side reasoning 제거를 보장하지 않는다. `/v1/chat/completions` 요청은 OpenAI-compatible field를 기본으로 사용한다. Provider-pool pure `passthrough`는 selected provider가 지원하는 OpenAI-compatible 표준 field와 provider extension field를 보존해야 하며, `chat_template_kwargs` 같은 provider-native option을 IOP allowlist로 막지 않는다. `think`, `reasoning_effort`, `thinking_token_budget`, `include_reasoning`은 normalized backend 또는 provider별 차이를 보완하기 위한 IOP 확장 field다.
- 현재 dev-corp provider-pool device mapping은 `gemma4:26b` -> Mac Studio provider capacity `5`, `ornith:35b` -> DGX Spark 01/02 provider 합산 capacity `8`이다. 세부 endpoint와 runtime args는 `agent-test/dev-corp/inventory.yaml`을 기준으로 한다. - 현재 dev-corp provider-pool device mapping은 `gemma4:26b` -> Mac Studio provider capacity `5`, `ornith:35b` -> DGX Spark 01/02 provider 합산 capacity `8`이다. 세부 endpoint와 runtime args는 `agent-test/dev-corp/inventory.yaml`을 기준으로 한다.
- dev-corp provider-pool 안정 smoke: `think`, `reasoning_effort`, `thinking_token_budget`을 생략하고 현재 provider 기본값을 유지한다. - dev-corp provider-pool 안정 smoke: 기본 smoke에서는 `think`, `reasoning_effort`, `thinking_token_budget`을 생략하고 현재 provider 기본값을 유지한다. Provider-native passthrough를 검증할 때는 selected provider가 직접 지원하는 field를 그대로 보낸다.
- 현재 dev-corp public capacity smoke 표준은 public OpenAI-compatible base `https://digitalplatform.iop.ai.kr/v1``/chat/completions`에서 `ornith:35b` 9/6 동시 요청과 `gemma4:26b` 9/6 동시 요청을 각각 확인하는 방식이다. - 현재 dev-corp public capacity smoke 표준은 public OpenAI-compatible base `https://digitalplatform.iop.ai.kr/v1``/chat/completions`에서 `ornith:35b` 9/6 동시 요청과 `gemma4:26b` 9/6 동시 요청을 각각 확인하는 방식이다.
- 일반 표준 caller의 `stream=false` 측정은 `/v1/chat/completions`에서 요청 파라미터만으로 확인한다. - 일반 표준 caller의 `stream=false` 측정은 `/v1/chat/completions`에서 요청 파라미터만으로 확인한다.
- `include_reasoning=false`는 non-provider normalized route의 hide 동작 기준이다. dev-corp provider-pool pure `passthrough`에서는 client가 reasoning field를 선택적으로 무시/제거한다. - `include_reasoning=false`는 non-provider normalized route의 hide 동작 기준이다. dev-corp provider-pool pure `passthrough`에서는 client가 reasoning field를 선택적으로 무시/제거한다.
- `think=false` 또는 `reasoning_effort=none`은 hide-only 옵션이 아니라 thinking disable 요청이다. dev-corp 기본 안정 smoke에서는 쓰지 않는다. - `think=false` 또는 `reasoning_effort=none`은 hide-only 옵션이 아니라 thinking disable 요청이다. Provider-native field가 있는 경우 해당 field를 우선 사용해 passthrough 보존을 검증한다.
- Gemma4 passthrough 파라미터의 세부 표와 금지/허용 범위는 `agent-contract/outer/openai-compatible-api.md`의 dev-corp `gemma4:26b` provider-pool passthrough 파라미터 범위를 기준으로 한다. - Provider-pool passthrough 파라미터의 세부 계약과 금지/허용 범위는 `agent-contract/outer/openai-compatible-api.md`를 기준으로 한다.
예시 (dev-corp `gemma4:26b` provider-pool non-stream 측정, think 생략): 예시 (dev-corp `gemma4:26b` provider-pool non-stream 측정, think 생략):

View file

@ -6,10 +6,11 @@
주요 현재 동작: 주요 현재 동작:
- Chat Completions provider route의 기본 응답 mode는 provider-original `passthrough`다. - IOP OpenAI-compatible 표면의 기본 베이스는 OpenAI-compatible request/response surface 보존이다. Provider-pool passthrough는 selected provider가 지원하는 표준 field와 provider extension field를 IOP allowlist로 제한하지 않는다.
- provider-pool model group route는 caller가 `metadata.iop_response_mode`를 명시하면 값과 무관하게 거부하고, 생략 시 provider-original `passthrough`로 동작한다. - `think`, `reasoning_effort`, `thinking_token_budget`, `include_reasoning` 같은 IOP field는 OpenAI-compatible 기본 surface 위의 확장이다. Provider-native field를 대체하거나 금지하는 수단으로 해석하지 않는다.
- direct legacy provider route는 `passthrough+sideband` extension selector를 지원한다. `passthrough+sideband``transformed`는 IOP 확장/변환 응답이며 provider-original byte identity로 취급하지 않는다. - 라우팅의 1차 기준은 request `model`이 가리키는 provider capability다. OpenAI-compatible provider이면 provider-original `passthrough`, 그 외 CLI/Ollama/native 실행이면 normalized path를 사용한다.
- dev-corp `gemma4:26b` provider-pool에서 `think`/`stream`/`include_reasoning` 요청 파라미터만 바꿔 측정하는 범위는 계약 원문의 `dev-corp gemma4:26b provider-pool passthrough 파라미터 범위` 표를 기준으로 한다. - `metadata`는 route/response selector가 아니다. Edge는 모델 기반 route 선택 뒤 IOP가 아는 metadata key만 workspace, task, principal, usage/observability 문맥으로 발췌한다.
- dev-corp provider-pool에서 기본 smoke 파라미터와 provider-native field passthrough 기대 동작은 계약 원문의 provider-pool passthrough 섹션을 기준으로 한다.
- dev-corp Ollama `gemma4:26b` 이미지 입력 호출 가이드는 [dev-corp-ollama-gemma4-image-call-guide.md](./dev-corp-ollama-gemma4-image-call-guide.md)를 기준으로 한다. - dev-corp Ollama `gemma4:26b` 이미지 입력 호출 가이드는 [dev-corp-ollama-gemma4-image-call-guide.md](./dev-corp-ollama-gemma4-image-call-guide.md)를 기준으로 한다.
- dev-corp Pi coding agent 설정 가이드는 [dev-corp-pi-settings-guide.md](./dev-corp-pi-settings-guide.md)를 기준으로 한다. - dev-corp Pi coding agent 설정 가이드는 [dev-corp-pi-settings-guide.md](./dev-corp-pi-settings-guide.md)를 기준으로 한다.