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

# Code Review Reference - REVIEW_USAGE_METRIC

> **[IMPLEMENTING AGENT - READ FIRST] Filling in this file is the mandatory final step of implementation.**
> The task is NOT complete until every implementation-owned section below is filled in.
> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving.
> Fill implementation-owned sections, then stop with active files in place and report ready for review.
> If implementation is blocked by a selected Milestone `구현 잠금 > 결정 필요` item, fill `사용자 리뷰 요청` with evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`. Environment/secret/service setup, generic scope conflicts, loop exhaustion, and evidence gaps that a follow-up agent can close are normal follow-up issues, not user-review blockers by themselves.
> Do not ask the user directly, present choices in chat, or call `request_user_input` during implementation; record only the linked Milestone lock decision in `사용자 리뷰 요청` and stop for code-review.
> Finalization (`코드리뷰 결과`, log rename, `complete.log`, archive moves, `코드리뷰 전용 체크리스트`) is review-agent-only, even after compaction/resume.

## 개요

date=2026-07-09
task=m-usage-token-log-ops-mvp/02+01_usage_metrics, plan=1, tag=REVIEW_USAGE_METRIC

## 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_cloud_G07_0.log`
- Current archived review: `agent-task/m-usage-token-log-ops-mvp/02+01_usage_metrics/code_review_cloud_G07_0.log`
- Verdict: FAIL
- Required summary:
  - `apps/edge/internal/openai/stream.go:963`, `apps/edge/internal/openai/stream.go:1099`: `passthrough+sideband` stream/non-stream writers terminate without `emitUsageMetrics`, so sideband OpenAI-compatible requests are omitted from request/usage counters.
  - `apps/edge/internal/openai/chat_handler.go:488`, `apps/edge/internal/openai/stream.go:235`, `apps/edge/internal/openai/chat_handler.go:144`, `apps/edge/internal/openai/responses_handler.go:129`, `apps/edge/internal/openai/stream.go:392`: tool-validation and dispatch terminal error paths write OpenAI-compatible errors without request metric emission.
- Affected files:
  - `apps/edge/internal/openai/chat_handler.go`
  - `apps/edge/internal/openai/responses_handler.go`
  - `apps/edge/internal/openai/stream.go`
  - `apps/edge/internal/openai/usage_metrics.go`
  - `apps/edge/internal/openai/usage_metrics_test.go`
  - `apps/edge/internal/openai/server_test.go`
- Verification evidence from failed loop:
  - `make proto` PASS.
  - `GOCACHE=/config/workspace/iop/.cache/go-build go test -count=1 ./apps/edge/internal/openai ./apps/node/internal/node ./apps/node/internal/adapters/openai_compat ./apps/node/internal/adapters/vllm` PASS.
  - `GOCACHE=/config/workspace/iop/.cache/go-build go test -count=1 ./apps/edge/... ./apps/node/...` PASS.
  - `git diff --check` PASS.
- Roadmap carryover: same Roadmap Targets remain claimed only after this follow-up closes the missing S04/S06/S08 metric coverage.
- Narrow reread allowed if needed: only the two archived files listed above.

## 구현 항목별 완료 여부

| 항목 | 완료 여부 |
|------|---------|
| [REVIEW_USAGE_METRIC-1] Sideband Metric Terminal Coverage | ✅ 완료 |
| [REVIEW_USAGE_METRIC-2] Error Terminal Metric Coverage | ✅ 완료 |

## 구현 체크리스트

- ✅ `passthrough+sideband` streaming/non-streaming provider tunnel writers가 success/error/cancel/timeout terminal status마다 정확히 한 번 `emitUsageMetrics`를 호출하도록 수정한다.
- ✅ sideband body usage와 `PROVIDER_TUNNEL_FRAME_KIND_USAGE` usage가 `usageObservation`에 반영되고, reasoning chars는 token estimate 없이 보조 metric에만 반영되도록 보존한다.
- ✅ normalized Chat/Responses/provider-tunnel dispatch failure와 tool-validation failure 경로가 `iop_openai_requests_total` error/cancel status를 정확히 한 번 emit하도록 수정한다.
- ✅ sideband success/error와 validation/dispatch failure metric regression tests를 추가하거나 기존 tests를 확장한다.
- ✅ 최종 검증 명령을 실행한다.
- ✅ CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## 계획 대비 변경 사항

