<!-- task=m-usage-token-log-ops-mvp/02+01_usage_metrics plan=4 tag=REVIEW_REVIEW_REVIEW_USAGE_METRIC_RETRY -->

# Plan - REVIEW_REVIEW_REVIEW_USAGE_METRIC_RETRY

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

이 plan은 코드리뷰 FAIL의 좁은 후속 수정이다. active `CODE_REVIEW-local-G04.md`의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채우는 것이 마지막 단계다. 구현 후 active 파일을 유지하고 리뷰 준비 상태로 보고한다.

구현 중 직접 사용자에게 질문하지 않는다. 선택된 Milestone `구현 잠금 > 결정 필요` 항목이 실구현을 막을 때만 review stub의 `사용자 리뷰 요청` 섹션에 근거를 기록하고 멈춘다. 현재 후속 범위에는 사용자 결정이 필요한 항목이 없다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/operational-observability-provider-management/milestones/usage-token-log-ops-mvp.md`
- Milestone link: [Milestone 문서](agent-roadmap/phase/operational-observability-provider-management/milestones/usage-token-log-ops-mvp.md)
- Task ids:
  - `openai-scope`: 1차 metric 대상은 OpenAI-compatible 호출로 제한한다.
  - `passthrough-safe`: pure `passthrough` response body를 바꾸지 않고 Edge 내부 metric만 emit한다.
  - `token-breakdown`: input/output/reasoning/cached_input token type을 구분한다.
  - `reasoning-observed`: provider가 reasoning token을 보고하지 않으면 token 추정 없이 보조 metric만 남긴다.
  - `prometheus-metrics`: low-cardinality Prometheus counter와 label allowlist를 구현한다.
- Completion mode: check-on-pass

## Archive Evidence Snapshot

- Current archived plan: `agent-task/m-usage-token-log-ops-mvp/02+01_usage_metrics/plan_local_G04_3.log`
- Current archived review: `agent-task/m-usage-token-log-ops-mvp/02+01_usage_metrics/code_review_local_G04_3.log`
- Verdict: FAIL
- Required summary:
  - `apps/edge/internal/openai/stream.go:1081` and `apps/edge/internal/openai/stream.go:1201`: sideband `USAGE` frame handling uses `sidebandUsageFromFrame()` and preserves only input/output, so `iop.Usage.ReasoningTokens` and `CachedInputTokens` are dropped from metrics.
  - `apps/edge/internal/openai/stream.go:989` and `apps/edge/internal/openai/stream.go:1158`: terminal merge adds frame input/output on top of body-parsed usage, which can double-count input/output when body and `USAGE` frame describe the same provider usage.
  - `apps/edge/internal/openai/usage_metrics_test.go:663`, `apps/edge/internal/openai/usage_metrics_test.go:717`, `apps/edge/internal/openai/usage_metrics_test.go:736`, and `apps/edge/internal/openai/usage_metrics_test.go:782`: body-only sideband regression tests cover input/output only; they do not prove reasoning/cached_input breakdown required by the plan and SDD S06.
- Affected files:
  - `apps/edge/internal/openai/stream.go`
  - `apps/edge/internal/openai/usage_metrics_test.go`
- Verification evidence from failed loop:
  - Initial review rerun without `GOTMPDIR` failed before tests with `go: creating work dir: mkdir /tmp/go-build...: no space left on device`; `/tmp` is on a full root filesystem in this environment.
  - `GOTMPDIR=/config/workspace/iop/.cache/go-tmp GOCACHE=/config/workspace/iop/.cache/go-build go test -count=1 ./apps/edge/internal/openai -run 'Sideband|UsageMetrics|ToolValidation|Dispatch|Reasoning|Retry'` PASS.
  - `GOTMPDIR=/config/workspace/iop/.cache/go-tmp GOCACHE=/config/workspace/iop/.cache/go-build go test -count=1 -run 'TestProviderTunnelSideband.*BodyOnly' ./apps/edge/internal/openai/` PASS.
  - `GOTMPDIR=/config/workspace/iop/.cache/go-tmp GOCACHE=/config/workspace/iop/.cache/go-build go test -count=1 ./apps/edge/internal/openai` PASS.
  - `git diff --check` PASS.
  - `gofmt -l apps/edge/internal/openai/server_test.go apps/edge/internal/openai/usage_metrics_test.go apps/edge/internal/openai/stream.go apps/edge/internal/openai/usage_metrics.go` returned no files.
- Roadmap carryover: same Roadmap Targets remain claimed only after sideband provider body/proto usage merge preserves input/output/reasoning/cached_input without double-counting.
- Narrow reread allowed if needed: only the two archived files listed above.

## 범위 결정 근거

후속 범위는 sideband provider usage merge와 그 regression coverage에 한정한다. dispatch failure, cancel metric, pure passthrough body preservation, label allowlist, normalized response usage, and responses endpoint metering are outside this loop unless a direct compile/test dependency forces a small adjustment.

## 구현 체크리스트

- [x] `writeProviderTunnelSidebandStream`의 terminal metric merge를 full `usageObservation` 단위로 수정한다. provider body usage와 `ProviderTunnelFrameKind_USAGE`가 함께 있을 때 input/output을 중복 집계하지 않고, `reasoning`/`cached_input`은 body 또는 proto 중 보고된 값을 잃지 않아야 한다.
- [x] `writeProviderTunnelSidebandResponse`의 terminal metric merge를 같은 규칙으로 수정한다. `sidebandUsageObservation` response schema는 필요 이상 확장하지 않되, metric emit에는 `iop.Usage`의 reasoning/cached_input을 반영해야 한다.
- [x] sideband streaming body-only regression test가 provider body의 `prompt_tokens_details.cached_tokens`와 `completion_tokens_details.reasoning_tokens`를 포함하고 input/output/reasoning/cached_input token counters를 모두 검증하도록 보강한다.
- [x] sideband non-streaming body-only regression test가 provider body의 `prompt_tokens_details.cached_tokens`와 `completion_tokens_details.reasoning_tokens`를 포함하고 input/output/reasoning/cached_input token counters를 모두 검증하도록 보강한다.
- [x] `USAGE` frame만 있거나 body와 `USAGE` frame이 함께 있는 sideband regression을 추가해 proto reasoning/cached_input 보존과 input/output 중복 방지를 검증한다.
- [x] 최종 검증 명령을 실행한다.
- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [REVIEW_REVIEW_REVIEW_USAGE_METRIC_RETRY-1] Sideband Usage Merge Completeness

문제: sideband stream/non-stream metric path가 body-parsed usage와 `USAGE` frame usage를 같은 정보로 다루지 않는다. 현재 frame path는 input/output만 보존하고 reasoning/cached_input을 버리며, body usage와 frame usage가 동시에 있으면 input/output을 단순 가산한다.

해결 방법:
- `ProviderTunnelFrameKind_USAGE`에서 metric용 full `usageObservation`을 보관한다.
- merge helper를 두거나 동일한 작은 함수로 body observation과 proto observation을 합친다.
- 권장 규칙: body observation이 해당 token type을 보고했으면 body 값을 우선하고, body 값이 0이고 proto 값이 있으면 proto 값을 사용한다. 이 규칙은 provider body와 frame이 같은 provider usage를 중복 전달해도 input/output을 두 번 세지 않고, proto-only reasoning/cached_input은 보존한다.
- sideband JSON/SSE response schema의 existing `usage` observation은 input/output만 유지해도 된다. 이번 후속의 핵심은 Prometheus token metric breakdown이다.

중간 검증:

```bash
$ GOTMPDIR=/config/workspace/iop/.cache/go-tmp GOCACHE=/config/workspace/iop/.cache/go-build go test -count=1 ./apps/edge/internal/openai -run 'TestProviderTunnelSideband.*UsageMetrics'
=== RUN   TestProviderTunnelSidebandStreamingEmitsUsageMetrics
--- PASS: TestProviderTunnelSidebandStreamingEmitsUsageMetrics (0.00s)
=== RUN   TestProviderTunnelSidebandNonStreamingEmitsUsageMetrics
--- PASS: TestProviderTunnelSidebandNonStreamingEmitsUsageMetrics (0.00s)
=== RUN   TestProviderTunnelSidebandStreamingEmitsUsageMetricsBodyOnly
--- PASS: TestProviderTunnelSidebandStreamingEmitsUsageMetricsBodyOnly (0.00s)
=== RUN   TestProviderTunnelSidebandNonStreamingEmitsUsageMetricsBodyOnly
--- PASS: TestProviderTunnelSidebandNonStreamingEmitsUsageMetricsBodyOnly (0.00s)
PASS
ok  	iop/apps/edge/internal/openai	0.030s
```

## 최종 검증

```bash
$ GOTMPDIR=/config/workspace/iop/.cache/go-tmp GOCACHE=/config/workspace/iop/.cache/go-build go test -count=1 ./apps/edge/internal/openai -run 'Sideband|UsageMetrics|ToolValidation|Dispatch|Reasoning|Retry'

$ GOTMPDIR=/config/workspace/iop/.cache/go-tmp GOCACHE=/config/workspace/iop/.cache/go-build go test -count=1 ./apps/edge/internal/openai

$ git diff --check
```
