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

# Plan - API Response Modes Semantics

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

구현 완료의 마지막 단계는 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채우는 것이다. 검증을 실행하고, 실제 stdout/stderr를 붙이고, active 파일을 그대로 둔 채 review ready를 보고한다. finalization, `complete.log` 작성, archive 이동은 code-review-skill 전용이다.

선택된 Milestone의 `구현 잠금 > 결정 필요` 항목이 실구현을 막을 때만 active review stub의 `사용자 리뷰 요청` 섹션을 정확한 근거와 함께 채우고 멈춘다. 구현 중 직접 사용자에게 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 환경/secret/service 준비, 일반 범위 조정, 반복 실패, follow-up agent가 닫을 수 있는 evidence gap은 사용자 리뷰 요청이 아니며 `검증 결과`나 `계획 대비 변경 사항`에 기록한다.

## 배경

현재 Milestone의 raw-tunnel epic은 완료되어 기본 omitted mode가 provider-original `passthrough`로 동작한다. response-modes epic은 이 기본 경로를 훼손하지 않으면서 `metadata.iop_response_mode`의 세 공식 모드와 opt-in sideband/transformed semantics를 닫아야 한다. 이번 세션에서 작은 선행 변경으로 mode parser와 unknown mode fail-fast test가 active diff에 들어갔으므로, 구현 에이전트는 이를 보존하고 실제 sideband/transformed 출력 계약까지 이어서 완성한다.

## 사용자 리뷰 요청 흐름

사용자 리뷰 요청은 선택된 Milestone lock decision이 실구현을 차단할 때만 active `CODE_REVIEW-*-G??.md`의 `사용자 리뷰 요청` 섹션에 기록한다. 이 섹션은 `agent-ops/skills/common/_templates/implementation-user-review-request-section.md` 형식을 그대로 따른다. 구현 중 direct user prompt, chat 선택지 제시, `request_user_input`, `USER_REVIEW.md` 직접 생성은 금지이며 code-review가 validation과 실제 `USER_REVIEW.md` 작성을 소유한다.

## Archive Evidence Snapshot

- `agent-task/archive/2026/07/m-openai-compatible-raw-tunnel-sideband-passthrough/01_node_relay/complete.log`: PASS. Node relay가 OpenAI-compatible provider response를 기존 Edge-Node socket tunnel frame으로 전달하는 선행 작업을 완료했다.
- `agent-task/archive/2026/07/m-openai-compatible-raw-tunnel-sideband-passthrough/02+01_edge_stream/complete.log`: PASS. Edge stream pure passthrough 기본값과 streaming/non-streaming byte identity 경로가 완료되어 이번 subtask의 predecessor `02`를 만족한다.
- 더 자세한 선행 맥락이 필요할 때만 위 두 `complete.log`를 좁게 읽는다. `agent-task/archive/**`를 광범위하게 검색하지 않는다.

## 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:
  - `mode-contract`: OpenAI-compatible request metadata/config 계약에 `metadata.iop_response_mode`와 `passthrough`, `passthrough+sideband`, `transformed` 모드를 정의하고 omitted mode는 `passthrough`로 해석한다.
  - `sideband-overlay`: `passthrough+sideband`는 provider original stream과 IOP sideband를 명시적 IOP extension stream/schema로 함께 노출하되 pure `passthrough` body에는 IOP field/event를 주입하지 않는다.
  - `transform-output`: `transformed`는 provider raw stream을 IOP schema/output으로 수정할 수 있지만 byte-identical provider stream으로 표시하지 않는다.
- Completion mode: check-on-pass

## 분석 결과

### 읽은 파일

- `agent-ops/rules/project/rules.md`
- `agent-ops/rules/common/rules-roadmap.md`
- `agent-ops/skills/common/router.md`
- `agent-ops/skills/common/plan/SKILL.md`
- `agent-ops/skills/common/_templates/implementation-user-review-request-section.md`
- `agent-ops/rules/project/domain/edge/rules.md`
- `agent-test/local/rules.md`
- `agent-test/local/edge-smoke.md`
- `agent-contract/index.md`
- `agent-contract/outer/openai-compatible-api.md`
- `agent-contract/inner/edge-node-runtime-wire.md`
- `agent-ops/rules/common/rules-agent-spec.md`
- `agent-spec/index.md`
- `agent-spec/input/openai-compatible-surface.md`
- `agent-roadmap/phase/routing-policy-model-orchestration/PHASE.md`
- `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`
- `apps/edge/internal/openai/chat_handler.go`
- `apps/edge/internal/openai/stream.go`
- `apps/edge/internal/openai/server_test.go`

