ㅑ<!-- task=m-usage-token-log-ops-mvp/02+01_usage_metrics plan=0 tag=USAGE_METRIC -->

# Plan - USAGE_METRIC

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

`CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채우는 것이 마지막 단계다. 구현 후 active 파일을 유지하고 리뷰 준비 상태로 보고한다. 선택된 Milestone `구현 잠금 > 결정 필요` 항목이 실구현을 막을 때만 review stub의 `사용자 리뷰 요청`을 채우고 멈춘다. 직접 사용자에게 질문하거나 `USER_REVIEW.md`, `complete.log`, archive log를 만들지 않는다. 환경/secret/service 차단과 검증 증거 공백은 일반 후속 이슈로 기록한다.

## 배경

이 plan은 OpenAI-compatible 사용량 측정의 핵심 구현이다. 선행 `01_identity_alias_auth`가 request context에 `principal_ref`, `principal_alias`, `token_ref`, `edge_id` 후보를 전달할 수 있게 만든 뒤, 이 plan은 OpenAI-compatible 경로만 metric 대상으로 삼고, pure passthrough body를 바꾸지 않으며, provider/runtime usage를 Prometheus counter로 노출한다.

## 선행 의존성

- 선행 subtask: `agent-task/m-usage-token-log-ops-mvp/01_identity_alias_auth`
- 시작 조건: active 경로 또는 같은 task group archive 후보의 선행 `complete.log`가 존재하고, `identity-scope`/`token-alias`가 code-review PASS로 완료되어 있어야 한다.
- 선행 완료 전에는 이 plan을 구현하지 말고 active 파일만 유지한다.

## 사용자 리뷰 요청 흐름

구현 중 직접 사용자 프롬프트를 띄우지 않는다. 선택된 Milestone lock decision만 active review stub의 `사용자 리뷰 요청` 섹션에 기록하며, code-review가 검증 후 실제 `USER_REVIEW.md` 작성 여부를 결정한다.

## 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`, provider-reported `reasoning`, provider-reported `cached_input` token type을 구분한다.
  - `reasoning-observed`: provider가 reasoning token을 보고하지 않으면 token 추정 없이 관측 여부와 character count만 남긴다.
  - `prometheus-metrics`: low-cardinality Prometheus counter와 label allowlist를 구현한다.
- Completion mode: check-on-pass

## 분석 결과

### 읽은 파일

- `agent-roadmap/phase/operational-observability-provider-management/PHASE.md`
- `agent-roadmap/phase/operational-observability-provider-management/milestones/usage-token-log-ops-mvp.md`
- `agent-roadmap/sdd/operational-observability-provider-management/usage-token-log-ops-mvp/SDD.md`
- `agent-contract/outer/openai-compatible-api.md`
- `agent-contract/inner/edge-node-runtime-wire.md`
- `agent-contract/inner/edge-config-runtime-refresh.md`
- `agent-spec/input/openai-compatible-surface.md`
- `agent-spec/runtime/edge-node-execution.md`
- `agent-spec/runtime/provider-pool-config-refresh.md`
- `agent-ops/rules/project/domain/edge/rules.md`
- `agent-ops/rules/project/domain/node/rules.md`
- `agent-ops/rules/project/domain/platform-common/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-test/local/platform-common-smoke.md`
- `proto/iop/runtime.proto`
- `packages/go/observability/observability.go`
- `apps/edge/internal/input/manager.go`
- `apps/edge/internal/bootstrap/runtime.go`
- `apps/edge/internal/openai/server.go`
- `apps/edge/internal/openai/chat_handler.go`
- `apps/edge/internal/openai/responses_handler.go`
- `apps/edge/internal/openai/run_result.go`
- `apps/edge/internal/openai/stream.go`
- `apps/edge/internal/openai/types.go`
- `apps/edge/internal/openai/server_test.go`
- `apps/node/internal/runtime/types.go`
- `apps/node/internal/node/node.go`
- `apps/node/internal/adapters/openai_compat/openai_compat.go`
- `apps/node/internal/adapters/vllm/vllm.go`

### SDD 기준

