<!-- task=m-openai-compatible-raw-tunnel-sideband-passthrough/02+01_edge_stream plan=0 tag=API -->

# Plan - API

## 이 파일을 읽는 구현 에이전트에게

`01_node_relay`의 `complete.log`가 있어야 시작한다. 구현 후 `CODE_REVIEW-cloud-G07.md`의 구현 에이전트 소유 섹션을 실제 구현/검증 출력으로 채운다. Milestone lock decision만 review stub의 `사용자 리뷰 요청`에 기록하며, 사용자에게 직접 질문하지 않는다.

## 배경

Node relay가 완료되면 Edge는 OpenAI-compatible provider route에 대해 normalized `RunEvent` stream 대신 request-bound tunnel frame stream을 caller에게 그대로 써야 한다. 이 plan은 `edge-stream` Task만 완료 대상으로 삼고 pure `passthrough` 기본값을 구현한다. sideband/transformed mode는 별도 Epic 범위다.

## 사용자 리뷰 요청 흐름

사용자 리뷰 요청은 선택된 Milestone lock decision이 실구현을 차단할 때만 active review stub에 기록한다. 직접 채팅 질문, 선택지 제시, `request_user_input`, `USER_REVIEW.md` 작성, archive/complete.log 작성은 금지한다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/routing-policy-model-orchestration/milestones/openai-compatible-raw-tunnel-sideband-passthrough.md`
- Milestone link: [Milestone 문서](agent-roadmap/phase/routing-policy-model-orchestration/milestones/openai-compatible-raw-tunnel-sideband-passthrough.md)
- Task ids:
  - `edge-stream`: Edge OpenAI-compatible handler는 tunnel status/headers/body chunks를 caller에게 그대로 write하고 pure passthrough를 기본값으로 적용한다.
- Completion mode: check-on-pass

## 분석 결과

### 읽은 파일

- `agent-roadmap/phase/routing-policy-model-orchestration/milestones/openai-compatible-raw-tunnel-sideband-passthrough.md`
- `agent-roadmap/sdd/routing-policy-model-orchestration/openai-compatible-raw-tunnel-sideband-passthrough/SDD.md`
- `agent-contract/inner/edge-node-runtime-wire.md`
- `agent-contract/outer/openai-compatible-api.md`
- `agent-spec/input/openai-compatible-surface.md`
- `agent-spec/runtime/edge-node-execution.md`
- `agent-test/local/rules.md`
- `agent-test/local/edge-smoke.md`
- `agent-test/local/platform-common-smoke.md`
- `apps/edge/internal/service/run_dispatch.go`
- `apps/edge/internal/openai/chat_handler.go`
- `apps/edge/internal/openai/run_result.go`
- `apps/edge/internal/openai/stream.go`
- `apps/edge/internal/openai/server.go`
- `apps/edge/internal/transport/server.go`
- `proto/iop/runtime.proto`

### SDD 기준

- SDD: `agent-roadmap/sdd/routing-policy-model-orchestration/openai-compatible-raw-tunnel-sideband-passthrough/SDD.md`, 상태 `[승인됨]`, 잠금 해제.
- Target scenarios: S04.
- Evidence Map: omitted mode default passthrough test와 streaming/non-streaming response comparison.
- 구현 체크리스트는 pure passthrough에 IOP sideband를 섞지 않고 status/header/body byte identity를 보존하는 데 맞췄다.

### 테스트 환경 규칙

- test_env: `local`.
- 읽은 env/profile: `agent-test/local/rules.md`, `agent-test/local/edge-smoke.md`, `agent-test/local/platform-common-smoke.md`.
- 적용 명령: `go test ./apps/edge/internal/transport ./apps/edge/internal/service ./apps/edge/internal/openai`.
- OpenAI-compatible boundary 변경이므로 full-cycle 실제 구동은 code-review에서 남은 위험으로 확인해야 한다. 이 plan의 deterministic completion evidence는 `httptest` streaming/non-streaming byte comparison이다.

### 테스트 커버리지 공백

- 현재 [apps/edge/internal/service/run_dispatch.go](/config/workspace/iop/apps/edge/internal/service/run_dispatch.go:68)는 `RunEvent` channel만 반환한다.
- [apps/edge/internal/openai/chat_handler.go](/config/workspace/iop/apps/edge/internal/openai/chat_handler.go:83)는 모든 Chat Completions 요청을 `SubmitRun` normalized path로 보낸다.
- [apps/edge/internal/openai/stream.go](/config/workspace/iop/apps/edge/internal/openai/stream.go:1)는 OpenAI SSE chunk를 재작성한다. provider-original passthrough test가 없다.

### 심볼 참조

- renamed/removed symbols: none.
- 새 symbol 후보: `SubmitProviderTunnel`, `ProviderTunnelHandle`, `ProviderTunnelStream`, transport tunnel subscription/router.

### 분할 판단

- split decision policy를 먼저 평가했다.
- task group: `agent-task/m-openai-compatible-raw-tunnel-sideband-passthrough/`.
- predecessor: `01_node_relay` must have `complete.log`; active/archive lookup은 구현 시작 시 확인한다.
- 이 task는 Edge service/OpenAI HTTP integration이므로 Node relay foundation과 분리한다.

### 범위 결정 근거

- 포함: Chat Completions streaming/non-streaming pure passthrough 기본값, tunnel status/header/body write, caller disconnect cancel.
- 제외: `passthrough+sideband`, `transformed`, Responses API parity hardening, assembled-log persistence, provider qualification matrix.
- CLI/non-compatible/legacy normalized route는 유지한다.

### 빌드 등급

- `cloud-G07`: 외부 HTTP wire semantics와 Edge service/provider-pool dispatch가 함께 바뀌며, 단순 local compile만으로 byte identity를 충분히 보장하기 어렵다.

## 의존 관계 및 구현 순서

- `02+01_edge_stream`은 sibling `01_node_relay`의 `complete.log` 이후 시작한다. 구현 시작 전 `agent-task/m-openai-compatible-raw-tunnel-sideband-passthrough/01_node_relay/complete.log` 또는 matching archive complete.log를 확인한다.

## 구현 체크리스트

- [ ] `01_node_relay` predecessor `complete.log`를 확인하고 경로를 review stub에 기록한다.
- [ ] Edge transport가 `ProviderTunnelFrame`을 request-bound channel로 라우팅하되 `events.Bus`에는 publish하지 않게 구현한다.
- [ ] Edge service에 provider-pool/direct route 공통 `SubmitProviderTunnel` 경로를 추가하고 admission slot release를 END/ERROR/cancel에서 보장한다.
- [ ] OpenAI Chat Completions handler가 OpenAI-compatible provider route의 omitted mode를 pure `passthrough`로 처리하고 provider status/header/body bytes를 caller에게 그대로 쓴다. 검증: streaming/non-streaming Chat Completions fixture가 provider 원본과 byte-identical하게 반환된다.
- [ ] caller disconnect/context cancel이 Node cancel/control path로 전파되는지 test한다.
- [ ] `go test ./apps/edge/internal/transport ./apps/edge/internal/service ./apps/edge/internal/openai`를 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [API-1] Tunnel Frame Routing

문제:

```go
// before: apps/edge/internal/service/run_dispatch.go:68
type RunStream struct {
	Events     <-chan *iop.RunEvent
	NodeEvents <-chan *iop.EdgeNodeEvent
}
```

현재 Edge service는 RunEvent stream만 알고 tunnel frame stream을 구독할 수 없다.

해결 방법: transport/server 또는 service에 tunnel frame router를 두고 `tunnel_id`별 buffered channel을 제공한다. `ProviderTunnelFrame` listener는 `RunEvent` handler나 `events.Bus.PublishRun`을 호출하지 않는다.

수정 파일 및 체크리스트:

- [ ] `apps/edge/internal/transport/server.go`
- [ ] `apps/edge/internal/transport/server_test.go` 또는 새 `tunnel_test.go`
- [ ] `apps/edge/internal/service/run_dispatch.go`

테스트 작성: tunnel frame이 구독자에게 도착하고 `events.Bus` run subscriber에는 도착하지 않는 negative test를 추가한다.

중간 검증: `go test ./apps/edge/internal/transport ./apps/edge/internal/service` PASS.

### [API-2] OpenAI Passthrough Writer

문제:

```go
// before: apps/edge/internal/openai/chat_handler.go:83
handle, err := s.service.SubmitRun(r.Context(), submitReq)
```

현재 Chat Completions는 provider-compatible route도 normalized run path로 보내며 [apps/edge/internal/openai/stream.go](/config/workspace/iop/apps/edge/internal/openai/stream.go:1)에서 SSE를 재작성한다.

해결 방법: provider-compatible dispatch(`ProviderPool` 또는 `openai_compat`/`vllm`)의 omitted mode는 `SubmitProviderTunnel`을 사용한다. Response start frame에서 status/header를 쓰고 body frame은 순서대로 그대로 write/flush한다. Pure passthrough에는 IOP sideband field/event를 추가하지 않는다.

수정 파일 및 체크리스트:

- [ ] `apps/edge/internal/openai/chat_handler.go`
- [ ] `apps/edge/internal/openai/stream.go`
- [ ] `apps/edge/internal/openai/run_result.go`
- [ ] `apps/edge/internal/openai/server.go`
- [ ] 새 또는 기존 `apps/edge/internal/openai/*_test.go`

테스트 작성: `httptest` provider fixture로 streaming SSE와 non-streaming JSON body를 각각 tunnel frame으로 흘려 caller response body가 provider body와 byte-identical한지 확인한다.

중간 검증: `go test ./apps/edge/internal/openai` PASS.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `apps/edge/internal/transport/server.go` | API-1 |
| `apps/edge/internal/transport/*_test.go` | API-1 |
| `apps/edge/internal/service/run_dispatch.go` | API-1 |
| `apps/edge/internal/openai/chat_handler.go` | API-2 |
| `apps/edge/internal/openai/stream.go` | API-2 |
| `apps/edge/internal/openai/run_result.go` | API-2 |
| `apps/edge/internal/openai/server.go` | API-2 |
| `apps/edge/internal/openai/*_test.go` | API-2 |

## 최종 검증

```bash
go test ./apps/edge/internal/transport ./apps/edge/internal/service ./apps/edge/internal/openai
```

기대 결과: PASS. Go test cache output 허용. 모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