### SDD 기준

- SDD: `agent-roadmap/sdd/routing-policy-model-orchestration/openai-compatible-raw-tunnel-sideband-passthrough/SDD.md`
- 상태: `[승인됨]`, SDD 잠금 `해제`, 사용자 리뷰 `없음`.
- S05 maps to `mode-contract` and `sideband-overlay`: explicit `passthrough+sideband` must expose provider original body plus IOP sideband observation through an explicit extension stream/schema. This drives the sideband fixture and routing/output checklist.
- S06 maps to `sideband-overlay`: pure `passthrough` must not contain IOP sideband field/event. This drives negative tests on existing pure passthrough streaming/non-streaming fixtures.
- S07 maps to `transform-output`: explicit `transformed` output must be labeled/metadata-marked as IOP transformed and excluded from byte-identity claims. This drives response metadata/label tests on normalized output.
- S11 maps to `mode-contract`: unknown `metadata.iop_response_mode` must fail fast with invalid request. The active diff already adds the parser and regression test; implementation must preserve or refine it.

### 테스트 환경 규칙

- test_env: `local`.
- `agent-test/local/rules.md` was present and read. The OpenAI-compatible Edge boundary maps to `agent-test/local/edge-smoke.md`, which was present and read.
- Applied verification source: deterministic Go handler/service/transport tests in this checkout, because the requested work is contract-level Edge behavior and the package tests exercise `/v1/chat/completions` handler fixtures without needing a live Edge process or external provider.
- Verified command availability/layout before writing this plan:
  - `go test -count=1 ./apps/edge/internal/service ./apps/edge/internal/transport ./apps/edge/internal/openai` passes in the current checkout.
- Required verification does not leave the current checkout. No remote runner, secret, external provider, Docker service, emulator, or long-running runtime preflight is required.
- Fresh execution matters for mode routing and stream behavior, so Go test cache output is not acceptable; use `-count=1`.

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

- `mode-contract`: omitted mode pure passthrough and explicit transformed path already have tests; the active diff adds explicit `passthrough`, `passthrough+sideband`, `transformed`, and unknown mode routing coverage. Remaining gap: contract/spec/doc parity is intentionally deferred to `contract-docs`, but code fixtures must use the final mode names.
- `sideband-overlay`: existing pure passthrough byte-identity tests cover no mutation by body equality, and `stream.go:465` ignores usage frames in pure passthrough. Missing: opt-in `passthrough+sideband` extension schema/stream test and explicit negative assertion that pure passthrough contains no IOP sideband marker/event.
- `transform-output`: existing transformed-mode tests cover normalized tool/reasoning/strict output behavior. Missing: response label/metadata evidence that transformed output is not provider-original byte identity and is separated from passthrough assertions.

### 심볼 참조

- Removed/renamed symbol in active diff: `passthroughResponseMode` removed.
- `rg --sort path -n "passthroughResponseMode|parseResponseMode|responseModePassthroughSideband|responseModeTransformed|iop_response_mode" apps/edge/internal/openai` found no remaining `passthroughResponseMode` references.
- Current `parseResponseMode` references: `apps/edge/internal/openai/chat_handler.go:47`, definition at `apps/edge/internal/openai/chat_handler.go:283`.
- `iop_response_mode` test fixtures are concentrated in `apps/edge/internal/openai/server_test.go` and should continue to use the exact SDD mode strings.

### 분할 판단

- Split decision policy was evaluated before choosing this plan.
- Shared task group: `agent-task/m-openai-compatible-raw-tunnel-sideband-passthrough`.
- Existing completed predecessors:
  - `01_node_relay`: satisfied by archived `agent-task/archive/2026/07/m-openai-compatible-raw-tunnel-sideband-passthrough/01_node_relay/complete.log`.
  - `02+01_edge_stream`: satisfied by archived `agent-task/archive/2026/07/m-openai-compatible-raw-tunnel-sideband-passthrough/02+01_edge_stream/complete.log`.
- This subtask directory is `03+02_response_modes_semantics`; runtime dependency source of truth is predecessor `02`.
- A further split between `mode-contract`, `sideband-overlay`, and `transform-output` was considered. They share the same public response-mode entry point, the same handler tests, and the same SDD contract boundary; splitting would risk temporary contradictory mode semantics. The scope stays within one primary ownership boundary, `apps/edge/internal/openai`, with optional narrow service/transport touch only if sideband observation needs tunnel result metadata.

### 범위 결정 근거

