# SDD: Provider 기준 Usage Attribution Hot Path

## 위치

- Milestone: [Milestone 문서](../../../phase/operational-observability-provider-management/milestones/provider-usage-attribution-hot-path.md)
- Phase: [PHASE.md](../../../phase/operational-observability-provider-management/PHASE.md)

## 상태

[승인됨]

## SDD 잠금

- 상태: 해제
- 사용자 리뷰: 없음
- 잠금 항목: 없음

## 문제 / 비목표

- 문제: 현재 OpenAI-compatible token counter는 provider-reported token 수를 받지만, 요청의 가상 `model_group`으로 귀속한다. 단독·하이브리드·재시도/fallback 실행에서는 실제 provider 사용량과 집계 series가 달라질 수 있다.
- 비목표:
  - request ledger 저장, 장기 보관, export 또는 billing/chargeback 구현
  - provider routing, queue admission, capacity 정책 변경
  - provider가 보고하지 않은 token 수의 billing-grade 추정

## Source of Truth

| 영역 | 기준 | 메모 |
|------|------|------|
| Roadmap | [Milestone 문서](../../../phase/operational-observability-provider-management/milestones/provider-usage-attribution-hot-path.md) | 완료 Task와 검증 기준 |
| Code | `apps/edge/internal/openai/usage_metrics.go` | OpenAI request/token metric label과 emitter source of truth |
| Code | `apps/edge/internal/service/run_types.go` | actual dispatch의 provider/model binding source of truth |
| Contract | [OpenAI-compatible API 계약](../../../../agent-contract/outer/openai-compatible-api.md) | external model route와 OpenAI usage 관측 계약 |
| Config | `packages/go/config` 및 `configs/edge.yaml` | `models[].usage_attribution`, direct route `provider_id`와 fallback provider identity 기준 |
| User Decision | 현재 사용자 요청 | 동일 논리 model group만 virtual rollup, 그 밖의 실제 provider별·시도별 귀속을 사용한다. |

## State Machine

| 상태 | 진입 조건 | 다음 상태 | 근거 |
|------|-----------|-----------|------|
| route resolved | 요청 model의 group/direct route와 `usage_attribution`이 확정됨 | provider selected | OpenAI route resolution/config validation |
| provider selected | provider-pool candidate 또는 config가 검증한 direct provider binding이 확정됨 | provider usage observed / dispatch failed | `RunDispatch` 또는 tunnel dispatch binding |
| provider usage observed | 해당 실제 provider 호출이 token usage를 보고함 | emitted | provider binding별 metric emission |
| emitted | token series가 provider binding으로 기록됨 | provider selected / terminal | retry/fallback이면 새 실제 provider selection, 아니면 terminal |
| terminal | 모든 호출 시도가 종료됨 | 없음 | request terminal status metric |

## Interface Contract

- 계약 원문: [OpenAI-compatible API 계약](../../../../agent-contract/outer/openai-compatible-api.md)
- 입력:
  - `route_model`: 외부 request `model` alias; routing trace용 값이며 canonical usage key가 아니다.
  - `usage_attribution`: `models[].usage_attribution`의 `model_group` 또는 `provider`; 기본값은 `provider`이며 `model_group`은 operator가 동일 논리 모델이라고 승인할 때만 설정한다.
  - `provider_id`: 실제 dispatch한 globally unique provider resource identity
  - `served_model`: 해당 provider가 실제 호출한 model target
  - `usage`: 해당 provider 호출이 보고한 input/output/reasoning/cached-input token 수
  - direct binding: `openai.model_routes[].provider_id` 또는 top-level fallback `openai.provider_id`; 둘 다 없으면 provider-attribution direct route config를 거부한다.
