# SDD: OpenAI-compatible Raw Tunnel과 Sideband Passthrough

## 위치

- Milestone: [openai-compatible-raw-tunnel-sideband-passthrough](../../../phase/routing-policy-model-orchestration/milestones/openai-compatible-raw-tunnel-sideband-passthrough.md)
- Phase: [PHASE.md](../../../phase/routing-policy-model-orchestration/PHASE.md)

## 상태

[승인됨]

## SDD 잠금

- 상태: 해제
- 사용자 리뷰: 없음
- 잠금 항목:
  - [x] [D01] 공식 응답 모드는 `passthrough`, `passthrough+sideband`, `transformed` 셋으로 둔다.
  - [x] [D02] OpenAI-compatible inference provider의 기본 응답은 provider-original `passthrough`로 둔다.
  - [x] [D03] Edge는 Node-local provider에 직접 닿지 않고, Node가 provider response를 기존 Edge-Node proto-socket 위 tunnel frame으로 relay한다.
  - [x] [D04] pure `passthrough` body에는 IOP sideband를 섞지 않는다. sideband/transformed output은 요청 opt-in으로만 활성화한다.
  - [x] [D05] 저장/관측 기본값은 raw byte payload가 아니라 사람이 읽을 수 있는 assembled output과 구조화된 sideband observation으로 둔다.
  - [x] [D06] `passthrough+sideband`는 provider-original byte identity를 주장하지 않는 IOP 확장 모드이며, 같은 client SSE/response stream에 IOP event/field를 합치는 방식은 이 모드에서만 허용한다.

## 문제 / 비목표

- 문제: 기존 OpenAI-compatible 실행 경로는 Node provider 응답을 내부 `RunEvent`와 Edge OpenAI handler에서 정규화/재조립한다. 이 과정은 vLLM Qwen 계열의 `reasoning_content`, Gemma 계열의 content/tool-call chunk 차이처럼 provider별 출력 차이를 IOP 표준 레이어에 억지로 맞추며 누락, 변형, 순서 왜곡, stream semantics 불일치를 만들 수 있다. OpenAI-compatible provider에서는 provider가 만든 응답이 이미 외부 client가 기대하는 wire format이므로, 성능과 정확성의 기준은 원본 stream passthrough여야 한다.
- 비목표:
  - 기존 normalized `RunEvent` 경로 제거
  - Edge가 Node provider endpoint에 직접 접속하는 구조
  - 별도 Node socket 또는 side stream server 추가
  - raw provider byte payload 기본 장기 저장
  - pure `passthrough` stream에 IOP sideband field/event 주입
  - routing score, billing/chargeback, 장기 retention 정책 확정

## Source of Truth

| 영역 | 기준 | 메모 |
|------|------|------|
| Roadmap | [openai-compatible-raw-tunnel-sideband-passthrough](../../../phase/routing-policy-model-orchestration/milestones/openai-compatible-raw-tunnel-sideband-passthrough.md) | 목표, 기능 Task, 범위, 제외 범위 기준 |
| Outer Contract | [openai-compatible-api.md](../../../../agent-contract/outer/openai-compatible-api.md) | `metadata.iop_response_mode`, default passthrough, Chat Completions/Responses surface 기준 |
| Inner Contract | [edge-node-runtime-wire.md](../../../../agent-contract/inner/edge-node-runtime-wire.md) | 기존 Edge-Node proto-socket과 새 tunnel frame 의미 기준 |
| Spec | [openai-compatible-surface.md](../../../../agent-spec/input/openai-compatible-surface.md), [edge-node-execution.md](../../../../agent-spec/runtime/edge-node-execution.md) | 현재 OpenAI-compatible handler와 Edge-Node 실행 구조 기준 |
| Code | `proto/iop/runtime.proto`, `apps/edge/internal/openai`, `apps/edge/internal/service`, `apps/edge/internal/transport`, `apps/edge/internal/events`, `apps/node/internal/transport`, `apps/node/internal/adapters/openai_compat`, `apps/node/internal/adapters/vllm`, `packages/go/config` | proto, Edge handler, Node provider relay, config/mode parsing 구현 기준 |
| External Provider | OpenAI-compatible inference engines | `passthrough` mode에서 provider HTTP/SSE response bytes가 source of truth다 |
| User Decision | D01-D06 | 2026-07-07 대화에서 mode set, default, socket path, sideband separation, readable assembled persistence, explicit sideband-combined mode 확정 |

## State Machine