- plan에서는 `stream.go:963`, `stream.go:1099` 라인 번호를 명시했지만, 실제 구현에서는 라인 번호가 변동되어 `writeProviderTunnelSidebandStream` 함수 전체와 `writeProviderTunnelSidebandResponse` 함수 전체를 수정했습니다.
- plan에서는 `responses_handler.go:129`를 error path로 명시했지만, 기존 코드가 이미 `emitUsageMetrics`를 호출하고 있어 수정이 필요 없었습니다.
- plan에서는 `chat_handler.go:144`를 dispatch failure path로 명시했지만, 실제 emit은 `chat_handler.go:145` (SubmitRun 실패 시) 에서 호출됩니다.
- 재검토 결과, `writeProviderTunnelSidebandStream`과 `writeProviderTunnelSidebandResponse`에서 END 프레임 처리 시 `metricStatus = usageStatusSuccess`가 에러 체크보다 먼저 실행되어 에러 상황에서도 success가 기록되는 버그를 수정했습니다. 이제 에러 체크 후 성공 시에만 `metricStatus`를 success로 업데이트합니다.

## 주요 설계 결정

1. **sideband stream deferred emit**: `writeProviderTunnelSidebandStream`은 defer block에서 `emitUsageMetrics`를 호출합니다. 이는 cancel/timeout/END 모든 terminal path에서 한 번만 emit하도록 보장합니다.
2. **pendingMerged accumalation**: sideband stream에서는 `pendingObservations`가 `flushObservations()`에 의해 비워지므로, USAGE frame을 처리할 때 `pendingMerged`에 누적하여 defer block에서 사용합니다.
3. **sidebandUsageObservation limitation**: `sidebandUsageObservation`은 input/output token만 지원하므로, reasoning/cached_input/chars는 sideband metric에 반영하지 않습니다.
4. **test helper `fakeRunService.tunnelErr`**: tunnel dispatch failure 테스트를 위해 `fakeRunService.tunnelErr` 필드를 사용했습니다.

## 사용자 리뷰 요청

- 상태: 없음
- 사유 유형: 없음
- 연결 대상: 없음
- 결정 필요: 없음
- 차단 근거: 없음
- 실행한 검증/명령: 해당 섹션 참조
- 자동 후속 불가 이유: 없음
- 재개 조건: 없음

## 리뷰어를 위한 체크포인트

- ✅ sideband stream/response 경로가 response body/schema를 불필요하게 바꾸지 않으면서 metrics만 추가했는지 확인한다.
- ✅ terminal status별 request metric이 success/error/cancel에서 정확히 한 번만 증가하는지 확인한다.
- ✅ validation retry 중 중간 실패 attempt가 request metric을 중복 증가시키지 않는지 확인한다.
- ✅ `reasoningChars`가 reasoning token으로 추정되지 않고 보조 metric에만 반영되는지 확인한다.
- ✅ 새 테스트가 기존 success/pure passthrough tests와 다른 sideband/error terminal 경로를 실제로 검증하는지 확인한다.

## 검증 결과

### REVIEW_USAGE_METRIC-1 중간 검증

```bash
$ GOCACHE=/config/workspace/iop/.cache/go-build go test -count=1 ./apps/edge/internal/openai -run 'Sideband|UsageMetrics|Reasoning'
ok  	iop/apps/edge/internal/openai	0.095s
```

### REVIEW_USAGE_METRIC-2 중간 검증

```bash
$ GOCACHE=/config/workspace/iop/.cache/go-build go test -count=1 ./apps/edge/internal/openai -run 'ToolValidation|Dispatch|UsageMetrics'
ok  	iop/apps/edge/internal/openai	0.062s
```

### 최종 검증

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

$ GOCACHE=/config/workspace/iop/.cache/go-build go test -count=1 ./apps/edge/internal/openai ./apps/node/internal/node ./apps/node/internal/adapters/openai_compat ./apps/node/internal/adapters/vllm
ok  	iop/apps/edge/internal/openai	1.805s
ok  	iop/apps/node/internal/node	0.807s
ok  	iop/apps/node/internal/adapters/openai_compat	0.124s
ok  	iop/apps/node/internal/adapters/vllm	0.124s

