fix(openai): usage 귀속 마일스톤을 종료한다
Stream Gate 준비 실패에서도 요청 terminal 관측 계약을 지키고, direct provider identity 마이그레이션 누락으로 기존 검증과 운영 설정이 깨지지 않게 한다.
This commit is contained in:
parent
04879f2b43
commit
f98d1b434b
20 changed files with 213 additions and 87 deletions
|
|
@ -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 범위에는 실패가 없다.
|
||||
|
||||
## 범위 제외
|
||||
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 안에서 재실행한다.
|
||||
|
|
|
|||
|
|
@ -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 완료 상태를 반영했다.
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -289,6 +289,7 @@ bootstrap:
|
|||
openai:
|
||||
enabled: true
|
||||
listen: "0.0.0.0:8080"
|
||||
provider_id: "test-provider"
|
||||
nodes:
|
||||
- id: "node-a"
|
||||
alias: "a"
|
||||
|
|
|
|||
|
|
@ -216,6 +216,7 @@ refresh:
|
|||
openai:
|
||||
enabled: true
|
||||
listen: %q
|
||||
provider_id: "test-provider"
|
||||
adapter: "openai_compat"
|
||||
target: ""
|
||||
a2a:
|
||||
|
|
|
|||
|
|
@ -87,6 +87,7 @@ refresh:
|
|||
openai:
|
||||
enabled: true
|
||||
listen: %q
|
||||
provider_id: "test-provider"
|
||||
adapter: "openai_compat"
|
||||
target: ""
|
||||
a2a:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
`
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ refresh:
|
|||
openai:
|
||||
enabled: true
|
||||
listen: "0.0.0.0:18081"
|
||||
provider_id: "ollama-gemma"
|
||||
adapter: "ollama"
|
||||
target: "gemma4:26b"
|
||||
|
||||
|
|
|
|||
|
|
@ -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` 전용) |
|
||||
|
||||
---
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue