<!-- task=m-seulgivibe-openai-compatible-provider/03+01,02_responses_passthrough plan=2 tag=REVIEW_REVIEW_SEULGI_RESPONSES -->

# Implementation Plan - REVIEW_REVIEW_SEULGI_RESPONSES

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

`CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채우는 것이 필수 완료 조건이다. 구현 후 검증을 실행하고, 실제 stdout/stderr를 붙이고, active 파일은 그대로 둔 뒤 review ready로 보고한다. selected Milestone `구현 잠금 > 결정 필요` 항목이 실구현을 막는 경우에만 review stub의 `사용자 리뷰 요청`에 정확한 근거를 기록하고 멈춘다. 구현 중 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 외부 secret/service 준비, 검증 증거 공백, 일반 범위 조정은 사용자 리뷰 요청이 아니라 후속 plan 또는 검증 기록으로 처리한다. finalization은 code-review-skill 전용이다.

## 배경

이전 loop는 Responses passthrough metric labels를 endpoint=`responses`로 넘기도록 고쳤지만, regression test가 실제 Node tunnel relay와 다른 proto `USAGE` frame을 직접 주입했다. 현재 `openai_compat` tunnel은 provider body를 `BODY` frame으로만 relay하므로, `/v1/responses` provider body의 `usage.input_tokens`/`usage.output_tokens`를 Edge writer가 직접 관측해야 provider-reported token metrics가 실제 경로에서 유지된다.

## 사용자 리뷰 요청 흐름

선택된 Milestone lock decision이 실구현을 차단할 때만 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. 구현 에이전트는 직접 사용자에게 질문하지 않고, code-review가 그 요청을 검증해 `USER_REVIEW.md` 작성 여부를 결정한다.

## Archive Evidence Snapshot

- Current task path: `agent-task/m-seulgivibe-openai-compatible-provider/03+01,02_responses_passthrough`
- Archived loop logs:
  - Plan: `agent-task/m-seulgivibe-openai-compatible-provider/03+01,02_responses_passthrough/plan_cloud_G07_1.log`
  - Review: `agent-task/m-seulgivibe-openai-compatible-provider/03+01,02_responses_passthrough/code_review_cloud_G07_1.log`
- Verdict: FAIL
- Required summary:
  - `apps/edge/internal/openai/usage_metrics_test.go:251` injects a proto `USAGE` frame for Responses usage metrics.
  - Real `apps/node/internal/adapters/openai_compat/openai_compat.go:132`-`170` emits provider response `BODY` frames and then `END`, with no `USAGE` frame.
  - `apps/edge/internal/openai/stream.go:884`-`892` parses Chat usage keys only, so Responses body usage keys are not observed into token metrics.
- Verification evidence from this review:
  - `GOCACHE=/tmp/iop-gocache go test ./apps/edge/internal/openai -count=1` PASS.
  - `GOCACHE=/tmp/iop-gocache go test ./apps/node/internal/adapters/openai_compat -count=1` PASS.
  - `GOCACHE=/tmp/iop-gocache go test ./apps/edge/... -count=1` PASS.
  - stale unsupported text search PASS.
- Roadmap carryover: keep `responses-passthrough` as the only completion target.
- Narrow reread allowed when needed: only the two archived loop logs listed above; do not search `agent-task/archive/**` broadly.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/routing-policy-model-orchestration/milestones/seulgivibe-openai-compatible-provider.md`
- Milestone link: [Milestone 문서](agent-roadmap/phase/routing-policy-model-orchestration/milestones/seulgivibe-openai-compatible-provider.md)
- Task ids:
  - `responses-passthrough`: OpenAI-compatible provider route에서 `/v1/responses` raw passthrough가 동작
- Completion mode: check-on-pass

## 분석 결과

### 읽은 파일

- `agent-ops/rules/project/rules.md`
- `agent-ops/rules/common/rules-roadmap.md`
- `agent-roadmap/current.md`
- `agent-ops/skills/common/router.md`
- `agent-ops/skills/common/code-review/SKILL.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-ops/rules/project/domain/node/rules.md`
- `agent-ops/rules/project/domain/testing/rules.md`
- `agent-test/local/rules.md`
- `agent-test/local/edge-smoke.md`
- `agent-test/local/node-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/milestones/seulgivibe-openai-compatible-provider.md`
- `agent-roadmap/sdd/routing-policy-model-orchestration/seulgivibe-openai-compatible-provider/SDD.md`
- `apps/edge/internal/openai/stream.go`
- `apps/edge/internal/openai/usage_metrics_test.go`
- `apps/edge/internal/openai/responses_handler.go`
- `apps/edge/internal/openai/types.go`
- `apps/edge/internal/openai/server_test.go`
- `apps/node/internal/adapters/openai_compat/openai_compat.go`
- `apps/node/internal/adapters/openai_compat/openai_compat_test.go`
- `agent-task/m-seulgivibe-openai-compatible-provider/03+01,02_responses_passthrough/plan_cloud_G07_1.log`
- `agent-task/m-seulgivibe-openai-compatible-provider/03+01,02_responses_passthrough/code_review_cloud_G07_1.log`

### SDD 기준

- SDD: `agent-roadmap/sdd/routing-policy-model-orchestration/seulgivibe-openai-compatible-provider/SDD.md`
- 상태: `[승인됨]`
- SDD 잠금: 해제
- 대상 Acceptance Scenario:
  - S04 -> `responses-passthrough`: Codex-style `/v1/responses` payload를 provider route에서 strict normalized parser 없이 raw tunnel로 전달한다.
- Evidence Map:
  - S04 requires Edge OpenAI Responses tunnel tests and `go test ./apps/edge/internal/openai -count=1` evidence.
- 반영:
  - 이번 follow-up은 raw tunnel 자체가 아니라 S04 completion evidence의 usage metric trust를 닫는다.
  - `go test ./apps/edge/internal/openai -count=1`를 최종 evidence로 유지한다.

### 테스트 환경 규칙

- `test_env=local`.
- `agent-test/local/rules.md`, `agent-test/local/edge-smoke.md`, `agent-test/local/node-smoke.md`를 읽었다.
- 적용 규칙: Edge OpenAI-compatible 입력 표면 변경이므로 대상 package test와 `go test ./apps/edge/...`를 실행한다.
- 현재 worktree에는 Node adapter tunnel test 변경도 있으므로 `go test ./apps/node/internal/adapters/openai_compat -count=1`를 최종 검증에 포함한다.
- 외부 provider live smoke는 endpoint/credential이 필요하므로 이 follow-up의 필수 검증이 아니다.
- Go test cache output은 허용하지 않는다. 모든 Go 검증은 `-count=1`을 사용한다.

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

- 공백: `/v1/responses` provider passthrough가 실제 Node tunnel처럼 body-only frame으로 Responses usage JSON을 돌려줄 때 endpoint=`responses`, response_mode=`passthrough`, model_group=request alias, usage_source=`provider_reported`와 token counters가 증가하는 테스트가 없다.
- 공백: streaming Responses SSE에서 `response.completed`/root usage event가 body-only로 들어오는 경우의 token metric 관측 테스트가 없다.
- 기존 커버: Chat passthrough body-only usage, Chat sideband body/proto merge, Responses dispatch/body/auth/stream passthrough, Responses label-only proto usage frame regression.

### 심볼 참조

- renamed/removed symbol 없음.
- 변경 예정 call site:
  - `apps/edge/internal/openai/stream.go`: `writeProviderTunnelResponse` 내부 `providerChatAssembler` 생성 및 usage parsing.

### 분할 판단

- split decision policy를 평가했다.
- 단일 plan 유지: 하나의 Required이며, touch 예정 파일은 `apps/edge/internal/openai/stream.go`와 `apps/edge/internal/openai/usage_metrics_test.go`로 좁다.
- API/foundation과 broad rollout split 불필요: public contract 변경 없이 parser/test 보강만 한다.
- 서로 다른 검증 profile split 불필요: 모두 local Go test로 검증한다.
- predecessor `01`, `02`는 이전 loop에서 이미 만족된 상태로 본다. 이번 follow-up은 같은 split subtask `03+01,02_responses_passthrough`의 FAIL 수습이다.

### 범위 결정 근거

- 포함: Responses provider passthrough body/SSE usage 관측, targeted regression tests.
- 제외: provider response body 변형, sideband support 추가, Node tunnel `USAGE` frame 생산 변경, 외부 live provider smoke, contract/spec 문서 갱신.
- 이유: contract/spec 문구는 이미 Responses passthrough를 반영했고, 이번 Required는 실제 metric evidence trust 문제다.

### 빌드 등급

`local-G05`. 범위가 bounded이고 로컬 Go test로 deterministic하게 검증 가능하지만, shared passthrough writer의 usage parser를 건드리므로 G05로 둔다.

## 구현 체크리스트

- [ ] [REVIEW_REVIEW_SEULGI_RESPONSES-1] `/v1/responses` provider passthrough가 실제 body-only Responses usage JSON/SSE에서 provider-reported usage metrics를 관측하도록 고치고 regression tests를 추가한다.
- [ ] `GOCACHE=/tmp/iop-gocache go test ./apps/edge/internal/openai -run 'TestResponsesProviderTunnelPassthroughObservesUsageMetrics|TestResponsesProviderTunnelPassthroughStreamingObservesUsageMetrics|TestProviderTunnelPassthroughPreservesBodyAndObservesUsage' -count=1`를 실행한다.
- [ ] `GOCACHE=/tmp/iop-gocache go test ./apps/edge/internal/openai -count=1`를 실행한다.
- [ ] `GOCACHE=/tmp/iop-gocache go test ./apps/edge/... -count=1`를 실행한다.
- [ ] `GOCACHE=/tmp/iop-gocache go test ./apps/node/internal/adapters/openai_compat -count=1`를 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [REVIEW_REVIEW_SEULGI_RESPONSES-1] Responses Body Usage Metrics

#### 문제

`apps/edge/internal/openai/usage_metrics_test.go:251`는 Responses usage metric regression에서 proto `USAGE` frame을 직접 주입한다.

```go
// apps/edge/internal/openai/usage_metrics_test.go:251
frames := make(chan *iop.ProviderTunnelFrame, 5)
frames <- &iop.ProviderTunnelFrame{Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_RESPONSE_START, StatusCode: 200}
frames <- &iop.ProviderTunnelFrame{Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_BODY, Body: []byte(providerBody)}
frames <- &iop.ProviderTunnelFrame{
    Kind:  iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_USAGE,
    Usage: &iop.Usage{InputTokens: 9, OutputTokens: 6},
}
```

하지만 실제 `apps/node/internal/adapters/openai_compat/openai_compat.go:132`-`170`의 tunnel relay는 body chunks와 `END`만 emit한다. Edge body parser도 `apps/edge/internal/openai/stream.go:884`-`892`에서 Chat usage keys만 읽는다.

```go
// apps/edge/internal/openai/stream.go:884
type providerUsageEnvelope struct {
    PromptTokens        int `json:"prompt_tokens"`
    CompletionTokens    int `json:"completion_tokens"`
    PromptTokensDetails *struct {
        CachedTokens int `json:"cached_tokens"`
    } `json:"prompt_tokens_details"`
    CompletionTokensDetails *struct {
        ReasoningTokens int `json:"reasoning_tokens"`
    } `json:"completion_tokens_details"`
}
```

#### 해결 방법

`providerUsageEnvelope`가 Chat Completions usage와 Responses usage를 모두 관측하도록 확장한다. Body는 절대 변형하지 않는다.

After:

```go
type providerUsageEnvelope struct {
    PromptTokens     int `json:"prompt_tokens"`
    CompletionTokens int `json:"completion_tokens"`
    InputTokens      int `json:"input_tokens"`
    OutputTokens     int `json:"output_tokens"`
    PromptTokensDetails *struct {
        CachedTokens int `json:"cached_tokens"`
    } `json:"prompt_tokens_details"`
    CompletionTokensDetails *struct {
        ReasoningTokens int `json:"reasoning_tokens"`
    } `json:"completion_tokens_details"`
    InputTokensDetails *struct {
        CachedTokens int `json:"cached_tokens"`
    } `json:"input_tokens_details"`
    OutputTokensDetails *struct {
        ReasoningTokens int `json:"reasoning_tokens"`
    } `json:"output_tokens_details"`
}
```

`recordUsage`는 Chat keys를 우선하고 없으면 Responses keys를 사용한다. `consumeSSELine`은 root `usage`와 Responses streaming event의 nested `response.usage`를 모두 확인한다. Non-streaming `observation()`은 기존 root `usage` parse가 Responses keys까지 읽게 만들면 충분하다.

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

- [ ] `apps/edge/internal/openai/stream.go`: `providerUsageEnvelope`에 Responses usage fields를 추가한다.
- [ ] `apps/edge/internal/openai/stream.go`: `recordUsage`가 `prompt/completion` 또는 `input/output` usage를 `usageObservation`으로 변환한다.
- [ ] `apps/edge/internal/openai/stream.go`: streaming body parser가 Responses SSE의 nested `response.usage`를 관측한다.
- [ ] `apps/edge/internal/openai/usage_metrics_test.go`: `TestResponsesProviderTunnelPassthroughObservesUsageMetrics`를 body-only Responses usage fixture로 바꾼다.
- [ ] `apps/edge/internal/openai/usage_metrics_test.go`: streaming Responses body-only usage regression test `TestResponsesProviderTunnelPassthroughStreamingObservesUsageMetrics`를 추가한다.

#### 테스트 작성

- Update `TestResponsesProviderTunnelPassthroughObservesUsageMetrics`.
  - Fixture: `staticProviderTunnelFrames` 또는 equivalent body-only frames, no proto `USAGE` frame.
  - Provider body: Responses JSON with `usage.input_tokens`, `usage.output_tokens`, `usage.input_tokens_details.cached_tokens`, `usage.output_tokens_details.reasoning_tokens`.
  - Assertions: body preserved, request counter delta under endpoint=`responses`, response_mode=`passthrough`, model_group=request alias, usage_source=`provider_reported`; old wrong chat/empty-model counter unchanged; token counters input/output/reasoning/cached_input increment.
- Add `TestResponsesProviderTunnelPassthroughStreamingObservesUsageMetrics`.
  - Fixture: SSE body-only frames with `Content-Type: text/event-stream`, a Responses delta event, and a usage-bearing event such as `data: {"type":"response.completed","response":{"usage":{...}}}`.
  - Assertions: raw SSE body preserved, endpoint=`responses` request counter has provider_reported source, token counters increment, no proto `USAGE` frame is used.
- Keep existing Chat passthrough usage tests green.

#### 중간 검증

```bash
GOCACHE=/tmp/iop-gocache go test ./apps/edge/internal/openai -run 'TestResponsesProviderTunnelPassthroughObservesUsageMetrics|TestResponsesProviderTunnelPassthroughStreamingObservesUsageMetrics|TestProviderTunnelPassthroughPreservesBodyAndObservesUsage' -count=1
```

Expected: all listed tests pass with fresh execution.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `apps/edge/internal/openai/stream.go` | REVIEW_REVIEW_SEULGI_RESPONSES-1 |
| `apps/edge/internal/openai/usage_metrics_test.go` | REVIEW_REVIEW_SEULGI_RESPONSES-1 |

## 최종 검증

```bash
GOCACHE=/tmp/iop-gocache go test ./apps/edge/internal/openai -run 'TestResponsesProviderTunnelPassthroughObservesUsageMetrics|TestResponsesProviderTunnelPassthroughStreamingObservesUsageMetrics|TestProviderTunnelPassthroughPreservesBodyAndObservesUsage' -count=1
GOCACHE=/tmp/iop-gocache go test ./apps/edge/internal/openai -count=1
GOCACHE=/tmp/iop-gocache go test ./apps/edge/... -count=1
GOCACHE=/tmp/iop-gocache go test ./apps/node/internal/adapters/openai_compat -count=1
```

Expected: all commands pass with fresh execution; cached output is not acceptable for the Go commands.

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