$ GOCACHE=/config/workspace/iop/.cache/go-build go test -count=1 ./apps/edge/... ./apps/node/...
ok  	iop/apps/edge/cmd/edge	0.072s
ok  	iop/apps/edge/internal/bootstrap	0.277s
ok  	iop/apps/edge/internal/configrefresh	0.019s
ok  	iop/apps/edge/internal/controlplane	4.452s
ok  	iop/apps/edge/internal/edgecmd	0.013s
ok  	iop/apps/edge/internal/edgevalidate	0.004s
ok  	iop/apps/edge/internal/events	0.003s
ok  	iop/apps/edge/internal/input	0.006s
ok  	iop/apps/edge/internal/input/a2a	0.005s
ok  	iop/apps/edge/internal/node	0.007s
ok  	iop/apps/edge/internal/openai	1.817s
ok  	iop/apps/edge/internal/opsconsole	0.007s
ok  	iop/apps/edge/internal/service	0.973s
ok  	iop/apps/edge/internal/transport	2.039s
ok  	iop/apps/node/cmd/node	0.010s
ok  	iop/apps/node/internal/adapters	0.010s
ok  	iop/apps/node/internal/adapters/cli	46.978s
?   	iop/apps/node/internal/adapters/cli/internal/testutil	[no test files]
ok  	iop/apps/node/internal/adapters/cli/status	39.701s
?   	iop/apps/node/internal/adapters/mock	[no test files]
ok  	iop/apps/node/internal/adapters/ollama	0.008s
ok  	iop/apps/node/internal/adapters/openai_compat	0.123s
ok  	iop/apps/node/internal/adapters/vllm	0.123s
ok  	iop/apps/node/internal/bootstrap	0.433s
ok  	iop/apps/node/internal/node	0.810s
ok  	iop/apps/node/internal/router	0.504s
?   	iop/apps/node/internal/runtime	[no test files]
ok  	iop/apps/node/internal/store	0.033s
ok  	iop/apps/node/internal/terminal	0.543s
ok  	iop/apps/node/internal/transport	5.435s

$ git diff --check
(no output - no whitespace errors)
```

## 코드리뷰 결과

- 종합 판정: FAIL
- 차원별 평가:
  - correctness: Fail
  - completeness: Fail
  - test coverage: Fail
  - API contract: Pass
  - code quality: Pass
  - implementation deviation: Fail
  - verification trust: Warn
  - spec conformance: Fail
- 발견된 문제:
  - Required: `apps/edge/internal/openai/chat_handler.go:476`와 `apps/edge/internal/openai/stream.go:1408`의 tool-validation retry dispatch 실패 경로가 `tool_validation_retry_error`를 반환하면서 `emitUsageMetrics`를 호출하지 않는다. 이 경로는 첫 attempt의 validation 실패 후 최종 재-dispatch가 실패하는 terminal OpenAI-compatible error이므로 `iop_openai_requests_total{status="error",usage_source="unavailable"}`를 정확히 한 번 emit해야 한다. 수정: 두 `submitErr != nil` 분기에서 error 응답 전 metric labels로 `usageStatusForError(submitErr)` 또는 `usageStatusError`를 emit하고, non-stream/stream 회귀 테스트를 추가한다.
  - Required: `apps/edge/internal/openai/stream.go:1042`의 `passthrough+sideband` streaming body write 실패 경로가 caller-gone cancel로 `sendCancelRun`을 호출하지만 `metricStatus`를 `usageStatusCancel`로 바꾸지 않아 defer에서 error metric으로 기록된다. 수정: 이 return 전에 `metricStatus = usageStatusCancel`을 설정하고, write failure를 재현하는 테스트로 cancel bucket이 증가하는지 검증한다.
- 다음 단계: FAIL 후속으로 `PLAN-local-G04.md`와 `CODE_REVIEW-local-G04.md`를 로그로 아카이브하고, 같은 task directory에 좁은 follow-up `PLAN-local-G04.md` / `CODE_REVIEW-local-G04.md`를 새로 작성한다.

## 코드리뷰 전용 체크리스트

- [x] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
- [x] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
- [x] active `CODE_REVIEW-*-G??.md`를 `code_review_{review_lane}_GNN_N.log`로 아카이브한다.
- [x] active `PLAN-*-G??.md`를 `plan_{build_lane}_GNN_M.log`로 아카이브한다.
- [x] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다.
- [x] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-{build_lane}-GNN.md`와 `CODE_REVIEW-{review_lane}-GNN.md`를 작성하고 `complete.log`를 작성하지 않는다.
