From c836f1e562ff77b3da1c3727f4b6341b223a0995 Mon Sep 17 00:00:00 2001 From: toki Date: Sat, 11 Jul 2026 04:47:11 +0900 Subject: [PATCH] feat: update edge config, usage docs, tests, and roadmap --- .../inner/edge-config-runtime-refresh.md | 2 +- agent-ops/rules/project/rules.md | 1 + .../skills/project/iop-user-crud-ops/SKILL.md | 108 +++++++++++ .../project/openai-usage-token-issue/SKILL.md | 3 +- .../daily-usage-cost-roi-report-mvp.md | 87 +++++++++ ...ai-compatible-output-validation-filters.md | 5 + .../PHASE.md | 4 + ...t-execution-log-usage-ledger-foundation.md | 1 + .../seulgivibe-openai-compatible-provider.md | 1 + .../SDD.md | 15 +- agent-spec/input/openai-compatible-surface.md | 11 +- .../runtime/provider-pool-config-refresh.md | 3 +- .../internal/openai/identity_metering_test.go | 25 +++ docs/openai-usage-grafana.md | 175 +++++++++++++++++- packages/go/config/config_test.go | 37 ++++ 15 files changed, 459 insertions(+), 19 deletions(-) create mode 100644 agent-ops/skills/project/iop-user-crud-ops/SKILL.md create mode 100644 agent-roadmap/archive/phase/operational-observability-provider-management/milestones/daily-usage-cost-roi-report-mvp.md diff --git a/agent-contract/inner/edge-config-runtime-refresh.md b/agent-contract/inner/edge-config-runtime-refresh.md index a9bb3aa..600762b 100644 --- a/agent-contract/inner/edge-config-runtime-refresh.md +++ b/agent-contract/inner/edge-config-runtime-refresh.md @@ -29,7 +29,7 @@ tracked config에는 public 예시와 기본 구조만 두고, 실제 endpoint/c ## 핵심 규칙 -- `openai.principal_tokens[]`는 raw token을 저장하지 않고 hash/reference로 principal 매핑을 관리한다. 각 entry는 `token_ref` (non-empty, unique), `token_hash_sha256` (64-char hex, duplicate hash rejection), `principal_ref` (non-empty), optional `principal_alias` 필드를 갖는다. tracked config에는 raw token을 저장하지 않고 hash/reference만 둔다. +- `openai.principal_tokens[]`는 raw token을 저장하지 않고 hash/reference로 principal 매핑을 관리한다. 각 entry는 `token_ref` (non-empty, unique), `token_hash_sha256` (64-char hex, duplicate hash rejection), `principal_ref` (non-empty), optional `principal_alias` 필드를 갖는다. 여러 entry가 같은 `principal_ref`와 `principal_alias`를 공유할 수 있으며, 이때 `token_ref`가 앱/통합/용도별 사용량 분해 기준이 된다. tracked config에는 raw token을 저장하지 않고 hash/reference만 둔다. - `openai` deep diff는 restart-required로 분류한다. `openai.principal_tokens[]` 변경은 credential/hash 변경으로 restart-required classifier에 포함된다. - `openai.model_routes[]`는 외부 OpenAI-compatible `model` id를 내부 `adapter + target` route로 매핑하는 compatibility catalog다. - `long_context_threshold_tokens`는 Edge root의 입력 토큰 추정 기준 long-context 분류 threshold다. 기본값은 `100000`이며 0 이하 값은 config load에서 거부한다. diff --git a/agent-ops/rules/project/rules.md b/agent-ops/rules/project/rules.md index cb1917b..b2fad3c 100644 --- a/agent-ops/rules/project/rules.md +++ b/agent-ops/rules/project/rules.md @@ -94,6 +94,7 @@ ## 스킬 라우팅 +- UI 없는 사용자 CRUD, OpenAI-compatible 사용자/principal 추가·조회·수정·비활성화·삭제, principal token 운영 CRUD: `agent-ops/skills/project/iop-user-crud-ops/SKILL.md` - OpenAI-compatible usage token 발급, principal_ref token 등록, principal alias 매핑, raw IOP token 1회 전달: `agent-ops/skills/project/openai-usage-token-issue/SKILL.md` - dev-corp 배포, dev-corp runtime 배포, 회사망 mac-mini Edge/Node dev-corp 환경 배포, dev-corp provider pool 배포, dev-corp OpenAI-compatible capacity smoke 검증: `agent-ops/skills/project/dev-corp-runtime-deploy/SKILL.md` - dev 배포, dev-runtime 배포, Edge/Node dev 환경 배포, provider pool 배포, OpenAI-compatible capacity smoke 검증: `agent-ops/skills/project/dev-runtime-deploy/SKILL.md` diff --git a/agent-ops/skills/project/iop-user-crud-ops/SKILL.md b/agent-ops/skills/project/iop-user-crud-ops/SKILL.md new file mode 100644 index 0000000..e377e2d --- /dev/null +++ b/agent-ops/skills/project/iop-user-crud-ops/SKILL.md @@ -0,0 +1,108 @@ +--- +name: iop-user-crud-ops +version: 1.0.0 +description: UI 표면이 없는 단계에서 OpenAI-compatible 사용자/principal CRUD를 private Edge config와 token 매핑으로 운영하는 절차 +--- + +# iop-user-crud-ops + +## 목적 + +IOP 내부 사용자 CRUD UI/API가 완성되기 전까지 OpenAI-compatible 사용자를 `principal_ref`, `principal_alias`, `token_ref` 매핑으로 등록, 조회, 수정, 비활성화, 삭제한다. +현재 IOP는 사용자/테넌트 source of truth를 소유하지 않으므로, 이 스킬은 외부 principal 참조값과 IOP token 매핑을 운영하는 임시 CRUD 절차다. + +## 언제 호출할지 + +- UI 없이 OpenAI-compatible 사용자를 추가, 조회, 수정, 비활성화, 삭제해야 할 때 +- 특정 `principal_ref`에 token을 추가하거나 기존 token을 회전, 폐기해야 할 때 +- 운영자가 Grafana 사용량 label에 노출될 `principal_alias`, `token_ref`를 관리해야 할 때 +- 사용자 CRUD API/UI가 없어 private Edge config와 operator-private 기록으로 임시 운영해야 할 때 + +## 입력 + +- `operation`: `create`, `read`, `list`, `update`, `disable`, `delete`, `rotate-token` 중 하나 (필수) +- `env`: 대상 환경. 예: `local`, `dev`, `dev-corp`, `field` (필수) +- `config_path`: 대상 Edge private config 또는 운영 overlay 경로 (필수) +- `principal_ref`: 외부 사용자/테넌트/운영 시스템의 안정 참조값 (create/read/update/disable/delete 필수) +- `principal_alias`: Grafana와 운영 표면에 보여줄 낮은 cardinality alias (create/update 선택, create에서는 없으면 안전한 별칭 생성) +- `token_ref`: 앱/통합/용도별 token 참조값 (create/rotate-token 선택) +- `operator_record_path`: raw token 없이 user/token 상태를 남길 operator-private 기록 위치 (선택) +- `raw_token_delivery`: raw token을 operator에게 1회 전달할 비공개 경로 또는 절차 (새 token 발급 시 필수) + +## 먼저 확인할 것 + +- [ ] 대상 환경의 실제 source of truth가 tracked `configs/*.yaml` 예시가 아니라 private Edge config 또는 운영 overlay인지 확인한다. +- [ ] 현재 구현에는 별도 `users[]` config, 사용자 CRUD API, Control Plane UI source of truth가 없음을 확인한다. +- [ ] `principal_ref`가 raw email, provider token, provider identity, secret이 아니라 외부 시스템 참조값인지 확인한다. +- [ ] `principal_alias`와 `token_ref`가 metric label에 적합한 낮은 cardinality ASCII 값인지 확인한다. +- [ ] 새 raw token 발급이 필요하면 `agent-ops/skills/project/openai-usage-token-issue/SKILL.md` 절차와 누출 금지 기준을 함께 적용한다. +- [ ] `disable`, `delete`, `rotate-token`은 기존 caller를 끊을 수 있으므로 대상 `principal_ref`와 `token_ref`가 모호하지 않은지 확인한다. + +## 실행 절차 + +1. **대상 상태 확인** + - `config_path`의 `openai.principal_tokens[]`를 구조화 파서로 읽는다. + - 같은 `principal_ref`에 연결된 `token_ref`, `principal_alias` 목록을 확인한다. + - `token_ref`와 `token_hash_sha256` 중복, 64자 hex hash 형식, 빈 `principal_ref` 여부를 확인한다. + +2. **작업별 변경안 작성** + - `create`: 기존 `principal_ref`가 없으면 alias를 정하고, 필요한 token entry를 추가한다. 새 raw token이 필요하면 `openai-usage-token-issue` 절차로 생성한 hash/reference만 config에 넣는다. + - `read`/`list`: raw token과 전체 hash 없이 `principal_ref`, `principal_alias`, `token_ref` 목록과 활성 config 반영 여부만 보고한다. + - `update`: alias 변경은 같은 `principal_ref`의 모든 관련 token entry에 일관되게 반영한다. `principal_ref` 변경은 외부 source of truth의 rename 근거가 있을 때만 수행한다. + - `disable`: 현 config schema에는 `status` 필드가 없으므로 active config에서 대상 token entry를 제거하고, 필요하면 operator-private 기록에 disabled/tombstone 상태를 남긴다. + - `delete`: active config에서 대상 principal의 token entry를 제거한다. 감사 목적의 tombstone은 operator-private 기록에만 남긴다. + - `rotate-token`: 새 token entry를 추가하고 운영자가 전환을 확인한 뒤 이전 token entry를 제거한다. 앱/통합별 continuity가 필요하면 `token_ref` 재사용 여부를 먼저 확정한다. + +3. **private 기록 갱신** + - `operator_record_path`가 있으면 raw token 없이 `principal_ref`, `principal_alias`, `token_ref`, hash fingerprint 또는 hash prefix, status, 변경 사유, 변경 시각만 남긴다. + - tracked `docs/`, `agent-roadmap/`, `agent-spec/`, `configs/` 예시에는 private principal 원문, raw token, provider credential을 쓰지 않는다. + +4. **config 검증과 반영** + - Edge config load/check 명령 또는 해당 환경의 검증 절차로 변경 config가 유효한지 확인한다. + - `openai.principal_tokens[]` 변경은 restart-required로 분류되므로 live apply 완료로 보고하지 않는다. + - 환경별 배포 스킬이 적용되는 대상이면 해당 배포 스킬의 restart/검증 절차를 따른다. + +5. **동작 검증** + - create/rotate-token은 새 bearer token으로 OpenAI-compatible 인증이 성공하는지 확인한다. 검증 출력에 raw token을 찍지 않는다. + - disable/delete는 폐기된 token이 `401 unauthorized`가 되는지 확인한다. + - 가능한 경우 usage metric 또는 Grafana query에서 `principal_ref`, `principal_alias`, `token_ref` label이 기대값으로 나오는지 확인한다. + - raw token 누출 검색은 token 원문이 출력되지 않는 조용한 방식으로 수행한다. + +6. **결과 보고** + - 작업 종류, 대상 환경, `principal_ref`, `principal_alias`, 영향을 받은 `token_ref`, config 반영 방식, restart 필요 여부, 검증 결과만 보고한다. + - raw token, 전체 token hash, provider token, private endpoint는 보고하지 않는다. + +## 실행 결과 검증 + +- [ ] active config의 `openai.principal_tokens[]`가 중복 없는 `token_ref`, 64자 hex `token_hash_sha256`, 비어 있지 않은 `principal_ref`를 가진다. +- [ ] 새 token을 발급한 경우 raw token이 operator에게 1회만 전달되고 tracked 파일과 최종 보고에 남지 않았다. +- [ ] `disable`/`delete` 후 해당 token은 인증에 실패하고, 남겨야 할 audit/tombstone은 operator-private 기록에만 남았다. +- [ ] `openai.principal_tokens[]` 변경이 restart-required임을 보고했고, live apply로 오인하지 않았다. +- [ ] 최종 보고에 raw token, 전체 hash, provider credential, raw email 같은 민감값이 포함되지 않았다. +- 검증 실패 시: 변경 반영을 중단하거나 이전 private config로 되돌리고, 실패한 principal/token_ref와 실패 단계만 민감값 없이 보고한다. + +## 출력 형식 + +```text +IOP user CRUD ops +- operation: +- env: +- principal_ref: +- principal_alias: +- token_refs: +- config_path: +- operator_record: +- restart_required: yes +- verification: +- raw_token_reported: no +- notes: <주의사항 또는 후속 작업> +``` + +## 금지 사항 + +- 현재 구현에 없는 `users[]`, `status`, tenant/org CRUD schema를 Edge config에 임의로 추가하지 않는다. +- raw token, 전체 token hash, provider token, private endpoint, raw email을 tracked 파일이나 최종 보고에 남기지 않는다. +- `metadata.user` 또는 caller-provided `metadata.iop_principal_*`를 사용자 identity source로 취급하지 않는다. +- `openai.principal_tokens[]` 변경을 live apply로 처리됐다고 보고하지 않는다. +- 대상 `principal_ref` 또는 `token_ref`가 모호한 상태에서 disable/delete를 진행하지 않는다. +- 사용자 CRUD UI/API, billing, 조직 IAM, rate limit enforcement까지 이 스킬 책임으로 확장하지 않는다. diff --git a/agent-ops/skills/project/openai-usage-token-issue/SKILL.md b/agent-ops/skills/project/openai-usage-token-issue/SKILL.md index 71a7d53..9c1a88e 100644 --- a/agent-ops/skills/project/openai-usage-token-issue/SKILL.md +++ b/agent-ops/skills/project/openai-usage-token-issue/SKILL.md @@ -21,7 +21,7 @@ IOP는 사용자/테넌트 source of truth를 소유하지 않고, 외부 princi - `principal_ref`: 외부 사용자/테넌트 프로젝트 또는 운영 시스템의 principal 참조값 (필수) - `principal_alias`: Grafana에 노출할 내부 alias. 없으면 `principal_ref`에서 secret이 아닌 짧은 별칭을 정한다. (선택) -- `token_ref`: metric label과 설정에 쓸 안정 token 참조값. 없으면 token hash prefix로 만든다. (선택) +- `token_ref`: metric label과 설정에 쓸 안정 token 참조값. 한 `principal_ref`가 여러 앱/통합을 운영하면 앱/통합/용도별로 서로 다른 `token_ref`를 발급한다. 없으면 token hash prefix로 만든다. (선택) - `output_path`: raw token 없이 매핑 기록을 저장할 비공개/운영 전용 파일 경로. tracked docs/config에 쓰지 않는다. (선택) ## 먼저 확인할 것 @@ -29,6 +29,7 @@ IOP는 사용자/테넌트 source of truth를 소유하지 않고, 외부 princi - [ ] `principal_ref`가 secret, raw email, provider token, provider identity가 아니라 외부 시스템 참조값인지 확인한다. - [ ] raw token을 tracked `docs/`, `agent-roadmap/`, `agent-spec/`, `configs/`, git diff, shell history, 최종 보고에 남기지 않을 전달 경로를 정한다. - [ ] `token_ref`와 `principal_alias`가 낮은 cardinality label로 안전한 값인지 확인한다. +- [ ] 같은 `principal_ref`에 여러 앱/통합용 token이 필요한 경우 각 token의 앱/통합/용도 구분이 `token_ref`에 반영되는지 확인한다. - [ ] 기존 token을 회전하는 경우 기존 `token_ref`를 재사용할지 새 `token_ref`를 만들지 운영 정책을 확인한다. ## 실행 절차 diff --git a/agent-roadmap/archive/phase/operational-observability-provider-management/milestones/daily-usage-cost-roi-report-mvp.md b/agent-roadmap/archive/phase/operational-observability-provider-management/milestones/daily-usage-cost-roi-report-mvp.md new file mode 100644 index 0000000..18631e4 --- /dev/null +++ b/agent-roadmap/archive/phase/operational-observability-provider-management/milestones/daily-usage-cost-roi-report-mvp.md @@ -0,0 +1,87 @@ +# Milestone: 일별 Usage 비용/ROI 리포트 MVP + +## 위치 + +- Roadmap: [ROADMAP.md](../../../../ROADMAP.md) +- Phase: [PHASE.md](../../../../phase/operational-observability-provider-management/PHASE.md) + +## 목표 + +이미 수집되는 OpenAI-compatible token usage metric을 바탕으로, 운영자가 일별로 누가 어디서 token을 얼마나 썼고 그 사용량을 클라우드 모델 가격 기준으로 환산하면 어느 정도 비용이었는지 빠르게 볼 수 있게 한다. +1차 목표는 request-level ledger나 상세 실행 로그가 아니라 daily/monthly rollup과 cloud-equivalent cost/avoided-cost 관점의 ROI 판단이다. +클라우드 가격은 런타임이 자동으로 최신값을 가져오지 않고, 운영자가 기준 모델, 단가, 기준일을 명시한 baseline으로 관리한다. +사용자 한 명이 여러 앱/통합을 운영하는 일반 API usage 구조를 전제로, 같은 `principal_ref` 아래 여러 `token_ref`를 발급하고 `token_ref`별 사용량을 분해해 볼 수 있어야 한다. + +## 상태 + +[완료] + +## 승격 조건 + +- 없음: urgent 운영 니즈가 daily token usage, 사용 위치별 breakdown, cloud-equivalent cost, ROI 판단용 rollup으로 좁혀졌고 request-level ledger와 qualification report는 후속으로 유지한다. + +## 구현 잠금 + +- 상태: 해제 +- SDD: 불필요 +- SDD 사유: 기존 OpenAI-compatible usage metric과 Grafana/query 문서를 얇게 확장하는 작은 리포트 Milestone이다. 새 API/proto/runtime wire, 저장소 schema, 권한 모델, request-level ledger를 확정하지 않는다. +- 결정 필요: 없음 + +## 범위 + +- 일별/월별 token usage rollup query와 dashboard/report 기준 +- `principal_ref`, `principal_alias`, `token_ref`, `model_group`, `endpoint`, `response_mode`, `token_type` 기준 breakdown +- 같은 `principal_ref` 아래 여러 앱/통합 token이 있을 때 사용자 합산과 token/app별 분해를 함께 보는 기준 +- 운영자가 관리하는 cloud price baseline 기준 +- input/output/cached/reasoning token type별 cloud-equivalent cost 산식 +- IOP/local/provider 사용량을 cloud baseline으로 환산한 avoided-cost 또는 ROI 판단용 요약 +- Grafana query 또는 문서화된 조회 절차를 1차 표면으로 사용 + +## 기능 + +### Epic: [usage-roi] Daily Usage Cost and ROI + +운영자가 현재 필요한 수준의 비용/ROI 판단을 request-level ledger 없이 빠르게 볼 수 있는 usage rollup capability를 묶는다. + +- [x] [daily-rollup] `iop_openai_usage_tokens_total` 기준 일별/월별 token rollup query가 principal/token/model/endpoint/token_type별로 정리되어 있다. +- [x] [multi-token-principal] 한 `principal_ref`에 여러 앱/통합별 `token_ref`를 발급하고, 사용자 합산과 token/app별 breakdown을 동시에 볼 수 있는 전제가 코드/계약/운영 skill 기준으로 확인되어 있다. +- [x] [usage-origin] "어디서 얼만큼 사용했는지"를 `principal_alias`, `token_ref`, `model_group`, `endpoint`, `response_mode` 기준으로 볼 수 있는 breakdown이 정리되어 있다. +- [x] [price-baseline] cloud-equivalent cost 계산에 사용할 price baseline 필드가 정리되어 있다. 최소 필드는 cloud provider/model, token type, price per token 또는 per 1M tokens, currency, effective date다. +- [x] [cost-formula] token type별 사용량에 price baseline을 곱해 daily/monthly cloud-equivalent cost를 계산하는 산식과 query/report 예시가 정리되어 있다. +- [x] [roi-summary] IOP/local/provider 사용량을 cloud baseline으로 환산한 avoided-cost 또는 ROI 판단용 summary가 정리되어 있다. 실제 billing, chargeback, infra cost accounting은 후속 범위로 둔다. +- [x] [grafana-report] Grafana dashboard/query 또는 문서 표면에서 daily usage, usage origin, cloud-equivalent cost, ROI summary를 확인하는 절차가 정리되어 있다. + +## 완료 리뷰 + +- 상태: 통과 +- 요청일: 2026-07-10 +- 완료 근거: + - [openai-usage-grafana.md](../../../../../docs/openai-usage-grafana.md)에 principal/token/model/endpoint/response_mode/token_type 기준 daily/monthly rollup, usage origin breakdown, Grafana report 절차를 정리했다. + - [openai-usage-grafana.md](../../../../../docs/openai-usage-grafana.md)에 operator-managed cloud price baseline 필드, token type별 cost 산식, cloud-equivalent avoided-cost ROI summary 기준을 정리했다. + - multi-token principal 전제는 `apps/edge/internal/openai/identity_metering_test.go`, `packages/go/config/config_test.go`, `agent-contract/inner/edge-config-runtime-refresh.md`, `agent-ops/skills/project/openai-usage-token-issue/SKILL.md`의 현재 변경으로 확인했다. + - Spec sync: [openai-compatible-surface.md](../../../../../agent-spec/input/openai-compatible-surface.md)에 daily/monthly rollup, usage origin, cloud-equivalent cost, avoided-cost ROI 문서 표면을 반영했다. +- 리뷰 필요: + - [x] 사용자가 완료 결과를 확인했다 + - [x] archive 이동을 승인했다 +- 리뷰 코멘트: 2026-07-10 종료 검토에서 기능 Task, 구현 잠금, SDD gate, code/spec evidence, local 검증이 충족되어 완료 archive로 이동했다. + +## 범위 제외 + +- request-level ledger 저장소, 요청별 상세 감사/디버깅 조회 +- prompt/response/reasoning preview 저장, redaction, retention 정책 +- Control Plane 또는 Client usage dashboard 구현 +- live cloud pricing API 연동 또는 최신 가격 자동 동기화 +- 실제 결제, chargeback, 조직별 비용 배부, full infra cost accounting +- 사용자별 token 제한 warn/reject enforcement +- provider/device/model qualification report + +## 작업 컨텍스트 + +- 관련 경로: `docs/openai-usage-grafana.md`, `apps/edge/internal/openai/usage_metrics.go`, `apps/edge/internal/openai/usage_metrics_test.go`, [openai-compatible-api.md](../../../../../agent-contract/outer/openai-compatible-api.md) +- 표준선(선택): source metric은 기존 `iop_openai_usage_tokens_total`과 `iop_openai_requests_total`을 우선 사용한다. +- 표준선(선택): `principal_ref`는 사용자/테넌트 참조값이고, `token_ref`는 앱/통합/용도별 API token 식별자다. 같은 `principal_ref`에 여러 `token_ref`를 허용한다. +- 표준선(선택): cloud price baseline은 운영자가 명시한 정적 기준값이며, 가격 기준일과 모델명을 함께 표시한다. +- 표준선(선택): ROI는 1차에서 "cloud equivalent avoided cost" 수준으로 표현하고, 실제 인프라 비용과 billing-grade ROI는 후속으로 둔다. +- 선행 작업: 사용자별 OpenAI-compatible 토큰 측정 MVP +- 후속 작업: 요청 실행 로그와 Usage Ledger 기반, 사용자별 token 제한 enforcement, 운영 리포트 +- 확인 필요: 없음 diff --git a/agent-roadmap/phase/knowledge-tool-optimization-extension/milestones/openai-compatible-output-validation-filters.md b/agent-roadmap/phase/knowledge-tool-optimization-extension/milestones/openai-compatible-output-validation-filters.md index 6dfddf5..3f2bd2e 100644 --- a/agent-roadmap/phase/knowledge-tool-optimization-extension/milestones/openai-compatible-output-validation-filters.md +++ b/agent-roadmap/phase/knowledge-tool-optimization-extension/milestones/openai-compatible-output-validation-filters.md @@ -34,6 +34,7 @@ OpenAI-compatible Chat Completions provider 경로에서 모델 출력 이상을 ## 범위 - OpenAI-compatible `/v1/chat/completions` provider route의 출력 검증 필터 모듈과 response path 선택 +- 출력 검증 filter별 enable/disable 정책을 environment(`dev`, `dev-corp`), model group/model/provider, 기능 단위로 평가하는 config/registry 계층 - 반복 출력 루프 감지용 rolling stream inspector, upstream abort, continuation repair, 1회 repair 제한 - `metadata.scheme` JSON schema 계약 수신, 마지막 user message prompt append, buffered validation, schema 위반 시 bounded retry - `passthrough`, `passthrough_guarded`, `contract_schema` 내부 response path 구분과 실행 로그/side observation. 이 이름들은 caller가 임의로 넣는 `metadata.iop_response_mode` 값이 아니라 IOP 내부 경로/로그 기준이다. @@ -47,6 +48,7 @@ OpenAI-compatible provider 응답을 사용자에게 노출하기 전에 필터 - [ ] [contract-doc] OpenAI-compatible 계약 문서와 구현 타입이 `metadata.scheme`, 내부 `contract_schema`/`passthrough_guarded` path, normalized CLI-only 경계를 설명한다. 검증: [openai-compatible-api.md](../../../../agent-contract/outer/openai-compatible-api.md)와 관련 Go 타입/handler 테스트가 새 계약과 일치한다. - [ ] [filter-pipeline] Edge Chat Completions provider route에 여러 출력 검증 filter를 붙일 수 있는 모듈 파이프라인이 생기고, 요청별로 pure passthrough, guarded stream, schema contract 경로를 결정한다. 검증: `go test ./apps/edge/internal/openai -count=1`에서 path selection과 unknown/unsupported 조합 테스트가 통과한다. +- [ ] [filter-policy] 각 출력 검증 filter는 공통 interface/registry를 통해 enable/disable 정책을 평가하고, environment(`dev`, `dev-corp`)와 model group/model/provider별로 반복루프 guard, schema contract 같은 기능을 독립적으로 켜고 끌 수 있다. 검증: qwen/gemma fixture 기반 config/handler tests에서 필터별 활성/비활성, 정책 우선순위, disabled filter observation이 통과한다. - [ ] [repeat-guard] streaming 응답에서 반복 루프를 rolling window로 감지하면 downstream SSE를 닫지 않고 upstream provider request만 abort한 뒤 emitted safe prefix와 bad tail summary로 continuation repair 요청을 이어 붙인다. 검증: 반복 chunk fixture 기반 stream test가 1회 repair, safe prefix 보존, `[DONE]` 단일 종료, tool side-effect 구간 차단을 확인한다. - [ ] [schema-contract] `metadata.scheme`이 있으면 `stream=true` 요청이어도 downstream content streaming을 보류하고, 마지막 user message에 scheme 계약 block을 append한 뒤 JSON parse/schema validation, 실패 시 1회 재요청, 성공 시 validated JSON만 반환한다. 검증: valid JSON, invalid-then-repair, retry-exhausted, multimodal user content append fixture가 통과한다. - [ ] [ops-evidence] 출력 필터 결과가 요청 실행 로그와 smoke에서 원인 축을 구분할 수 있게 남는다. 검증: dev-corp Pi TUI smoke에서 반복루프 중단/재요청 또는 schema validation 결과가 model/provider/IOP/CLI 축과 함께 관찰된다. @@ -78,7 +80,10 @@ OpenAI-compatible provider 응답을 사용자에게 노출하기 전에 필터 - 표준선(선택): 반복루프 필터는 streaming passthrough UX를 유지하는 `passthrough_guarded` 경로이며, 이미 흘린 정상 prefix를 버리지 않고 continuation repair로 이어 쓴다. - 표준선(선택): schema 출력 계약은 `metadata.scheme` 하나로 표현하고 wrapper/options를 추가하지 않는다. 계약이 있으면 streaming 요청보다 contract validation을 우선하며, 검증 전 content delta를 흘리지 않는다. - 표준선(선택): `metadata.scheme`은 JSON schema로 간주하며 IOP가 마지막 user message에 출력 계약 블록을 append해 provider로 전달한다. +- 표준선(선택): 출력 검증 filter 확장은 Go class 상속보다 공통 interface와 공유 policy/base helper를 기준으로 묶는다. 모든 filter는 동일 enablement context를 받고, 모델/환경별 정책은 registry에서 일관되게 평가한다. +- 표준선(선택): optional online filter가 비활성화된 모델은 pure passthrough로 처리할 수 있지만, caller가 `metadata.scheme`처럼 필수 계약을 요청했는데 해당 filter가 비활성화된 모델은 silent passthrough가 아니라 unsupported/400으로 거부한다. - 표준선(선택): OpenAI-compatible provider 출력 검증은 normalized 경로로 전환하지 않는다. normalized는 CLI 전용으로 유지한다. +- 우선순위 순서: 현재 active 1순위다. 이 Milestone을 먼저 구현하고, 그 다음 [Seulgivibe OpenAI-compatible Provider 연동](../../routing-policy-model-orchestration/milestones/seulgivibe-openai-compatible-provider.md)을 진행한다. - 선행 작업: [OpenAI-compatible Tool Call Boundary Hardening](../../../archive/phase/knowledge-tool-optimization-extension/milestones/openai-compatible-tool-call-boundary-hardening.md), [OpenAI-compatible Raw Tunnel과 Sideband Passthrough](../../../archive/phase/routing-policy-model-orchestration/milestones/openai-compatible-raw-tunnel-sideband-passthrough.md) - 후속 작업: 단계 호출과 검증 최적화 MVP, Tool Call 판정 모델 Gate 리뷰 - 확인 필요: 없음 diff --git a/agent-roadmap/phase/operational-observability-provider-management/PHASE.md b/agent-roadmap/phase/operational-observability-provider-management/PHASE.md index 62f5be5..67f4f60 100644 --- a/agent-roadmap/phase/operational-observability-provider-management/PHASE.md +++ b/agent-roadmap/phase/operational-observability-provider-management/PHASE.md @@ -36,6 +36,10 @@ provider 확장 Phase에서 검증한 Ollama, vLLM, SGLang, Lemonade 같은 추 - 경로: [usage-token-log-ops-mvp](../../archive/phase/operational-observability-provider-management/milestones/usage-token-log-ops-mvp.md) - 요약: OpenAI-compatible 호출을 IOP bearer token 기반 `principal_ref`/alias로 귀속하고, pure passthrough body를 유지한 채 input/output/reasoning/cached token 사용량을 Prometheus metric으로 측정해 Grafana 조회 기준과 후속 제한 정책 재사용 기준까지 정리했다. +- [완료] 일별 Usage 비용/ROI 리포트 MVP + - 경로: [daily-usage-cost-roi-report-mvp](../../archive/phase/operational-observability-provider-management/milestones/daily-usage-cost-roi-report-mvp.md) + - 요약: 기존 OpenAI-compatible token usage metric을 일별/월별로 rollup하고, 운영자가 관리하는 cloud price baseline으로 환산해 사용자/토큰/model/endpoint별 cloud-equivalent cost와 ROI 판단용 avoided-cost를 Grafana/query 중심으로 보는 문서 표면을 완료했다. + - [스케치] 요청 실행 로그와 Usage Ledger 기반 - 경로: [request-execution-log-usage-ledger-foundation](milestones/request-execution-log-usage-ledger-foundation.md) - 요약: 사용자 요청 하나의 device/provider/model 선택, queue/dispatch/start/first-token/end 시간, token breakdown, status/error를 구조화된 실행 로그와 usage ledger로 남기는 로그 시스템 개편 후보를 스케치한다. diff --git a/agent-roadmap/phase/operational-observability-provider-management/milestones/request-execution-log-usage-ledger-foundation.md b/agent-roadmap/phase/operational-observability-provider-management/milestones/request-execution-log-usage-ledger-foundation.md index 51bad4e..ef5783c 100644 --- a/agent-roadmap/phase/operational-observability-provider-management/milestones/request-execution-log-usage-ledger-foundation.md +++ b/agent-roadmap/phase/operational-observability-provider-management/milestones/request-execution-log-usage-ledger-foundation.md @@ -93,6 +93,7 @@ provider/tool-call bridge에서 native tool call, text fallback, synthesized too - 표준선(선택): Edge는 runtime execution과 provider routing의 원본 이벤트를 가장 먼저 알고, Control Plane은 연결 view와 운영 조회/export 표면을 제공한다. - 표준선(선택): usage는 provider-reported 값을 우선하고, provider가 주지 않는 값은 estimated 또는 unavailable로 명시해 정확도와 추정을 분리한다. - 표준선(선택): tool-call 추적은 기본적으로 raw 원문 저장보다 `run_id` 기준 판정 필드, 길이, hash, 짧은 redacted preview를 우선하고, bounded raw capture는 명시적으로 켠 진단 모드로 제한한다. +- 우선순위: [OpenAI-compatible 출력 검증 필터](../../knowledge-tool-optimization-extension/milestones/openai-compatible-output-validation-filters.md)와 [Seulgivibe OpenAI-compatible Provider 연동](../../routing-policy-model-orchestration/milestones/seulgivibe-openai-compatible-provider.md) 이후 재개한다. - SDD gate: [SDD.md](../../../sdd/operational-observability-provider-management/request-execution-log-usage-ledger-foundation/SDD.md), 사용자 리뷰 [USER_REVIEW.md](../../../sdd/operational-observability-provider-management/request-execution-log-usage-ledger-foundation/USER_REVIEW.md) - 선행 작업: 사용량, 토큰, 로그 운영 추적 MVP - 후속 작업: Provider-Device-Model Qualification 리포트, 운영 리포트, 품질 기반 routing/fallback 고도화 diff --git a/agent-roadmap/phase/routing-policy-model-orchestration/milestones/seulgivibe-openai-compatible-provider.md b/agent-roadmap/phase/routing-policy-model-orchestration/milestones/seulgivibe-openai-compatible-provider.md index 0872fa4..d053c9c 100644 --- a/agent-roadmap/phase/routing-policy-model-orchestration/milestones/seulgivibe-openai-compatible-provider.md +++ b/agent-roadmap/phase/routing-policy-model-orchestration/milestones/seulgivibe-openai-compatible-provider.md @@ -77,6 +77,7 @@ Seulgivibe를 IOP 내부에서는 provider-first OpenAI-compatible resource로 - 표준선(선택): Seulgivibe는 새 wire adapter가 아니라 OpenAI-compatible provider family로 관리하고, provider별 특수 처리는 generation passthrough 밖의 auth/catalog/config 경계에만 둔다. - 표준선(선택): 사용자별 provider token은 request-time raw value로만 받고, Edge가 provider tunnel request header로 변환한다. Node나 host-local helper script가 사용자 token source of truth가 되지 않는다. - 표준선(선택): provider `/models` endpoint가 실패해도 IOP `/v1/models`는 top-level `models[]` catalog를 source of truth로 노출한다. +- 우선순위 순서: [OpenAI-compatible 출력 검증 필터](../../knowledge-tool-optimization-extension/milestones/openai-compatible-output-validation-filters.md) 완료 후 본 Milestone을 진행한다. - 선행 작업: [OpenAI-compatible Raw Tunnel과 Sideband Passthrough](../../../archive/phase/routing-policy-model-orchestration/milestones/openai-compatible-raw-tunnel-sideband-passthrough.md), [Model Alias Provider Pool과 Provider Catalog](../../operational-observability-provider-management/milestones/provider-catalog-device-status.md) - 후속 작업: 자동 route scorer 구현, provider auth per-provider granularity, Seulgivibe live smoke profile 정리 - 확인 필요: 없음 diff --git a/agent-roadmap/sdd/knowledge-tool-optimization-extension/openai-compatible-output-validation-filters/SDD.md b/agent-roadmap/sdd/knowledge-tool-optimization-extension/openai-compatible-output-validation-filters/SDD.md index 28b6af1..08f7d90 100644 --- a/agent-roadmap/sdd/knowledge-tool-optimization-extension/openai-compatible-output-validation-filters/SDD.md +++ b/agent-roadmap/sdd/knowledge-tool-optimization-extension/openai-compatible-output-validation-filters/SDD.md @@ -34,18 +34,19 @@ | Code | `apps/edge/internal/openai`, `apps/node/internal/adapters/openai_compat` | OpenAI-compatible Chat Completions provider route와 stream bridge 구현 기준 | | Contract | [openai-compatible-api.md](../../../../agent-contract/outer/openai-compatible-api.md) | `metadata.scheme`, 내부 response path, streaming/gated 정책 원문 | | External Provider | OpenAI-compatible provider pool | provider 원본 요청/응답은 IOP 필터 정책에 따라 upstream abort/retry 대상이 된다. | -| User Decision | 없음 | 사용자가 `metadata.scheme` 단일 계약, normalized CLI-only 경계, streaming guard 방향을 확정했다. | +| User Decision | 현재 사용자 요청 | 사용자가 `metadata.scheme` 단일 계약, normalized CLI-only 경계, streaming guard 방향, 모델/환경별 filter enable/disable와 공통 interface/base 계층을 확정했다. | ## State Machine | 상태 | 진입 조건 | 다음 상태 | 근거 | |------|-----------|-----------|------| -| `ingress` | `/v1/chat/completions` 요청 수신 | `passthrough`, `guarded_stream`, `contract_schema` | request metadata, internal filter path policy | +| `ingress` | `/v1/chat/completions` 요청 수신 | `passthrough`, `guarded_stream`, `contract_schema`, `policy_rejected` | request metadata, environment/model/provider별 internal filter path policy | | `passthrough` | `metadata.scheme` 없음, guarded filter 비활성 또는 적용 대상 아님 | `done`, `provider_error` | provider SSE/non-stream 응답 | | `guarded_stream` | streaming 응답에 반복루프 등 online filter 적용 | `repairing`, `done`, `guard_error` | rolling inspector event | | `repairing` | 반복루프 감지 후 upstream provider request abort 성공 | `guarded_stream`, `guard_error` | continuation repair provider 응답 | | `contract_schema` | `metadata.scheme` 있음 | `schema_retry`, `done`, `schema_error` | JSON parse/schema validation | | `schema_retry` | schema validation 실패이며 retry budget 남음 | `contract_schema`, `schema_error` | retry counter | +| `policy_rejected` | caller가 요청한 필수 filter가 선택된 environment/model/provider에서 비활성 또는 미지원 | `done` | OpenAI-compatible invalid_request_error | | `guard_error` | repair 불가, retry exhausted, tool side-effect 구간 진입 | `done` | OpenAI-compatible error or terminal SSE error | | `schema_error` | retry 후에도 JSON/schema 불일치 | `done` | OpenAI-compatible validation error | | `done` | 성공 응답 또는 terminal error 전송 | 없음 | final chunk/body | @@ -63,6 +64,13 @@ - `passthrough_guarded`: downstream SSE 연결을 유지하되 IOP가 upstream stream을 감시하고 필요 시 upstream만 abort/retry한다. 내부 실행/로그 path이며 provider-original byte-identical로 표시하지 않는다. - `contract_schema`: 전체 응답을 수집/검증한 뒤 valid JSON만 반환한다. `stream=true` 요청에서도 검증 전 `delta.content`를 흘리지 않는다. - `tool_validation_error`, `schema_validation_error`, `guard_error`: 복구 불가 또는 retry exhausted terminal error. +- 내부 filter interface/policy: + - 구현은 Go class 상속이 아니라 공통 filter interface와 공유 policy/base helper를 기준으로 한다. + - 모든 filter는 filter id, feature name, 지원 response path, 기본 활성값, request 적용 여부, 실행/검증 결과를 같은 interface로 노출한다. + - `FilterContext`는 최소한 environment, model group/model alias, provider id/type, endpoint, stream 여부, caller-required feature를 포함한다. + - enablement 정책은 filter feature별로 평가하며, 더 구체적인 규칙이 우선한다. 우선순위는 `environment + model/provider + feature`, `model/provider + feature`, `environment + feature`, filter 기본값 순서다. + - 반복루프 guard 같은 optional online filter가 비활성화되면 해당 filter만 skip하고 pure passthrough 또는 남은 filter path로 진행한다. + - `metadata.scheme`처럼 caller가 필수 출력 계약을 요청한 filter가 비활성화되면 silent passthrough로 낮추지 않고 `policy_rejected`로 종료한다. - 내부 path 주의: - `passthrough_guarded`와 `contract_schema`는 caller가 임의로 지정하는 `metadata.iop_response_mode` 값이 아니라 IOP 내부 실행/로그 path 이름이다. - caller가 지정할 수 있는 공개 response mode 값은 [계약 원문](../../../../agent-contract/outer/openai-compatible-api.md)에서 별도로 명시한 값만 허용한다. @@ -84,6 +92,7 @@ | S05 | `schema-contract` | request `metadata.scheme`에 JSON schema가 있다 | provider가 schema-valid JSON을 반환한다 | IOP가 JSON parse/schema validation 후 valid JSON만 반환한다 | | S06 | `schema-contract` | request `metadata.scheme`에 JSON schema가 있다 | provider가 invalid JSON 또는 schema 위반 JSON을 반환한다 | IOP가 schema와 validation error summary로 1회 재요청하고, 실패가 반복되면 schema validation error를 반환한다 | | S07 | `ops-evidence` | dev-corp Pi TUI에서 반복루프 또는 schema 계약 smoke를 실행한다 | 로그와 TUI 출력을 확인한다 | 문제 축이 model/provider, IOP guard, CLI/normalized 경계로 구분되고 최종 stream/body가 오염되지 않는다 | +| S08 | `filter-policy` | dev/dev-corp에서 qwen/gemma model/provider별 반복루프 guard와 schema-contract enablement가 다르게 설정되어 있다 | 요청이 선택된 route로 들어온다 | filter registry가 가장 구체적인 정책을 적용하고, optional disabled filter는 skip하며, required disabled schema-contract는 unsupported/400으로 거부한다 | ## Evidence Map @@ -96,6 +105,7 @@ | S05 | valid JSON schema fixture | `agent-task/m-openai-compatible-output-validation-filters/...` | `Roadmap Completion`에 `schema-contract`, validated JSON body/SSE assertion | | S06 | invalid-then-repair, retry-exhausted fixtures | `agent-task/m-openai-compatible-output-validation-filters/...` | `Roadmap Completion`에 `schema-contract`, retry count/error assertion | | S07 | dev-corp Pi TUI smoke log, Edge provider log | `agent-task/m-openai-compatible-output-validation-filters/...` | `Roadmap Completion`에 `ops-evidence`, dev-corp smoke evidence path 또는 로그 요약 | +| S08 | qwen/gemma environment policy fixture, handler tests | `agent-task/m-openai-compatible-output-validation-filters/...` | `Roadmap Completion`에 `filter-policy`, per-env/per-model enable/disable, policy precedence, disabled required filter 400 assertion | ## Cross-repo Dependencies @@ -115,4 +125,5 @@ ## 작업 컨텍스트 - 표준선: OpenAI-compatible provider 출력 검증은 Edge OpenAI handler/stream bridge에 두고, normalized는 CLI 전용으로 유지한다. 반복루프는 streaming repair filter, `metadata.scheme`은 buffered contract validation으로 분리한다. +- 표준선: filter 확장은 공통 interface, registry, shared policy/base helper를 통해 추가한다. enable/disable는 `dev`/`dev-corp`, qwen/gemma 같은 model group/model/provider, filter feature 단위로 선언하고 handler는 같은 `FilterContext`로 평가한다. - 후속 SDD: 없음 diff --git a/agent-spec/input/openai-compatible-surface.md b/agent-spec/input/openai-compatible-surface.md index 38bad5d..7d9e3c8 100644 --- a/agent-spec/input/openai-compatible-surface.md +++ b/agent-spec/input/openai-compatible-surface.md @@ -35,7 +35,7 @@ source_evidence: notes: usage metric label, token breakdown, passthrough usage regression 검증 - type: docs path: docs/openai-usage-grafana.md - notes: Grafana query와 daily/monthly rollup 조회 가이드 + notes: Grafana query, daily/monthly rollup, usage origin, cloud-equivalent cost, avoided-cost ROI 조회 가이드 --- # 스펙: OpenAI-Compatible 입력 표면 @@ -51,6 +51,7 @@ Edge가 OpenAI-compatible HTTP 요청을 받아 내부 `adapter + target` 실행 | OpenAI-compatible HTTP server | `openai.enabled=true`이면 Edge input manager가 `/healthz`, `/v1/models`, `/v1/chat/completions`, `/v1/responses`, `/api/` route를 제공한다. | | bearer auth | `openai.bearer_token`이 있으면 matching bearer authorization header를 요구한다. | | principal token auth | `openai.principal_tokens[]`가 설정된 경우 raw token의 SHA-256 hash를 `token_hash_sha256`과 매칭하고, 매칭 시 `iop_principal_ref`, `iop_principal_alias`, `iop_token_ref`, `iop_principal_source` metadata를 채운다. | +| multi-token principal | 같은 `principal_ref`에 여러 `token_ref`를 연결할 수 있으며, 사용량 metric은 사용자 합산과 token/app별 breakdown을 모두 가능하게 한다. | | model catalog | `/v1/models`는 provider-pool `models[]`, legacy `openai.model_routes[]`, `openai.models` 또는 `openai.target` 순서로 노출 모델을 만든다. | | model dispatch | request `model`은 provider-pool catalog, legacy model route, single target fallback 순서로 해석된다. | | provider-pool handoff | provider-pool catalog에 model이 있으면 service 요청은 `ProviderPool=true`로 전달되고 adapter/target은 provider selection 이후 확정된다. | @@ -62,7 +63,7 @@ Edge가 OpenAI-compatible HTTP 요청을 받아 내부 `adapter + target` 실행 | sideband extension | `passthrough+sideband`는 provider body와 IOP route/usage/assembled observation을 명시적 extension stream/envelope로 함께 노출하며 provider-original byte identity로 표시하지 않는다. | | OpenAI usage metering | Edge는 OpenAI-compatible request terminal status와 provider-reported `input`, `output`, `reasoning`, `cached_input` token usage를 Prometheus counter로 집계한다. | | reasoning observation metric | provider가 reasoning token을 보고하지 않고 reasoning text만 관측되면 token 추정 없이 관측 횟수와 character count 보조 metric만 emit한다. | -| Grafana usage surface | 1차 조회 표면은 Prometheus/Grafana query guide이며 Control Plane/Client dashboard와 request-level ledger는 후속 범위다. | +| 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는 후속 범위다. | | Responses API | `/v1/responses`는 현재 string input의 non-streaming 요청만 지원한다. | | strict output | strict output이 켜져 있으면 XML completion contract 기반 instruction 또는 prompt prefix를 추가할 수 있다. | | tool call 처리 | Chat Completions `tools`는 provider native metadata 복원 또는 text tool-call synthesis/validation 경로를 사용한다. | @@ -116,6 +117,7 @@ sequenceDiagram - Node complete event metadata의 `openai_tool_calls`와 `openai_text_tool_fallback`은 response tool call 복원에 쓰인다. - usage metric은 `iop_openai_requests_total`, `iop_openai_usage_tokens_total`, `iop_openai_reasoning_observed_total`, `iop_openai_reasoning_chars_total`로 emit된다. - usage label은 `edge_id`, `principal_ref`, `principal_alias`, `token_ref`, `model_group`, `endpoint`, `response_mode`, `status`, `usage_source`, `token_type`처럼 낮은 cardinality 값만 사용한다. +- `principal_ref`는 사용자/테넌트 참조값이고 `token_ref`는 앱/통합/용도별 token 참조값이다. 같은 principal에 여러 token이 있으면 `principal_ref` 기준 합산과 `token_ref` 기준 분해를 함께 사용할 수 있다. - `request_id`, `session_id`, raw bearer token, provider token, raw prompt/response는 metric label에 넣지 않는다. - provider body usage와 provider tunnel `USAGE` frame이 모두 있으면 body input/output을 우선하고 proto-only reasoning/cached input을 보조로 병합해 중복 집계를 피한다. @@ -124,7 +126,7 @@ sequenceDiagram - `go test ./apps/edge/internal/openai` - `go test ./apps/edge/internal/service` - `go test ./apps/edge/internal/openai -run 'Sideband|UsageMetrics|ToolValidation|Dispatch|Reasoning|Retry'` -- `rg --fixed-strings "sum by (principal_alias, token_type)" docs/openai-usage-grafana.md` +- `rg --fixed-strings "cloud_equivalent_cost" docs/openai-usage-grafana.md` - `make test-openai-ollama` - provider별 실제 runtime smoke는 환경별 agent-test/dev 또는 dev-corp profile을 따른다. @@ -143,7 +145,7 @@ sequenceDiagram - `openai.principal_tokens[]` 변경은 restart-required로 분류된다. - principal token auth가 실패하면 legacy `openai.bearer_token`이 unmapped fallback으로 동작한다. - provider가 별도 reasoning token을 보고하지 않으면 reasoning text를 token으로 추정하지 않는다. -- Grafana guide는 metric 조회 예시이며 billing, chargeback, long-term ledger, 사용자별 제한 enforcement의 source of truth가 아니다. +- Grafana guide는 metric 조회와 operator-managed price baseline 예시이며 live cloud pricing, billing, chargeback, long-term ledger, 사용자별 제한 enforcement의 source of truth가 아니다. ## 변경 기록 @@ -151,3 +153,4 @@ sequenceDiagram - 2026-07-07: 기능 목록 중심으로 축소하고 주요 흐름을 Mermaid sequence diagram으로 정리. - 2026-07-08: Chat Completions provider raw tunnel, response mode, sideband/transformed semantics를 현재 코드와 계약 기준으로 반영. - 2026-07-10: principal token 기반 usage metering, Prometheus metric, Grafana query guide, reasoning/cached token breakdown을 Milestone completion evidence 기준으로 반영. +- 2026-07-10: 일별 Usage 비용/ROI 리포트 MVP 종료 검토에서 daily/monthly rollup, usage origin breakdown, cloud-equivalent cost, avoided-cost ROI 문서 표면을 반영. diff --git a/agent-spec/runtime/provider-pool-config-refresh.md b/agent-spec/runtime/provider-pool-config-refresh.md index a3f3f1d..ac8af1b 100644 --- a/agent-spec/runtime/provider-pool-config-refresh.md +++ b/agent-spec/runtime/provider-pool-config-refresh.md @@ -51,7 +51,7 @@ Edge 설정에서 provider-pool이 어떻게 모델 실행 후보를 고르고, | mutable apply | 적용 가능한 변경은 Edge `Cfg`, `NodeStore`, service/input model catalog, OpenAI long-context threshold를 copy-on-write로 교체한다. | | Node config refresh push | 변경이 있으면 Edge가 연결된 Node에 node-specific `NodeConfigRefreshRequest`를 push한다. | | Node registry swap | Node는 refresh payload로 새 adapter registry를 만들고 router registry를 swap한다. old registry stop은 active run이 있으면 drain 이후로 지연한다. | -| principal token mapping config | `openai.principal_tokens[]`는 raw token 없이 `token_ref`, `token_hash_sha256`, `principal_ref`, optional alias를 관리하고 OpenAI usage metering의 principal label 후보를 제공한다. | +| principal token mapping config | `openai.principal_tokens[]`는 raw token 없이 `token_ref`, `token_hash_sha256`, `principal_ref`, optional alias를 관리하고 OpenAI usage metering의 principal/token label 후보를 제공한다. 같은 principal에 여러 token entry를 둘 수 있다. | ## 범위 @@ -92,6 +92,7 @@ sequenceDiagram - provider capacity, priority, max queue, queue timeout, enabled toggle, model generation policy는 live apply 대상으로 분류된다. - Edge listener, control plane, openai/a2a listener, bootstrap artifact path, node 추가/삭제, node token/alias, adapter 설정 변경은 restart-required 대상이다. - `openai.principal_tokens[]`는 `token_ref`와 `token_hash_sha256` 중복을 거부하고, raw token 원문은 tracked config에 저장하지 않는다. +- 여러 `openai.principal_tokens[]` entry가 같은 `principal_ref`를 공유할 수 있으며, 이때 `token_ref`가 앱/통합/용도별 사용량 분해 기준이다. - `openai.principal_tokens[]` 변경은 credential/hash 변경으로 보고 restart-required로 분류된다. - refresh result는 changed nodes/providers/models와 restart-required paths를 stable non-nil slice로 보고한다. diff --git a/apps/edge/internal/openai/identity_metering_test.go b/apps/edge/internal/openai/identity_metering_test.go index 31a759b..094b3fd 100644 --- a/apps/edge/internal/openai/identity_metering_test.go +++ b/apps/edge/internal/openai/identity_metering_test.go @@ -73,6 +73,31 @@ func TestRoutesPrincipalTokenMappingFallsBackToLegacyBearer(t *testing.T) { } } +func TestResolvePrincipalAllowsMultipleTokensForSamePrincipal(t *testing.T) { + cfg := config.EdgeOpenAIConf{ + PrincipalTokens: []config.OpenAIPrincipalTokenConf{ + {TokenRef: "alice-app-a", TokenHashSHA256: sha256Hex("alice-app-a-token"), PrincipalRef: "user:alice", PrincipalAlias: "alice"}, + {TokenRef: "alice-app-b", TokenHashSHA256: sha256Hex("alice-app-b-token"), PrincipalRef: "user:alice", PrincipalAlias: "alice"}, + }, + } + + first, ok := resolvePrincipal(cfg, "Bearer alice-app-a-token") + if !ok { + t.Fatal("expected first app token to authenticate") + } + if first.PrincipalRef != "user:alice" || first.PrincipalAlias != "alice" || first.TokenRef != "alice-app-a" { + t.Fatalf("unexpected first principal: %+v", first) + } + + second, ok := resolvePrincipal(cfg, "Bearer alice-app-b-token") + if !ok { + t.Fatal("expected second app token to authenticate") + } + if second.PrincipalRef != "user:alice" || second.PrincipalAlias != "alice" || second.TokenRef != "alice-app-b" { + t.Fatalf("unexpected second principal: %+v", second) + } +} + func TestHealthzDoesNotRequireBearerTokenWithPrincipalTokensConfigured(t *testing.T) { cfg := config.EdgeOpenAIConf{ PrincipalTokens: []config.OpenAIPrincipalTokenConf{ diff --git a/docs/openai-usage-grafana.md b/docs/openai-usage-grafana.md index 20ae40c..db32e66 100644 --- a/docs/openai-usage-grafana.md +++ b/docs/openai-usage-grafana.md @@ -1,6 +1,6 @@ # OpenAI-compatible Usage – Grafana Query Guide -> **Purpose**: 이 문서는 운영자가 Prometheus metric을 Grafana 패널로 조회해 사용자별 OpenAI-compatible token 사용량을 볼 수 있도록 한다. Control Plane/Client dashboard, request-level ledger, 사용자별 제한 enforcement는 만들지 않는다. +> **Purpose**: 이 문서는 운영자가 Prometheus metric을 Grafana 패널로 조회해 사용자별 OpenAI-compatible token 사용량, 사용 출처, cloud-equivalent cost, avoided-cost ROI 판단 기준을 볼 수 있도록 한다. Control Plane/Client dashboard, request-level ledger, live cloud pricing sync, billing/chargeback, 사용자별 제한 enforcement는 만들지 않는다. --- @@ -66,6 +66,24 @@ token_type, usage_source --- +## Principal / Token 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만 기록하는 절차를 따른다. + +관련 기준: + +- `agent-contract/outer/openai-compatible-api.md`: OpenAI-compatible principal token auth 계약 +- `agent-contract/inner/edge-config-runtime-refresh.md`: `openai.principal_tokens[]` config 계약 +- `agent-ops/skills/project/openai-usage-token-issue/SKILL.md`: 운영 token 발급 절차 +- `apps/edge/internal/openai/identity_metering_test.go`: 같은 principal의 여러 token resolution 테스트 +- `packages/go/config/config_test.go`: 같은 principal의 여러 `principal_tokens[]` config load 테스트 + +--- + ## PromQL Examples ### 기본: 사용자별 사용량 (principal_alias 기준) @@ -92,6 +110,20 @@ sum by (principal_ref, token_type) (iop_openai_usage_tokens_total) sum by (token_ref, token_type) (iop_openai_usage_tokens_total) ``` +### 사용자 합산 + 앱/통합별 breakdown + +```promql +# 같은 principal_ref의 전체 사용량 +sum by (principal_ref, principal_alias, token_type) ( + iop_openai_usage_tokens_total +) + +# 같은 principal_ref 안에서 token_ref별 사용량 +sum by (principal_ref, principal_alias, token_ref, token_type) ( + iop_openai_usage_tokens_total +) +``` + ### model_group별 usage ```promql @@ -113,6 +145,15 @@ sum by (endpoint, status) (iop_openai_requests_total) sum by (response_mode, status) (iop_openai_requests_total{status="success"}) ``` +### usage origin breakdown + +```promql +# "어디서 얼만큼 사용했는지"를 보는 table용 breakdown +sum by (principal_alias, token_ref, model_group, endpoint, response_mode, token_type) ( + iop_openai_usage_tokens_total +) +``` + ### token_type별 사용량 전체 ```promql @@ -148,8 +189,11 @@ sum by (principal_alias) (iop_openai_reasoning_chars_total) | 패널 | 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 | | 요청 상태 분포 (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 | > Grafana JSON dashboard import file은 본 scope에 포함되지 않는다. 위 PromQL을 바탕으로 패널을 수동 구성한다. @@ -161,8 +205,8 @@ sum by (principal_alias) (iop_openai_reasoning_chars_total) ### 일일 토큰 합계 ```promql -# 일일 입력/출력 토큰 합계 (principal_alias별) -sum by (principal_alias, token_type) ( +# 일일 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) ( increase(iop_openai_usage_tokens_total[1d]) ) ``` @@ -177,8 +221,8 @@ sum by (principal_alias, token_type) ( ### 월간 토큰 합계 ```promql -# 월간 입력/출력 토큰 합계 (principal_alias별) -sum by (principal_alias, token_type) ( +# 최근 30일 token rollup. 달력 월은 Grafana time range를 월 단위로 잡고 $__range를 사용한다. +sum by (principal_ref, principal_alias, token_ref, model_group, endpoint, response_mode, token_type) ( increase(iop_openai_usage_tokens_total[30d]) ) ``` @@ -193,8 +237,115 @@ sum by (principal_alias, token_type) ( ### 일일/월간 rollup 활용 시나리오 - `principal_alias` 또는 `principal_ref`를 `by` clause에 추가해 사용자별 일일/월간 토큰 사용량을 산출한다. +- `token_ref`를 추가하면 같은 사용자 아래 앱/통합/용도별 사용량을 분해한다. - `token_type`을 함께 group by하면 input/output/reasoning/cached_input을 구분한다. -- `model_group`을 추가하면 모델별 비용 기반 추정 가능하다 (billing 산출은 이 문서 범위를 벗남). +- `model_group`, `endpoint`, `response_mode`를 추가하면 사용 위치와 응답 경로별 origin breakdown을 만든다. + +--- + +## Cloud Price Baseline + +가격은 런타임이 자동으로 가져오지 않는다. 운영자가 기준일과 기준 모델을 명시한 정적 baseline을 Grafana dashboard 변수, annotation, table panel, 또는 운영 문서에 둔다. + +최소 필드: + +| Field | 예시 | 설명 | +|-------|------|------| +| `cloud_provider` | `example-cloud` | 가격 기준 provider | +| `baseline_model` | `example-model` | 비교 기준 cloud model | +| `model_group` | `example-model` | IOP metric의 `model_group`과 매핑할 값 | +| `token_type` | `input`, `output`, `cached_input`, `reasoning` | 가격을 적용할 token type | +| `price_per_1m_tokens` | `1.00` | 1M tokens당 단가 | +| `currency` | `USD` | 통화 | +| `effective_date` | `2026-07-10` | 가격 기준일 | +| `source_note` | `operator baseline` | 운영자가 남기는 기준 설명 | + +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 | + +`reasoning` token은 provider가 별도 단가를 제공하면 별도 baseline을 둔다. 별도 단가가 없으면 운영자가 `output`과 같은 단가로 볼지, report에서 `reasoning`을 제외할지 baseline에 명시한다. + +--- + +## Cost Formula + +기본 산식: + +```text +cloud_equivalent_cost = (tokens / 1_000_000) * price_per_1m_tokens +daily_cloud_equivalent_cost = sum(cloud_equivalent_cost by day, principal/token/model/endpoint/token_type) +monthly_cloud_equivalent_cost = sum(cloud_equivalent_cost over selected month) +``` + +Prometheus metric 자체에는 price table이 없으므로 1차 표면에서는 다음 중 하나를 사용한다. + +- Grafana table transformation으로 token rollup 결과에 static price column을 붙이고 calculated field를 만든다. +- model/token_type별 panel query에 운영자가 관리하는 scalar price를 곱한다. +- Prometheus recording rule을 운영자가 별도 관리한다. 이 repo는 recording rule 파일을 만들지 않는다. + +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]) +) / 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]) +) / 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]) +) / 1000000 * 0.50 +``` + +여러 token type을 합산할 때는 token type별 cost query를 Grafana transformation으로 더한다. 단일 query에서 서로 다른 가격을 자동 join하려면 별도 recording rule 또는 external price table이 필요하다. + +--- + +## ROI / Avoided-Cost Summary + +이 MVP의 ROI는 billing-grade 정산이 아니라 cloud-equivalent avoided cost다. + +```text +cloud_equivalent_cost = cloud baseline으로 환산한 사용량 비용 +avoided_cost = IOP/local/provider에서 처리한 사용량의 cloud_equivalent_cost +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`다. + +Grafana table 구성 예: + +1. Query A: daily token rollup by `principal_alias`, `token_ref`, `model_group`, `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`. +5. Reduce: `cloud_equivalent_cost` 합계와 token 합계를 보여준다. + +--- + +## Grafana Report 절차 + +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을 확인한다. +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 위험을 판단한다. --- @@ -203,13 +354,17 @@ sum by (principal_alias, token_type) ( ### 후속 Milestone으로 남기는 항목 - 사용자별 daily/monthly token **warn** 또는 **reject** enforcement는 이 Milestone의 범위가 아니며 후속 Milestone으로 Planned된다. -- billing, price, ROI amount 산출은 별도 ledger/cost model 범위다. +- 실제 결제, chargeback, 조직별 비용 배부, full infra cost accounting은 후속 범위다. +- live cloud pricing API 연동 또는 최신 가격 자동 동기화는 후속 범위다. - request-level ledger / audit log는 Loki/queriable log축 후속 작업의 대상이다. ### 현재 문서가 제공하는 것 -- `principal_ref`, `token_ref`, `model_group`, `token_type` 라벨 기반 daily/monthly rollup PromQL -- 사용자별 사용량 조회를 통한 운영 판단 기준 +- `principal_ref`, `principal_alias`, `token_ref`, `model_group`, `endpoint`, `response_mode`, `token_type` 라벨 기반 daily/monthly rollup PromQL +- 같은 principal의 여러 token/app usage를 합산과 breakdown으로 함께 보는 기준 +- 운영자가 관리하는 cloud price baseline 필드 +- token type별 cloud-equivalent cost 산식과 query/report 예시 +- avoided-cost 수준의 ROI summary 기준 - provider reasoning report 누락 시 `reasoning_observed_total` / `reasoning_chars_total`로 보조 확인 이 rollup 기준은 후속 enforce Milestone에서 Prometheus query를 그대로 재사용할 수 있도록 설계되었다. @@ -230,4 +385,4 @@ sum by (principal_alias, token_type) ( ## Appendix: Grafana Scrape Target -Edge metrics는 보통 `localhost:19092` (local) 또는 해당 Edge metrics port에서 Prometheus scrape된다. Grafana data source에 Prometheus를 추가하고 위 PromQL을 테스트 패널에서 검증한다. \ No newline at end of file +Edge metrics는 보통 `localhost:19092` (local) 또는 해당 Edge metrics port에서 Prometheus scrape된다. Grafana data source에 Prometheus를 추가하고 위 PromQL을 테스트 패널에서 검증한다. diff --git a/packages/go/config/config_test.go b/packages/go/config/config_test.go index 201f390..040e4bb 100644 --- a/packages/go/config/config_test.go +++ b/packages/go/config/config_test.go @@ -289,6 +289,43 @@ openai: } } +func TestLoadEdge_OpenAIPrincipalTokensAllowMultipleTokensPerPrincipal(t *testing.T) { + dir := t.TempDir() + f := filepath.Join(dir, "edge.yaml") + yaml := ` +server: + listen: "0.0.0.0:9090" +openai: + enabled: true + principal_tokens: + - token_ref: "alice-app-a" + token_hash_sha256: "` + strings.Repeat("a1", 32) + `" + principal_ref: "user:alice" + principal_alias: "alice" + - token_ref: "alice-app-b" + token_hash_sha256: "` + strings.Repeat("b2", 32) + `" + principal_ref: "user:alice" + principal_alias: "alice" +` + if err := os.WriteFile(f, []byte(yaml), 0o600); err != nil { + t.Fatalf("write yaml: %v", err) + } + + cfg, err := config.LoadEdge(f) + if err != nil { + t.Fatalf("load: %v", err) + } + if len(cfg.OpenAI.PrincipalTokens) != 2 { + t.Fatalf("expected 2 principal_tokens, got %d", len(cfg.OpenAI.PrincipalTokens)) + } + if cfg.OpenAI.PrincipalTokens[0].PrincipalRef != cfg.OpenAI.PrincipalTokens[1].PrincipalRef { + t.Fatalf("expected both tokens to map to the same principal: %+v", cfg.OpenAI.PrincipalTokens) + } + if cfg.OpenAI.PrincipalTokens[0].TokenRef == cfg.OpenAI.PrincipalTokens[1].TokenRef { + t.Fatalf("token_ref must distinguish app/integration tokens: %+v", cfg.OpenAI.PrincipalTokens) + } +} + func TestLoadEdge_OpenAIPrincipalTokensRejectInvalid(t *testing.T) { validHash := strings.Repeat("a1", 32) for _, tc := range []struct {