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

# Plan - API

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

`CODE_REVIEW-local-G07.md`의 구현 에이전트 소유 섹션을 채우는 것이 구현의 마지막 단계다. 검증 명령은 실제로 실행하고 stdout/stderr를 붙인다. 선택된 Milestone `구현 잠금 > 결정 필요` 항목이 실구현을 막을 때만 review stub의 `사용자 리뷰 요청`을 채우고, 사용자에게 직접 질문하지 않는다. 환경/secret/service blocker나 evidence gap은 사용자 리뷰가 아니라 검증 결과 또는 follow-up으로 기록한다.

## 배경

`wire-contract`는 직접 선행 처리되어 `ProviderTunnelRequest`/`ProviderTunnelFrame` proto와 parser contract가 생겼다. 다음 큰 작업은 Node가 OpenAI-compatible provider HTTP/SSE response를 파싱/정규화하지 않고 tunnel frame으로 relay하는 것이다. 이 plan은 `node-relay` Task만 완료 대상으로 삼고 Edge HTTP passthrough 통합은 후속 split task가 맡는다.

## 사용자 리뷰 요청 흐름

사용자 리뷰 요청은 선택된 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:
  - `node-relay`: Node는 OpenAI-compatible provider HTTP/SSE response를 열고 provider가 보낸 chunk를 tunnel frame으로 relay한다.