- Do not change Node relay protocol or generated proto unless implementation proves the current `ProviderTunnelFrame` fields cannot carry the required sideband observation. Existing `wire-contract` and `node-relay` tasks are complete.
- Do not implement Responses API raw tunnel parity here; the Milestone context says Chat Completions first and Responses parity hardening is follow-up.
- Do not change routing score, provider qualification, billing/chargeback, raw payload retention policy, or normalized path deprecation.
- Do not inject IOP sideband into pure `passthrough` body; the only allowed mixed response body is explicit `passthrough+sideband`.
- Do not update agent-contract/spec/docs as completion evidence here beyond code comments/test fixture names; `contract-docs` is a separate roadmap Task.

### 빌드 등급

- `cloud-G06`: behavior is promised by SDD/outer contract beyond a single file, and a plausible local-only implementation could pass simple handler tests while mislabeling sideband/transformed semantics. Scope is moderate and bounded to Edge OpenAI response handling.

## 구현 체크리스트

- [ ] `mode-contract`: official mode constants/parser/routing guard are complete, omitted mode stays pure `passthrough`, explicit sideband/transformed choose the intended non-byte-identity paths, and unknown mode returns invalid request. 검증: mode 누락, explicit sideband, explicit transformed routing test가 각각 기대 경로를 타고 unknown mode는 fail-fast error가 된다.
- [ ] `sideband-overlay`: explicit `passthrough+sideband` exposes provider-original content plus IOP observation through a contracted extension stream/schema, while pure `passthrough` contains no IOP sideband field/event. 검증: pure passthrough 응답에 IOP sideband가 없고 opt-in sideband mode에서 같은 response stream 또는 계약된 extension surface로 route/provider/assembled observation이 확인된다.
- [ ] `transform-output`: explicit `transformed` response is labeled/metadata-marked as IOP transformed output and is excluded from provider byte-identity assertions. 검증: transformed response가 IOP output으로 라벨링되고 original passthrough 검증과 분리된다.
- [ ] Final verification command passes: `go test -count=1 ./apps/edge/internal/service ./apps/edge/internal/transport ./apps/edge/internal/openai`.
- [ ] `git diff --check` passes.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## 의존 관계 및 구현 순서

`03+02_response_modes_semantics` depends only on predecessor index `02`, satisfied by `agent-task/archive/2026/07/m-openai-compatible-raw-tunnel-sideband-passthrough/02+01_edge_stream/complete.log`. Implement in this order: `mode-contract` final check, `sideband-overlay`, then `transform-output`, because sideband/transformed routing depends on the finalized parser/dispatch semantics.

### [API-1] Mode Contract

#### 문제

`apps/edge/internal/openai/chat_handler.go:75` previously treated all unknown non-`passthrough` modes as normalized path. SDD S11 requires unknown `metadata.iop_response_mode` to fail fast, and the Milestone `mode-contract` task requires explicit `passthrough`, `passthrough+sideband`, and `transformed` routing tests.

Before active diff:

```go
// apps/edge/internal/openai/chat_handler.go:72
if passthroughResponseMode(runMeta) && routeUsesProviderTunnel(dispatch) {
    estimate := s.estimateChatInputTokens(basePrompt, runMeta, req.Tools, req.ToolChoice)
    contextClass := classifyContext(estimate, s.longContextThreshold())
    s.tunnelChatCompletionPassthrough(w, r, req, dispatch, runMeta, rawBody, estimate, contextClass)
    return
}
```

Current active diff shape:

```go
// apps/edge/internal/openai/chat_handler.go:47
responseMode, err := parseResponseMode(runMeta)
if err != nil {
    writeError(w, http.StatusBadRequest, "invalid_request_error", err.Error())
    return
}

// apps/edge/internal/openai/chat_handler.go:75
if responseMode == responseModePassthrough && routeUsesProviderTunnel(dispatch) {
```

#### 해결 방법

Keep `parseResponseMode` as the single source of truth. It must trim whitespace, default empty mode to `passthrough`, accept only `passthrough`, `passthrough+sideband`, `transformed`, and return an OpenAI-compatible invalid request error before route dispatch for unknown modes.

After target:

