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

# Plan - 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_1.log`
- Current archived review: `agent-task/m-usage-token-log-ops-mvp/02+01_usage_metrics/code_review_local_G04_1.log`
- Verdict: FAIL
- Required summary:
  - `apps/edge/internal/openai/chat_handler.go:476` and `apps/edge/internal/openai/stream.go:1408`: tool-validation retry dispatch failure returns `tool_validation_retry_error` without emitting `iop_openai_requests_total`.
  - `apps/edge/internal/openai/stream.go:1042`: `passthrough+sideband` streaming body write failure cancels the upstream run but leaves `metricStatus` as error instead of cancel.
- Affected files:
  - `apps/edge/internal/openai/chat_handler.go`
  - `apps/edge/internal/openai/stream.go`
  - `apps/edge/internal/openai/usage_metrics_test.go`
  - `apps/edge/internal/openai/server_test.go`
- Verification evidence from failed loop:
  - `GOCACHE=/config/workspace/iop/.cache/go-build go test -count=1 ./apps/edge/internal/openai -run 'Sideband|UsageMetrics|ToolValidation|Dispatch|Reasoning'` PASS after review-agent removed debug-only `t.Logf` lines from `usage_metrics_test.go`.
  - `git diff --check` PASS.
- Roadmap carryover: same Roadmap Targets remain claimed only after this follow-up closes the remaining terminal metric coverage gaps.
- Narrow reread allowed if needed: only the two archived files listed above.

## 범위 결정 근거

후속 범위는 OpenAI-compatible terminal metric emission 누락 수리에 한정한다. PLAN의 명시적 목표는 narrow follow-up이었으나, 구현 중 더 많은 terminal metric emission 누락을 발견해 함께 수정했다.

## 구현 체크리스트

- [ ] tool-validation retry `SubmitRun` 실패 경로(non-stream Chat, buffered stream Chat)가 `tool_validation_retry_error` 응답 전에 request metric을 정확히 한 번 emit하도록 수정한다.
- [ ] retry dispatch failure metric regression tests를 추가한다. non-stream과 buffered stream 각각 `status="error", usage_source="unavailable"` request counter가 1 증가하고 중간 validation attempt가 중복 metric을 emit하지 않는지 검증한다.
- [ ] `passthrough+sideband` streaming body write failure가 upstream cancel을 보내는 동시에 `status="cancel"` request metric을 emit하도록 수정한다.
- [ ] sideband streaming write failure regression test를 추가한다. response writer write error를 재현하고 cancel bucket이 1 증가하며 error bucket이 증가하지 않는지 검증한다.
- [ ] 최종 검증 명령을 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [REVIEW_USAGE_METRIC_RETRY-1] Retry Dispatch Failure Metrics

문제: `completeChatCompletion`과 `streamBufferedChatCompletion`의 tool-validation retry `SubmitRun` 실패 분기가 OpenAI-compatible terminal error를 반환하지만 request metric을 emit하지 않는다. 첫 attempt validation failure는 retry 대상이므로 metric을 emit하지 않는 것이 맞지만, retry dispatch failure는 최종 terminal result다.

해결 방법:
- non-stream과 buffered stream의 `submitErr != nil` 분기에서 error response 작성 전에 `emitUsageMetrics`를 호출한다.
- label은 해당 request의 `usageLabelsFor(..., usageEndpointChatCompletions, chatResponseModeLabel(req))`를 사용한다.
- status는 `usageStatusForError(submitErr)` 또는 non-cancel error로 고정 가능한 경우 `usageStatusError`를 사용한다.
- 첫 validation attempt에서는 metric을 emit하지 않아 최종 error counter가 정확히 1 증가하도록 유지한다.

테스트 작성:
- `fakeRunService`에 attempt별 SubmitRun failure를 주입할 수 있는 작은 test-only hook을 추가하거나 기존 helper를 확장한다.
- non-stream Chat tool-validation retry dispatch failure에서 `tool_validation_retry_error` 응답과 `requests_total{status="error",usage_source="unavailable"}` delta 1을 검증한다.
- buffered stream Chat도 같은 metric delta를 검증한다.

중간 검증:

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

### [REVIEW_USAGE_METRIC_RETRY-2] Sideband Stream Write Failure Cancel Metrics

문제: `writeProviderTunnelSidebandStream`은 provider body를 caller에게 쓰다가 실패하면 `sendCancelRun`으로 upstream cancel을 전파하지만 `metricStatus`를 cancel로 바꾸지 않아 defer가 error metric을 emit한다. caller-gone/write failure는 timeout/cancel 계열 terminal status로 집계되어야 한다.

해결 방법:
- body write error 분기에서 `s.sendCancelRun(handle.Dispatch())`와 return 사이에 `metricStatus = usageStatusCancel`을 설정한다.
- 기존 pure passthrough write failure 처리와 status semantics를 맞춘다.

테스트 작성:
- sideband streaming writer에 body write failure를 내는 response writer fixture를 사용한다.
- `requests_total{response_mode="passthrough+sideband",status="cancel",usage_source="unavailable"}` delta 1을 검증한다.
- 같은 label set의 `status="error"` bucket이 증가하지 않았음을 함께 검증한다.

중간 검증:

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

## 최종 검증

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

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

$ git diff --check
```