| 상태 | 진입 조건 | 다음 상태 | 근거 |
|------|-----------|-----------|------|
| accepted | Edge OpenAI-compatible endpoint가 요청을 받았다 | mode_resolved 또는 error | request id, model, metadata/config |
| mode_resolved | 요청 mode가 해석되었다. 누락 시 `passthrough`로 둔다 | routed 또는 error | `passthrough`, `passthrough+sideband`, `transformed`; unknown mode는 error |
| routed | Edge가 실행 Node/provider target을 선택했다 | tunnel_requested 또는 normalized_dispatched | OpenAI-compatible provider는 tunnel, legacy/non-compatible은 normalized path 가능 |
| tunnel_requested | Edge가 기존 Edge-Node socket으로 tunnel request를 보냈다 | provider_started 또는 error | tunnel request frame |
| provider_started | Node가 provider HTTP/SSE request를 열었다 | response_started 또는 error | upstream status/header 관측 |
| response_started | Node가 provider status/header를 Edge에 전달했다 | body_streaming 또는 completed/error | tunnel response-start frame |
| body_streaming | provider body bytes가 sequence를 가진 tunnel frame으로 이동한다 | completed, cancelled, error | ordered body chunk frames |
| sideband_observing | 요청이 `passthrough+sideband` 또는 `transformed`이고 IOP observation이 필요하다 | body_streaming, transformed_streaming, completed/error | assembled content/reasoning/tool/usage observation |
| transformed_streaming | 요청이 `transformed`이고 IOP output writer가 provider stream을 수정한다 | completed/error | transformed response frame 또는 OpenAI-compatible output writer |
| completed | provider EOF, finish marker, non-streaming body complete가 전달됐다 | 없음 | final frame, trailer/usage 후보 |
| cancelled | caller disconnect 또는 explicit cancel이 발생했다 | 없음 | cancel/control frame, upstream request cancel |
| error | provider, socket, parser, mode, timeout 오류가 발생했다 | 없음 | error frame과 response error policy |

## Interface Contract

- 계약 원문: [openai-compatible-api.md](../../../../agent-contract/outer/openai-compatible-api.md), [edge-node-runtime-wire.md](../../../../agent-contract/inner/edge-node-runtime-wire.md)
- 입력:
  - OpenAI-compatible Chat Completions 및 Responses request
  - 요청 mode: `metadata.iop_response_mode`
  - mode 값: `passthrough`, `passthrough+sideband`, `transformed`
  - mode 누락: OpenAI-compatible inference provider에서는 `passthrough`
  - unknown mode: fail-fast invalid request error
  - Edge-selected target: node id, provider id, served model, adapter/target metadata
  - tunnel control: request id/run id, cancel, timeout, stream/non-stream flag
- 출력:
  - `passthrough`: provider status/header/body bytes를 caller에게 byte-faithful하게 전달
  - `passthrough+sideband`: provider original body와 IOP sideband observation을 명시적 IOP extension stream/schema로 함께 제공하며, 같은 client SSE/response stream에 IOP event/field를 합칠 수 있다.
  - `transformed`: IOP가 provider stream을 읽고 사용자 지정 schema 또는 수정된 output을 반환
  - 실행 로그: assembled `content`, `reasoning`, `tool_calls`, usage/finish/provider observation, sideband summary
- 금지:
  - pure `passthrough` body에 IOP sideband field/event를 삽입하지 않는다.
  - `passthrough+sideband` output을 provider-original byte-identical response로 표시하지 않는다.
  - OpenAI-compatible provider raw bytes를 `RunEvent.delta` 또는 lossy `events.Bus` fanout의 payload로 보내지 않는다.
  - 기존 Edge-Node socket과 별개인 Node stream socket을 새로 열지 않는다.
  - raw provider byte payload를 기본 장기 저장하지 않는다.
  - `transformed` output을 provider-original byte stream으로 표시하지 않는다.

## Acceptance Scenarios

| ID | Milestone Task | Given | When | Then |
|----|----------------|-------|------|------|
| S01 | `wire-contract` | Edge가 OpenAI-compatible provider target을 선택했다 | Edge가 Node에 tunnel request를 보낸다 | 기존 Edge-Node socket 위에서 status/header/body/end/error frame이 sequence와 함께 정의된다 |
| S02 | `node-relay` | vLLM Qwen fixture가 `reasoning_content` stream chunk를 반환한다 | Node가 provider response를 relay한다 | provider chunk bytes가 정규화 누락 없이 Edge까지 동일하게 도착한다 |
| S03 | `node-relay` | Gemma fixture가 content/tool-call chunk를 반환한다 | Node가 provider response를 relay한다 | provider chunk bytes가 모델별 parser 차이 없이 Edge까지 동일하게 도착한다 |
| S04 | `edge-stream` | 요청 mode가 누락되어 있다 | Edge가 OpenAI-compatible streaming response를 반환한다 | 기본값 `passthrough`가 적용되고 provider body가 byte-identical하게 반환된다 |
| S05 | `mode-contract` | 요청이 `passthrough+sideband`를 명시한다 | Edge/Node가 stream을 처리한다 | provider original body와 IOP sideband observation이 명시적 IOP extension stream/schema로 노출된다 |
| S06 | `sideband-overlay` | 요청이 pure `passthrough`다 | provider stream이 caller에게 전달된다 | body 안에 IOP sideband field/event가 없다 |
| S07 | `transform-output` | 요청이 `transformed`를 명시한다 | IOP schema/output writer가 provider stream을 처리한다 | 반환값은 IOP transformed output으로 표시되고 passthrough byte identity 검증 대상에서 제외된다 |
| S08 | `assembled-log` | provider stream이 content, reasoning, tool call을 포함한다 | 요청이 완료된다 | 실행 로그에는 readable assembled string과 structured observation이 남고 raw chunk payload는 기본 저장되지 않는다 |
| S09 | `backpressure-cancel` | caller가 느리거나 중간에 disconnect한다 | tunnel stream이 진행 중이다 | chunk drop/reorder 없이 backpressure가 적용되고 upstream provider request가 cancel된다 |
| S10 | `contract-docs` | 구현이 완료된다 | 계약/spec/docs를 검토한다 | mode 이름, default, tunnel path, sideband/transformed semantics가 문서와 test fixture에서 일치한다 |
| S11 | `mode-contract` | 요청이 unknown `metadata.iop_response_mode`를 명시한다 | Edge가 mode를 해석한다 | silent fallback 없이 invalid request error를 반환한다 |