```go
// apps/edge/internal/openai/chat_handler.go:276
const (
    responseModeMetadataKey         = "iop_response_mode"
    responseModePassthrough         = "passthrough"
    responseModePassthroughSideband = "passthrough+sideband"
    responseModeTransformed         = "transformed"
)

func parseResponseMode(runMeta map[string]string) (string, error) {
    mode := strings.TrimSpace(runMeta[responseModeMetadataKey])
    if mode == "" {
        return responseModePassthrough, nil
    }
    switch mode {
    case responseModePassthrough, responseModePassthroughSideband, responseModeTransformed:
        return mode, nil
    default:
        return "", fmt.Errorf("metadata.%s must be one of %s, %s, or %s", responseModeMetadataKey, responseModePassthrough, responseModePassthroughSideband, responseModeTransformed)
    }
}
```

#### 수정 파일 및 체크리스트

- [ ] `apps/edge/internal/openai/chat_handler.go`: keep official constants, parser, and route guard.
- [ ] `apps/edge/internal/openai/server_test.go`: keep `TestChatCompletionsResponseModeRouting`; update expected sideband route after API-2 lands.

#### 테스트 작성

Write/update tests in `apps/edge/internal/openai/server_test.go`:

- `TestChatCompletionsResponseModeRouting`: omitted behavior remains covered by `TestChatCompletionsProviderPoolDispatch`; explicit `passthrough` uses tunnel; explicit `passthrough+sideband` follows API-2 path; explicit `transformed` follows normalized/transformed path; unknown `rawish` returns HTTP 400 with the allowed-mode message.

#### 중간 검증

```bash
go test -count=1 ./apps/edge/internal/openai -run 'TestChatCompletionsResponseModeRouting|TestChatCompletionsProviderPoolDispatch|TestChatCompletionsExplicitNonPassthroughModeKeepsNormalizedPath'
```

Expected: `ok`.

### [API-2] Sideband Overlay

#### 문제

`apps/edge/internal/openai/stream.go:309` implements pure tunnel passthrough and `apps/edge/internal/openai/stream.go:465` ignores usage frames so pure passthrough remains byte-identical. SDD S05/S06 require a separate explicit `passthrough+sideband` mode that can expose IOP observation through a contracted extension surface while never contaminating pure `passthrough`.

Current pure passthrough:

```go
// apps/edge/internal/openai/stream.go:309
// tunnelChatCompletionPassthrough serves a Chat Completions request over the
// raw provider tunnel (SDD S04/S06): provider status, headers, and body bytes
// are written to the caller byte-identically. No IOP sideband fields or events
// are added to the response body.
func (s *Server) tunnelChatCompletionPassthrough(... ) {
    ...
    metadata[responseModeMetadataKey] = responseModePassthrough
    ...
    s.writeProviderTunnelResponse(w, r, handle)
}

// apps/edge/internal/openai/stream.go:465
case iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_USAGE:
    // Usage frames are sideband observation candidates; pure passthrough
    // never merges them into the response body (SDD D04).
    continue
```

#### 해결 방법

Add an explicit sideband path selected only by `responseModePassthroughSideband`. Keep pure passthrough byte writer unchanged. For streaming Chat Completions, choose one contracted extension surface and test it end-to-end:

- Prefer SSE extension events only in `passthrough+sideband`, e.g. preserve provider `data:` body frames and append/emit `event: iop.sideband` with JSON observation for route/provider/usage/assembled summary.
- For non-streaming Chat Completions, wrap or annotate only under an explicit IOP extension schema, not as provider-original byte identity.
- Any sideband marker must include enough evidence for route/provider/assembled observation and must not appear in pure `passthrough` tests.

Example target sketch:

```go
// apps/edge/internal/openai/chat_handler.go:75
switch {
case responseMode == responseModePassthrough && routeUsesProviderTunnel(dispatch):
    s.tunnelChatCompletionPassthrough(...)
    return
case responseMode == responseModePassthroughSideband && routeUsesProviderTunnel(dispatch):
    s.tunnelChatCompletionPassthroughSideband(...)
    return
}
```

```go
// apps/edge/internal/openai/stream.go
func (s *Server) tunnelChatCompletionPassthroughSideband(...) {
    // Submit the same provider tunnel request with metadata mode passthrough+sideband.
    // Relay provider bytes through the contracted extension writer and emit
    // only explicit IOP sideband observations.
}
```

#### 수정 파일 및 체크리스트

- [ ] `apps/edge/internal/openai/chat_handler.go`: dispatch `responseModePassthroughSideband` to the new sideband path for provider tunnel routes only.
- [ ] `apps/edge/internal/openai/stream.go`: add the explicit sideband writer without changing `writeProviderTunnelResponse` pure byte writer semantics.
- [ ] `apps/edge/internal/openai/server_test.go`: add sideband fixture tests and pure passthrough negative assertions.
- [ ] `apps/edge/internal/service` / `apps/edge/internal/transport`: touch only if the sideband observation requires existing tunnel result metadata not currently exposed to Edge.