- SDD: `agent-roadmap/sdd/operational-observability-provider-management/usage-token-log-ops-mvp/SDD.md`, 상태 `[승인됨]`, SDD 잠금 `해제`.
- 대상 Acceptance Scenario: S04(`openai-scope`), S05(`passthrough-safe`), S06(`token-breakdown`), S07(`reasoning-observed`), S08(`prometheus-metrics`).
- Evidence Map은 OpenAI-compatible route만 metric 대상인 근거, passthrough body 불변성, token type별 metric test, reasoning token 미추정 test, Prometheus label allowlist/secret 제외 test를 요구한다.

### 테스트 환경 규칙

- test_env: `local`.
- 읽은 규칙: `agent-test/local/rules.md`, `agent-test/local/edge-smoke.md`, `agent-test/local/node-smoke.md`, `agent-test/local/platform-common-smoke.md`.
- proto 변경 시 `make proto`를 실행한다.
- 적용 명령:

```bash
make proto
go test -count=1 ./apps/edge/internal/openai ./apps/node/internal/node ./apps/node/internal/adapters/openai_compat ./apps/node/internal/adapters/vllm
go test -count=1 ./apps/edge/... ./apps/node/...
git diff --check
```

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

- `proto/iop/runtime.proto`의 `Usage`는 현재 `input_tokens`, `output_tokens`만 가진다.
- Edge `openAIUsage`와 Node `UsageStats`도 input/output만 보존한다.
- provider tunnel pure passthrough는 raw bytes를 유지하지만 provider body/SSE usage를 metric으로 관측하는 test가 없다.
- OpenAI-compatible provider normal path의 `prompt_tokens_details.cached_tokens`, `completion_tokens_details.reasoning_tokens` parsing test가 없다.
- custom Prometheus collector, label allowlist, secret/high-cardinality exclusion test가 없다.

### 심볼 참조

- 변경 대상 후보:
  - [runtime.proto](/config/workspace/iop/proto/iop/runtime.proto:113)
  - [types.go](/config/workspace/iop/apps/node/internal/runtime/types.go:71)
  - [node.go](/config/workspace/iop/apps/node/internal/node/node.go:325)
  - [run_result.go](/config/workspace/iop/apps/edge/internal/openai/run_result.go:77)
  - [stream.go](/config/workspace/iop/apps/edge/internal/openai/stream.go:551)
  - [openai_compat.go](/config/workspace/iop/apps/node/internal/adapters/openai_compat/openai_compat.go:349)
  - [vllm.go](/config/workspace/iop/apps/node/internal/adapters/vllm/vllm.go:318)
  - [observability.go](/config/workspace/iop/packages/go/observability/observability.go:1)
  - [manager.go](/config/workspace/iop/apps/edge/internal/input/manager.go:21)
- 새 helper 후보: `usageObservation`, `providerUsageFromJSON`, `providerUsageFromSSEData`, `openAIUsageMetrics`, `emitUsageMetrics`, `usageLabelValues`.

### 분할 판단

- split policy를 먼저 적용했다.
- shared task group: `m-usage-token-log-ops-mvp`.
- sibling subtasks:
  - `01_identity_alias_auth`: predecessor. 아직 active plan 상태면 이 plan 구현을 시작하지 않는다.
  - `02+01_usage_metrics`: 이 plan. proto/usage observation/metrics ownership.
  - `03+02_grafana_rollup_docs`: metric names/labels가 확정된 뒤 docs/query를 작성한다.
- 이 plan은 여러 파일을 건드리지만 하나의 runtime behavior slice다. Grafana 문서와 limit follow-up은 03으로 분리해 code/proto/metric 구현에 집중한다.

### 범위 결정 근거

- 포함: OpenAI-compatible `/v1/chat/completions`와 `/v1/responses` normalized runtime 경로, provider tunnel chat passthrough 경로의 usage 관측.
- 포함: `edge.id`를 OpenAI server에 전달해 `edge_id` label 후보로 사용한다. `apps/edge/internal/input/manager.go`에서 `cfg.Edge.ID`를 `openai.Server`에 주입하는 방식이 가장 작다.
- 포함: Prometheus counter label allowlist. 후보 labels는 `edge_id`, `principal_ref`, `principal_alias`, `token_ref`, `model_group`, `endpoint`, `response_mode`, `token_type`, `usage_source`, `status`다.
- 제외: CLI usage status, A2A, local-only adapter usage, Control Plane/Client dashboard, request-level ledger, Loki, token limit enforcement, 가격/ROI 금액 산출.
- 제외: pure passthrough response body 변경. usage frame이나 IOP sideband event를 provider body에 섞지 않는다.