## Evidence Map

| Scenario | Required Evidence | `agent-task` 연결 | 완료 Evidence 기대 |
|----------|-------------------|------------------|---------------------------|
| S01 | proto diff, generated code, Edge/Node tunnel contract test | `agent-task/m-openai-compatible-raw-tunnel-sideband-passthrough/...` | `Roadmap Completion`에 `wire-contract`와 S01 충족 근거 |
| S02 | Qwen reasoning fixture byte comparison test | `agent-task/m-openai-compatible-raw-tunnel-sideband-passthrough/...` | `Roadmap Completion`에 `node-relay`와 S02 충족 근거 |
| S03 | Gemma fixture byte comparison test | `agent-task/m-openai-compatible-raw-tunnel-sideband-passthrough/...` | `Roadmap Completion`에 `node-relay`와 S03 충족 근거 |
| S04 | omitted mode default passthrough test와 streaming/non-streaming response comparison | `agent-task/m-openai-compatible-raw-tunnel-sideband-passthrough/...` | `Roadmap Completion`에 `edge-stream`와 S04 충족 근거 |
| S05 | explicit sideband mode test와 sideband observation fixture | `agent-task/m-openai-compatible-raw-tunnel-sideband-passthrough/...` | `Roadmap Completion`에 `mode-contract`, `sideband-overlay`와 S05 충족 근거 |
| S06 | pure passthrough response에 IOP field/event가 없음을 확인하는 negative test | `agent-task/m-openai-compatible-raw-tunnel-sideband-passthrough/...` | `Roadmap Completion`에 `sideband-overlay`와 S06 충족 근거 |
| S07 | transformed mode output contract test와 label/metadata 확인 | `agent-task/m-openai-compatible-raw-tunnel-sideband-passthrough/...` | `Roadmap Completion`에 `transform-output`와 S07 충족 근거 |
| S08 | execution log fixture에서 assembled content/reasoning/tool_calls 확인과 raw chunk 미저장 확인 | `agent-task/m-openai-compatible-raw-tunnel-sideband-passthrough/...` | `Roadmap Completion`에 `assembled-log`와 S08 충족 근거 |
| S09 | slow client, cancel, provider error, socket backpressure test | `agent-task/m-openai-compatible-raw-tunnel-sideband-passthrough/...` | `Roadmap Completion`에 `backpressure-cancel`와 S09 충족 근거 |
| S10 | agent-contract/agent-spec/docs/config example diff와 link validation | `agent-task/m-openai-compatible-raw-tunnel-sideband-passthrough/...` | `Roadmap Completion`에 `contract-docs`와 S10 충족 근거 |
| S11 | unknown mode validation test | `agent-task/m-openai-compatible-raw-tunnel-sideband-passthrough/...` | `Roadmap Completion`에 `mode-contract`와 S11 충족 근거 |

## Cross-repo Dependencies

- 없음

## Drift Check

- [x] Milestone 기능 Task와 Acceptance Scenario가 일치한다.
- [x] Evidence Map이 code-review/complete.log에서 검증 가능하다.
- [x] agent-contract를 쓰는 경우 SDD에 계약 원문을 복제하지 않았다.
- [x] 사용자 리뷰가 필요한 항목은 없다.

## 사용자 리뷰 이력

- 2026-07-07: 사용자 결정으로 공식 mode set, default original passthrough, 기존 Edge-Node socket tunnel, pure passthrough와 sideband 분리, explicit sideband-combined mode, readable assembled persistence 기준을 확정했다.

## 작업 컨텍스트

- 표준선: raw provider bytes는 `passthrough` 전송의 source of truth이고, IOP sideband와 transformed output은 별도 opt-in layer다.
- 표준선: 기존 socket 설계를 활용하며, raw stream은 lossy event bus가 아니라 request-bound ordered frame으로 다룬다.
- 표준선: Chat Completions를 먼저 연결하되 Responses API parity를 같은 Milestone 범위의 후속 Task로 둔다.
- 표준선: 기존 normalized path는 유지하고, raw tunnel 안정화 뒤 별도 deprecation review에서 제거 여부를 판단한다.
- 후속 SDD: 없음