- 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/runtime/edge-node-execution.md`
- `agent-spec/input/openai-compatible-surface.md`
- `agent-test/local/rules.md`
- `agent-test/local/node-smoke.md`
- `agent-test/local/platform-common-smoke.md`
- `proto/iop/runtime.proto`
- `apps/node/internal/transport/parser.go`
- `apps/node/internal/transport/session.go`
- `apps/node/internal/transport/parser_test.go`
- `apps/node/internal/transport/session_test.go`
- `apps/node/internal/node/node.go`
- `apps/node/internal/node/node_test.go`
- `apps/node/internal/runtime/types.go`
- `apps/node/internal/adapters/openai_compat/openai_compat.go`
- `apps/node/internal/adapters/openai_compat/openai_compat_test.go`
- `apps/node/internal/adapters/vllm/vllm.go`
- `apps/node/internal/adapters/vllm/vllm_test.go`

### SDD 기준

- SDD: `agent-roadmap/sdd/routing-policy-model-orchestration/openai-compatible-raw-tunnel-sideband-passthrough/SDD.md`, 상태 `[승인됨]`, 잠금 해제.
- Target scenarios: S02, S03.
- Evidence Map: Qwen reasoning fixture byte comparison, Gemma content/tool-call fixture byte comparison.
- 구현 체크리스트는 provider response bytes를 `RunEvent.delta`로 재조립하지 않고 `ProviderTunnelFrame.body`로 전달하는 것과 cancel/error frame 처리에 맞췄다.

### 테스트 환경 규칙

- test_env: `local`.
- 읽은 env/profile: `agent-test/local/rules.md`, `agent-test/local/node-smoke.md`, `agent-test/local/platform-common-smoke.md`.
- 적용 명령: `go test ./apps/node/internal/transport ./apps/node/internal/node ./apps/node/internal/adapters/openai_compat ./apps/node/internal/adapters/vllm`, 필요 시 `go test ./apps/node/...`.
- 외부 provider/DGX field smoke는 이 plan의 필수 검증에서 제외한다. byte identity는 `httptest` fixture로 deterministic하게 확인한다.

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

- 현재 `openai_compat.Execute`와 `vllm.Execute`는 SSE line을 JSON으로 파싱해 `RuntimeEvent`로 바꾼다. raw byte relay test가 없다.
- Node `transport.Handler`에는 tunnel request handler가 없다.
- cancel은 run manager 중심으로만 검증되어 tunnel upstream request cancel evidence가 없다.

### 심볼 참조

- renamed/removed symbols: none.
- 새 symbol 후보: `runtime.ProviderTunnelAdapter`, `runtime.ProviderTunnelRequest`, `runtime.ProviderTunnelSink`, `Node.OnProviderTunnelRequest`, `transport.Handler.OnProviderTunnelRequest`.

### 분할 판단

- split decision policy를 먼저 평가했다.
- shared task group: `agent-task/m-openai-compatible-raw-tunnel-sideband-passthrough/`.
- `01_node_relay`: Node transport/adapter relay 기반. predecessor 없음.
- `02+01_edge_stream`: Edge service/OpenAI passthrough 통합. predecessor `01_node_relay`의 `complete.log` 필요.
- API/foundation(Node relay)과 call-site/HTTP integration(Edge stream)이 분리되어 split이 필요하다.

### 범위 결정 근거

- 포함: Node가 `ProviderTunnelRequest`를 받아 OpenAI-compatible adapter endpoint를 열고 ordered `ProviderTunnelFrame`을 보냄.
- 제외: Edge OpenAI handler의 caller response write, response mode/sideband/transformed, assembled log persistence, external field provider smoke.
- 기존 normalized `RunRequest`/`RunEvent` path는 유지한다.

### 빌드 등급

- `local-G07`: Node transport handler와 provider adapters를 함께 바꾸지만 `httptest` byte fixture와 package tests로 deterministic 검증 가능하다.

## 구현 체크리스트

- [ ] Node transport `Handler`와 `Session`에 `ProviderTunnelRequest` listener를 추가하고 handler nil/error path를 test한다.
- [ ] Node domain/runtime에 tunnel adapter interface와 frame sink를 추가한다.
- [ ] `openai_compat`와 `vllm` adapter가 provider HTTP status/header/body bytes를 ordered `ProviderTunnelFrame`으로 relay하게 구현한다. 검증: Qwen `reasoning_content` fixture와 Gemma content/tool-call fixture가 byte comparison으로 동일하게 왕복한다.
- [ ] cancel/context cancellation 시 upstream request body read가 중단되고 error/end frame semantics가 일관되는지 test한다.
- [ ] `go test ./apps/node/internal/transport ./apps/node/internal/node ./apps/node/internal/adapters/openai_compat ./apps/node/internal/adapters/vllm`를 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [API-1] Node Tunnel Handler

문제: [apps/node/internal/transport/session.go](/config/workspace/iop/apps/node/internal/transport/session.go:15)의 handler는 `RunRequest`, cancel, command, config refresh만 처리한다.

해결 방법:

```go
// before: apps/node/internal/transport/session.go:15
type Handler interface {
	OnRunRequest(ctx context.Context, sess *Session, req *iop.RunRequest) error
	OnCancel(ctx context.Context, sess *Session, req *iop.CancelRequest) error
	OnCommandRequest(ctx context.Context, sess *Session, req *iop.NodeCommandRequest) (*iop.NodeCommandResponse, error)
	OnConfigRefresh(ctx context.Context, sess *Session, req *iop.NodeConfigRefreshRequest) (*iop.NodeConfigRefreshResponse, error)
}
```

`OnProviderTunnelRequest(ctx, sess, req)`를 추가하고 `newSession`에 `AddListenerTyped[*iop.ProviderTunnelRequest]`를 추가한다.

수정 파일 및 체크리스트:

- [ ] `apps/node/internal/transport/session.go`
- [ ] `apps/node/internal/transport/session_test.go`
- [ ] `apps/node/internal/transport/parser_test.go`

테스트 작성: `ProviderTunnelRequest`가 handler로 전달되고 handler nil이면 무시/로그만 되는 session test를 추가한다.

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

### [API-2] Adapter Raw Relay

문제: [apps/node/internal/adapters/openai_compat/openai_compat.go](/config/workspace/iop/apps/node/internal/adapters/openai_compat/openai_compat.go:198)부터 provider stream을 scanner/JSON parser로 바꾸고, [apps/node/internal/adapters/vllm/vllm.go](/config/workspace/iop/apps/node/internal/adapters/vllm/vllm.go:198)도 같은 normalized path를 사용한다.

해결 방법: `runtime.ProviderTunnelAdapter`를 추가하고 `openai_compat`/`vllm`이 `TunnelProvider(ctx, req, sink)`를 구현한다. HTTP error status는 IOP error로 변환하지 말고 status/header/body frame으로 전달한다. 네트워크/build request 오류만 `ERROR` frame으로 보낸다.

수정 파일 및 체크리스트:

- [ ] `apps/node/internal/runtime/types.go`
- [ ] `apps/node/internal/node/node.go`
- [ ] `apps/node/internal/adapters/openai_compat/openai_compat.go`
- [ ] `apps/node/internal/adapters/openai_compat/openai_compat_test.go`
- [ ] `apps/node/internal/adapters/vllm/vllm.go`
- [ ] `apps/node/internal/adapters/vllm/vllm_test.go`

테스트 작성: Qwen reasoning SSE와 Gemma content/tool-call SSE raw fixture를 `httptest`로 반환하고 frame body concatenation이 원본 bytes와 같은지 확인한다.

중간 검증: `go test ./apps/node/internal/node ./apps/node/internal/adapters/openai_compat ./apps/node/internal/adapters/vllm` PASS.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `apps/node/internal/transport/session.go` | API-1 |
| `apps/node/internal/transport/session_test.go` | API-1 |
| `apps/node/internal/runtime/types.go` | API-2 |
| `apps/node/internal/node/node.go` | API-2 |
| `apps/node/internal/adapters/openai_compat/openai_compat.go` | API-2 |
| `apps/node/internal/adapters/openai_compat/openai_compat_test.go` | API-2 |
| `apps/node/internal/adapters/vllm/vllm.go` | API-2 |
| `apps/node/internal/adapters/vllm/vllm_test.go` | API-2 |

## 최종 검증

```bash
go test ./apps/node/internal/transport ./apps/node/internal/node ./apps/node/internal/adapters/openai_compat ./apps/node/internal/adapters/vllm
```

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