### 빌드 등급

- `cloud-G07`: proto, Edge/Node runtime, OpenAI passthrough stream, Prometheus metric을 함께 바꾸는 cross-runtime 작업이다. 외부 API body는 유지해야 하지만 runtime wire와 metric schema 영향이 크다.

## 구현 체크리스트

- [ ] 선행 `01_identity_alias_auth`의 active 또는 same-task-group archive `complete.log`와 PASS evidence를 확인한다.
- [ ] Runtime usage schema를 `reasoning_tokens`, `cached_input_tokens`까지 확장하고 Go generated code를 갱신한다.
- [ ] Node adapters와 Edge usage mapping이 input/output/reasoning/cached_input을 보존하도록 수정한다.
- [ ] pure passthrough response body를 바꾸지 않는 provider usage 관측 경로를 추가한다.
- [ ] OpenAI-compatible 경로 전용 Prometheus collectors와 label allowlist test를 추가한다.
- [ ] reasoning token 미보고 시 token 추정 없이 보조 metric만 emit하는 test를 추가한다.
- [ ] 최종 검증 명령을 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [USAGE_METRIC-1] Runtime Usage Schema

문제: `Usage`/`UsageStats`/OpenAI usage response type은 input/output만 보존한다. provider가 보고하는 cached input 또는 reasoning token을 metric까지 전달할 수 없다.

수정 파일 및 체크리스트:
- [ ] `proto/iop/runtime.proto`: `Usage`에 `reasoning_tokens`, `cached_input_tokens`를 optional-compatible scalar field로 추가한다.
- [ ] `proto/gen/iop/runtime.pb.go`: `make proto`로 재생성한다.
- [ ] `apps/node/internal/runtime/types.go`: `UsageStats`에 `ReasoningTokens`, `CachedInputTokens` 추가.
- [ ] `apps/node/internal/node/node.go`: session sink와 provider tunnel sink의 proto mapping을 확장한다.
- [ ] `apps/edge/internal/openai/run_result.go`, `types.go`: OpenAI-compatible response usage 구조와 mapping을 확장한다.

테스트 작성:
- runtime usage mapping 단위 test가 있으면 확장하고, 없으면 가장 가까운 existing Edge/Node package test에 regression case를 추가한다.

중간 검증:

```bash
make proto
go test -count=1 ./apps/node/internal/node ./apps/edge/internal/openai
```

### [USAGE_METRIC-2] Provider Usage Parsing

문제: OpenAI-compatible provider payload에는 `usage.prompt_tokens_details.cached_tokens`와 `usage.completion_tokens_details.reasoning_tokens`가 있을 수 있지만 현재 adapter와 Edge stream assembler는 이를 metric용 관측값으로 보존하지 않는다.

해결 방법:
- normalized runtime path에서 `openai_compat`와 `vllm` adapter가 provider usage details를 `UsageStats`에 채운다.
- raw provider tunnel path에서는 response body를 변경하지 않고 Edge stream/non-stream processing 중 provider usage JSON/SSE data를 관측해 내부 `usageObservation`으로 저장한다.
- `usage_source`는 provider가 해당 token type을 보고한 경우 `provider_reported`, 값이 없거나 알 수 없으면 `unavailable`로 둔다.

수정 파일 및 체크리스트:
- [ ] `apps/node/internal/adapters/openai_compat/openai_compat.go`: stream complete event와 non-stream response usage details parsing.
- [ ] `apps/node/internal/adapters/vllm/vllm.go`: OpenAI-compatible usage details parsing.
- [ ] `apps/edge/internal/openai/stream.go`: pure passthrough body를 그대로 relay하면서 provider usage를 관측하는 parser/assembler 확장.
- [ ] `apps/edge/internal/openai/server_test.go`: streaming/non-stream passthrough body byte-equivalence와 usage observation test.

테스트 작성:
- `TestProviderTunnelPassthroughPreservesBodyAndObservesUsage`.
- `TestProviderUsageParsesReasoningAndCachedInputTokens`.

중간 검증:

```bash
go test -count=1 ./apps/node/internal/adapters/openai_compat ./apps/node/internal/adapters/vllm ./apps/edge/internal/openai -run 'Usage|Passthrough|Reasoning|Cached'
```

### [USAGE_METRIC-3] Prometheus Metric Emit

문제: metrics endpoint는 promhttp만 제공하고 OpenAI-compatible usage/request counters가 없다.

해결 방법:
- `apps/edge/internal/openai/usage_metrics.go`를 추가해 package-level Prometheus collectors를 등록한다.
- `NewServer`에 `edge_id`를 주입할 수 있는 setter 또는 option을 추가하고, `apps/edge/internal/input/manager.go`에서 `cfg.Edge.ID`를 전달한다.
- request terminal status별 `iop_openai_requests_total`을 증가시킨다.
- usage 관측값이 있으면 token type별 `iop_openai_usage_tokens_total`을 증가시킨다.
- reasoning text/content를 관측했지만 reasoning token이 provider-reported가 아니면 `iop_openai_reasoning_observed_total`, `iop_openai_reasoning_chars_total`만 증가시키고 token counter에는 reasoning 추정치를 넣지 않는다.
- label allowlist에서 `request_id`, `session_id`, raw token, raw prompt/response, provider token/provider identity를 제외한다.

수정 파일 및 체크리스트:
- [ ] `apps/edge/internal/openai/usage_metrics.go`: collectors, label builder, emit helpers.
- [ ] `apps/edge/internal/openai/server.go`: `edge_id`/metric emitter wiring.
- [ ] `apps/edge/internal/input/manager.go`: `cfg.Edge.ID` 전달.
- [ ] `apps/edge/internal/openai/chat_handler.go`, `responses_handler.go`, `stream.go`: success/error/cancel terminal status와 usage emit 호출.
- [ ] `apps/edge/internal/openai/server_test.go` 또는 `usage_metrics_test.go`: collector output and label allowlist.

테스트 작성:
- `TestOpenAIUsageMetricsLabelsExcludeSecretsAndHighCardinality`.
- `TestOpenAIUsageMetricsCountTokenTypes`.
- `TestOpenAIReasoningObservedDoesNotEstimateTokens`.
- `TestOpenAIScopeExcludesA2AAndNonOpenAISurfaces`는 code evidence 또는 package-level docs/test로 대체 가능하다. 이 plan에서 A2A 코드를 건드리지 않음을 명시한다.

중간 검증:

```bash
go test -count=1 ./apps/edge/internal/openai -run 'Metric|Usage|Reasoning'
```

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `proto/iop/runtime.proto` | USAGE_METRIC-1 |
| `proto/gen/iop/runtime.pb.go` | USAGE_METRIC-1 |
| `apps/node/internal/runtime/types.go` | USAGE_METRIC-1 |
| `apps/node/internal/node/node.go` | USAGE_METRIC-1 |
| `apps/node/internal/adapters/openai_compat/openai_compat.go` | USAGE_METRIC-2 |
| `apps/node/internal/adapters/vllm/vllm.go` | USAGE_METRIC-2 |
| `apps/edge/internal/openai/run_result.go` | USAGE_METRIC-1 |
| `apps/edge/internal/openai/types.go` | USAGE_METRIC-1 |
| `apps/edge/internal/openai/stream.go` | USAGE_METRIC-2, USAGE_METRIC-3 |
| `apps/edge/internal/openai/server.go` | USAGE_METRIC-3 |
| `apps/edge/internal/input/manager.go` | USAGE_METRIC-3 |
| `apps/edge/internal/openai/chat_handler.go` | USAGE_METRIC-3 |
| `apps/edge/internal/openai/responses_handler.go` | USAGE_METRIC-3 |
| `apps/edge/internal/openai/usage_metrics.go` | USAGE_METRIC-3 |
| `apps/edge/internal/openai/*_test.go` | USAGE_METRIC-2, USAGE_METRIC-3 |
| `apps/node/internal/adapters/*/*_test.go` | USAGE_METRIC-2 |

## 최종 검증

```bash
make proto
go test -count=1 ./apps/edge/internal/openai ./apps/node/internal/node ./apps/node/internal/adapters/openai_compat ./apps/node/internal/adapters/vllm
go test -count=1 ./apps/edge/... ./apps/node/...
git diff --check
```

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