Merge remote-tracking branch 'origin/provider-usage-attribution-hot-path' into dev

This commit is contained in:
toki 2026-07-31 18:43:21 +09:00
commit 84aabb7df6
4 changed files with 933 additions and 0 deletions

View file

@ -0,0 +1,141 @@
<!-- task=m-provider-usage-attribution-hot-path/01_attribution_binding_foundation plan=0 tag=API -->
# Code Review Reference - API
> **[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 `Implementation Checklist`; 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, record the exact blocker, attempted commands/output, and resume condition only in implementation-owned evidence fields.
> Do not ask the user directly, present choices, call user-input tools, create control-plane stop files, or classify the next state.
> Finalization (`Code Review Result`, log rename, `complete.log`, archive moves, `Review-Only Checklist`) is review-agent-only, even after compaction/resume.
> Follow the ownership table at the bottom of this file for which sections you own.
## Overview
date=2026-07-31
task=m-provider-usage-attribution-hot-path/01_attribution_binding_foundation, plan=0, tag=API
## For the Review Agent
> **[REVIEW AGENT ONLY]** The finalization steps below are review-agent only. Implementing agents must not execute this section.
Compare implementation of each item against source files and verify that output in `Verification Results` matches code.
Review completion means the following steps are finished:
1. Append verdict and `review_rework_count` / `evidence_integrity_failure` routing signals.
2. Archive `CODE_REVIEW-cloud-G07.md``code_review_cloud_G07_0.log` and `PLAN-local-G07.md``plan_local_G07_0.log`.
3. If PASS, write `complete.log` and move active task directory to `agent-task/archive/YYYY/MM/m-provider-usage-attribution-hot-path/01_attribution_binding_foundation/`. If WARN/FAIL, fully write the next filesystem state required by the code-review skill.
4. If PASS, report completion event metadata. This packet has no Roadmap Targets, so it must not check a Milestone task.
5. Check applicable `Review-Only Checklist` items at the final `.log` location before reporting.
---
## Implementation Item Completion
| Item | Status |
|------|---------|
| API-1 Define and validate attribution config | [ ] |
| API-2 Carry actual dispatch binding end to end | [ ] |
| API-3 Synchronize contract and current specs | [ ] |
## Implementation Checklist
- [ ] Add the provider-default/model-group-approved attribution policy and validated direct provider identity config.
- [ ] Propagate attribution policy and actual provider/model/node binding through OpenAI route contexts and every service dispatch result.
- [ ] Add deterministic config and dispatch tests, then synchronize the config contract and matching implementation specs.
- [ ] Run fresh focused, race, formatting, contract-pointer, and diff verification.
- [ ] Fill implementation-owned sections in CODE_REVIEW-*-G??.md with actual implementation notes and verification output.
## Review-Only Checklist
> **[REVIEW AGENT ONLY]** This checklist is used only by the review agent.
> Implementing agents must not modify or check this section.
- [ ] Append one verdict of `PASS`, `WARN`, or `FAIL` and verified `review_rework_count`, `evidence_integrity_failure` to `Code Review Result`.
- [ ] Verify that verdict, `Dimension Assessment`, and Required/Suggested/Nit classifications match.
- [ ] Archive active `CODE_REVIEW-cloud-G07.md` to `code_review_cloud_G07_0.log`.
- [ ] Archive active `PLAN-local-G07.md` to `plan_local_G07_0.log`.
- [ ] Verify that the Agent-Ops managed block in `.gitignore` unignores `agent-task/**/*.md` and `agent-task/**/*.log` and ignores `agent-roadmap/current.md`.
- [ ] If PASS, write `complete.log` based on `agent-ops/skills/common/code-review/templates/complete-log-template.md` and leave no active `.md` files.
- [ ] If PASS, move active task directory `agent-task/m-provider-usage-attribution-hot-path/01_attribution_binding_foundation/` to `agent-task/archive/YYYY/MM/m-provider-usage-attribution-hot-path/01_attribution_binding_foundation/` and update this checklist at the final archive path.
- [ ] If PASS, report completion event metadata without modifying roadmap or directly calling `update-roadmap`.
- [ ] If PASS for split work, keep the active parent because dependent sibling `02+01_attempt_usage_emission` remains.
- [ ] If WARN/FAIL, write the next filesystem state matching code-review verdict and do not write `complete.log`.
## Deviations from Plan
_Record any deviations from the plan and the rationale here._
## Key Design Decisions
_Record key design decisions here._
## Reviewer Checkpoints
- Omitted `models[].usage_attribution` resolves to `provider`; only `provider` and `model_group` are accepted.
- Direct provider attribution resolves route-level `provider_id` before top-level fallback, rejects a missing binding after existing validation, and does not require a provider-catalog cross-reference for a legacy adapter-only route.
- Direct and pool normalized/tunnel results preserve actual provider id, served target, and node id without adapter substitution.
- Existing valid config fixtures and handler test doubles carry explicit stable provider identities; intentionally invalid config fixtures retain their original diagnostic.
- No protobuf/wire, routing policy, queue, metric vector, Grafana, ledger, or billing behavior changed.
- Config contract and matching current specs agree with executable tests.
## Verification Results
### Attribution config
```bash
mkdir -p /tmp/iop-s1-provider-usage-attribution-gocache
GOCACHE=/tmp/iop-s1-provider-usage-attribution-gocache go test -count=1 ./packages/go/config -run 'UsageAttribution|DirectProviderBinding|OpenAIRouteCatalog|ProviderPoolLegacyCompatibility|StreamEvidenceGate'
```
_Paste actual stdout/stderr and exit status._
### Dispatch binding
```bash
GOCACHE=/tmp/iop-s1-provider-usage-attribution-gocache go test -count=1 ./apps/edge/internal/service -run 'ActualProviderBinding'
GOCACHE=/tmp/iop-s1-provider-usage-attribution-gocache go test -count=1 ./apps/edge/internal/openai -run 'UsageAttributionRouteDispatchBinding|ProviderPoolDispatch|LegacyRoute'
```
_Paste actual stdout/stderr and exit status._
### Contract and spec pointers
```bash
rg --sort path -n 'usage_attribution|provider_id' configs/edge.yaml agent-contract/inner/edge-config-runtime-refresh.md agent-spec/input/openai-compatible-surface.md agent-spec/runtime/provider-pool-config-refresh.md agent-spec/runtime/edge-node-execution.md
```
_Paste actual stdout/stderr and exit status._
### Final verification
```bash
mkdir -p /tmp/iop-s1-provider-usage-attribution-gocache
test -z "$(gofmt -l packages/go/config/provider_types.go packages/go/config/edge_types.go packages/go/config/load.go packages/go/config/validate.go packages/go/config/usage_attribution_config_test.go packages/go/config/edge_openai_config_test.go packages/go/config/provider_catalog_validation_config_test.go packages/go/config/stream_evidence_gate_config_test.go apps/edge/internal/openai/route_resolution.go apps/edge/internal/openai/chat_completion.go apps/edge/internal/openai/chat_handler.go apps/edge/internal/openai/provider_tunnel.go apps/edge/internal/openai/responses_handler.go apps/edge/internal/openai/stream_gate_dispatcher.go apps/edge/internal/openai/provider_dispatch_test.go apps/edge/internal/openai/server_test_support_test.go apps/edge/internal/openai/provider_test_support_test.go apps/edge/internal/service/run_types.go apps/edge/internal/service/run_submit.go apps/edge/internal/service/provider_tunnel.go apps/edge/internal/service/provider_pool.go apps/edge/internal/service/usage_attribution_dispatch_test.go)"
GOCACHE=/tmp/iop-s1-provider-usage-attribution-gocache go test -count=1 ./packages/go/config ./apps/edge/internal/service ./apps/edge/internal/openai
GOCACHE=/tmp/iop-s1-provider-usage-attribution-gocache go test -race -count=1 ./apps/edge/internal/service ./apps/edge/internal/openai
rg --sort path -n 'usage_attribution|provider_id' configs/edge.yaml agent-contract/inner/edge-config-runtime-refresh.md agent-spec/input/openai-compatible-surface.md agent-spec/runtime/provider-pool-config-refresh.md agent-spec/runtime/edge-node-execution.md
git diff --check
```
_Paste actual stdout/stderr and exit status._
---
> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section?**
> If anything is blank, go back and fill it in before saving this file.
> Leave review-agent-only sections unchanged.
## Section Ownership
| Section | Owner | Note |
|---------|-------|------|
| Header comment, Overview, Review Agent Instructions | Fixed at stub creation | Implementing agent must not modify or execute these |
| Implementation Item Completion (item names) | Fixed at stub creation | Implementing agent checks `[ ]``[x]` only |
| Implementation Checklist (item text/order) | Fixed at stub creation from plan | Implementing agent checks `[ ]``[x]` only |
| Review-Only Checklist | Review agent only | Implementing agent must not modify or check |
| Deviations from Plan, Key Design Decisions | Implementing agent | Replace placeholders with actual content |
| Reviewer Checkpoints | Fixed at stub creation | Reviewer verifies them |
| Verification Results | Implementing agent | Paste actual output; command changes require a deviation |
| Code Review Result | Review agent appends | Not included in the stub |

View file

@ -0,0 +1,332 @@
<!-- task=m-provider-usage-attribution-hot-path/01_attribution_binding_foundation plan=0 tag=API -->
# Provider Usage Attribution Binding Foundation
## For the Implementing Agent
Filling implementation-owned sections in `CODE_REVIEW-cloud-G07.md` is mandatory. Run every verification command, paste actual notes/output, leave the active pair in place, and report ready for review. Finalization belongs only to the code-review skill. If blocked, record the exact blocker, attempted commands/output, and resume condition in implementation-owned evidence fields; do not ask the user, call user-input tools, create stop files, classify the next state, archive logs, or write `complete.log`.
## Background
OpenAI usage is currently keyed by the request model group, while direct dispatch results do not retain a verified provider identity. Before per-attempt metrics can be correct, config, route resolution, and both normalized/tunnel service paths need one stable actual-provider binding contract.
## Analysis
### Files Read
- `agent-ops/rules/project/rules.md`
- `agent-ops/rules/common/rules-roadmap.md`
- `agent-ops/rules/common/rules-agent-spec.md`
- `agent-ops/rules/project/domain/edge/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/profiles/edge-smoke.md`
- `agent-test/local/profiles/platform-common-smoke.md`
- `agent-roadmap/current.md`
- `agent-roadmap/phase/operational-observability-provider-management/PHASE.md`
- `agent-roadmap/phase/operational-observability-provider-management/milestones/provider-usage-attribution-hot-path.md`
- `agent-roadmap/sdd/operational-observability-provider-management/provider-usage-attribution-hot-path/SDD.md`
- `agent-contract/index.md`
- `agent-contract/inner/edge-config-runtime-refresh.md`
- `agent-contract/inner/edge-node-runtime-wire.md`
- `agent-contract/outer/openai-compatible-api.md`
- `agent-spec/index.md`
- `agent-spec/input/openai-compatible-surface.md`
- `agent-spec/runtime/provider-pool-config-refresh.md`
- `agent-spec/runtime/edge-node-execution.md`
- `packages/go/config/provider_types.go`
- `packages/go/config/edge_types.go`
- `packages/go/config/load.go`
- `packages/go/config/validate.go`
- `packages/go/config/edge_openai_config_test.go`
- `packages/go/config/provider_catalog_validation_config_test.go`
- `packages/go/config/stream_evidence_gate_config_test.go`
- `apps/edge/internal/openai/route_resolution.go`
- `apps/edge/internal/openai/dispatch_context.go`
- `apps/edge/internal/openai/chat_handler.go`
- `apps/edge/internal/openai/chat_completion.go`
- `apps/edge/internal/openai/provider_tunnel.go`
- `apps/edge/internal/openai/responses_handler.go`
- `apps/edge/internal/openai/provider_dispatch_test.go`
- `apps/edge/internal/openai/server_test_support_test.go`
- `apps/edge/internal/openai/provider_test_support_test.go`
- `apps/edge/internal/openai/stream_gate_dispatcher.go`
- `apps/edge/internal/service/run_types.go`
- `apps/edge/internal/service/run_submit.go`
- `apps/edge/internal/service/provider_tunnel.go`
- `apps/edge/internal/service/provider_pool.go`
- `configs/edge.yaml`
- `Makefile`
- `scripts/e2e-provider-capacity-smoke.sh`
### SDD Criteria
The approved, unlocked SDD is `agent-roadmap/sdd/operational-observability-provider-management/provider-usage-attribution-hot-path/SDD.md`. This prerequisite packet prepares S01 (`group-policy`) and S02 (`dispatch-binding`) but intentionally has no `Roadmap Targets`: it does not emit the final provider-attributed metric series and therefore cannot complete either task. Evidence Map rows S01 and S02 require config/route basis tests plus direct, pool-normalized, and pool-tunnel binding assertions; those requirements define API-1 through API-3 and the focused verification below.
### Verification Context
No handoff was supplied. Verification sources actually read were `agent-test/local/rules.md`, `agent-test/local/profiles/edge-smoke.md`, `agent-test/local/profiles/platform-common-smoke.md`, `Makefile`, and `scripts/e2e-provider-capacity-smoke.sh`. Preflight on branch `provider-usage-attribution-hot-path` at HEAD `88f518f0294d461d5e05b9a14229fed3d6126d12` found a clean checkout, Go `go1.26.2 linux/arm64`, module `/config/workspace/iop-s1/go.mod`, and `go`, `curl`, and `jq` available. The inherited `GOCACHE` points at an inaccessible sibling-workspace temporary path; using `/tmp/iop-s1-provider-usage-attribution-gocache` made fresh baseline tests pass for `./packages/go/config`, `./apps/edge/internal/service`, and `./apps/edge/internal/openai`. Existing valid OpenAI config fixtures and handler test doubles predate explicit provider binding, so their minimal migration is part of compatibility verification rather than a behavioral expansion. No external runner, credential, host, or port is required. Cached output is not acceptable.
### Test Coverage Gaps
- Existing config tests do not know `models[].usage_attribution` or direct `provider_id`.
- Existing OpenAI dispatch tests distinguish pool/direct paths but do not assert attribution policy or verified provider identity.
- Provider-pool dispatch already returns actual provider/model/node values, but direct normalized and direct tunnel results leave `ProviderID` empty.
- No test rejects a provider-attribution direct route whose route-level and top-level provider ids are both absent.
- Existing valid config fixtures in `edge_openai_config_test.go`, `provider_catalog_validation_config_test.go`, and `stream_evidence_gate_config_test.go` enable OpenAI without the new explicit fallback binding; a new validation rule would otherwise turn unrelated passing and diagnostic cases into missing-provider failures.
- Shared OpenAI test services in `server_test_support_test.go` and `provider_test_support_test.go` construct successful `RunDispatch` values without a provider id, so strict runtime binding would break broad handler coverage unless the fixtures receive stable test-only identities.
### Symbol References
- `validateOpenAIRoutes` is called by `packages/go/config/load.go:63` before principal, stream-gate, provider, and model validation; keep it as the early structural route validator and add a separate full-config attribution-binding validator after existing validation so legacy diagnostics retain precedence.
- `actualOpenAIProvider` is called by initial and recovery attempt binding construction in `apps/edge/internal/openai/stream_gate_runtime.go`, `apps/edge/internal/openai/responses_stream_gate.go`, and `apps/edge/internal/openai/stream_gate_dispatcher.go`.
- `SubmitRunRequest`, `SubmitProviderTunnelRequest`, and `RunDispatch` are constructed throughout Edge service/OpenAI tests; add fields without renaming existing symbols so keyed literals remain source-compatible.
### Split Judgment
- `01_attribution_binding_foundation`: stable contract is a validated attribution policy plus actual provider/served-model/node binding on every successful direct or provider-pool dispatch. PASS evidence is config rejection/default tests and direct/pool normalized/tunnel dispatch assertions.
- `02+01_attempt_usage_emission`: consumes these fields to separate attempt usage emission from the one-per-request terminal counter.
This packet has no predecessor. No active or archived sibling index exists for this task group.
### Scope Rationale
Do not change provider selection, capacity, queueing, retry budgets, response bytes, metric label vectors, Grafana queries, or request-ledger/billing behavior. `agent-contract/outer/openai-compatible-api.md` and usage emitter semantics stay for the dependent packet. A direct `provider_id` is an explicit stable attribution identity; this packet does not require it to reference `nodes[].providers[].id`, because legacy direct adapter routes can be valid without a provider-catalog candidate. It must never be inferred from adapter text. No protobuf/wire schema change is needed because provider, served-model, and node identity already exist on Edge-local dispatch results.
### Final Routing
- evaluation_mode: `first-pass`
- finalizer: `finalize-task-policy.sh pair`
- build: all closures true; scores `scope=2,state=1,blast=2,evidence=1,verification=1`; grade `G07`, route `local`, basis `local-fit`, filename `PLAN-local-G07.md`
- review: all closures true; scores `scope=2,state=1,blast=2,evidence=1,verification=1`; grade `G07`, route `cloud`, basis `official-review`, filename `CODE_REVIEW-cloud-G07.md`
- large_indivisible_context: `false`
- positive loop risks: `boundary_contract`, `variant_product` (count `2`)
- recovery signals: review rework `0`, evidence-integrity failure `false`
- capability-gap evidence: none
## Implementation Checklist
- [ ] Add the provider-default/model-group-approved attribution policy and validated direct provider identity config.
- [ ] Propagate attribution policy and actual provider/model/node binding through OpenAI route contexts and every service dispatch result.
- [ ] Add deterministic config and dispatch tests, then synchronize the config contract and matching implementation specs.
- [ ] Run fresh focused, race, formatting, contract-pointer, and diff verification.
- [ ] Fill implementation-owned sections in CODE_REVIEW-*-G??.md with actual implementation notes and verification output.
### [API-1] Define and Validate Attribution Config
#### Problem
`packages/go/config/provider_types.go:155` has no attribution policy, while `packages/go/config/edge_types.go:105` and `packages/go/config/edge_types.go:118` have no route-level or fallback provider id. `packages/go/config/validate.go:12` validates route structure before the rest of `LoadEdge`, so a direct route cannot be proven attributable and adding the check there would mask established validation diagnostics.
#### Solution
Add canonical `provider` and `model_group` attribution constants, `ModelCatalogEntry.UsageAttribution`, and an effective accessor that defaults omission to `provider`. Accept only those two values. Add `ProviderID` to `OpenAIRouteEntry` and `EdgeOpenAIConf`; resolve a nonblank route-level value before the nonblank top-level fallback. Every enabled OpenAI direct fallback and each explicit model route that can dispatch directly must have that syntactic binding; a provider-catalog cross-reference is deliberately not required for legacy adapter-only routes. Keep `validateOpenAIRoutes` as the early structural check, validate model policy through the existing model-validation phase, and call a new `validateOpenAIAttributionBindings(cfg.OpenAI)` only after all pre-existing `LoadEdge` validation succeeds. It then rejects a provider-attribution route whose effective binding is absent while explicitly approved provider-pool groups retain `model_group`.
```go
// Before (packages/go/config/provider_types.go:155)
type ModelCatalogEntry struct {
ID string `mapstructure:"id" yaml:"id"`
// After
const (
UsageAttributionProvider = "provider"
UsageAttributionModelGroup = "model_group"
)
type ModelCatalogEntry struct {
ID string `mapstructure:"id" yaml:"id"`
UsageAttribution string `mapstructure:"usage_attribution" yaml:"usage_attribution,omitempty"`
}
```
```go
// Existing structural validation remains early.
if err := validateOpenAIRoutes(cfg.OpenAI.ModelRoutes); err != nil {
return nil, err
}
// New attribution validation runs after established config validation.
if err := validateOpenAIAttributionBindings(cfg.OpenAI); err != nil {
return nil, err
}
```
#### Modified Files and Checklist
- [ ] `packages/go/config/provider_types.go` — add constants, field, default accessor, and enum validation.
- [ ] `packages/go/config/edge_types.go` — add route-level and top-level `provider_id`.
- [ ] `packages/go/config/load.go` — invoke attribution binding validation after all established config checks.
- [ ] `packages/go/config/validate.go` — add the separate full-config binding precedence and missing/blank provider-id validator.
- [ ] `configs/edge.yaml` — document provider-default policy, approved group opt-in, and legacy binding examples without private values.
- [ ] `packages/go/config/usage_attribution_config_test.go` — add focused YAML/default/invalid/direct/fallback cases.
- [ ] `packages/go/config/edge_openai_config_test.go` — migrate valid enabled/direct fixtures and preserve existing validation diagnostics.
- [ ] `packages/go/config/provider_catalog_validation_config_test.go` — give the provider-pool legacy-compatibility fixture an explicit fallback identity.
- [ ] `packages/go/config/stream_evidence_gate_config_test.go` — migrate enabled OpenAI fixtures without changing stream-gate assertions.
#### Test Strategy
Write `TestModelUsageAttributionValidationAndDefault` and `TestOpenAIDirectProviderBindingValidation` in `packages/go/config/usage_attribution_config_test.go`. Use temporary public YAML fixtures covering omitted/provider/model_group/invalid policy, route-level override, top-level fallback, blank values, and missing binding rejection. Minimally add stable public `provider_id` values to existing valid fixtures; keep intentionally invalid route/queue/stream-gate cases failing for their original reason so attribution validation does not mask earlier diagnostics.
#### Verification
```bash
mkdir -p /tmp/iop-s1-provider-usage-attribution-gocache
GOCACHE=/tmp/iop-s1-provider-usage-attribution-gocache go test -count=1 ./packages/go/config -run 'UsageAttribution|DirectProviderBinding|OpenAIRouteCatalog|ProviderPoolLegacyCompatibility|StreamEvidenceGate'
```
Expected: fresh tests pass and missing provider-attribution direct bindings fail only at config load.
### [API-2] Carry Actual Dispatch Binding End to End
#### Problem
`apps/edge/internal/openai/route_resolution.go:54` drops provider identity and attribution policy. `apps/edge/internal/service/run_types.go:57` describes `ProviderID` as pool-only, and `apps/edge/internal/service/run_submit.go:182` returns a direct `RunDispatch` without it. `apps/edge/internal/openai/stream_gate_dispatcher.go:276` substitutes adapter or node id when provider id is missing, contrary to SDD S02.
#### Solution
Add `ProviderID` and `UsageAttribution` to `routeDispatch`, both service request DTOs, and `RunDispatch`. Resolve catalog policy at provider-pool ingress; resolve route-level provider id before the top-level fallback for direct routes. Propagate the immutable values through Chat/Responses normalized and tunnel request builders. Provider-pool normalized/tunnel results keep the selected candidate's actual provider, served target, and node; direct results use the validated configured provider and resolved node. Make `actualOpenAIProvider` return only `RunDispatch.ProviderID`, so missing identity fails at the binding boundary instead of becoming an adapter alias. Update shared handler test services that bypass `LoadEdge` to echo the request binding or use an explicit fixed test-only provider/node identity. For broad legacy fixtures that directly construct `fakeRunResult`/`fakeTunnelHandle`, their test-only dispatch accessor may fill the fixed identity only when absent; the strict missing-binding regression must use a non-normalizing spy. Never derive any fixture identity from adapter text, and keep focused request/dispatch propagation assertions so the fixture default cannot conceal a missing production field.
```go
// Before (apps/edge/internal/service/run_types.go:45)
type RunDispatch struct {
RunID string
NodeID string
ProviderID string // non-empty for provider-pool dispatches
}
// After
type RunDispatch struct {
RunID string
NodeID string
ProviderID string
UsageAttribution string
}
```
```go
// Before (apps/edge/internal/openai/stream_gate_dispatcher.go:276)
func actualOpenAIProvider(dispatch edgeservice.RunDispatch) string {
if provider := strings.TrimSpace(dispatch.ProviderID); provider != "" {
return provider
}
if provider := strings.TrimSpace(dispatch.Adapter); provider != "" {
return provider
}
return strings.TrimSpace(dispatch.NodeID)
}
// After
func actualOpenAIProvider(dispatch edgeservice.RunDispatch) string {
return strings.TrimSpace(dispatch.ProviderID)
}
```
#### Modified Files and Checklist
- [ ] `apps/edge/internal/openai/route_resolution.go` — resolve provider id and effective policy.
- [ ] `apps/edge/internal/openai/chat_completion.go` — copy binding into normalized Chat requests.
- [ ] `apps/edge/internal/openai/chat_handler.go` — copy binding into provider-pool base requests.
- [ ] `apps/edge/internal/openai/provider_tunnel.go` — copy binding into direct Chat/Responses tunnel requests.
- [ ] `apps/edge/internal/openai/responses_handler.go` — copy binding into normalized and provider-pool Responses requests.
- [ ] `apps/edge/internal/openai/stream_gate_dispatcher.go` — remove adapter/node provider substitution.
- [ ] `apps/edge/internal/service/run_types.go` — extend request/dispatch DTOs.
- [ ] `apps/edge/internal/service/run_submit.go` — fill direct and queued run dispatch binding.
- [ ] `apps/edge/internal/service/provider_tunnel.go` — fill direct and queued tunnel dispatch binding.
- [ ] `apps/edge/internal/service/provider_pool.go` — preserve policy across one-shot normalized/tunnel selection.
- [ ] `apps/edge/internal/openai/server_test_support_test.go` — return explicit provider/node binding from normalized handler fakes.
- [ ] `apps/edge/internal/openai/provider_test_support_test.go` — return explicit provider/node binding from direct/tunnel services and normalize only shared legacy fake handles.
#### Test Strategy
Extend `apps/edge/internal/openai/provider_dispatch_test.go` with `TestUsageAttributionRouteDispatchBinding` for catalog, route override, and top-level fallback request DTOs. Add `apps/edge/internal/service/usage_attribution_dispatch_test.go` with `TestRunDispatchActualProviderBinding` and `TestTunnelDispatchActualProviderBinding`; assert direct, pool-normalized, and pool-tunnel `ProviderID`, `Target`, `NodeID`, and `UsageAttribution`. Include a non-normalizing regression spy proving that an empty provider id fails and adapter text is never accepted as provider identity. Run the existing handler suites against the migrated shared test services to prove strict binding does not regress unrelated direct, pool, and stream-gate behavior.
#### Verification
```bash
GOCACHE=/tmp/iop-s1-provider-usage-attribution-gocache go test -count=1 ./apps/edge/internal/service -run 'ActualProviderBinding'
GOCACHE=/tmp/iop-s1-provider-usage-attribution-gocache go test -count=1 ./apps/edge/internal/openai -run 'UsageAttributionRouteDispatchBinding|ProviderPoolDispatch|LegacyRoute'
```
Expected: all binding variants pass with actual provider/model/node values and no adapter fallback.
### [API-3] Synchronize Contract and Current Specs
#### Problem
`agent-contract/inner/edge-config-runtime-refresh.md:40` defines legacy model routes only as adapter/target mappings, and `agent-spec/input/openai-compatible-surface.md:81` describes dispatch without provider-attribution policy. Leaving them unchanged would make config refresh and current implementation specs disagree with code.
#### Solution
Document the new fields, default/allowed values, direct binding precedence, validation failure, and refresh classification. Record that these are Edge-local binding fields and do not change `ProviderTunnelRequest`/`RunRequest` protobuf payloads. Update only current matching specs; do not add Grafana queries or claim final attempt metric behavior.
#### Modified Files and Checklist
- [ ] `agent-contract/inner/edge-config-runtime-refresh.md` — define config schema/default/validation/refresh behavior.
- [ ] `agent-spec/input/openai-compatible-surface.md` — record route and dispatch binding behavior.
- [ ] `agent-spec/runtime/provider-pool-config-refresh.md` — record model policy default and live-apply semantics.
- [ ] `agent-spec/runtime/edge-node-execution.md` — clarify Edge-local dispatch identity preservation without a wire change.
#### Test Strategy
No separate code test is needed; API-1/API-2 tests are the executable contract. Verify exact schema terms and prohibit accidental Grafana/ledger scope.
#### Verification
```bash
rg --sort path -n 'usage_attribution|provider_id' configs/edge.yaml agent-contract/inner/edge-config-runtime-refresh.md agent-spec/input/openai-compatible-surface.md agent-spec/runtime/provider-pool-config-refresh.md agent-spec/runtime/edge-node-execution.md
```
Expected: output shows the same default, allowed values, precedence, and Edge-local binding meaning in every current source.
## Dependencies and Execution Order
1. Complete API-1 before runtime propagation so every request carries validated values.
2. Complete API-2 before API-3 so documentation matches the implemented field flow.
3. Run all final verification after tests and current specs are synchronized.
## Modified Files Summary
| File | Item |
|------|------|
| `packages/go/config/provider_types.go` | API-1 |
| `packages/go/config/edge_types.go` | API-1 |
| `packages/go/config/load.go` | API-1 |
| `packages/go/config/validate.go` | API-1 |
| `configs/edge.yaml` | API-1 |
| `packages/go/config/usage_attribution_config_test.go` | API-1 |
| `packages/go/config/edge_openai_config_test.go` | API-1 |
| `packages/go/config/provider_catalog_validation_config_test.go` | API-1 |
| `packages/go/config/stream_evidence_gate_config_test.go` | API-1 |
| `apps/edge/internal/openai/route_resolution.go` | API-2 |
| `apps/edge/internal/openai/chat_completion.go` | API-2 |
| `apps/edge/internal/openai/chat_handler.go` | API-2 |
| `apps/edge/internal/openai/provider_tunnel.go` | API-2 |
| `apps/edge/internal/openai/responses_handler.go` | API-2 |
| `apps/edge/internal/openai/stream_gate_dispatcher.go` | API-2 |
| `apps/edge/internal/service/run_types.go` | API-2 |
| `apps/edge/internal/service/run_submit.go` | API-2 |
| `apps/edge/internal/service/provider_tunnel.go` | API-2 |
| `apps/edge/internal/service/provider_pool.go` | API-2 |
| `apps/edge/internal/openai/provider_dispatch_test.go` | API-2 |
| `apps/edge/internal/openai/server_test_support_test.go` | API-2 |
| `apps/edge/internal/openai/provider_test_support_test.go` | API-2 |
| `apps/edge/internal/service/usage_attribution_dispatch_test.go` | API-2 |
| `agent-contract/inner/edge-config-runtime-refresh.md` | API-3 |
| `agent-spec/input/openai-compatible-surface.md` | API-3 |
| `agent-spec/runtime/provider-pool-config-refresh.md` | API-3 |
| `agent-spec/runtime/edge-node-execution.md` | API-3 |
| `agent-task/m-provider-usage-attribution-hot-path/01_attribution_binding_foundation/CODE_REVIEW-cloud-G07.md` | API-1, API-2, API-3 |
## Final Verification
```bash
mkdir -p /tmp/iop-s1-provider-usage-attribution-gocache
test -z "$(gofmt -l packages/go/config/provider_types.go packages/go/config/edge_types.go packages/go/config/load.go packages/go/config/validate.go packages/go/config/usage_attribution_config_test.go packages/go/config/edge_openai_config_test.go packages/go/config/provider_catalog_validation_config_test.go packages/go/config/stream_evidence_gate_config_test.go apps/edge/internal/openai/route_resolution.go apps/edge/internal/openai/chat_completion.go apps/edge/internal/openai/chat_handler.go apps/edge/internal/openai/provider_tunnel.go apps/edge/internal/openai/responses_handler.go apps/edge/internal/openai/stream_gate_dispatcher.go apps/edge/internal/openai/provider_dispatch_test.go apps/edge/internal/openai/server_test_support_test.go apps/edge/internal/openai/provider_test_support_test.go apps/edge/internal/service/run_types.go apps/edge/internal/service/run_submit.go apps/edge/internal/service/provider_tunnel.go apps/edge/internal/service/provider_pool.go apps/edge/internal/service/usage_attribution_dispatch_test.go)"
GOCACHE=/tmp/iop-s1-provider-usage-attribution-gocache go test -count=1 ./packages/go/config ./apps/edge/internal/service ./apps/edge/internal/openai
GOCACHE=/tmp/iop-s1-provider-usage-attribution-gocache go test -race -count=1 ./apps/edge/internal/service ./apps/edge/internal/openai
rg --sort path -n 'usage_attribution|provider_id' configs/edge.yaml agent-contract/inner/edge-config-runtime-refresh.md agent-spec/input/openai-compatible-surface.md agent-spec/runtime/provider-pool-config-refresh.md agent-spec/runtime/edge-node-execution.md
git diff --check
```
Expected: formatting is clean; fresh package and race tests pass; schema/spec pointers agree; diff check passes. Cached test output is not acceptable. After completing all code changes, fill implementation-owned sections in `CODE_REVIEW-*-G??.md`.

View file

@ -0,0 +1,155 @@
<!-- task=m-provider-usage-attribution-hot-path/02+01_attempt_usage_emission plan=0 tag=API -->
# Code Review Reference - API
> **[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 `Implementation Checklist`; 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, record the exact blocker, attempted commands/output, and resume condition only in implementation-owned evidence fields.
> Do not ask the user directly, present choices, call user-input tools, create control-plane stop files, or classify the next state.
> Finalization (`Code Review Result`, log rename, `complete.log`, archive moves, `Review-Only Checklist`) is review-agent-only, even after compaction/resume.
> Follow the ownership table at the bottom of this file for which sections you own.
## Overview
date=2026-07-31
task=m-provider-usage-attribution-hot-path/02+01_attempt_usage_emission, plan=0, tag=API
## Roadmap Targets
- Milestone: `agent-roadmap/phase/operational-observability-provider-management/milestones/provider-usage-attribution-hot-path.md`
- Milestone link: [Milestone 문서](agent-roadmap/phase/operational-observability-provider-management/milestones/provider-usage-attribution-hot-path.md)
- Task ids:
- `group-policy`: approved model-group rollup policy over one canonical provider series
- `dispatch-binding`: actual provider, served model, and node binding reaches the emitter
- `attempt-usage`: retry/fallback usage is emitted once per actual provider attempt
- Completion mode: check-on-pass
## For the Review Agent
> **[REVIEW AGENT ONLY]** The finalization steps below are review-agent only. Implementing agents must not execute this section.
Compare implementation of each item against source files and verify that output in `Verification Results` matches code.
Review completion means the following steps are finished:
1. Append verdict and `review_rework_count` / `evidence_integrity_failure` routing signals.
2. Archive `CODE_REVIEW-cloud-G10.md``code_review_cloud_G10_0.log` and `PLAN-cloud-G10.md``plan_cloud_G10_0.log`.
3. If PASS, write `complete.log` and move active task directory to `agent-task/archive/YYYY/MM/m-provider-usage-attribution-hot-path/02+01_attempt_usage_emission/`. If WARN/FAIL, fully write the next filesystem state required by the code-review skill.
4. If PASS, report completion event metadata for `group-policy`, `dispatch-binding`, and `attempt-usage` without modifying the roadmap.
5. Check applicable `Review-Only Checklist` items at the final `.log` location before reporting.
---
## Implementation Item Completion
| Item | Status |
|------|---------|
| API-1 Separate request and attempt metric contracts | [ ] |
| API-2 Finalize usage at every attempt lifecycle boundary | [ ] |
| API-3 Lock S01-S03 evidence and current contract | [ ] |
## Implementation Checklist
- [ ] Split request-terminal accounting from provider-attributed attempt usage and enforce the approved group policy without duplicate counters.
- [ ] Bind and finalize usage for every direct, tunnel, normalized, retry, fallback, success, error, and cancel attempt exactly once.
- [ ] Add deterministic S01-S03 tests and synchronize the minimum current OpenAI contract/spec.
- [ ] Run fresh focused, full-package, race, local provider-capacity smoke, formatting, and diff verification.
- [ ] Fill implementation-owned sections in CODE_REVIEW-*-G??.md with actual implementation notes and verification output.
## Review-Only Checklist
> **[REVIEW AGENT ONLY]** This checklist is used only by the review agent.
> Implementing agents must not modify or check this section.
- [ ] Append one verdict of `PASS`, `WARN`, or `FAIL` and verified `review_rework_count`, `evidence_integrity_failure` to `Code Review Result`.
- [ ] Verify that verdict, `Dimension Assessment`, and Required/Suggested/Nit classifications match.
- [ ] Archive active `CODE_REVIEW-cloud-G10.md` to `code_review_cloud_G10_0.log`.
- [ ] Archive active `PLAN-cloud-G10.md` to `plan_cloud_G10_0.log`.
- [ ] Verify that the Agent-Ops managed block in `.gitignore` unignores `agent-task/**/*.md` and `agent-task/**/*.log` and ignores `agent-roadmap/current.md`.
- [ ] If PASS, write `complete.log` based on `agent-ops/skills/common/code-review/templates/complete-log-template.md` and leave no active `.md` files.
- [ ] If PASS, move active task directory `agent-task/m-provider-usage-attribution-hot-path/02+01_attempt_usage_emission/` to `agent-task/archive/YYYY/MM/m-provider-usage-attribution-hot-path/02+01_attempt_usage_emission/` and update this checklist at the final archive path.
- [ ] If PASS, report milestone completion event metadata for the listed Task ids without editing roadmap or directly calling `update-roadmap`.
- [ ] If PASS for split work, remove the active parent if no sibling remains, or verify why it stays.
- [ ] If WARN/FAIL, write the next filesystem state matching code-review verdict and do not write `complete.log`.
## Deviations from Plan
_Record any deviations from the plan and the rationale here._
## Key Design Decisions
_Record key design decisions here._
## Reviewer Checkpoints
- Predecessor `01_attribution_binding_foundation` is satisfied by the active `complete.log` or an exact archived completion path supplied by the task-loop dependency resolver, without broad archive search.
- Canonical token/reasoning usage begins at actual provider id and served model; approved groups use only query-time rollup.
- Provider-reported usage from every observed retry/fallback attempt is finalized once, including replaced or aborted attempts; unobserved attempts emit no usage.
- Each attempt uses its own actual execution/response mode, while the HTTP terminal counter increments once at the final committed mode across success, error, cancel, dispatch failure, and provider switch.
- Node identity reaches the immutable recorder binding but is not a public metric label; attempt/run/session identity remains internal deduplication state.
- No adapter fallback, duplicate group counter, secret/high-cardinality label, routing policy, Grafana query, ledger, or billing change is present, and reasoning characters alone do not alter existing `usage_source` semantics.
- SDD Evidence Map S01-S03 has named deterministic tests and actual output.
## Verification Results
### Request and attempt metric contract
```bash
mkdir -p /tmp/iop-s1-provider-usage-attribution-gocache
GOCACHE=/tmp/iop-s1-provider-usage-attribution-gocache go test -count=1 ./apps/edge/internal/openai -run 'UsageAttributionPolicy|UsageRecorder|UsageMetricsLabels'
```
_Paste actual stdout/stderr and exit status._
### Attempt lifecycle
```bash
GOCACHE=/tmp/iop-s1-provider-usage-attribution-gocache go test -count=1 ./apps/edge/internal/openai -run 'ProviderSwitchEmitsUsage|Attempt.*Usage|ActualOpenAIProvider'
GOCACHE=/tmp/iop-s1-provider-usage-attribution-gocache go test -race -count=1 ./apps/edge/internal/openai -run 'ProviderSwitchEmitsUsage|Attempt.*Usage'
```
_Paste actual stdout/stderr and exit status._
### S01-S03 contract and evidence
```bash
rg --sort path -n 'route_model|usage_attribution|provider_id|served_model|terminal counter' agent-contract/outer/openai-compatible-api.md agent-spec/input/openai-compatible-surface.md
GOCACHE=/tmp/iop-s1-provider-usage-attribution-gocache go test -count=1 ./apps/edge/internal/openai -run 'UsageAttribution|ProviderSwitch|UsageMetrics'
```
_Paste actual stdout/stderr and exit status._
### Final verification
```bash
mkdir -p /tmp/iop-s1-provider-usage-attribution-gocache
test -z "$(gofmt -l apps/edge/internal/openai/usage_metrics.go apps/edge/internal/openai/dispatch_context.go apps/edge/internal/openai/stream_gate_dispatcher.go apps/edge/internal/openai/stream_gate_runtime.go apps/edge/internal/openai/responses_stream_gate.go apps/edge/internal/openai/chat_handler.go apps/edge/internal/openai/chat_completion.go apps/edge/internal/openai/buffered_sse.go apps/edge/internal/openai/chat_stream_session.go apps/edge/internal/openai/provider_tunnel.go apps/edge/internal/openai/responses_handler.go apps/edge/internal/openai/responses_completion.go apps/edge/internal/openai/provider_observation.go apps/edge/internal/openai/usage_metrics_test.go apps/edge/internal/openai/usage_attribution_attempt_test.go apps/edge/internal/openai/stream_gate_dispatcher_test.go)"
GOCACHE=/tmp/iop-s1-provider-usage-attribution-gocache go test -count=1 ./packages/go/config ./apps/edge/internal/service ./apps/edge/internal/openai
GOCACHE=/tmp/iop-s1-provider-usage-attribution-gocache go test -race -count=1 ./apps/edge/internal/openai
./scripts/e2e-provider-capacity-smoke.sh
rg --sort path -n 'route_model|usage_attribution|provider_id|served_model|terminal counter' agent-contract/outer/openai-compatible-api.md agent-spec/input/openai-compatible-surface.md
git diff --check
```
_Paste actual stdout/stderr and exit status._
---
> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section?**
> If anything is blank, go back and fill it in before saving this file.
> Leave review-agent-only sections unchanged.
## Section Ownership
| Section | Owner | Note |
|---------|-------|------|
| Header comment, Overview, Review Agent Instructions | Fixed at stub creation | Implementing agent must not modify or execute these |
| Roadmap Targets | Fixed at stub creation from plan | Implementing agent must not modify; code-review copies it into `complete.log` only on PASS |
| Implementation Item Completion (item names) | Fixed at stub creation | Implementing agent checks `[ ]``[x]` only |
| Implementation Checklist (item text/order) | Fixed at stub creation from plan | Implementing agent checks `[ ]``[x]` only |
| Review-Only Checklist | Review agent only | Implementing agent must not modify or check |
| Deviations from Plan, Key Design Decisions | Implementing agent | Replace placeholders with actual content |
| Reviewer Checkpoints | Fixed at stub creation | Reviewer verifies them |
| Verification Results | Implementing agent | Paste actual output; command changes require a deviation |
| Code Review Result | Review agent appends | Not included in the stub |

View file

@ -0,0 +1,305 @@
<!-- task=m-provider-usage-attribution-hot-path/02+01_attempt_usage_emission plan=0 tag=API -->
# Actual Provider Attempt Usage Emission
## For the Implementing Agent
Filling implementation-owned sections in `CODE_REVIEW-cloud-G10.md` is mandatory. Run every verification command, paste actual notes/output, leave the active pair in place, and report ready for review. Finalization belongs only to the code-review skill. If blocked, record the exact blocker, attempted commands/output, and resume condition in implementation-owned evidence fields; do not ask the user, call user-input tools, create stop files, classify the next state, archive logs, or write `complete.log`.
## Background
The current emitter couples token usage and the HTTP terminal counter in one call and retains only the final recovery attempt's usage. This packet consumes the validated dispatch binding foundation and records provider-reported usage once per actual attempt while preserving exactly one request terminal count.
## Roadmap Targets
- Milestone: `agent-roadmap/phase/operational-observability-provider-management/milestones/provider-usage-attribution-hot-path.md`
- Milestone link: [Milestone 문서](agent-roadmap/phase/operational-observability-provider-management/milestones/provider-usage-attribution-hot-path.md)
- Task ids:
- `group-policy`: approved model-group rollup policy over one canonical provider series
- `dispatch-binding`: actual provider, served model, and node binding reaches the emitter
- `attempt-usage`: retry/fallback usage is emitted once per actual provider attempt
- Completion mode: check-on-pass
## Analysis
### Files Read
- `agent-ops/rules/project/rules.md`
- `agent-ops/rules/common/rules-roadmap.md`
- `agent-ops/rules/common/rules-agent-spec.md`
- `agent-ops/rules/project/domain/edge/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/profiles/edge-smoke.md`
- `agent-test/local/profiles/platform-common-smoke.md`
- `agent-roadmap/current.md`
- `agent-roadmap/phase/operational-observability-provider-management/PHASE.md`
- `agent-roadmap/phase/operational-observability-provider-management/milestones/provider-usage-attribution-hot-path.md`
- `agent-roadmap/sdd/operational-observability-provider-management/provider-usage-attribution-hot-path/SDD.md`
- `agent-contract/index.md`
- `agent-contract/inner/edge-config-runtime-refresh.md`
- `agent-contract/outer/openai-compatible-api.md`
- `agent-spec/index.md`
- `agent-spec/input/openai-compatible-surface.md`
- `apps/edge/internal/openai/usage_metrics.go`
- `apps/edge/internal/openai/dispatch_context.go`
- `apps/edge/internal/openai/stream_gate_dispatcher.go`
- `apps/edge/internal/openai/stream_gate_runtime.go`
- `apps/edge/internal/openai/responses_stream_gate.go`
- `apps/edge/internal/openai/chat_handler.go`
- `apps/edge/internal/openai/chat_completion.go`
- `apps/edge/internal/openai/buffered_sse.go`
- `apps/edge/internal/openai/normalized_sse.go`
- `apps/edge/internal/openai/chat_stream_session.go`
- `apps/edge/internal/openai/provider_tunnel.go`
- `apps/edge/internal/openai/provider_observation.go`
- `apps/edge/internal/openai/responses_handler.go`
- `apps/edge/internal/openai/responses_completion.go`
- `apps/edge/internal/openai/run_result.go`
- `apps/edge/internal/openai/usage_metrics_test.go`
- `apps/edge/internal/openai/stream_gate_dispatcher_test.go`
- `apps/edge/internal/openai/server_test_support_test.go`
- `apps/edge/internal/openai/provider_test_support_test.go`
- `apps/edge/internal/service/run_types.go`
- `scripts/e2e-provider-capacity-smoke.sh`
- `Makefile`
### SDD Criteria
The approved, unlocked SDD is `agent-roadmap/sdd/operational-observability-provider-management/provider-usage-attribution-hot-path/SDD.md`. This packet targets S01/`group-policy`, S02/`dispatch-binding`, and S03/`attempt-usage`. Evidence Map S01 requires config/route policy plus provider-series/group-rollup delta assertions; S02 requires direct, pool-normalized, and pool-tunnel actual binding assertions; S03 requires a deterministic provider switch with per-provider token deltas and request counter delta one. API-1 maps S01/S02 to the emitter contract, API-2 maps all attempt paths to S03, and API-3 supplies the exact Evidence Map tests and final verification.
### Verification Context
No handoff was supplied. Verification sources actually read were `agent-test/local/rules.md`, `agent-test/local/profiles/edge-smoke.md`, `agent-test/local/profiles/platform-common-smoke.md`, `Makefile`, and `scripts/e2e-provider-capacity-smoke.sh`. Preflight on branch `provider-usage-attribution-hot-path` at HEAD `88f518f0294d461d5e05b9a14229fed3d6126d12` found a clean checkout, Go `go1.26.2 linux/arm64`, module `/config/workspace/iop-s1/go.mod`, and `go`, `curl`, and `jq` available. The inherited `GOCACHE` is unreadable; `/tmp/iop-s1-provider-usage-attribution-gocache` produced fresh passing baselines for config, service, and OpenAI packages. The repository-native e2e smoke uses local temporary processes and ports and needs no external credential. Cached output is not acceptable.
### Test Coverage Gaps
- `emitUsageMetrics` increments request and token counters together, so it cannot represent two provider attempts and one request terminal.
- `openAIStreamGateUsageHolder` intentionally stores only the latest successful attempt and discards aborted-attempt usage.
- Runtime-enabled Chat and Responses recovery event-source factories do not bind each observation to that attempt's `RunDispatch`.
- Legacy tool-validation retry loops close failed attempts without emitting any provider-reported usage they returned.
- Existing metric tests assert request-model `model_group`; none asserts actual provider/served-model separation, approved group rollup basis, or a provider switch.
- Existing tests do not cover a retry that changes execution path/response mode, so attempt labels could accidentally inherit the final request mode.
### Symbol References
- `emitUsageMetrics` call sites exist in `chat_handler.go`, `chat_completion.go`, `buffered_sse.go`, `chat_stream_session.go`, `provider_tunnel.go`, `responses_handler.go`, `responses_completion.go`, `stream_gate_runtime.go`, and `responses_stream_gate.go`; every call must move to explicit request-terminal or attempt-usage ownership.
- `usageLabelsFor`, `responsesDispatchContext.usageLabels`, and `chatDispatchContext.usageLabels` are the request-model label constructors and must be replaced or narrowed consistently.
- `openAIStreamGateUsageHolder` is constructed and consumed in Chat, Responses, and tunnel runtime builders; preserve response rendering's final-attempt observation separately from metric attempt accumulation.
- `openAIAttemptTransport` and `openAIAttemptController` are created for initial and recovery attempts in `stream_gate_dispatcher.go`, `stream_gate_runtime.go`, and `responses_stream_gate.go`.
### Split Judgment
- `01_attribution_binding_foundation`: predecessor stable contract is validated `ProviderID`, `Target`, `NodeID`, and `UsageAttribution` on every dispatch.
- `02+01_attempt_usage_emission`: stable contract is one provider-attributed token/reasoning emission per observed attempt plus one request terminal emission.
Dependency `01_attribution_binding_foundation` is currently missing its active `complete.log`; do not implement this packet until the task-loop dependency resolver confirms `agent-task/m-provider-usage-attribution-hot-path/01_attribution_binding_foundation/complete.log` or supplies the exact matching archived predecessor `complete.log`. Do not search `agent-task/archive/**` broadly.
### Scope Rationale
Do not change provider routing/admission, retry selection, recovery limits, response body framing, token estimation, metric names, Grafana queries, pricing, request ledgers, or billing. S04 `metric-contract` and S05 `grafana-migration` remain unchecked; this packet updates only the minimum current contract/spec text required by S01-S03 code semantics.
### Final Routing
- evaluation_mode: `first-pass`
- finalizer: `finalize-task-policy.sh pair`
- build: all closures true; scores `scope=2,state=2,blast=2,evidence=2,verification=2`; grade `G10`, route `cloud`, basis `grade-boundary`, filename `PLAN-cloud-G10.md`
- review: all closures true; scores `scope=2,state=2,blast=2,evidence=2,verification=2`; grade `G10`, route `cloud`, basis `official-review`, filename `CODE_REVIEW-cloud-G10.md`
- large_indivisible_context: `false`
- positive loop risks: `temporal_state`, `concurrent_consistency`, `boundary_contract`, `variant_product` (count `4`)
- recovery signals: review rework `0`, evidence-integrity failure `false`
- capability-gap evidence: none
## Implementation Checklist
- [ ] Split request-terminal accounting from provider-attributed attempt usage and enforce the approved group policy without duplicate counters.
- [ ] Bind and finalize usage for every direct, tunnel, normalized, retry, fallback, success, error, and cancel attempt exactly once.
- [ ] Add deterministic S01-S03 tests and synchronize the minimum current OpenAI contract/spec.
- [ ] Run fresh focused, full-package, race, local provider-capacity smoke, formatting, and diff verification.
- [ ] Fill implementation-owned sections in CODE_REVIEW-*-G??.md with actual implementation notes and verification output.
### [API-1] Separate Request and Attempt Metric Contracts
#### Problem
`apps/edge/internal/openai/usage_metrics.go:164` makes `emitUsageMetrics` increment both `iop_openai_requests_total` and token/reasoning counters using one request-model label set. This cannot preserve SDD rules that canonical usage starts at actual provider binding, approved groups roll up only at query time, and the HTTP terminal counter increments once.
#### Solution
Replace the combined emitter with a request-local recorder that owns two distinct operations:
1. `RecordAttempt(binding, observation)` adds provider-reported token/reasoning values once for one dispatch binding.
2. `FinishRequest(status, responseMode)` increments the request terminal counter once at the final committed mode and derives `usage_source` from whether any attempt reported the existing provider token fields.
Use separate low-cardinality label structs. Request labels keep caller identity, `route_model`, endpoint, the final committed response mode, status, and usage source. Each attempt derives its response mode from that attempt's actual execution path and binds `usage_attribution`, actual `provider_id`, `served_model`, and `node_id`. Carry `node_id` through the immutable recorder binding and cover it with unit assertions, but do not add it to a public metric label vector in this packet; the public token/reasoning vectors use only the approved stable subset. Use an internal attempt/run identity only for exactly-once state and never expose it as a label. `usage_attribution=model_group` enables only query-time grouping by `route_model`; it never emits a second token counter. Preserve the existing `usage_source` rule: auxiliary reasoning-character observations alone do not reclassify a request as provider-reported.
```go
// Before (apps/edge/internal/openai/usage_metrics.go:170)
func emitUsageMetrics(l usageLabels, status string, obs usageObservation) {
openAIRequestsTotal.WithLabelValues(...).Inc()
addTokenCount(l, tokenTypeInput, obs.inputTokens)
}
// After
type openAIUsageRecorder struct {
// request terminal once + per-attempt deduplication state
}
func (r *openAIUsageRecorder) RecordAttempt(binding usageDispatchBinding, obs usageObservation)
func (r *openAIUsageRecorder) FinishRequest(status, responseMode string)
```
#### Modified Files and Checklist
- [ ] `apps/edge/internal/openai/usage_metrics.go` — define split labels, dispatch binding, request recorder, per-attempt dedupe, and exactly-once terminal logic.
- [ ] `apps/edge/internal/openai/dispatch_context.go` — create one recorder at ingress and derive actual attempt labels from `RunDispatch`.
- [ ] `apps/edge/internal/openai/usage_metrics_test.go` — update allowlist/helpers and cover default/group/direct/hybrid bases with no duplicate group counter.
#### Test Strategy
Extend `apps/edge/internal/openai/usage_metrics_test.go` with `TestUsageAttributionPolicyEmitsSingleCanonicalProviderSeries`, `TestUsageRecorderFinishesRequestOnce`, and updated secret/high-cardinality guards. Assert default `provider`, approved `model_group`, actual provider/served model, internal node preservation, no public `node_id` or attempt/run/session label, no adapter substitution, no second group counter, duplicate attempt finalization ignored, final request response mode preserved, and terminal request delta one.
#### Verification
```bash
mkdir -p /tmp/iop-s1-provider-usage-attribution-gocache
GOCACHE=/tmp/iop-s1-provider-usage-attribution-gocache go test -count=1 ./apps/edge/internal/openai -run 'UsageAttributionPolicy|UsageRecorder|UsageMetricsLabels'
```
Expected: policy/label/dedupe tests pass and one usage observation changes exactly one canonical provider series.
### [API-2] Finalize Usage at Every Attempt Lifecycle Boundary
#### Problem
`apps/edge/internal/openai/stream_gate_runtime.go:83` retains only last-attempt usage, while `apps/edge/internal/openai/stream_gate_dispatcher.go:296` closes or aborts an attempt without metric finalization. Legacy retries in `apps/edge/internal/openai/chat_completion.go:72` and `apps/edge/internal/openai/buffered_sse.go:55` also close the failed attempt before recording its returned usage.
#### Solution
Carry the actual `RunDispatch` and that attempt's execution/response mode on each `openAIAttemptTransport`, then create an attempt observation owned jointly by its event source and idempotent controller. Event sources record provider-reported usage/reasoning; `CloseAttempt` and `AbortAttempt` claim and flush observed values exactly once before releasing transport ownership, while an aborted attempt with no observation emits no token/reasoning delta. Keep the existing final-attempt holder only for response body rendering. Update non-runtime normalized, live SSE, buffered, Responses, and provider-tunnel paths to use the same recorder contract. In legacy tool-validation retries, record usage returned by the rejected attempt before starting the replacement. A dispatch failure with no binding emits only the request terminal; an attempt with reported usage emits that usage even when recovery replaces it. All terminal paths call `FinishRequest`, but its once guard ensures one HTTP request count and the terminal request uses the final committed response mode rather than overwriting prior attempt modes.
```go
// Before (apps/edge/internal/openai/stream_gate_dispatcher.go:296)
type openAIAttemptController struct {
dispatch edgeservice.RunDispatch
closeTransport func()
}
// After
type openAIAttemptController struct {
dispatch edgeservice.RunDispatch
observation *openAIAttemptUsage
closeTransport func()
}
```
```go
// Before (apps/edge/internal/openai/stream_gate_runtime.go:83)
// openAIStreamGateUsageHolder carries the last-attempt usage observation...
// After
// The response holder remains final-attempt-only; metric observations are
// finalized by each attempt controller against its immutable dispatch binding.
```
#### Modified Files and Checklist
- [ ] `apps/edge/internal/openai/stream_gate_dispatcher.go` — attach dispatch and attempt usage to initial/recovery controllers.
- [ ] `apps/edge/internal/openai/stream_gate_runtime.go` — record normalized/buffered/tunnel observations per attempt and separate response usage.
- [ ] `apps/edge/internal/openai/responses_stream_gate.go` — bind Responses normalized/tunnel attempts and finish the request once.
- [ ] `apps/edge/internal/openai/chat_handler.go` — own the request recorder for dispatch failures and selected pool paths.
- [ ] `apps/edge/internal/openai/chat_completion.go` — emit each legacy non-stream attempt before retry/return.
- [ ] `apps/edge/internal/openai/buffered_sse.go` — emit each legacy buffered attempt before retry/return.
- [ ] `apps/edge/internal/openai/chat_stream_session.go` — bind live SSE terminal usage to the actual dispatch.
- [ ] `apps/edge/internal/openai/provider_tunnel.go` — bind direct/pool tunnel observations and terminal request status.
- [ ] `apps/edge/internal/openai/responses_handler.go` — own the Responses request recorder across direct/pool branches.
- [ ] `apps/edge/internal/openai/responses_completion.go` — record normalized Responses attempt usage separately from request terminal.
- [ ] `apps/edge/internal/openai/provider_observation.go` — expose one immutable attempt observation without changing provider bytes.
#### Test Strategy
Add `apps/edge/internal/openai/usage_attribution_attempt_test.go` with `TestProviderSwitchEmitsUsagePerActualAttemptAndOneRequest`. Use deterministic in-memory provider A/provider B dispatches that report different input/output/reasoning/cached counts, force one recovery switch across distinct execution/response modes, and assert independent provider-and-mode series deltas plus request delta one at the final committed mode. Extend `apps/edge/internal/openai/stream_gate_dispatcher_test.go` to prove Close/Abort finalization idempotence, observed usage on an aborted/replaced attempt, zero usage for an unobserved aborted attempt and pre-dispatch failure, and no adapter fallback.
#### Verification
```bash
GOCACHE=/tmp/iop-s1-provider-usage-attribution-gocache go test -count=1 ./apps/edge/internal/openai -run 'ProviderSwitchEmitsUsage|Attempt.*Usage|ActualOpenAIProvider'
GOCACHE=/tmp/iop-s1-provider-usage-attribution-gocache go test -race -count=1 ./apps/edge/internal/openai -run 'ProviderSwitchEmitsUsage|Attempt.*Usage'
```
Expected: provider A and B series receive only their own counts, request terminal delta is one, and race/deduplication checks pass.
### [API-3] Lock S01-S03 Evidence and Current Contract
#### Problem
`agent-contract/outer/openai-compatible-api.md:174` and `agent-spec/input/openai-compatible-surface.md:157` still describe request-alias `model_group` attribution. Without synchronized current semantics and deterministic scenario names, code review cannot map SDD Evidence Map S01-S03 to completion.
#### Solution
Update the current OpenAI contract/spec only for the implemented S01-S03 semantics: `route_model`, actual provider/served model, policy default/opt-in, one canonical series, per-attempt execution mode, and one request terminal at the final committed mode. Record that node identity reaches the recorder binding but is not introduced as a public metric label here. Explicitly leave Grafana query migration and the full S04 label-contract closure to the second epic. Keep raw/high-cardinality prohibitions unchanged.
#### Modified Files and Checklist
- [ ] `agent-contract/outer/openai-compatible-api.md` — record S01-S03 attribution and attempt/request cardinality contract.
- [ ] `agent-spec/input/openai-compatible-surface.md` — synchronize current dispatch/metric behavior and test pointers.
- [ ] `apps/edge/internal/openai/usage_attribution_attempt_test.go` — provide named S03 provider-switch evidence.
- [ ] `apps/edge/internal/openai/stream_gate_dispatcher_test.go` — provide controller/binding regression evidence.
#### Test Strategy
The new and extended tests are required; do not replace them with mock-only label construction. Review must see one handler/runtime path that switches actual providers and reads provider-reported usage.
#### Verification
```bash
rg --sort path -n 'route_model|usage_attribution|provider_id|served_model|terminal counter' agent-contract/outer/openai-compatible-api.md agent-spec/input/openai-compatible-surface.md
GOCACHE=/tmp/iop-s1-provider-usage-attribution-gocache go test -count=1 ./apps/edge/internal/openai -run 'UsageAttribution|ProviderSwitch|UsageMetrics'
```
Expected: contract/spec language matches passing S01-S03 tests and does not add Grafana query changes.
## Dependencies and Execution Order
1. `01_attribution_binding_foundation` must produce `complete.log` before implementation starts; the dependency is encoded by `02+01_attempt_usage_emission` and must be resolved through the active path or an exact archived completion path supplied by the task loop, without broad archive discovery.
2. Complete API-1 before replacing lifecycle call sites.
3. Complete API-2 before locking API-3 evidence and running final verification.
## Modified Files Summary
| File | Item |
|------|------|
| `apps/edge/internal/openai/usage_metrics.go` | API-1 |
| `apps/edge/internal/openai/dispatch_context.go` | API-1 |
| `apps/edge/internal/openai/usage_metrics_test.go` | API-1 |
| `apps/edge/internal/openai/stream_gate_dispatcher.go` | API-2 |
| `apps/edge/internal/openai/stream_gate_runtime.go` | API-2 |
| `apps/edge/internal/openai/responses_stream_gate.go` | API-2 |
| `apps/edge/internal/openai/chat_handler.go` | API-2 |
| `apps/edge/internal/openai/chat_completion.go` | API-2 |
| `apps/edge/internal/openai/buffered_sse.go` | API-2 |
| `apps/edge/internal/openai/chat_stream_session.go` | API-2 |
| `apps/edge/internal/openai/provider_tunnel.go` | API-2 |
| `apps/edge/internal/openai/responses_handler.go` | API-2 |
| `apps/edge/internal/openai/responses_completion.go` | API-2 |
| `apps/edge/internal/openai/provider_observation.go` | API-2 |
| `agent-contract/outer/openai-compatible-api.md` | API-3 |
| `agent-spec/input/openai-compatible-surface.md` | API-3 |
| `apps/edge/internal/openai/usage_attribution_attempt_test.go` | API-3 |
| `apps/edge/internal/openai/stream_gate_dispatcher_test.go` | API-3 |
| `agent-task/m-provider-usage-attribution-hot-path/02+01_attempt_usage_emission/CODE_REVIEW-cloud-G10.md` | API-1, API-2, API-3 |
## Final Verification
```bash
mkdir -p /tmp/iop-s1-provider-usage-attribution-gocache
test -z "$(gofmt -l apps/edge/internal/openai/usage_metrics.go apps/edge/internal/openai/dispatch_context.go apps/edge/internal/openai/stream_gate_dispatcher.go apps/edge/internal/openai/stream_gate_runtime.go apps/edge/internal/openai/responses_stream_gate.go apps/edge/internal/openai/chat_handler.go apps/edge/internal/openai/chat_completion.go apps/edge/internal/openai/buffered_sse.go apps/edge/internal/openai/chat_stream_session.go apps/edge/internal/openai/provider_tunnel.go apps/edge/internal/openai/responses_handler.go apps/edge/internal/openai/responses_completion.go apps/edge/internal/openai/provider_observation.go apps/edge/internal/openai/usage_metrics_test.go apps/edge/internal/openai/usage_attribution_attempt_test.go apps/edge/internal/openai/stream_gate_dispatcher_test.go)"
GOCACHE=/tmp/iop-s1-provider-usage-attribution-gocache go test -count=1 ./packages/go/config ./apps/edge/internal/service ./apps/edge/internal/openai
GOCACHE=/tmp/iop-s1-provider-usage-attribution-gocache go test -race -count=1 ./apps/edge/internal/openai
./scripts/e2e-provider-capacity-smoke.sh
rg --sort path -n 'route_model|usage_attribution|provider_id|served_model|terminal counter' agent-contract/outer/openai-compatible-api.md agent-spec/input/openai-compatible-surface.md
git diff --check
```
Expected: formatting is clean; fresh package and race tests pass; local provider-capacity smoke passes; S01-S03 contract/spec pointers match; diff check passes. Cached test output is not acceptable. After completing all code changes, fill implementation-owned sections in `CODE_REVIEW-*-G??.md`.