#### 테스트 작성

Write/update tests in `apps/edge/internal/openai/server_test.go`:

- `TestChatCompletionsPassthroughSidebandStreamingExposesIOPExtension`: provider SSE fixture plus usage/route observation emits provider content and explicit IOP sideband event/schema only in `passthrough+sideband`.
- `TestChatCompletionsPassthroughDoesNotExposeSideband`: pure passthrough fixture with usage frame does not contain `iop.sideband`, `iop_response_mode`, route/provider sideband marker, or any chosen extension key.
- Update `TestChatCompletionsResponseModeRouting` sideband case to expect the sideband path instead of the temporary normalized path.

#### 중간 검증

```bash
go test -count=1 ./apps/edge/internal/openai -run 'TestChatCompletionsPassthroughSideband|TestChatCompletionsPassthroughDoesNotExposeSideband|TestChatCompletionsResponseModeRouting|TestChatCompletionsPassthroughStreamingByteIdentity|TestChatCompletionsPassthroughNonStreamingByteIdentity'
```

Expected: `ok`.

### [API-3] Transform Output

#### 문제

Existing transformed-mode fixtures use the normalized RunEvent path, but SDD S07 and the Milestone `transform-output` task require the output to be explicitly identifiable as IOP transformed output and excluded from passthrough byte-identity claims. Without a label/metadata test, future changes can accidentally treat transformed output as provider-original bytes.

Relevant current behavior:

```go
// apps/edge/internal/openai/server_test.go:4399
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
    "model":"pool-model",
    "messages":[{"role":"user","content":"hello"}],
    "metadata":{"iop_response_mode":"transformed"}
}`))
...
if len(fake.tunnelReqsSnapshot()) != 0 {
    t.Error("explicit non-passthrough mode must not use the tunnel")
}
```

#### 해결 방법

Define a minimal transformed-output label that is visible in testable response metadata/header/body metadata without breaking OpenAI-compatible clients. Prefer a response header or metadata already accepted by local patterns over adding non-standard body fields to core OpenAI objects. The label must be present only for `transformed`, absent in pure passthrough, and not claimed as byte-identical provider output.

Example target sketch:

```go
// apps/edge/internal/openai/chat_handler.go
if responseMode == responseModeTransformed {
    metadata[responseModeMetadataKey] = responseModeTransformed
    // response writer or header marks output as transformed.
}
```

#### 수정 파일 및 체크리스트

- [ ] `apps/edge/internal/openai/chat_handler.go`: propagate explicit transformed mode into run metadata/output labeling.
- [ ] `apps/edge/internal/openai/stream.go`: if response headers are the chosen label surface, set them consistently for non-streaming and streaming transformed writers.
- [ ] `apps/edge/internal/openai/server_test.go`: assert transformed label/metadata and absence from pure passthrough.

#### 테스트 작성

Write/update tests in `apps/edge/internal/openai/server_test.go`:

- `TestChatCompletionsTransformedModeLabelsIOPOutput`: explicit transformed response includes the chosen IOP transformed label and uses normalized path.
- `TestChatCompletionsPassthroughDoesNotLabelTransformed`: pure passthrough response does not include the transformed label and remains byte-identical in existing passthrough fixtures.

#### 중간 검증

```bash
go test -count=1 ./apps/edge/internal/openai -run 'TestChatCompletionsTransformedModeLabelsIOPOutput|TestChatCompletionsPassthroughDoesNotLabelTransformed|TestChatCompletionsExplicitNonPassthroughModeKeepsNormalizedPath|TestChatCompletionsPassthroughNonStreamingByteIdentity|TestChatCompletionsPassthroughStreamingByteIdentity'
```

Expected: `ok`.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `apps/edge/internal/openai/chat_handler.go` | API-1, API-2, API-3 |
| `apps/edge/internal/openai/stream.go` | API-2, API-3 |
| `apps/edge/internal/openai/server_test.go` | API-1, API-2, API-3 |
| `apps/edge/internal/service/*` | API-2 only if sideband observation needs tunnel metadata access |
| `apps/edge/internal/transport/*` | API-2 only if sideband observation needs tunnel metadata access |

## 최종 검증

```bash
go test -count=1 ./apps/edge/internal/service ./apps/edge/internal/transport ./apps/edge/internal/openai
git diff --check
```

Expected: both commands pass. Go test cache output is not acceptable; keep `-count=1`.

모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
