From f98d1b434baed18ea28f7b79ca1336cf622524ef Mon Sep 17 00:00:00 2001 From: toki Date: Fri, 31 Jul 2026 21:20:19 +0900 Subject: [PATCH] =?UTF-8?q?fix(openai):=20usage=20=EA=B7=80=EC=86=8D=20?= =?UTF-8?q?=EB=A7=88=EC=9D=BC=EC=8A=A4=ED=86=A4=EC=9D=84=20=EC=A2=85?= =?UTF-8?q?=EB=A3=8C=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stream Gate 준비 실패에서도 요청 terminal 관측 계약을 지키고, direct provider identity 마이그레이션 누락으로 기존 검증과 운영 설정이 깨지지 않게 한다. --- .../provider-usage-attribution-hot-path.md | 22 +-- .../SDD.md | 0 .../PHASE.md | 6 +- agent-roadmap/priority-queue.md | 61 ++++----- agent-spec/input/openai-compatible-surface.md | 5 +- apps/edge/README.md | 2 + .../cmd/edge/bootstrap_node_command_test.go | 1 + .../edge/cmd/edge/root_config_command_test.go | 1 + .../bootstrap/runtime_refresh_test.go | 1 + .../bootstrap/runtime_test_support_test.go | 1 + .../configrefresh/path_refresh_test.go | 2 + apps/edge/internal/openai/chat_handler.go | 2 + .../edge/internal/openai/responses_handler.go | 2 + .../internal/openai/usage_metrics_test.go | 64 +++++++++ docs/edge-local-dev-guide.md | 1 + docs/openai-usage-grafana.md | 125 ++++++++++++------ scripts/e2e-openai-cli-workspace.sh | 1 + scripts/e2e-openai-lemonade.sh | 1 + scripts/e2e-openai-ollama.sh | 1 + scripts/e2e-openai-vllm.sh | 1 + 20 files changed, 213 insertions(+), 87 deletions(-) rename agent-roadmap/{ => archive}/phase/operational-observability-provider-management/milestones/provider-usage-attribution-hot-path.md (73%) rename agent-roadmap/{ => archive}/sdd/operational-observability-provider-management/provider-usage-attribution-hot-path/SDD.md (100%) diff --git a/agent-roadmap/phase/operational-observability-provider-management/milestones/provider-usage-attribution-hot-path.md b/agent-roadmap/archive/phase/operational-observability-provider-management/milestones/provider-usage-attribution-hot-path.md similarity index 73% rename from agent-roadmap/phase/operational-observability-provider-management/milestones/provider-usage-attribution-hot-path.md rename to agent-roadmap/archive/phase/operational-observability-provider-management/milestones/provider-usage-attribution-hot-path.md index fbe987e..1278267 100644 --- a/agent-roadmap/phase/operational-observability-provider-management/milestones/provider-usage-attribution-hot-path.md +++ b/agent-roadmap/archive/phase/operational-observability-provider-management/milestones/provider-usage-attribution-hot-path.md @@ -11,7 +11,7 @@ OpenAI-compatible hot path의 provider-reported token usage를 가상 요청 모 ## 상태 -[계획] +[완료] ## 승격 조건 @@ -44,24 +44,26 @@ OpenAI-compatible hot path의 provider-reported token usage를 가상 요청 모 단일·하이브리드 실행에서 token 사용량을 실제 provider 호출에 정확히 귀속하는 hot-path capability를 묶는다. -- [ ] [group-policy] `models[].usage_attribution=model_group`으로 명시 승인된 동일 논리 모델 group만 가상 rollup query를 허용하고, 기본값 및 그 밖의 route에는 provider 기준 attribution을 강제한다. 검증: group·direct·hybrid config/route test에서 attribution basis가 기대값과 일치하고 별도 group counter가 중복 emit되지 않는다. -- [ ] [dispatch-binding] direct/legacy의 `openai.model_routes[].provider_id`와 top-level fallback `openai.provider_id`, provider-pool의 normalized/tunnel dispatch가 실제 `provider_id`, served model, node identity를 metric emitter까지 전달한다. 검증: adapter 이름만으로 provider를 대체하지 않고 각 dispatch binding을 검증한다. -- [ ] [attempt-usage] hybrid selection, retry, fallback에서 provider-reported token 및 reasoning observation usage를 실제 호출 시도별 provider binding으로 emit한다. 검증: provider가 바뀌는 deterministic test에서 token series가 각 provider에 분리되어 증가하고 request terminal counter는 한 번만 증가한다. +- [x] [group-policy] `models[].usage_attribution=model_group`으로 명시 승인된 동일 논리 모델 group만 가상 rollup query를 허용하고, 기본값 및 그 밖의 route에는 provider 기준 attribution을 강제한다. 검증: group·direct·hybrid config/route test에서 attribution basis가 기대값과 일치하고 별도 group counter가 중복 emit되지 않는다. +- [x] [dispatch-binding] direct/legacy의 `openai.model_routes[].provider_id`와 top-level fallback `openai.provider_id`, provider-pool의 normalized/tunnel dispatch가 실제 `provider_id`, served model, node identity를 metric emitter까지 전달한다. 검증: adapter 이름만으로 provider를 대체하지 않고 각 dispatch binding을 검증한다. +- [x] [attempt-usage] hybrid selection, retry, fallback에서 provider-reported token 및 reasoning observation usage를 실제 호출 시도별 provider binding으로 emit한다. 검증: provider가 바뀌는 deterministic test에서 token series가 각 provider에 분리되어 증가하고 request terminal counter는 한 번만 증가한다. ### Epic: [operations] Usage Metric Migration provider 기준 운영 조회를 기존 OpenAI usage metric과 Grafana 가이드에 정착시킨다. -- [ ] [metric-contract] metric label allowlist와 OpenAI-compatible 관측 계약을 provider attribution 기준으로 갱신하고 기존 `model_group`은 `route_model` trace와 승인된 rollup query로 migration한다. 검증: secret/high-cardinality label guard와 metric contract test가 통과한다. -- [ ] [grafana-migration] Grafana query와 usage 운영 가이드를 provider 기준 집계 및 승인된 model-group rollup 기준으로 갱신한다. 검증: 대표 provider·group query가 metric label schema와 일치한다. +- [x] [metric-contract] metric label allowlist와 OpenAI-compatible 관측 계약을 provider attribution 기준으로 갱신하고 기존 `model_group`은 `route_model` trace와 승인된 rollup query로 migration한다. 검증: secret/high-cardinality label guard와 metric contract test가 통과한다. +- [x] [grafana-migration] Grafana query와 usage 운영 가이드를 provider 기준 집계 및 승인된 model-group rollup 기준으로 갱신한다. 검증: 대표 provider·group query가 metric label schema와 일치한다. ## 완료 리뷰 -- 상태: 없음 -- 요청일: 없음 -- 완료 근거: 계획 Milestone이며 기능 Task가 아직 충족되지 않았다. +- 상태: 통과 +- 요청일: 2026-07-31 +- 완료 근거: `group-policy`, `dispatch-binding`, `attempt-usage`, `metric-contract`의 현재 코드·계약·검증 evidence와 `grafana-migration`의 [Grafana query guide](../../../../docs/openai-usage-grafana.md) actual-provider/승인-group PromQL schema 검증이 충족되었다. 종료 감사에서 provider-pool Stream Gate 준비 실패의 request terminal 누락을 Chat/Responses 양쪽에서 보완하고, direct-route `provider_id` 필수 계약을 기존 성공 fixture·E2E 설정·운영 가이드에 동기화했다. `go test -count=1 ./packages/go/streamgate ./apps/edge/internal/openai`, `go test -count=1 ./packages/go/config ./apps/edge/internal/service`, `go test -count=1 ./apps/edge/cmd/edge ./apps/edge/internal/bootstrap ./apps/edge/internal/configrefresh`, `go test -race -count=1 ./packages/go/streamgate ./packages/go/config ./apps/edge/internal/openai`가 통과했다. +- Spec sync: [OpenAI-compatible 입력 표면](../../../../agent-spec/input/openai-compatible-surface.md)과 [Edge-Node 실행 경로](../../../../agent-spec/runtime/edge-node-execution.md)가 actual-provider dispatch binding, attempt usage, exactly-once terminal 및 Grafana migration 상태와 일치한다. 결과: Spec updated. +- 남은 차단 항목: 없음 - 검토 항목: 기능 Task의 deterministic attribution 검증, Grafana query migration, SDD Evidence Map 충족과 구현 잠금 해제를 함께 확인한다. -- 리뷰 코멘트: 없음 +- 리뷰 코멘트: 전체 `go test ./...` 보조 실행의 잔여 실패는 다음 `IOP Agent CLI Runtime` 범위의 fake-owner Unix socket/권한 fixture와 taskloop 규칙 검사에 한정되며, 현재 마일스톤의 Edge·Node·config·OpenAI 범위에는 실패가 없다. ## 범위 제외 diff --git a/agent-roadmap/sdd/operational-observability-provider-management/provider-usage-attribution-hot-path/SDD.md b/agent-roadmap/archive/sdd/operational-observability-provider-management/provider-usage-attribution-hot-path/SDD.md similarity index 100% rename from agent-roadmap/sdd/operational-observability-provider-management/provider-usage-attribution-hot-path/SDD.md rename to agent-roadmap/archive/sdd/operational-observability-provider-management/provider-usage-attribution-hot-path/SDD.md diff --git a/agent-roadmap/phase/operational-observability-provider-management/PHASE.md b/agent-roadmap/phase/operational-observability-provider-management/PHASE.md index 77a7d10..7c1d6a1 100644 --- a/agent-roadmap/phase/operational-observability-provider-management/PHASE.md +++ b/agent-roadmap/phase/operational-observability-provider-management/PHASE.md @@ -46,9 +46,9 @@ Phase를 가로지르는 실제 다음 작업 선택은 [전역 마일스톤 실 - 경로: [provider-resource-admission-ownership-alignment](../../archive/phase/operational-observability-provider-management/milestones/provider-resource-admission-ownership-alignment.md) - 요약: 공유 provider의 capacity·long-context lease, 공통 queue policy, Node reconnect/offline fencing과 Control Plane snapshot을 정렬하고 local two-alias capacity-1 smoke까지 검증했다. -- [계획] Provider 기준 Usage Attribution Hot Path - - 경로: [provider-usage-attribution-hot-path](milestones/provider-usage-attribution-hot-path.md) - - 요약: OpenAI-compatible token usage를 실제 호출 provider·served model·실행 시도에 귀속하고, 명시적으로 같은 논리 모델로 승인된 group에서만 가상 model group 집계를 허용한다. +- [완료] Provider 기준 Usage Attribution Hot Path + - 경로: [provider-usage-attribution-hot-path](../../archive/phase/operational-observability-provider-management/milestones/provider-usage-attribution-hot-path.md) + - 요약: OpenAI-compatible token usage를 actual provider·served model·실행 시도에 귀속하고, 승인된 model group의 query-time rollup과 Grafana 운영 조회를 정착시켰다. - [계획] Provider 부하 메트릭과 Live Queue Dashboard - 경로: [provider-load-metrics-queue-dashboard](milestones/provider-load-metrics-queue-dashboard.md) diff --git a/agent-roadmap/priority-queue.md b/agent-roadmap/priority-queue.md index 8b5b72c..b7e386c 100644 --- a/agent-roadmap/priority-queue.md +++ b/agent-roadmap/priority-queue.md @@ -4,89 +4,86 @@ ## 실행 순서 -1. [Provider 기준 Usage Attribution Hot Path](phase/operational-observability-provider-management/milestones/provider-usage-attribution-hot-path.md) - OpenAI-compatible token usage를 실제 provider·served model·실행 시도에 귀속하고, 명시적으로 같은 논리 모델로 승인된 group에서만 가상 model group 집계를 허용한다. +1. [IOP Agent CLI Runtime](phase/automation-runtime-bridge/milestones/iop-agent-cli-runtime.md) + 현재 Python 감시·dispatcher와 Node CLI runtime의 전체 동등성을 단일 Go CLI Provider·AgentTaskManager 및 독립 `iop-agent` binary로 이전한다. -2. [IOP Agent CLI Runtime](phase/automation-runtime-bridge/milestones/iop-agent-cli-runtime.md) - 현재 Python 감시·dispatcher와 Node CLI runtime의 전체 동등성을 단일 Go CLI Provider·AgentTaskManager 및 독립 `iop-agent` binary로 이전한다. - -3. [OpenAI-compatible 출력 검증 필터](phase/knowledge-tool-optimization-extension/milestones/openai-compatible-output-validation-filters.md) +2. [OpenAI-compatible 출력 검증 필터](phase/knowledge-tool-optimization-extension/milestones/openai-compatible-output-validation-filters.md) 실제 의미 필터 전에 deterministic diagnostic mock으로 실제 Stream Evidence Gate의 pass·observe-only·blocking recovery를 관측하는 smoke를 통과시키고, OpenAI-compatible single-stream 반복과 incoming request history에 누적된 assistant 반복, JSON contract 검증/repair 경로를 안정화한다. -4. [OpenAI-compatible Incomplete Tool Call Syntax Gate](phase/knowledge-tool-optimization-extension/milestones/openai-compatible-incomplete-tool-call-syntax-gate.md) +3. [OpenAI-compatible Incomplete Tool Call Syntax Gate](phase/knowledge-tool-optimization-extension/milestones/openai-compatible-incomplete-tool-call-syntax-gate.md) terminal provider 응답의 incomplete tool-call syntax를 deterministic하게 판정한다. -5. [OpenAI-compatible Runtime Output Integrity Filter](phase/knowledge-tool-optimization-extension/milestones/openai-compatible-runtime-output-integrity-filter.md) +4. [OpenAI-compatible Runtime Output Integrity Filter](phase/knowledge-tool-optimization-extension/milestones/openai-compatible-runtime-output-integrity-filter.md) terminal output invariant와 공통 filter/retry pipeline을 정의한다. -6. [LLM 판별 기반 Missing Tool Call 재시도 Gate](phase/knowledge-tool-optimization-extension/milestones/llm-judged-missing-tool-call-retry-gate.md) +5. [LLM 판별 기반 Missing Tool Call 재시도 Gate](phase/knowledge-tool-optimization-extension/milestones/llm-judged-missing-tool-call-retry-gate.md) tool 사용 의도 누락 케이스를 LLM judge와 buffered retry 후보로 검토한다. -7. [Tool Call 판정 모델 Gate 리뷰](phase/knowledge-tool-optimization-extension/milestones/tool-call-validator-model-gate-review.md) +6. [Tool Call 판정 모델 Gate 리뷰](phase/knowledge-tool-optimization-extension/milestones/tool-call-validator-model-gate-review.md) schema만으로 어려운 tool-call 후보에 validator 모델을 쓸지 검토한다. -8. [Provider 부하 메트릭과 Live Queue Dashboard](phase/operational-observability-provider-management/milestones/provider-load-metrics-queue-dashboard.md) +7. [Provider 부하 메트릭과 Live Queue Dashboard](phase/operational-observability-provider-management/milestones/provider-load-metrics-queue-dashboard.md) Edge provider-pool의 capacity, in-flight, queued와 queue wait를 Prometheus/Grafana로 관측해 provider별 live 부하와 적체·회복을 분석한다. -9. [Pi CLI Provider Integration](phase/automation-runtime-bridge/milestones/pi-cli-provider-integration.md) +8. [Pi CLI Provider Integration](phase/automation-runtime-bridge/milestones/pi-cli-provider-integration.md) Pi를 Node CLI provider 실행 후보에 추가하고 OpenAI-compatible route smoke로 안정화한다. -10. [CLI Agent Group Grade Routing](phase/automation-runtime-bridge/milestones/cli-agent-group-grade-routing.md) +9. [CLI Agent Group Grade Routing](phase/automation-runtime-bridge/milestones/cli-agent-group-grade-routing.md) lane/grade 파일명과 `metadata.agent_group.task_file` 기반 CLI agent group 라우팅 계약을 정리한다. -11. [에이전트 작업 루프 오케스트레이션 MVP](phase/automation-runtime-bridge/milestones/agent-workflow-loop-orchestration-mvp.md) +10. [에이전트 작업 루프 오케스트레이션 MVP](phase/automation-runtime-bridge/milestones/agent-workflow-loop-orchestration-mvp.md) 일반 사용자 요청을 direct/Plan/Milestone으로 분류하고, 사용자 agent의 tool call로 만든 작업 파일을 IOP가 읽어 다음 실행·리뷰·완료 단계까지 연결한다. -12. [Provider 사용량 알림과 운영 표면](phase/automation-runtime-bridge/milestones/provider-usage-notification-operations-surface.md) +11. [Provider 사용량 알림과 운영 표면](phase/automation-runtime-bridge/milestones/provider-usage-notification-operations-surface.md) 공통 runtime의 quota/status/failure event를 소비해 macOS·Desktop·후속 외부 채널에 전달하는 알림과 이력 표면을 스케치한다. -13. [단계 호출과 검증 최적화 MVP](phase/knowledge-tool-optimization-extension/milestones/knowledge-tool-validation-optimization.md) +12. [단계 호출과 검증 최적화 MVP](phase/knowledge-tool-optimization-extension/milestones/knowledge-tool-validation-optimization.md) planner/generator/verifier 단계 호출과 runtime schema 검증 실행 모드를 스케치한다. -14. [원격 코딩/유지보수 작업 환경](phase/automation-runtime-bridge/milestones/remote-workspace-operations-environment.md) +13. [원격 코딩/유지보수 작업 환경](phase/automation-runtime-bridge/milestones/remote-workspace-operations-environment.md) workspace-bound execution 기반 원격 코딩/유지보수 운영 경계를 스케치한다. -15. [Personal Local Edge 패키징과 배포 모드 프로파일](phase/personal-edge-packaging-deployment/milestones/personal-local-edge-deployment-profiles.md) +14. [Personal Local Edge 패키징과 배포 모드 프로파일](phase/personal-edge-packaging-deployment/milestones/personal-local-edge-deployment-profiles.md) personal/server/fleet 배포 모드와 capability gate 경계를 스케치한다. -16. [요청 실행 로그와 Usage Ledger 기반](phase/operational-observability-provider-management/milestones/request-execution-log-usage-ledger-foundation.md) +15. [요청 실행 로그와 Usage Ledger 기반](phase/operational-observability-provider-management/milestones/request-execution-log-usage-ledger-foundation.md) 요청별 provider/model 선택, timing, token, status/error를 구조화된 ledger로 남기는 기반을 스케치한다. -17. [Update Plane 안정 프로토콜](phase/update-plane-self-update-foundation/milestones/update-plane-stable-protocol.md) +16. [Update Plane 안정 프로토콜](phase/update-plane-self-update-foundation/milestones/update-plane-stable-protocol.md) hello/status, manifest, command, event, recovery 최소 계약을 스케치한다. -18. [Host-local Manager 기반 자체 업데이트](phase/update-plane-self-update-foundation/milestones/host-local-manager-self-update.md) +17. [Host-local Manager 기반 자체 업데이트](phase/update-plane-self-update-foundation/milestones/host-local-manager-self-update.md) manager/updater의 release staging, 검증, restart, rollback 실행 모델을 정리한다. -19. [Edge/Node 롤아웃과 복구 정책](phase/update-plane-self-update-foundation/milestones/edge-node-rollout-recovery-policy.md) +18. [Edge/Node 롤아웃과 복구 정책](phase/update-plane-self-update-foundation/milestones/edge-node-rollout-recovery-policy.md) Edge/Node rolling update, 실패/재연결/rollback 보고 정책을 스케치한다. -20. [Provider Runtime 설정과 모델 획득 오케스트레이션](phase/operational-observability-provider-management/milestones/provider-runtime-model-acquisition-orchestration.md) +19. [Provider Runtime 설정과 모델 획득 오케스트레이션](phase/operational-observability-provider-management/milestones/provider-runtime-model-acquisition-orchestration.md) provider runtime launch/profile, model download/cache/verification 경계를 스케치한다. -21. [Provider-Device-Model Qualification 리포트와 Lifecycle 관리](phase/operational-observability-provider-management/milestones/provider-device-model-qualification-report.md) +20. [Provider-Device-Model Qualification 리포트와 Lifecycle 관리](phase/operational-observability-provider-management/milestones/provider-device-model-qualification-report.md) provider/device/model별 compatibility, performance, quality, lifecycle 리포트 경계를 정리한다. -22. [Provider 입력 컨텍스트 선택과 축소](phase/knowledge-tool-optimization-extension/milestones/request-context-assembly-optimization.md) +21. [Provider 입력 컨텍스트 선택과 축소](phase/knowledge-tool-optimization-extension/milestones/request-context-assembly-optimization.md) provider dispatch 전에 무관한 과거 요청-답변 단위를 제거하고, 유지한 답변·tool/search 결과 안에서도 필요한 문단·코드 블록·구간만 남기는 입력 context 최적화를 스케치한다. -23. [장기 기억과 RAG 업데이트 사이클 (2차)](phase/knowledge-tool-optimization-extension/milestones/long-term-memory-rag-second-wave.md) +22. [장기 기억과 RAG 업데이트 사이클 (2차)](phase/knowledge-tool-optimization-extension/milestones/long-term-memory-rag-second-wave.md) repo 장기 기억, RAG 저장소, update cycle, MCP 기반 context 절약 후보를 스케치한다. -24. [Advisor와 Context Hook 확장 (2차)](phase/knowledge-tool-optimization-extension/milestones/advisor-context-hook-second-wave.md) +23. [Advisor와 Context Hook 확장 (2차)](phase/knowledge-tool-optimization-extension/milestones/advisor-context-hook-second-wave.md) advisor 역할과 여러 기능을 실행 흐름에 연결하는 Context Hook 경계를 스케치한다. -25. [oto 자동화 스케줄러와 CI-CD 연동 (2차)](phase/automation-runtime-bridge/milestones/oto-automation-scheduler-second-wave.md) +24. [oto 자동화 스케줄러와 CI-CD 연동 (2차)](phase/automation-runtime-bridge/milestones/oto-automation-scheduler-second-wave.md) oto 기반 자동화, scheduler, CI-CD 연동 후보를 스케치한다. -26. [Flutter Desktop Control UI](phase/automation-runtime-bridge/milestones/flutter-desktop-control-ui.md) +25. [Flutter Desktop Control UI](phase/automation-runtime-bridge/milestones/flutter-desktop-control-ui.md) `iop-agent` local proto-socket을 소비해 YAML 전체 설정과 project·실행·오류·로그를 관리하는 macOS Flutter 설정·운영 UI를 제공한다. -27. [Unity 3D Desktop Character](phase/automation-runtime-bridge/milestones/unity-3d-desktop-character.md) +26. [Unity 3D Desktop Character](phase/automation-runtime-bridge/milestones/unity-3d-desktop-character.md) 같은 local proto-socket을 독립적으로 소비하고 작업 상태를 투명 배경 3D 캐릭터와 animation으로 표현하는 macOS Unity client를 제공한다. -28. [IOP Hot Path One-shot 실행 경로](phase/knowledge-tool-optimization-extension/milestones/iop-hot-path-one-shot-execution.md) +27. [IOP Hot Path One-shot 실행 경로](phase/knowledge-tool-optimization-extension/milestones/iop-hot-path-one-shot-execution.md) 외부 `model=iop` 요청을 Gemini 3.6 Flash와 RTX 5090 `ornith-fast`의 bounded one-shot 경로로 처리해 최대 속도와 실사용 품질의 균형을 맞춘다. -29. [Node Provider 실행 Liveness 관측과 안전 복구](phase/operational-observability-provider-management/milestones/node-provider-execution-liveness-recovery.md) +28. [Node Provider 실행 Liveness 관측과 안전 복구](phase/operational-observability-provider-management/milestones/node-provider-execution-liveness-recovery.md) Node가 5분간 provider 진행이 없는 request를 health와 분리 판정하고 local attempt를 fence한 뒤 기존 recovery owner가 안전한 요청만 공통 budget 안에서 재실행한다. diff --git a/agent-spec/input/openai-compatible-surface.md b/agent-spec/input/openai-compatible-surface.md index 99fcd1a..1a1f651 100644 --- a/agent-spec/input/openai-compatible-surface.md +++ b/agent-spec/input/openai-compatible-surface.md @@ -104,7 +104,7 @@ Edge가 OpenAI-compatible HTTP 요청을 받아 내부 `adapter + target` 실행 | provider-native field 보존 | provider raw tunnel route는 `model` served target rewrite와 auth/header 처리 외에 selected provider가 지원하는 OpenAI-compatible 표준 field와 provider extension field를 보존한다. | | OpenAI usage metering | Edge emits one request terminal and one canonical token/reasoning series for each actual provider attempt that reports usage. Rejected, aborted, and replacement attempts remain attributable to their own actual provider and served model. | | reasoning observation metric | provider가 reasoning token을 보고하지 않고 reasoning text만 관측되면 관측 횟수와 character count 보조 metric을 emit하고, 별도 estimated-token counter(`iop_openai_reasoning_estimated_tokens_total`)로 `estimation_method="chars_div_4"` 추정을 제공한다. | -| Grafana usage surface | 1차 조회 표면은 Prometheus/Grafana query guide이며 daily/monthly rollup, usage origin breakdown, operator-managed cloud price baseline, cloud-equivalent cost, avoided-cost ROI 기준을 문서로 제공한다. Control Plane/Client dashboard와 request-level ledger는 후속 범위다. | +| Grafana usage surface | 1차 조회 표면은 Prometheus/Grafana query guide이며 actual `provider_id`·`served_model` 기준 daily/monthly rollup과 `usage_attribution=model_group`으로 승인된 `route_model` query-time rollup, usage origin breakdown, operator-managed cloud price baseline, cloud-equivalent cost, avoided-cost ROI 기준을 문서로 제공한다. Control Plane/Client dashboard와 request-level ledger는 후속 범위다. | | Responses API | normalized(non-provider) `/v1/responses`는 string input의 non-streaming 요청만 지원한다. provider model group route는 `/v1/responses`를 raw passthrough로 provider `POST /v1/responses`에 전달한다. | | Responses provider passthrough | provider-pool model group route와 direct OpenAI-compatible provider route의 `/v1/responses`는 provider raw tunnel을 사용한다. Edge는 `model`만 served target으로 rewrite하고 unknown/Codex field와 `stream:true` raw SSE를 provider로 relay한다. Usage is recorded with endpoint=`responses`, response_mode=`passthrough`, route_model=request alias, and the selected actual provider/served model. | | strict output | strict output이 켜져 있으면 XML completion contract 기반 instruction 또는 prompt prefix를 추가할 수 있다. | @@ -195,7 +195,7 @@ sequenceDiagram - workspace는 prompt 본문에 섞지 않고 metadata에서 분리한다. - pure `passthrough` body는 provider-original byte stream이며 IOP 확장 envelope나 normalized label을 포함하지 않는다. - provider route와 non-provider normalized route의 차이는 selected provider capability에서 파생되며 caller metadata selector로 고르지 않는다. -- The implemented attribution binding changes the Prometheus label vector and terminal/attempt emission ownership described above. Grafana query migration, request ledgers, billing, and chargeback remain outside this implementation slice. +- Grafana guide는 actual provider 기준 canonical query와 승인된 model-group rollup을 분리한다. request ledger, billing, chargeback은 이 구현 범위 밖이다. - text tool-call synthesis는 요청 `tools[]` schema를 기준으로만 수행한다. 자연어 추론으로 tool call을 만들지 않는다. - private token이나 endpoint 원문은 tracked spec/docs에 남기지 않는다. - `metadata.user`는 identity source가 아니며 사용되지 않는다. @@ -224,3 +224,4 @@ sequenceDiagram - 2026-07-28: bounded ingress와 Stream Evidence Gate 활성 경로·한계·검증 포인터를 현재 구현 기준으로 반영. - 2026-07-31: provider-default/model-group opt-in attribution policy, direct provider id precedence, actual Edge-local dispatch binding을 반영했다. - 2026-07-31: Added request-local exactly-once terminal emission and actual-provider usage emission for every observed attempt, including recovery replacement and legacy tool-validation retry paths. +- 2026-07-31: Grafana query guide의 actual provider 집계와 승인된 model-group query-time rollup migration 완료 상태를 반영했다. diff --git a/apps/edge/README.md b/apps/edge/README.md index ae084f0..da04f0a 100644 --- a/apps/edge/README.md +++ b/apps/edge/README.md @@ -201,6 +201,7 @@ Edge 외부 입력은 OpenAI-compatible HTTP API와 A2A JSON-RPC HTTP API 두 openai: enabled: true listen: "0.0.0.0:18081" + provider_id: "ollama-dgx-provider" adapter: "ollama" target: "qwen3.6:35b-a3b-bf16" models: @@ -225,6 +226,7 @@ nodes: ```yaml openai: + provider_id: "legacy-direct-provider" model_routes: - model: "codex" adapter: "cli" diff --git a/apps/edge/cmd/edge/bootstrap_node_command_test.go b/apps/edge/cmd/edge/bootstrap_node_command_test.go index 3e09168..2e48eda 100644 --- a/apps/edge/cmd/edge/bootstrap_node_command_test.go +++ b/apps/edge/cmd/edge/bootstrap_node_command_test.go @@ -449,6 +449,7 @@ func TestNodeRegisterPreservesDefaultedNonNodeConfig(t *testing.T) { listen: "0.0.0.0:9090" openai: enabled: true + provider_id: "test-provider" ` if err := os.WriteFile(cfgPath, []byte(yamlStr), 0o600); err != nil { t.Fatalf("write yaml: %v", err) diff --git a/apps/edge/cmd/edge/root_config_command_test.go b/apps/edge/cmd/edge/root_config_command_test.go index eadbdda..9750029 100644 --- a/apps/edge/cmd/edge/root_config_command_test.go +++ b/apps/edge/cmd/edge/root_config_command_test.go @@ -289,6 +289,7 @@ bootstrap: openai: enabled: true listen: "0.0.0.0:8080" + provider_id: "test-provider" nodes: - id: "node-a" alias: "a" diff --git a/apps/edge/internal/bootstrap/runtime_refresh_test.go b/apps/edge/internal/bootstrap/runtime_refresh_test.go index 117f375..f049025 100644 --- a/apps/edge/internal/bootstrap/runtime_refresh_test.go +++ b/apps/edge/internal/bootstrap/runtime_refresh_test.go @@ -216,6 +216,7 @@ refresh: openai: enabled: true listen: %q + provider_id: "test-provider" adapter: "openai_compat" target: "" a2a: diff --git a/apps/edge/internal/bootstrap/runtime_test_support_test.go b/apps/edge/internal/bootstrap/runtime_test_support_test.go index 81f6756..aae5591 100644 --- a/apps/edge/internal/bootstrap/runtime_test_support_test.go +++ b/apps/edge/internal/bootstrap/runtime_test_support_test.go @@ -87,6 +87,7 @@ refresh: openai: enabled: true listen: %q + provider_id: "test-provider" adapter: "openai_compat" target: "" a2a: diff --git a/apps/edge/internal/configrefresh/path_refresh_test.go b/apps/edge/internal/configrefresh/path_refresh_test.go index 2089379..9702c47 100644 --- a/apps/edge/internal/configrefresh/path_refresh_test.go +++ b/apps/edge/internal/configrefresh/path_refresh_test.go @@ -70,6 +70,7 @@ func TestClassifyStreamEvidenceGateEnabledRestartRequired(t *testing.T) { openai: enabled: true listen: "127.0.0.1:8081" + provider_id: "test-provider" stream_evidence_gate: enabled: false ` @@ -77,6 +78,7 @@ openai: openai: enabled: true listen: "127.0.0.1:8081" + provider_id: "test-provider" stream_evidence_gate: enabled: true ` diff --git a/apps/edge/internal/openai/chat_handler.go b/apps/edge/internal/openai/chat_handler.go index 7b72bcc..203e588 100644 --- a/apps/edge/internal/openai/chat_handler.go +++ b/apps/edge/internal/openai/chat_handler.go @@ -256,11 +256,13 @@ func (s *Server) handleChatCompletionsProviderPool(w http.ResponseWriter, dc *ch if s.streamGateEnabled() { fctx, err := s.openAIChatOutputFilterContext(dc) if err != nil { + dc.finishUsageRequest(usageStatusError, responseModePassthrough) writeError(w, http.StatusInternalServerError, "run_error", "stream gate runtime unavailable") return } predicate, err := openAIStreamGateCandidatePredicate(s.streamGateConfig(), fctx) if err != nil { + dc.finishUsageRequest(usageStatusError, responseModePassthrough) writeError(w, http.StatusInternalServerError, "run_error", "stream gate runtime unavailable") return } diff --git a/apps/edge/internal/openai/responses_handler.go b/apps/edge/internal/openai/responses_handler.go index c457d46..ef27c91 100644 --- a/apps/edge/internal/openai/responses_handler.go +++ b/apps/edge/internal/openai/responses_handler.go @@ -379,11 +379,13 @@ func (s *Server) handleResponsesProviderPool(w http.ResponseWriter, requestCtx * if s.streamGateEnabled() { fctx, err := s.openAIResponsesOutputFilterContext(requestCtx) if err != nil { + requestCtx.finishUsageRequest(usageStatusError, responseModePassthrough) writeError(w, http.StatusInternalServerError, "run_error", "stream gate runtime unavailable") return } predicate, err := openAIStreamGateCandidatePredicate(s.streamGateConfig(), fctx) if err != nil { + requestCtx.finishUsageRequest(usageStatusError, responseModePassthrough) writeError(w, http.StatusInternalServerError, "run_error", "stream gate runtime unavailable") return } diff --git a/apps/edge/internal/openai/usage_metrics_test.go b/apps/edge/internal/openai/usage_metrics_test.go index 45bf4dc..0d17a8d 100644 --- a/apps/edge/internal/openai/usage_metrics_test.go +++ b/apps/edge/internal/openai/usage_metrics_test.go @@ -666,6 +666,70 @@ func TestChatCompletionsDispatchFailureEmitsErrorMetric(t *testing.T) { } } +func TestProviderPoolStreamGateSetupFailureEmitsOneErrorMetric(t *testing.T) { + const ( + rawToken = "sk-gate-setup-fail-token" + edgeID = "edge-gate-setup-fail" + model = "gate-pool-model" + ) + + for _, tc := range []struct { + name string + endpoint string + path string + body string + }{ + { + name: "chat completions", + endpoint: usageEndpointChatCompletions, + path: "/v1/chat/completions", + body: `{"model":"gate-pool-model","messages":[{"role":"user","content":"hello"}]}`, + }, + { + name: "responses", + endpoint: usageEndpointResponses, + path: "/v1/responses", + body: `{"model":"gate-pool-model","input":"hello"}`, + }, + } { + t.Run(tc.name, func(t *testing.T) { + fake := &providerFakeRunService{} + cfg := principalTokenCfg(rawToken, "ollama") + cfg.StreamEvidenceGate = config.StreamEvidenceGateConf{ + Enabled: true, + Filters: []config.StreamGateFilterPolicyConf{{Filter: "unsupported_test_filter"}}, + } + srv := NewServer(cfg, fake, nil) + srv.SetEdgeID(edgeID) + srv.SetModelCatalog([]config.ModelCatalogEntry{{ID: model, Providers: map[string]string{"prov-1": "served-model"}}}) + + before := testutil.ToFloat64(openAIRequestsTotal.WithLabelValues( + edgeID, "user:alice", "alice", "iop-tok-alice", model, + tc.endpoint, responseModePassthrough, usageStatusError, usageSourceUnavailable, + )) + + req := httptest.NewRequest(http.MethodPost, tc.path, strings.NewReader(tc.body)) + req.Header.Set("Authorization", "Bearer "+rawToken) + w := httptest.NewRecorder() + srv.routes().ServeHTTP(w, req) + + if w.Code != http.StatusInternalServerError { + t.Fatalf("expected 500, got %d body=%s", w.Code, w.Body.String()) + } + if got := fake.poolSubmitCountSnapshot(); got != 0 { + t.Fatalf("provider-pool dispatch count = %d, want 0", got) + } + after := testutil.ToFloat64(openAIRequestsTotal.WithLabelValues( + edgeID, "user:alice", "alice", "iop-tok-alice", model, + tc.endpoint, responseModePassthrough, usageStatusError, usageSourceUnavailable, + )) + if got := after - before; got != 1 { + t.Fatalf("requests_total error delta = %v, want 1", got) + } + }) + } +} + // TestToolValidationRetryDispatchFailureEmitsErrorMetric verifies that a // non-stream Chat tool-validation retry SubmitRun failure emits the error // request metric exactly once and returns tool_validation_retry_error (REVIEW_USAGE_METRIC_RETRY-1). diff --git a/docs/edge-local-dev-guide.md b/docs/edge-local-dev-guide.md index 1c87ba0..9bb0e20 100644 --- a/docs/edge-local-dev-guide.md +++ b/docs/edge-local-dev-guide.md @@ -54,6 +54,7 @@ refresh: openai: enabled: true listen: "0.0.0.0:18081" + provider_id: "ollama-gemma" adapter: "ollama" target: "gemma4:26b" diff --git a/docs/openai-usage-grafana.md b/docs/openai-usage-grafana.md index 7f7a74f..e215a3d 100644 --- a/docs/openai-usage-grafana.md +++ b/docs/openai-usage-grafana.md @@ -16,7 +16,10 @@ Provider-reported OpenAI-compatible token usage by token type. | `principal_ref` | `usr-abc123` | Principal foreign-key-like reference | | `principal_alias` | `john@acme` | Human-readable alias | | `token_ref` | `tok-xyz789` | Token identity (not a raw secret) | -| `model_group` | `gpt-4o` | Model group identifier | +| `route_model` | `gpt-4o` | Caller가 요청한 route alias; trace 및 승인된 model-group rollup key | +| `usage_attribution` | `provider`, `model_group` | 기본 provider 집계 또는 명시 승인된 model-group query-time rollup 정책 | +| `provider_id` | `provider-a` | 실제 호출된 provider resource identity | +| `served_model` | `gpt-4o-2024-08-06` | 해당 provider가 실제 호출한 model target | | `endpoint` | `chat.completions`, `responses` | OpenAI-compatible route | | `response_mode` | `passthrough`, `normalized` | Internal execution label; callers cannot set it via OpenAI metadata | | `token_type` | `input`, `output`, `reasoning`, `cached_input` | Token type | @@ -31,7 +34,7 @@ OpenAI-compatible requests processed by terminal status and usage source. | `principal_ref` | `usr-abc123` | Principal reference | | `principal_alias` | `john@acme` | Human-readable alias | | `token_ref` | `tok-xyz789` | Token identity | -| `model_group` | `gpt-4o` | Model group identifier | +| `route_model` | `gpt-4o` | Caller가 요청한 route alias; request terminal의 route dimension | | `endpoint` | `chat.completions`, `responses` | OpenAI-compatible route | | `response_mode` | `passthrough`, `normalized` | Internal execution label; callers cannot set it via OpenAI metadata | | `status` | `success`, `error`, `cancel` | Terminal request status | @@ -40,7 +43,7 @@ OpenAI-compatible requests processed by terminal status and usage source. ### 3. `iop_openai_reasoning_observed_total` (Counter) Requests where reasoning text was observed but the provider did not report reasoning token usage. -Same labels as `iop_openai_requests_total` excluding `status` and `usage_source`: `edge_id`, `principal_ref`, `principal_alias`, `token_ref`, `model_group`, `endpoint`, `response_mode`. +Token metric과 같은 실제 provider-attempt labels를 사용하되 `token_type`은 없다: `edge_id`, `principal_ref`, `principal_alias`, `token_ref`, `route_model`, `usage_attribution`, `provider_id`, `served_model`, `endpoint`, `response_mode`. ### 4. `iop_openai_reasoning_chars_total` (Counter) @@ -58,8 +61,9 @@ Estimated reasoning tokens for requests with observed reasoning text but without ``` edge_id, principal_ref, principal_alias, token_ref, -model_group, endpoint, response_mode, status, -token_type, usage_source, estimation_method +route_model, usage_attribution, provider_id, served_model, +endpoint, response_mode, status, token_type, +usage_source, estimation_method ``` `estimation_method`는 `iop_openai_reasoning_estimated_tokens_total` 전용 보조 라벨이며, 그 외 counter에는 사용되지 않습니다. @@ -67,18 +71,22 @@ token_type, usage_source, estimation_method ### 금지 label - `request_id`, `session_id` — request-level 상세는 후속 ledger/Loki 축에서 다룬다. +- `run_id`, `attempt_id`, `node_id` — 실행 시도 evidence는 request-local로 유지하고 public metric label로 노출하지 않는다. - Bearer token, provider API key, raw payload, raw prompt/response text — secret 누출 방지 (SDD S08). - 그 외 임의 label — cardinality 폭발을 막는다. --- -## Principal / Token Attribution 전제 +## Principal / Token / Provider Attribution 전제 - `principal_ref`는 사용자/테넌트/외부 운영 시스템의 안정 참조값이다. - `principal_alias`는 Grafana legend와 table에 보여줄 낮은 cardinality 별칭이다. - `token_ref`는 raw bearer token이 아니라 앱/통합/용도별 token 참조값이다. - 같은 `principal_ref` 아래 여러 `token_ref`를 둘 수 있다. 이 경우 `principal_ref` 기준 query는 사용자 합산, `token_ref` 기준 query는 앱/통합별 breakdown으로 본다. - 운영 token 발급은 raw token을 tracked 파일에 남기지 않고 hash/reference만 기록하는 절차를 따른다. +- canonical token/reasoning series는 실제 호출 시도의 `provider_id`와 `served_model`에 귀속한다. `route_model`은 요청 alias이며 기본 provider 집계 key가 아니다. +- `usage_attribution="model_group"`은 운영자가 동일 논리 모델임을 명시 승인한 route에서만 사용한다. 이 series만 `route_model`로 query-time rollup하며 별도 model-group counter는 없다. +- retry/fallback으로 provider가 바뀌면 각 실제 호출 시도가 별도 provider series로 남는다. `iop_openai_requests_total`은 최종 request terminal을 한 번만 기록하므로 provider 시도 횟수로 사용하지 않는다. 관련 기준: @@ -130,11 +138,24 @@ sum by (principal_ref, principal_alias, token_ref, token_type) ( ) ``` -### model_group별 usage +### 실제 provider별 usage ```promql -# model_group, token_type별 aggregation -sum by (model_group, token_type) (iop_openai_usage_tokens_total) +# 실제 provider와 served model, token_type별 aggregation +sum by (provider_id, served_model, token_type) ( + iop_openai_usage_tokens_total +) +``` + +### 승인된 model-group rollup + +`route_model` 집계는 `usage_attribution="model_group"`으로 명시 승인된 series에만 적용한다. 기본값인 `provider` series를 route alias로 합치지 않는다. + +```promql +# 동일 논리 모델로 승인된 route만 query-time rollup +sum by (route_model, token_type) ( + iop_openai_usage_tokens_total{usage_attribution="model_group"} +) ``` ### endpoint별 요청 수 @@ -157,7 +178,10 @@ sum by (response_mode, status) (iop_openai_requests_total{status="success"}) ```promql # "어디서 얼만큼 사용했는지"를 보는 table용 breakdown -sum by (principal_alias, token_ref, model_group, endpoint, response_mode, token_type) ( +sum by ( + principal_alias, token_ref, provider_id, served_model, + route_model, usage_attribution, endpoint, response_mode, token_type +) ( iop_openai_usage_tokens_total ) ``` @@ -182,13 +206,19 @@ provider가 reasoning token을 보고하지 않는 경우: ```promql # reasoning text가 관찰된 요청 수 (provider token 미보고) -sum by (principal_alias) (iop_openai_reasoning_observed_total) +sum by (principal_alias, provider_id, served_model) ( + iop_openai_reasoning_observed_total +) # reasoning text character 합계 -sum by (principal_alias) (iop_openai_reasoning_chars_total) +sum by (principal_alias, provider_id, served_model) ( + iop_openai_reasoning_chars_total +) # estimated reasoning tokens (provider가 token 수를 보고하지 않는 경우) -sum by (principal_alias, estimation_method) (iop_openai_reasoning_estimated_tokens_total) +sum by (principal_alias, provider_id, served_model, estimation_method) ( + iop_openai_reasoning_estimated_tokens_total +) ``` > **주의**: `iop_openai_reasoning_observed_total` / `iop_openai_reasoning_chars_total` / `iop_openai_reasoning_estimated_tokens_total`는 provider-reported reasoning token이 있을 경우 emit되지 않는다. 즉, 이 metric들은 "reasoning text는 있으나 token count 보고를 안 하는 provider"만 커버한다. `iop_openai_reasoning_estimated_tokens_total`은 `estimation_method="chars_div_4"`로 ceil(chars/4) 추정을 제공하지만 billing-grade 정산값이 아니다. @@ -200,14 +230,15 @@ sum by (principal_alias, estimation_method) (iop_openai_reasoning_estimated_toke | 패널 | query | visualization | |------|-------|---------------| | 사용자별 token 사용량 (line) | `sum by (principal_alias, token_type) (irate(iop_openai_usage_tokens_total[5m]))` | Time series, legend=`{{principal_alias}} {{token_type}}` | -| 일별 usage origin (table) | `sum by (principal_alias, token_ref, model_group, endpoint, response_mode, token_type) (increase(iop_openai_usage_tokens_total[1d]))` | Table | -| model_group별 token 비율 (pie) | `sum by (model_group, token_type) (iop_openai_usage_tokens_total)` | Stat or bar chart | +| 일별 usage origin (table) | `sum by (principal_alias, token_ref, provider_id, served_model, route_model, usage_attribution, endpoint, response_mode, token_type) (increase(iop_openai_usage_tokens_total[1d]))` | Table | +| 실제 provider별 token 비율 (pie) | `sum by (provider_id, served_model, token_type) (iop_openai_usage_tokens_total)` | Stat or bar chart | +| 승인된 model-group token 비율 (pie) | `sum by (route_model, token_type) (iop_openai_usage_tokens_total{usage_attribution="model_group"})` | Stat or bar chart | | 요청 상태 분포 (table) | `sum by (endpoint, status, usage_source) (iop_openai_requests_total)` | Table | | cloud-equivalent cost (stat/table) | token rollup query에 price scalar 또는 Grafana calculated field 적용 | Stat or Table | | avoided-cost ROI summary (stat) | cloud-equivalent cost 합계, 필요 시 별도 infra cost field 차감 | Stat | | reasoning 보조 지표 (singlestat) | `sum(iop_openai_reasoning_observed_total)` | Singlestat | | estimated reasoning tokens (bar) | `sum by (estimation_method) (iop_openai_reasoning_estimated_tokens_total)` | Bar chart | -| estimated reasoning vs provider-reported (line) | `sum by (estimation_method) (iop_openai_reasoning_estimated_tokens_total)` vs `sum by (principal_alias) (iop_openai_usage_tokens_total{token_type="reasoning"})` | Time series | +| estimated reasoning vs provider-reported (line) | `sum by (provider_id, served_model, estimation_method) (iop_openai_reasoning_estimated_tokens_total)` vs `sum by (provider_id, served_model) (iop_openai_usage_tokens_total{token_type="reasoning"})` | Time series | > Grafana JSON dashboard import file은 본 scope에 포함되지 않는다. 위 PromQL을 바탕으로 패널을 수동 구성한다. @@ -218,8 +249,11 @@ sum by (principal_alias, estimation_method) (iop_openai_reasoning_estimated_toke ### 일일 토큰 합계 ```promql -# 일일 token rollup (principal/token/model/endpoint/response_mode/token_type별) -sum by (principal_ref, principal_alias, token_ref, model_group, endpoint, response_mode, token_type) ( +# 일일 canonical token rollup (principal/token/provider/served-model/route/endpoint/response-mode/token-type별) +sum by ( + principal_ref, principal_alias, token_ref, provider_id, served_model, + route_model, usage_attribution, endpoint, response_mode, token_type +) ( increase(iop_openai_usage_tokens_total[1d]) ) ``` @@ -235,7 +269,10 @@ sum by (principal_ref, principal_alias, token_ref, model_group, endpoint, respon ```promql # 최근 30일 token rollup. 달력 월은 Grafana time range를 월 단위로 잡고 $__range를 사용한다. -sum by (principal_ref, principal_alias, token_ref, model_group, endpoint, response_mode, token_type) ( +sum by ( + principal_ref, principal_alias, token_ref, provider_id, served_model, + route_model, usage_attribution, endpoint, response_mode, token_type +) ( increase(iop_openai_usage_tokens_total[30d]) ) ``` @@ -252,7 +289,8 @@ sum by (principal_ref, principal_alias, token_ref, model_group, endpoint, respon - `principal_alias` 또는 `principal_ref`를 `by` clause에 추가해 사용자별 일일/월간 토큰 사용량을 산출한다. - `token_ref`를 추가하면 같은 사용자 아래 앱/통합/용도별 사용량을 분해한다. - `token_type`을 함께 group by하면 input/output/reasoning/cached_input을 구분한다. -- `model_group`, `endpoint`, `response_mode`를 추가하면 사용 위치와 응답 경로별 origin breakdown을 만든다. +- `provider_id`, `served_model`, `endpoint`, `response_mode`를 추가하면 실제 사용 위치와 응답 경로별 origin breakdown을 만든다. +- 동일 논리 모델로 승인된 합계가 필요할 때만 `usage_attribution="model_group"`을 선택하고 `route_model`로 group by한다. --- @@ -266,7 +304,9 @@ sum by (principal_ref, principal_alias, token_ref, model_group, endpoint, respon |-------|------|------| | `cloud_provider` | `example-cloud` | 가격 기준 provider | | `baseline_model` | `example-model` | 비교 기준 cloud model | -| `model_group` | `example-model` | IOP metric의 `model_group`과 매핑할 값 | +| `provider_id` | `example-provider` | IOP metric의 실제 provider와 매핑할 값 | +| `served_model` | `example-model` | IOP metric의 실제 served model과 매핑할 값 | +| `route_model` | `example-route` | `usage_attribution=model_group` 승인 시에만 사용하는 논리 model rollup key | | `token_type` | `input`, `output`, `cached_input`, `reasoning` | 가격을 적용할 token type | | `price_per_1m_tokens` | `1.00` | 1M tokens당 단가 | | `currency` | `USD` | 통화 | @@ -275,11 +315,11 @@ sum by (principal_ref, principal_alias, token_ref, model_group, endpoint, respon baseline 예시. 아래 숫자는 문서용 예시이며 최신 cloud 가격이 아니다. -| cloud_provider | baseline_model | model_group | token_type | price_per_1m_tokens | currency | effective_date | -|----------------|----------------|-------------|------------|----------------------|----------|----------------| -| example-cloud | example-model | example-model | input | 1.00 | USD | 2026-07-10 | -| example-cloud | example-model | example-model | output | 3.00 | USD | 2026-07-10 | -| example-cloud | example-model | example-model | cached_input | 0.50 | USD | 2026-07-10 | +| cloud_provider | baseline_model | provider_id | served_model | route_model | token_type | price_per_1m_tokens | currency | effective_date | +|----------------|----------------|-------------|--------------|-------------|------------|----------------------|----------|----------------| +| example-cloud | example-model | example-provider | example-model | example-route | input | 1.00 | USD | 2026-07-10 | +| example-cloud | example-model | example-provider | example-model | example-route | output | 3.00 | USD | 2026-07-10 | +| example-cloud | example-model | example-provider | example-model | example-route | cached_input | 0.50 | USD | 2026-07-10 | `reasoning` token은 provider가 별도 단가를 제공하면 별도 baseline을 둔다. 별도 단가가 없으면 운영자가 `output`과 같은 단가로 볼지, report에서 `reasoning`을 제외할지 baseline에 명시한다. @@ -306,22 +346,24 @@ Prometheus metric 자체에는 price table이 없으므로 1차 표면에서는 PromQL scalar 예시: ```promql -# example-model input daily cloud-equivalent cost, USD -sum by (principal_alias, token_ref, model_group, endpoint) ( - increase(iop_openai_usage_tokens_total{model_group="example-model", token_type="input"}[1d]) +# example-provider / example-model input daily cloud-equivalent cost, USD +sum by (principal_alias, token_ref, provider_id, served_model, endpoint) ( + increase(iop_openai_usage_tokens_total{provider_id="example-provider", served_model="example-model", token_type="input"}[1d]) ) / 1000000 * 1.00 -# example-model output daily cloud-equivalent cost, USD -sum by (principal_alias, token_ref, model_group, endpoint) ( - increase(iop_openai_usage_tokens_total{model_group="example-model", token_type="output"}[1d]) +# example-provider / example-model output daily cloud-equivalent cost, USD +sum by (principal_alias, token_ref, provider_id, served_model, endpoint) ( + increase(iop_openai_usage_tokens_total{provider_id="example-provider", served_model="example-model", token_type="output"}[1d]) ) / 1000000 * 3.00 -# example-model cached input daily cloud-equivalent cost, USD -sum by (principal_alias, token_ref, model_group, endpoint) ( - increase(iop_openai_usage_tokens_total{model_group="example-model", token_type="cached_input"}[1d]) +# example-provider / example-model cached input daily cloud-equivalent cost, USD +sum by (principal_alias, token_ref, provider_id, served_model, endpoint) ( + increase(iop_openai_usage_tokens_total{provider_id="example-provider", served_model="example-model", token_type="cached_input"}[1d]) ) / 1000000 * 0.50 ``` +동일 논리 모델로 승인된 route에 같은 baseline을 적용할 때만 `usage_attribution="model_group", route_model="example-route"` selector와 `route_model` group by를 사용할 수 있다. 승인되지 않은 provider series를 route alias 기준으로 비용 합산하지 않는다. + 여러 token type을 합산할 때는 token type별 cost query를 Grafana transformation으로 더한다. 단일 query에서 서로 다른 가격을 자동 join하려면 별도 recording rule 또는 external price table이 필요하다. --- @@ -339,14 +381,14 @@ net_avoided_cost = cloud_equivalent_cost - separately_known_local_infra_cost - local infra cost가 별도로 계상되지 않으면 `avoided_cost`만 표시한다. - local infra cost를 운영자가 별도 산정해 Grafana에 넣을 수 있으면 `net_avoided_cost`를 보조 field로 둔다. - 이 값은 실제 결제, chargeback, 조직별 비용 배부, full infra cost accounting 근거가 아니다. -- 최소 summary table column은 `date`, `principal_alias`, `token_ref`, `model_group`, `endpoint`, `tokens`, `currency`, `cloud_equivalent_cost`, `avoided_cost`다. +- 최소 summary table column은 `date`, `principal_alias`, `token_ref`, `provider_id`, `served_model`, `route_model`, `usage_attribution`, `endpoint`, `tokens`, `currency`, `cloud_equivalent_cost`, `avoided_cost`다. Grafana table 구성 예: -1. Query A: daily token rollup by `principal_alias`, `token_ref`, `model_group`, `endpoint`, `token_type`. +1. Query A: daily token rollup by `principal_alias`, `token_ref`, `provider_id`, `served_model`, `route_model`, `usage_attribution`, `endpoint`, `token_type`. 2. Transformation: baseline price table 또는 panel-level scalar를 token type별로 적용한다. 3. Calculated field: `tokens / 1000000 * price_per_1m_tokens`. -4. Group by: `date`, `principal_alias`, `token_ref`, `model_group`, `endpoint`. +4. Group by: `date`, `principal_alias`, `token_ref`, `provider_id`, `served_model`, `endpoint`. 승인된 model-group view만 `route_model`을 대신 사용한다. 5. Reduce: `cloud_equivalent_cost` 합계와 token 합계를 보여준다. --- @@ -356,11 +398,12 @@ Grafana table 구성 예: 1. Grafana time range를 `Today`, `Yesterday`, 최근 7일, 또는 이번 달로 설정한다. 2. `daily / monthly Rollup` query로 token usage table을 만든다. 3. `principal_ref`/`principal_alias` 기준 panel과 `token_ref` 기준 panel을 나란히 둔다. -4. `model_group`, `endpoint`, `response_mode`, `token_type` column을 유지해 usage origin을 확인한다. +4. `provider_id`, `served_model`, `route_model`, `usage_attribution`, `endpoint`, `response_mode`, `token_type` column을 유지해 실제 usage origin과 rollup 정책을 확인한다. 5. 운영 price baseline의 `effective_date`, `baseline_model`, `currency`를 dashboard text 또는 table에 함께 표시한다. 6. token type별 cost formula를 적용해 cloud-equivalent cost를 계산한다. 7. daily/monthly `avoided_cost` summary stat을 추가한다. 8. `usage_source="unavailable"` 요청 비율과 reasoning 보조 metric을 함께 확인해 cost report coverage 위험을 판단한다. +9. model-group view는 `usage_attribution="model_group"` selector가 있는 query만 사용하고, provider-default series가 섞이지 않았는지 확인한다. --- @@ -375,7 +418,8 @@ Grafana table 구성 예: ### 현재 문서가 제공하는 것 -- `principal_ref`, `principal_alias`, `token_ref`, `model_group`, `endpoint`, `response_mode`, `token_type` 라벨 기반 daily/monthly rollup PromQL +- `principal_ref`, `principal_alias`, `token_ref`, `provider_id`, `served_model`, `route_model`, `usage_attribution`, `endpoint`, `response_mode`, `token_type` 라벨 기반 daily/monthly rollup PromQL +- 실제 provider 기준 집계와 `usage_attribution="model_group"`으로 승인된 route만의 model-group rollup 분리 기준 - 같은 principal의 여러 token/app usage를 합산과 breakdown으로 함께 보는 기준 - 운영자가 관리하는 cloud price baseline 필드 - token type별 cloud-equivalent cost 산식과 query/report 예시 @@ -395,6 +439,7 @@ Grafana table 구성 예: | `usage_source` | `provider_reported`, `unavailable` | | `token_type` | `input`, `output`, `reasoning`, `cached_input` | | `response_mode` | `passthrough`, `normalized` | +| `usage_attribution` | `provider`, `model_group` | | `estimation_method` | `chars_div_4` (특히 `iop_openai_reasoning_estimated_tokens_total` 전용) | --- diff --git a/scripts/e2e-openai-cli-workspace.sh b/scripts/e2e-openai-cli-workspace.sh index 69f7895..38d7ecb 100755 --- a/scripts/e2e-openai-cli-workspace.sh +++ b/scripts/e2e-openai-cli-workspace.sh @@ -61,6 +61,7 @@ bootstrap: openai: enabled: true listen: "127.0.0.1:$OPENAI_PORT" + provider_id: "cli-smoke-provider" adapter: "cli" session_id: "openai" timeout_sec: 30 diff --git a/scripts/e2e-openai-lemonade.sh b/scripts/e2e-openai-lemonade.sh index 127b140..714368a 100755 --- a/scripts/e2e-openai-lemonade.sh +++ b/scripts/e2e-openai-lemonade.sh @@ -177,6 +177,7 @@ bootstrap: openai: enabled: true listen: "127.0.0.1:$OPENAI_PORT" + provider_id: "lemonade-smoke-provider" adapter: "openai_compat" session_id: "cline" timeout_sec: 30 diff --git a/scripts/e2e-openai-ollama.sh b/scripts/e2e-openai-ollama.sh index 44a0554..c6fd390 100755 --- a/scripts/e2e-openai-ollama.sh +++ b/scripts/e2e-openai-ollama.sh @@ -123,6 +123,7 @@ bootstrap: openai: enabled: true listen: "127.0.0.1:$OPENAI_PORT" + provider_id: "ollama-smoke-provider" adapter: "ollama" session_id: "cline" timeout_sec: 30 diff --git a/scripts/e2e-openai-vllm.sh b/scripts/e2e-openai-vllm.sh index 6af1a2e..bfb7b57 100755 --- a/scripts/e2e-openai-vllm.sh +++ b/scripts/e2e-openai-vllm.sh @@ -190,6 +190,7 @@ bootstrap: openai: enabled: true listen: "127.0.0.1:$OPENAI_PORT" + provider_id: "vllm-smoke-provider" adapter: "openai_compat" session_id: "smoke" timeout_sec: 30