- 출력:
  - canonical token/reasoning series: 실제 `provider_id`·`served_model`에 귀속된 provider-reported usage를 호출 시도마다 한 번 기록한다.
  - 승인된 group rollup: `usage_attribution=model_group`인 series만 `route_model` 기준으로 PromQL에서 합산한다. 별도 group counter를 emit하지 않아 이중 집계를 막는다.
  - request terminal counter: HTTP 요청당 한 번만 기록하며, provider별 token series의 호출 횟수 대용으로 사용하지 않는다.
- 금지:
  - direct route의 provider identity를 `adapter` 이름만으로 대체하지 않는다.
  - hybrid/retry/fallback 요청의 usage를 최초 또는 외부 request model에 단일 귀속하지 않는다.
  - virtual model group용 token counter를 provider token counter와 별도로 emit해 같은 usage를 이중 집계하지 않는다.
  - raw token, request/session id, prompt/response를 metric label에 넣지 않는다.

## Acceptance Scenarios

| ID | Milestone Task | Given | When | Then |
|----|----------------|-------|------|------|
| S01 | `group-policy` | `usage_attribution=model_group`으로 승인된 group과 기본 provider route | provider-reported usage가 수신됨 | 승인된 group만 query-time rollup이 가능하고, 모든 usage는 actual provider series 하나로 시작한다. |
| S02 | `dispatch-binding` | direct/legacy 또는 provider-pool route | dispatch가 성공함 | metric emitter가 adapter 대체값이 아닌 config/dispatch가 검증한 actual provider id와 served model을 받는다. |
| S03 | `attempt-usage` | hybrid request가 retry/fallback으로 다른 provider를 호출함 | 각 provider가 usage를 보고함 | 각 usage가 해당 실제 provider series에 한 번씩 기록되고 HTTP request counter는 한 번만 증가한다. |
| S04 | `metric-contract` | metric scrape와 OpenAI contract | label schema를 검증함 | secret/high-cardinality label 없이 provider attribution contract가 유지된다. |
| S05 | `grafana-migration` | provider·승인된 group series가 존재함 | 대표 Grafana query를 실행함 | provider 기준 집계와 허용된 group rollup이 모두 정확히 조회된다. |

## Evidence Map

| Scenario | Required Evidence | `agent-task` 연결 | 완료 Evidence 기대 |
|----------|-------------------|------------------|---------------------------|
| S01 | config/route table test와 provider series/group rollup metric delta assertion | `agent-task/m-provider-usage-attribution-hot-path/...` | `group-policy` Task id, test command와 PASS output |
| S02 | direct config provider ref, pool normalized·tunnel dispatch binding test | `agent-task/m-provider-usage-attribution-hot-path/...` | `dispatch-binding` Task id, provider id/served model assertion |
| S03 | deterministic retry/fallback provider switch test와 request counter assertion | `agent-task/m-provider-usage-attribution-hot-path/...` | `attempt-usage` Task id, provider별 token series delta와 request counter 1회 |
| S04 | `go test ./apps/edge/internal/openai`와 metric label guard | `agent-task/m-provider-usage-attribution-hot-path/...` | `metric-contract` Task id, command output |
| S05 | Grafana guide/query schema check | `agent-task/m-provider-usage-attribution-hot-path/...` | `grafana-migration` Task id, query evidence |

## Cross-repo Dependencies

- 없음

## Drift Check

- [x] Milestone 기능 Task와 Acceptance Scenario가 일치한다.
- [x] Evidence Map이 code-review/complete.log에서 검증 가능하다.
- [x] agent-contract를 쓰는 경우 SDD에 계약 원문을 복제하지 않았다.
- [x] 사용자 리뷰가 필요한 항목은 `USER_REVIEW.md`에만 남겼다.

## 사용자 리뷰 이력

- 없음

## 작업 컨텍스트

- 표준선: `models[].usage_attribution`의 기본값은 `provider`다. 동일 논리 model group이라고 operator가 명시 승인한 경우에만 `model_group`을 설정하고, canonical usage attribution은 항상 actual provider binding에서 출발한다.
- 후속 SDD: 없음
