From eab8bad73df684211ccf9fb2c67a2936ef1518f9 Mon Sep 17 00:00:00 2001 From: toki Date: Tue, 14 Jul 2026 14:43:57 +0900 Subject: [PATCH 1/2] feat: update dev-corp test configs and add openai reasoning usage estimation --- .../01_metric_core/CODE_REVIEW-local-G05.md | 147 ++++++++++ .../01_metric_core/PLAN-local-G05.md | 260 ++++++++++++++++++ .../CODE_REVIEW-cloud-G07.md | 162 +++++++++++ .../02+01_dev_corp_rollout/PLAN-cloud-G07.md | 255 +++++++++++++++++ agent-test/dev-corp/control-plane-smoke.md | 6 +- agent-test/dev-corp/edge-smoke.md | 4 +- agent-test/dev-corp/inventory.yaml | 4 +- agent-test/dev-corp/rules.md | 8 +- configs/prometheus/prometheus.dev-corp.yml | 2 +- docs/openai-usage-grafana.md | 2 +- 10 files changed, 837 insertions(+), 13 deletions(-) create mode 100644 agent-task/openai_reasoning_estimated_usage/01_metric_core/CODE_REVIEW-local-G05.md create mode 100644 agent-task/openai_reasoning_estimated_usage/01_metric_core/PLAN-local-G05.md create mode 100644 agent-task/openai_reasoning_estimated_usage/02+01_dev_corp_rollout/CODE_REVIEW-cloud-G07.md create mode 100644 agent-task/openai_reasoning_estimated_usage/02+01_dev_corp_rollout/PLAN-cloud-G07.md diff --git a/agent-task/openai_reasoning_estimated_usage/01_metric_core/CODE_REVIEW-local-G05.md b/agent-task/openai_reasoning_estimated_usage/01_metric_core/CODE_REVIEW-local-G05.md new file mode 100644 index 0000000..882991f --- /dev/null +++ b/agent-task/openai_reasoning_estimated_usage/01_metric_core/CODE_REVIEW-local-G05.md @@ -0,0 +1,147 @@ + + +# Code Review Reference - API + +> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.** +> The task is NOT complete until every implementation-owned section below is filled in. +> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving. +> Fill implementation-owned sections, then stop with active files in place and report ready for review. +> If implementation is blocked by a selected Milestone `구현 잠금 > 결정 필요` item, fill `사용자 리뷰 요청` with linked evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`. Environment/secret/service blockers, generic scope changes, repeated failures, and evidence gaps that a follow-up agent can close are normal follow-up issues, not user-review blockers by themselves. +> Do not ask the user directly, present choices in chat, or call `request_user_input` during implementation; record only Milestone lock decisions in `사용자 리뷰 요청` and stop for code-review. +> Finalization (`코드리뷰 결과`, log rename, `complete.log`, archive moves, `코드리뷰 전용 체크리스트`) is review-agent-only, even after compaction/resume. +> Follow the ownership table at the bottom of this file for which sections you own. + +## 개요 + +date=2026-07-14 +task=openai_reasoning_estimated_usage/01_metric_core, plan=0, tag=API + +## 이 파일을 읽는 리뷰 에이전트에게 + +> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다. + +각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요. +리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다. + +1. 판정을 append한다. +2. `CODE_REVIEW-local-G05.md` → `code_review_local_G05_0.log`, `PLAN-local-G05.md` → `plan_local_G05_0.log`로 아카이브한다. +3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/openai_reasoning_estimated_usage/01_metric_core/`로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다. +4. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다. + +--- + +## 구현 항목별 완료 여부 + +| 항목 | 완료 여부 | +|------|---------| +| [API-1] Reasoning estimated token metric 추가 | [ ] | +| [API-2] Usage metric regression tests 정리 | [ ] | +| [API-3] Grafana/spec 문서 갱신 | [ ] | + +## 구현 체크리스트 + +- [ ] `apps/edge/internal/openai/usage_metrics.go`에 provider 미보고 reasoning text를 별도 estimated-token metric으로 emit하는 로직을 추가한다. +- [ ] `apps/edge/internal/openai/usage_metrics_test.go`에 estimated metric 증가, provider-reported reasoning 우선, label allowlist regression을 추가/갱신한다. +- [ ] `docs/openai-usage-grafana.md`와 `agent-spec/input/openai-compatible-surface.md`에 reported reasoning token과 estimated reasoning token의 차이를 문서화한다. +- [ ] 최종 검증 명령을 모두 실행하고 예상 결과와 실제 출력 차이를 review stub에 기록한다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +## 코드리뷰 전용 체크리스트 + +> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다. +> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다. + +- [ ] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다. +- [ ] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다. +- [ ] active `CODE_REVIEW-*-G??.md`를 `code_review_local_G05_0.log`로 아카이브한다. +- [ ] active `PLAN-*-G??.md`를 `plan_local_G05_0.log`로 아카이브한다. +- [ ] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다. +- [ ] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다. +- [ ] PASS이면 active task 디렉터리 `agent-task/openai_reasoning_estimated_usage/01_metric_core/`를 `agent-task/archive/YYYY/MM/openai_reasoning_estimated_usage/01_metric_core/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다. +- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/openai_reasoning_estimated_usage/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다. +- [ ] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-local-G05.md`와 `CODE_REVIEW-local-G05.md`를 작성하고 `complete.log`를 작성하지 않는다. +- [ ] USER_REVIEW이면 `agent-ops/skills/common/code-review/templates/user-review-template.md` 기준으로 `USER_REVIEW.md`를 작성하고 active `PLAN-*.md`, `CODE_REVIEW-*.md`, `complete.log`를 남기지 않는다. + +## 계획 대비 변경 사항 + +_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._ + +## 주요 설계 결정 + +_구현 에이전트가 주요 설계 결정 사항을 기록한다._ + +## 사용자 리뷰 요청 + +_기본값은 `없음`이다. 구현 중 새 결정이 필요해 보여도 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 이 섹션은 선택된 Milestone `구현 잠금 > 결정 필요` 항목이 실구현을 차단할 때만 채운다. 외부 환경/secret/서비스 준비, 검증 증거 공백, 반복 실패, 일반 범위 조정은 사용자 리뷰 요청이 아니며 `검증 결과`, `계획 대비 변경 사항`, 또는 code-review의 일반 follow-up plan으로 처리한다._ + +- 상태: 없음 +- 사유 유형: 없음 +- 연결 대상: 없음 +- 결정 필요: 없음 +- 차단 근거: 없음 +- 실행한 검증/명령: 없음 +- 자동 후속 불가 이유: 없음 +- 재개 조건: 없음 + +## 리뷰어를 위한 체크포인트 + +- `iop_openai_usage_tokens_total{token_type="reasoning"}`가 provider-reported 전용으로 남아 있는지 확인한다. +- `iop_openai_reasoning_estimated_tokens_total`이 reasoning text 관측 + provider token 미보고 조건에서만 증가하는지 확인한다. +- estimated metric label에 raw token, prompt, request id, session id가 들어가지 않는지 확인한다. +- docs/spec가 estimated 값을 billing-grade 확정 토큰으로 표현하지 않는지 확인한다. + +## 검증 결과 + +_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._ + +필수 규칙: +- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다. +- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다. +- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다. +- 사용자 리뷰 요청으로 명령을 끝까지 실행하지 못했다면 `사용자 리뷰 요청`에 실행한 명령, 실제 출력, 미실행 명령의 사유를 기록한다. + +### API-1 중간 검증 +```bash +$ GOCACHE=/tmp/iop-go-cache go test -count=1 ./apps/edge/internal/openai -run 'UsageMetrics|Reasoning' +(output) +``` + +### API-2 중간 검증 +```bash +$ GOCACHE=/tmp/iop-go-cache go test -count=1 ./apps/edge/internal/openai -run 'OpenAI.*Usage|OpenAI.*Reasoning|ProviderTunnelPassthrough.*Usage|ResponsesProviderTunnelPassthrough.*Usage' +(output) +``` + +### API-3 중간 검증 +```bash +$ rg --sort path --fixed-strings "iop_openai_reasoning_estimated_tokens_total" apps/edge/internal/openai docs/openai-usage-grafana.md agent-spec/input/openai-compatible-surface.md +(output) +``` + +### 최종 검증 +```bash +$ GOCACHE=/tmp/iop-go-cache go test -count=1 ./apps/edge/internal/openai +(output) + +$ git diff --check +(output) +``` + +--- + +> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section: completion table, implementation checklist, changes from plan, design decisions, and verification output?** +> If anything is blank, go back and fill it in before saving this file. +> Leave review-agent-only sections unchanged. + +## Section Ownership + +| Section | Owner | Note | +|---------|-------|------| +| Header comment, 개요, 리뷰 에이전트 지시 | Fixed at stub creation | Implementing agent must not modify or execute finalization | +| 구현 항목별 완료 여부 | Implementing agent | Check only after implementation evidence exists | +| 구현 체크리스트 | Implementing agent | Check only; do not change item text | +| 코드리뷰 전용 체크리스트 | Review agent only | Implementing agent must not modify | +| 계획 대비 변경 사항, 주요 설계 결정 | Implementing agent | Replace placeholders with actual notes | +| 사용자 리뷰 요청 | Implementing agent | Keep 없음 unless selected Milestone lock blocks implementation | +| 리뷰어를 위한 체크포인트 | Fixed at stub creation | Review focus | +| 검증 결과 | Implementing agent | Paste actual stdout/stderr | diff --git a/agent-task/openai_reasoning_estimated_usage/01_metric_core/PLAN-local-G05.md b/agent-task/openai_reasoning_estimated_usage/01_metric_core/PLAN-local-G05.md new file mode 100644 index 0000000..5139309 --- /dev/null +++ b/agent-task/openai_reasoning_estimated_usage/01_metric_core/PLAN-local-G05.md @@ -0,0 +1,260 @@ + + +# Plan - API + +## 이 파일을 읽는 구현 에이전트에게 + +`CODE_REVIEW-local-G05.md`의 구현 에이전트 소유 섹션 작성까지가 구현의 마지막 단계다. 구현 후 검증 명령을 실행하고 실제 출력과 변경 내용을 review stub에 채운 뒤 active 파일을 그대로 둔 채 review ready로 보고한다. 선택된 Milestone `구현 잠금 > 결정 필요` 항목이 실구현을 막는 경우에만 review stub의 `사용자 리뷰 요청` 섹션을 근거와 함께 채우고 멈춘다. 직접 사용자에게 질문하거나, 선택지를 제시하거나, `USER_REVIEW.md`, `complete.log`, archive log를 만들지 않는다. 환경/secret/service blocker, 일반 범위 변경, 검증 증거 공백은 사용자 리뷰 요청이 아니라 검증 결과나 후속 plan 후보로 기록한다. + +## 배경 + +dev-corp Ornith 경로는 reasoning text는 관측되지만 provider가 `reasoning_tokens`를 보고하지 않는 경우가 있다. 현재 Edge는 이 경우 `iop_openai_reasoning_observed_total`과 `iop_openai_reasoning_chars_total`만 emit하고 token 추정치는 제공하지 않는다. 목표는 provider-reported `token_type="reasoning"`의 의미를 보존하면서, provider 미보고 케이스도 운영자가 추정 토큰으로 조회할 수 있게 하는 것이다. + +## 사용자 리뷰 요청 흐름 + +구현 중 직접 사용자에게 묻지 않는다. 선택된 Milestone lock 결정이 실제 구현을 차단하는 경우에만 `CODE_REVIEW-local-G05.md`의 `사용자 리뷰 요청` 섹션에 `agent-ops/skills/common/_templates/implementation-user-review-request-section.md` 형식으로 기록하고 멈춘다. 이 plan은 non-roadmap 독립 task이므로 일반 구현 판단은 plan 범위 안에서 처리한다. + +## 분석 결과 + +### 읽은 파일 + +- `agent-roadmap/current.md` +- `agent-test/dev-corp/rules.md` +- `agent-test/dev-corp/edge-smoke.md` +- `agent-test/dev-corp/platform-common-smoke.md` +- `agent-contract/outer/openai-compatible-api.md` +- `agent-spec/input/openai-compatible-surface.md` +- `docs/openai-usage-grafana.md` +- `apps/edge/internal/openai/usage_metrics.go` +- `apps/edge/internal/openai/usage_metrics_test.go` + +### SDD 기준 + +not applicable. 이 plan은 현재 Milestone 완료 체크를 주장하지 않는 non-roadmap 독립 task다. + +### 테스트 환경 규칙 + +- test_env: `dev-corp` +- `agent-test/dev-corp/rules.md`: present/read. dev-corp Edge는 public `iop.ai.kr`, OpenAI-compatible 기본 URL은 `https://digitalplatform.iop.ai.kr/v1`, Edge metrics는 `19105`를 사용한다. +- matched profiles: + - `agent-test/dev-corp/edge-smoke.md`: present/read. `apps/edge/internal/openai/**` 변경 시 `go test ./apps/edge/...` 또는 변경 패키지 테스트와 OpenAI-compatible 경계 확인이 필요하다. + - `agent-test/dev-corp/platform-common-smoke.md`: present/read. 이 plan은 proto/config를 변경하지 않으므로 platform-common 필수 검증은 적용하지 않는다. +- 이 core plan의 필수 검증은 local checkout 안에서 끝난다. dev-corp 원격 preflight, 배포, Prometheus live 확인은 sibling plan `openai_reasoning_estimated_usage/02+01_dev_corp_rollout`에서 수행한다. +- fallback verification source: `agent-spec/input/openai-compatible-surface.md`의 OpenAI usage 검증 명령과 기존 Go package tests. + +### 테스트 커버리지 공백 + +- 현재 `TestOpenAIReasoningObservedDoesNotEstimateTokens`는 reasoning text 미보고 케이스에서 token counter가 증가하지 않음을 검증한다. 신규 동작은 같은 케이스에서 별도 estimated metric이 증가해야 하므로 regression update가 필요하다. +- provider가 `reasoning_tokens`를 보고하는 경우 `token_type="reasoning"`만 증가하고 estimated metric이 증가하지 않는 guard가 없다. 새 test가 필요하다. +- metric label allowlist에 estimated metric의 `estimation_method`가 추가되면 secret/high-cardinality label 배제 검증을 확장해야 한다. + +### 심볼 참조 + +renamed/removed symbol: none. + +관련 참조: + +- `apps/edge/internal/openai/usage_metrics.go`: metric label/counter 정의, `usageObservation`, `emitUsageMetrics` +- `apps/edge/internal/openai/usage_metrics_test.go`: usage metric label, token type, reasoning observation tests +- `docs/openai-usage-grafana.md`: reasoning 보조 metric 문서와 PromQL +- `agent-spec/input/openai-compatible-surface.md`: usage metric surface 설명 + +### 분할 판단 + +split decision policy를 plan 파일 선택 전에 평가했다. 공유 task group은 `openai_reasoning_estimated_usage`다. + +- `01_metric_core`: source/test/docs/spec 변경. dependency 없음. +- `02+01_dev_corp_rollout`: dev-corp 배포/Prometheus/concurrency smoke. predecessor `01_metric_core`의 `complete.log`가 필요하다. + +코드 변경과 dev-corp rollout은 검증 환경과 실패 원인이 다르므로 분리한다. core plan은 local-G05로 안전하게 완료하고, runtime 반영은 cloud-G07 rollout plan에서 독립 검증한다. + +### 범위 결정 근거 + +- `iop_openai_usage_tokens_total{token_type="reasoning"}`는 provider-reported token 전용으로 유지한다. 추정값을 이 counter에 섞지 않는다. +- provider adapter, proto `Usage.reasoning_tokens`, Node tunnel parsing은 이미 provider-reported 값을 전달하므로 변경하지 않는다. +- Control Plane dashboard, billing/chargeback, request-level ledger, live cloud price sync는 제외한다. +- 현재 checkout에는 dev-corp metrics port 관련 dirty docs/test 변경이 있으므로 구현자는 같은 파일 수정 시 기존 변경을 보존한다. + +### 빌드 등급 + +lane=`local`, grade=`G05`. 변경은 Edge OpenAI usage metric 한 파일과 해당 unit tests/docs/spec에 한정되며 local Go tests로 대부분 검증된다. + +## 구현 체크리스트 + +- [ ] `apps/edge/internal/openai/usage_metrics.go`에 provider 미보고 reasoning text를 별도 estimated-token metric으로 emit하는 로직을 추가한다. +- [ ] `apps/edge/internal/openai/usage_metrics_test.go`에 estimated metric 증가, provider-reported reasoning 우선, label allowlist regression을 추가/갱신한다. +- [ ] `docs/openai-usage-grafana.md`와 `agent-spec/input/openai-compatible-surface.md`에 reported reasoning token과 estimated reasoning token의 차이를 문서화한다. +- [ ] 최종 검증 명령을 모두 실행하고 예상 결과와 실제 출력 차이를 review stub에 기록한다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +### [API-1] Reasoning estimated token metric 추가 + +#### 문제 + +`apps/edge/internal/openai/usage_metrics.go:79-82`의 chars metric help는 "Never converted to a token estimate"라고 고정되어 있다. `apps/edge/internal/openai/usage_metrics.go:85-96`도 `reasoningChars`를 보조 metric에만 쓰고, `apps/edge/internal/openai/usage_metrics.go:166-169`는 observed/chars만 증가시킨다. 따라서 provider가 token count를 주지 않는 think text는 token 단위로 조회할 수 없다. + +#### 해결 방법 + +`iop_openai_usage_tokens_total`은 provider-reported 전용으로 유지하고, 별도 counter를 추가한다. + +Before: + +```go +// apps/edge/internal/openai/usage_metrics.go:79 +openAIReasoningCharsTotal = promauto.NewCounterVec(prometheus.CounterOpts{ + Name: "iop_openai_reasoning_chars_total", + Help: "Observed reasoning text characters for requests without provider-reported reasoning tokens. Never converted to a token estimate.", +}, usageReasoningLabelNames) +``` + +After: + +```go +usageReasoningEstimateLabelNames = []string{ + "edge_id", "principal_ref", "principal_alias", "token_ref", + "model_group", "endpoint", "response_mode", "estimation_method", +} + +openAIReasoningEstimatedTokensTotal = promauto.NewCounterVec(prometheus.CounterOpts{ + Name: "iop_openai_reasoning_estimated_tokens_total", + Help: "Estimated reasoning tokens for requests with observed reasoning text but without provider-reported reasoning token usage.", +}, usageReasoningEstimateLabelNames) +``` + +`emitUsageMetrics`에서 `obs.reasoningChars > 0 && obs.reasoningTokens == 0`일 때만 `ceil(chars / 4)`를 `estimation_method="chars_div_4"`로 emit한다. `math` 표준 패키지를 쓰거나 integer helper `(chars + 3) / 4`를 사용한다. provider가 `reasoningTokens > 0`이면 estimated metric은 증가하지 않는다. + +#### 수정 파일 및 체크리스트 + +- [ ] `apps/edge/internal/openai/usage_metrics.go`: estimated metric label set과 counter 추가 +- [ ] `apps/edge/internal/openai/usage_metrics.go`: `estimatedReasoningTokens(chars int) int` helper 추가 +- [ ] `apps/edge/internal/openai/usage_metrics.go`: observed/chars emit path에서 estimated counter도 emit +- [ ] `apps/edge/internal/openai/usage_metrics.go`: comments/help text를 "estimated는 별도 metric" 기준으로 갱신 + +#### 테스트 작성 + +write tests. + +- `TestOpenAIReasoningObservedEstimatesTokens`: 5 chars reasoning text가 provider reasoning token 없이 오면 estimated delta가 `2`, observed delta가 `1`, chars delta가 `5`, `token_type="reasoning"` delta가 `0`. +- `TestOpenAIReasoningProviderReportedDoesNotEstimate`: provider가 `ReasoningTokens: 3`을 주고 reasoning text도 있으면 estimated delta가 `0`, `token_type="reasoning"` delta가 `3`. +- `TestOpenAIUsageMetricsLabelsExcludeSecretsAndHighCardinality`: `usageReasoningEstimateLabelNames`를 forbidden-label loop에 포함하고 `estimation_method`만 추가 허용. + +#### 중간 검증 + +```bash +GOCACHE=/tmp/iop-go-cache go test -count=1 ./apps/edge/internal/openai -run 'UsageMetrics|Reasoning' +``` + +Expected: PASS. Go test cache output is not acceptable here; use `-count=1`. + +### [API-2] Usage metric regression tests 정리 + +#### 문제 + +`apps/edge/internal/openai/usage_metrics_test.go:144-189`는 "DoesNotEstimateTokens"라는 기존 정책을 직접 고정한다. 새 정책은 provider-reported token counter에는 추정치를 섞지 않는 대신 별도 estimated counter를 제공해야 하므로 테스트 이름과 assertion이 현재 목표와 어긋난다. + +#### 해결 방법 + +기존 테스트를 rename/update하고, provider-reported 우선 테스트를 추가한다. 테스트 helper는 `requestTokenValue`처럼 estimated counter 전용 helper를 하나 둔다. + +Before: + +```go +// apps/edge/internal/openai/usage_metrics_test.go:180 +if got := requestTokenValue(t, labels, tokenTypeReasoning) - reasoningBefore; got != 0 { + t.Fatalf("reasoning token counter must not move when provider did not report reasoning tokens: got delta %v", got) +} +``` + +After: + +```go +if got := requestTokenValue(t, labels, tokenTypeReasoning) - reasoningBefore; got != 0 { + t.Fatalf("provider-reported reasoning token counter must not move for estimated reasoning: got delta %v", got) +} +if got := requestReasoningEstimateValue(t, labels, "chars_div_4") - estimatedBefore; got != 2 { + t.Fatalf("reasoning estimated tokens: got delta %v, want 2", got) +} +``` + +#### 수정 파일 및 체크리스트 + +- [ ] `apps/edge/internal/openai/usage_metrics_test.go`: estimated counter helper 추가 +- [ ] `apps/edge/internal/openai/usage_metrics_test.go`: reasoning observed test rename/update +- [ ] `apps/edge/internal/openai/usage_metrics_test.go`: provider-reported reasoning + text no-estimate test 추가 +- [ ] `apps/edge/internal/openai/usage_metrics_test.go`: label allowlist test에 estimated labels 포함 + +#### 테스트 작성 + +write tests as listed above. 기존 passthrough usage tests는 provider-reported `reasoning_tokens`가 기존 counter로 유지되는 regression으로 그대로 둔다. + +#### 중간 검증 + +```bash +GOCACHE=/tmp/iop-go-cache go test -count=1 ./apps/edge/internal/openai -run 'OpenAI.*Usage|OpenAI.*Reasoning|ProviderTunnelPassthrough.*Usage|ResponsesProviderTunnelPassthrough.*Usage' +``` + +Expected: PASS. Go test cache output is not acceptable here; use `-count=1`. + +### [API-3] Grafana/spec 문서 갱신 + +#### 문제 + +`docs/openai-usage-grafana.md:40-48`은 observed/chars metric만 설명하고, `docs/openai-usage-grafana.md:185`는 token 추정을 권유하지 않는다고 적는다. `agent-spec/input/openai-compatible-surface.md:66-68`, `agent-spec/input/openai-compatible-surface.md:124-128`, `agent-spec/input/openai-compatible-surface.md:155-156`도 reasoning text를 token으로 추정하지 않는 현 상태를 고정한다. + +#### 해결 방법 + +문서에서 provider-reported token과 estimated token을 분리해 설명한다. `iop_openai_reasoning_estimated_tokens_total`은 운영 관측용 추정값이며 billing-grade/chargeback 확정값이 아니라고 명시한다. + +Before: + +```markdown +Observed reasoning text characters for requests without provider-reported reasoning tokens. Never converted to a token estimate. +``` + +After: + +```markdown +Observed reasoning text characters for requests without provider-reported reasoning tokens. The Edge also emits `iop_openai_reasoning_estimated_tokens_total` as an explicit estimate; provider-reported `token_type="reasoning"` remains authoritative. +``` + +#### 수정 파일 및 체크리스트 + +- [ ] `docs/openai-usage-grafana.md`: Metric 목록에 `iop_openai_reasoning_estimated_tokens_total` 추가 +- [ ] `docs/openai-usage-grafana.md`: PromQL 예시와 dashboard 후보에 estimated reasoning token query 추가 +- [ ] `docs/openai-usage-grafana.md`: cloud-equivalent cost 섹션에서 estimated reasoning은 확정 정산값이 아니라고 명시 +- [ ] `agent-spec/input/openai-compatible-surface.md`: 기능 목록, 설정/데이터/이벤트, 한계 문구를 updated metric 기준으로 갱신 + +#### 테스트 작성 + +skip new executable tests. 문서/spec 변경은 final `rg --sort path --fixed-strings`로 신규 metric 명칭이 source/docs/spec에 모두 반영됐는지 확인한다. + +#### 중간 검증 + +```bash +rg --sort path --fixed-strings "iop_openai_reasoning_estimated_tokens_total" apps/edge/internal/openai docs/openai-usage-grafana.md agent-spec/input/openai-compatible-surface.md +``` + +Expected: source, test, docs, spec references are all present. + +## 수정 파일 요약 + +| 파일 | 항목 | +|------|------| +| `apps/edge/internal/openai/usage_metrics.go` | API-1 | +| `apps/edge/internal/openai/usage_metrics_test.go` | API-2 | +| `docs/openai-usage-grafana.md` | API-3 | +| `agent-spec/input/openai-compatible-surface.md` | API-3 | + +## 최종 검증 + +```bash +GOCACHE=/tmp/iop-go-cache go test -count=1 ./apps/edge/internal/openai -run 'UsageMetrics|Reasoning' +GOCACHE=/tmp/iop-go-cache go test -count=1 ./apps/edge/internal/openai -run 'OpenAI.*Usage|OpenAI.*Reasoning|ProviderTunnelPassthrough.*Usage|ResponsesProviderTunnelPassthrough.*Usage' +GOCACHE=/tmp/iop-go-cache go test -count=1 ./apps/edge/internal/openai +rg --sort path --fixed-strings "iop_openai_reasoning_estimated_tokens_total" apps/edge/internal/openai docs/openai-usage-grafana.md agent-spec/input/openai-compatible-surface.md +git diff --check +``` + +Expected: all commands PASS. Go test cache output is not acceptable for the Go commands; `-count=1` is required. + +모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다. diff --git a/agent-task/openai_reasoning_estimated_usage/02+01_dev_corp_rollout/CODE_REVIEW-cloud-G07.md b/agent-task/openai_reasoning_estimated_usage/02+01_dev_corp_rollout/CODE_REVIEW-cloud-G07.md new file mode 100644 index 0000000..2fe5174 --- /dev/null +++ b/agent-task/openai_reasoning_estimated_usage/02+01_dev_corp_rollout/CODE_REVIEW-cloud-G07.md @@ -0,0 +1,162 @@ + + +# Code Review Reference - API + +> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.** +> The task is NOT complete until every implementation-owned section below is filled in. +> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving. +> Fill implementation-owned sections, then stop with active files in place and report ready for review. +> If implementation is blocked by a selected Milestone `구현 잠금 > 결정 필요` item, fill `사용자 리뷰 요청` with linked evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`. Environment/secret/service blockers, generic scope changes, repeated failures, and evidence gaps that a follow-up agent can close are normal follow-up issues, not user-review blockers by themselves. +> Do not ask the user directly, present choices in chat, or call `request_user_input` during implementation; record only Milestone lock decisions in `사용자 리뷰 요청` and stop for code-review. +> Finalization (`코드리뷰 결과`, log rename, `complete.log`, archive moves, `코드리뷰 전용 체크리스트`) is review-agent-only, even after compaction/resume. +> Follow the ownership table at the bottom of this file for which sections you own. + +## 개요 + +date=2026-07-14 +task=openai_reasoning_estimated_usage/02+01_dev_corp_rollout, plan=0, tag=API + +## 이 파일을 읽는 리뷰 에이전트에게 + +> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다. + +각 항목의 구현을 실제 runtime evidence와 대조하고, `검증 결과` 섹션의 출력이 plan의 expected outcome과 일치하는지 확인하세요. +리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다. + +1. 판정을 append한다. +2. `CODE_REVIEW-cloud-G07.md` → `code_review_cloud_G07_0.log`, `PLAN-cloud-G07.md` → `plan_cloud_G07_0.log`로 아카이브한다. +3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/openai_reasoning_estimated_usage/02+01_dev_corp_rollout/`로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다. +4. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다. + +--- + +## 구현 항목별 완료 여부 + +| 항목 | 완료 여부 | +|------|---------| +| [API-1] dev-corp Edge binary rebuild and rollout | [ ] | +| [API-2] single-call reasoning estimated metric smoke | [ ] | +| [API-3] concurrent smoke and Prometheus accounting | [ ] | + +## 구현 체크리스트 + +- [ ] `openai_reasoning_estimated_usage/01_metric_core/complete.log` 존재를 확인하고 없으면 배포하지 않는다. +- [ ] dev-corp runner, public Edge, Prometheus, ports, binary/config path preflight를 실행하고 blocker를 기록한다. +- [ ] 구현 완료 source state를 mac-mini runner에 sync하고 `iop-edge` Linux ARM64 binary를 rebuild한다. +- [ ] public Edge host에 새 binary를 배포하고 Edge process를 restart한 뒤 `/healthz`, `/v1/models`, `/metrics`를 확인한다. +- [ ] smoke token으로 `ornith:35b` single-call을 실행하고 Prometheus에서 estimated reasoning metric 증가를 확인한다. +- [ ] 같은 token으로 6개 이상 concurrent `/v1/chat/completions`를 실행하고 request/estimated metric 증가를 확인한다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +## 코드리뷰 전용 체크리스트 + +> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다. +> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다. + +- [ ] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다. +- [ ] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다. +- [ ] active `CODE_REVIEW-*-G??.md`를 `code_review_cloud_G07_0.log`로 아카이브한다. +- [ ] active `PLAN-*-G??.md`를 `plan_cloud_G07_0.log`로 아카이브한다. +- [ ] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다. +- [ ] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다. +- [ ] PASS이면 active task 디렉터리 `agent-task/openai_reasoning_estimated_usage/02+01_dev_corp_rollout/`를 `agent-task/archive/YYYY/MM/openai_reasoning_estimated_usage/02+01_dev_corp_rollout/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다. +- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/openai_reasoning_estimated_usage/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다. +- [ ] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-cloud-G07.md`와 `CODE_REVIEW-cloud-G07.md`를 작성하고 `complete.log`를 작성하지 않는다. +- [ ] USER_REVIEW이면 `agent-ops/skills/common/code-review/templates/user-review-template.md` 기준으로 `USER_REVIEW.md`를 작성하고 active `PLAN-*.md`, `CODE_REVIEW-*.md`, `complete.log`를 남기지 않는다. + +## 계획 대비 변경 사항 + +_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._ + +## 주요 설계 결정 + +_구현 에이전트가 주요 설계 결정 사항을 기록한다._ + +## 사용자 리뷰 요청 + +_기본값은 `없음`이다. 구현 중 새 결정이 필요해 보여도 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 이 섹션은 선택된 Milestone `구현 잠금 > 결정 필요` 항목이 실구현을 차단할 때만 채운다. 외부 환경/secret/서비스 준비, 검증 증거 공백, 반복 실패, 일반 범위 조정은 사용자 리뷰 요청이 아니며 `검증 결과`, `계획 대비 변경 사항`, 또는 code-review의 일반 follow-up plan으로 처리한다._ + +- 상태: 없음 +- 사유 유형: 없음 +- 연결 대상: 없음 +- 결정 필요: 없음 +- 차단 근거: 없음 +- 실행한 검증/명령: 없음 +- 자동 후속 불가 이유: 없음 +- 재개 조건: 없음 + +## 리뷰어를 위한 체크포인트 + +- predecessor `01_metric_core/complete.log` 없이 배포가 진행되지 않았는지 확인한다. +- public Edge에 새 metric이 실제 `/metrics`로 노출되는지 확인한다. +- smoke token 원문이 review output에 남지 않았는지 확인한다. +- single-call과 6개 이상 concurrent call 모두 Prometheus evidence로 연결되는지 확인한다. +- provider-reported `reasoning` counter와 estimated counter를 혼동하지 않았는지 확인한다. + +## 검증 결과 + +_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._ + +필수 규칙: +- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다. +- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다. +- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다. +- token/secret 원문은 붙여 넣지 않는다. + +### API-1 중간 검증 +```bash +$ ssh toki@iop.ai.kr 'curl -fsS http://127.0.0.1:18086/healthz && curl -fsS http://127.0.0.1:19105/metrics | grep -F "iop_openai_reasoning_estimated_tokens_total"' +(output) +``` + +### API-2 중간 검증 +```bash +$ ssh fe@172.24.63.178 'curl -fsG http://127.0.0.1:19112/api/v1/query --data-urlencode "query=sum by (token_ref,estimation_method) (iop_openai_reasoning_estimated_tokens_total{token_ref=\"devcorp-smoke-usage\"})"' +(output) +``` + +### API-3 중간 검증 +```bash +$ test "$(ls /tmp/devcorp-reasoning-concurrent-*.json | wc -l | tr -d ' ')" -ge 6 +(output) +``` + +### 최종 검증 +```bash +$ test -f agent-task/openai_reasoning_estimated_usage/01_metric_core/complete.log +(output) + +$ ssh fe@172.24.63.178 'cd /Users/fe/agent-work/iop-dev-corp && git status --short --branch && git rev-parse HEAD && (/opt/homebrew/bin/go version || go version)' +(output) + +$ ssh fe@172.24.63.178 'curl -fsS http://127.0.0.1:19112/-/ready' +(output) + +$ ssh toki@iop.ai.kr 'cd /Users/toki/agent-work/iop-dev-corp && test -f build/dev-corp-runtime/edge.yaml && pgrep -af "iop-edge.*serve" && lsof -nP -iTCP:18086 -sTCP:LISTEN && lsof -nP -iTCP:19105 -sTCP:LISTEN' +(output) + +$ ssh fe@172.24.63.178 'curl -fsG http://127.0.0.1:19112/api/v1/query --data-urlencode "query=sum by (token_ref,status) (iop_openai_requests_total{token_ref=\"devcorp-smoke-usage\"})"' +(output) + +$ git diff --check -- agent-task/openai_reasoning_estimated_usage +(output) +``` + +--- + +> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section: completion table, implementation checklist, changes from plan, design decisions, and verification output?** +> If anything is blank, go back and fill it in before saving this file. +> Leave review-agent-only sections unchanged. + +## Section Ownership + +| Section | Owner | Note | +|---------|-------|------| +| Header comment, 개요, 리뷰 에이전트 지시 | Fixed at stub creation | Implementing agent must not modify or execute finalization | +| 구현 항목별 완료 여부 | Implementing agent | Check only after runtime evidence exists | +| 구현 체크리스트 | Implementing agent | Check only; do not change item text | +| 코드리뷰 전용 체크리스트 | Review agent only | Implementing agent must not modify | +| 계획 대비 변경 사항, 주요 설계 결정 | Implementing agent | Replace placeholders with actual notes | +| 사용자 리뷰 요청 | Implementing agent | Keep 없음 unless selected Milestone lock blocks implementation | +| 리뷰어를 위한 체크포인트 | Fixed at stub creation | Review focus | +| 검증 결과 | Implementing agent | Paste actual stdout/stderr with secrets masked | diff --git a/agent-task/openai_reasoning_estimated_usage/02+01_dev_corp_rollout/PLAN-cloud-G07.md b/agent-task/openai_reasoning_estimated_usage/02+01_dev_corp_rollout/PLAN-cloud-G07.md new file mode 100644 index 0000000..c8673f0 --- /dev/null +++ b/agent-task/openai_reasoning_estimated_usage/02+01_dev_corp_rollout/PLAN-cloud-G07.md @@ -0,0 +1,255 @@ + + +# Plan - API + +## 이 파일을 읽는 구현 에이전트에게 + +이 rollout plan은 `openai_reasoning_estimated_usage/01_metric_core`가 PASS되어 `complete.log`를 만든 뒤에만 구현한다. `CODE_REVIEW-cloud-G07.md`의 구현 에이전트 소유 섹션 작성까지가 구현의 마지막 단계다. 검증 출력에는 token/secret 원문을 남기지 않는다. 구현 후 active 파일을 그대로 둔 채 review ready로 보고하고, `USER_REVIEW.md`, `complete.log`, archive log는 만들지 않는다. + +## 배경 + +core plan이 estimated reasoning token metric을 추가해도 dev-corp public Edge에 배포되지 않으면 사용량 측정 상태가 운영에서 확인되지 않는다. 이 plan은 public `iop.ai.kr` Edge와 mac-mini Prometheus를 기준으로 새 metric이 실제 OpenAI-compatible 호출과 동시 호출에서 증가하는지 검증한다. + +## 사용자 리뷰 요청 흐름 + +구현 중 직접 사용자에게 묻지 않는다. 선택된 Milestone lock 결정이 실구현을 차단하는 경우에만 `CODE_REVIEW-cloud-G07.md`의 `사용자 리뷰 요청` 섹션에 기록한다. 이 plan은 non-roadmap 독립 rollout task이므로 SSH, secret, host readiness 문제는 사용자 리뷰가 아니라 검증 blocker 또는 follow-up으로 기록한다. + +## 분석 결과 + +### 읽은 파일 + +- `agent-roadmap/current.md` +- `agent-test/dev-corp/rules.md` +- `agent-test/dev-corp/edge-smoke.md` +- `agent-test/dev-corp/platform-common-smoke.md` +- `agent-contract/outer/openai-compatible-api.md` +- `agent-spec/input/openai-compatible-surface.md` +- `docs/openai-usage-grafana.md` +- `apps/edge/internal/openai/usage_metrics.go` +- `apps/edge/internal/openai/usage_metrics_test.go` + +### SDD 기준 + +not applicable. 이 plan은 현재 Milestone 기능 Task 완료 체크를 하지 않는 non-roadmap rollout task다. + +### 테스트 환경 규칙 + +- test_env: `dev-corp` +- `agent-test/dev-corp/rules.md`: present/read. public Edge runtime은 `iop.ai.kr`, OpenAI-compatible 기본 URL은 `https://digitalplatform.iop.ai.kr/v1`, Edge metrics는 `19105`, Prometheus는 mac-mini runner `19112`다. +- matched profiles: + - `agent-test/dev-corp/edge-smoke.md`: present/read. OpenAI-compatible 경계 변경은 `/healthz`, `/v1/models`, `/v1/chat/completions` 동등 smoke와 Edge service/node adapter 수렴 확인이 필요하다. + - `agent-test/dev-corp/platform-common-smoke.md`: present/read. 이 rollout은 proto/config 계약을 바꾸지 않으므로 platform-common command는 적용하지 않는다. + +#### 테스트 환경 프리플라이트 + +- runner: `ssh fe@172.24.63.178` +- runner repo root: `/Users/fe/agent-work/iop-dev-corp` +- public Edge host: `iop.ai.kr` +- public Edge runtime root: `/Users/toki/agent-work/iop-dev-corp` +- Edge config path: `/Users/toki/agent-work/iop-dev-corp/build/dev-corp-runtime/edge.yaml` +- Edge id: `dev-corp-edge` +- expected ports: OpenAI-compatible `18086`, Edge-Node `18087`, admin `19094`, metrics `19105`, Prometheus API `19112` on mac-mini +- source sync rule: if runner checkout is dirty/divergent/stale, sync the exact implemented source state before building. +- setup blocker: if `01_metric_core/complete.log` is missing, do not deploy. + +Preflight commands: + +```bash +test -f agent-task/openai_reasoning_estimated_usage/01_metric_core/complete.log +ssh fe@172.24.63.178 'cd /Users/fe/agent-work/iop-dev-corp && git status --short --branch && git rev-parse HEAD && (/opt/homebrew/bin/go version || go version)' +ssh fe@172.24.63.178 'curl -fsS http://127.0.0.1:19112/-/ready' +ssh toki@iop.ai.kr 'cd /Users/toki/agent-work/iop-dev-corp && test -f build/dev-corp-runtime/edge.yaml && pgrep -af "iop-edge.*serve" && lsof -nP -iTCP:18086 -sTCP:LISTEN && lsof -nP -iTCP:19105 -sTCP:LISTEN' +``` + +### 테스트 커버리지 공백 + +- Local unit tests prove metric emission in-process, but not that the public Edge binary was rebuilt and Prometheus scraped the new metric. +- Single-call smoke proves metric availability but not concurrent request accounting. +- Prometheus query can lag scrape interval; rollout verification must either wait for scrape or inspect direct Edge metrics plus Prometheus. + +### 심볼 참조 + +renamed/removed symbol: none. + +### 분할 판단 + +split decision policy를 평가했다. 이 plan은 `openai_reasoning_estimated_usage/02+01_dev_corp_rollout`이며 predecessor index `01`은 `openai_reasoning_estimated_usage/01_metric_core`다. 현재 plan 작성 시점에는 predecessor `complete.log`가 없으므로 구현 시작 전 `agent-task/openai_reasoning_estimated_usage/01_metric_core/complete.log` 존재를 확인해야 한다. + +### 범위 결정 근거 + +- source code 변경은 이 plan에서 하지 않는다. source 변경이 필요하면 `01_metric_core`로 되돌려 follow-up을 만든다. +- provider runtime tuning, capacity policy, Control Plane dashboard, Grafana dashboard JSON import는 제외한다. +- raw smoke token은 remote secret path에서 읽되 stdout/stderr, docs, task artifact에 기록하지 않는다. +- 기존 dev-corp metrics port 관련 dirty docs/test 변경은 보존한다. + +### 빌드 등급 + +lane=`cloud`, grade=`G07`. public Edge 배포, remote runner build, Prometheus scrape, 동시 호출 검증처럼 외부 runtime 상태가 핵심이며 local-only 검증으로 완료를 판단할 수 없다. + +## 의존 관계 및 구현 순서 + +1. `openai_reasoning_estimated_usage/01_metric_core/complete.log` 확인 +2. runner/public Edge preflight +3. source sync와 Edge binary rebuild +4. public Edge binary 교체 및 restart +5. single-call metric smoke +6. 6개 이상 concurrent smoke +7. Prometheus query evidence 기록 + +## 구현 체크리스트 + +- [ ] `openai_reasoning_estimated_usage/01_metric_core/complete.log` 존재를 확인하고 없으면 배포하지 않는다. +- [ ] dev-corp runner, public Edge, Prometheus, ports, binary/config path preflight를 실행하고 blocker를 기록한다. +- [ ] 구현 완료 source state를 mac-mini runner에 sync하고 `iop-edge` Linux ARM64 binary를 rebuild한다. +- [ ] public Edge host에 새 binary를 배포하고 Edge process를 restart한 뒤 `/healthz`, `/v1/models`, `/metrics`를 확인한다. +- [ ] smoke token으로 `ornith:35b` single-call을 실행하고 Prometheus에서 estimated reasoning metric 증가를 확인한다. +- [ ] 같은 token으로 6개 이상 concurrent `/v1/chat/completions`를 실행하고 request/estimated metric 증가를 확인한다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +### [API-1] dev-corp Edge binary rebuild and rollout + +#### 문제 + +core plan의 local tests가 PASS해도 public `iop.ai.kr` Edge binary가 갱신되지 않으면 dev-corp에서는 새 metric이 보이지 않는다. `agent-test/dev-corp/edge-smoke.md:36-44`와 `agent-test/dev-corp/edge-smoke.md:130-135`는 OpenAI-compatible 경계 변경 시 public Edge 기준 검증을 요구한다. + +#### 해결 방법 + +preflight로 runner/public Edge 상태를 확인하고, 구현 완료 source state를 runner에 sync한 뒤 Linux ARM64 Edge binary를 rebuild해 public Edge runtime에 배포한다. restart 후 direct health와 metrics endpoint에서 새 metric name이 노출되는지 확인한다. + +Source sync/build command 예시: + +```bash +rsync -a --delete --exclude .git --exclude build/dev-corp-runtime --exclude agent-task/archive ./ fe@172.24.63.178:/Users/fe/agent-work/iop-dev-corp/ +ssh fe@172.24.63.178 'cd /Users/fe/agent-work/iop-dev-corp && GOCACHE=/tmp/iop-go-cache /opt/homebrew/bin/go test -count=1 ./apps/edge/internal/openai && GOOS=linux GOARCH=arm64 CGO_ENABLED=0 GOCACHE=/tmp/iop-go-cache /opt/homebrew/bin/go build -o build/dev-corp-runtime/bin/iop-edge ./apps/edge/cmd/edge' +scp fe@172.24.63.178:/Users/fe/agent-work/iop-dev-corp/build/dev-corp-runtime/bin/iop-edge /tmp/iop-edge.dev-corp +scp /tmp/iop-edge.dev-corp toki@iop.ai.kr:/Users/toki/agent-work/iop-dev-corp/build/dev-corp-runtime/bin/iop-edge.new +ssh toki@iop.ai.kr 'cd /Users/toki/agent-work/iop-dev-corp && mv build/dev-corp-runtime/bin/iop-edge.new build/dev-corp-runtime/bin/iop-edge && pkill -f "iop-edge.*serve" && nohup build/dev-corp-runtime/bin/iop-edge serve --config build/dev-corp-runtime/edge.yaml > build/dev-corp-runtime/logs/iop-edge-rollout-$(date +%Y%m%d%H%M%S).log 2>&1 &' +``` + +If the existing process manager differs, record the replacement restart command in `계획 대비 변경 사항` with `pgrep -af` evidence. + +#### 수정 파일 및 체크리스트 + +- [ ] no source file edits in this plan +- [ ] runner source sync performed from completed core source state +- [ ] runner `iop-edge` rebuilt for `GOOS=linux GOARCH=arm64` +- [ ] public Edge binary replaced atomically with `.new` then `mv` +- [ ] public Edge restarted and listening ports verified + +#### 테스트 작성 + +skip new source tests. This is a rollout-only plan; local source tests are inherited from `01_metric_core` and rerun before build. + +#### 중간 검증 + +```bash +ssh toki@iop.ai.kr 'curl -fsS http://127.0.0.1:18086/healthz && curl -fsS http://127.0.0.1:19105/metrics | grep -F "iop_openai_reasoning_estimated_tokens_total"' +``` + +Expected: healthz succeeds and metrics output contains the new metric name. + +### [API-2] single-call reasoning estimated metric smoke + +#### 문제 + +Metric name exposure alone does not prove runtime request accounting. A real OpenAI-compatible request must produce reasoning text without provider-reported reasoning token count and increase the estimated metric for the smoke principal/token. + +#### 해결 방법 + +Read the smoke token from the existing remote secret path without echoing it. Call `ornith:35b` through public OpenAI-compatible URL and query Prometheus on the mac-mini runner for the smoke `token_ref`. + +Commands: + +```bash +SMOKE_TOKEN="$(ssh toki@iop.ai.kr 'cat /Users/toki/agent-work/iop-dev-corp/build/dev-corp-runtime/.secrets/openai_usage_metering_smoke_token')" +curl -fsS https://digitalplatform.iop.ai.kr/v1/models -H "Authorization: Bearer ${SMOKE_TOKEN}" >/tmp/devcorp-models.json +curl -fsS https://digitalplatform.iop.ai.kr/v1/chat/completions \ + -H "Authorization: Bearer ${SMOKE_TOKEN}" \ + -H "Content-Type: application/json" \ + -d '{"model":"ornith:35b","messages":[{"role":"user","content":"짧게 생각 과정을 내부적으로 사용해서 2+2 답만 말해."}],"max_tokens":64,"include_reasoning":true}' \ + >/tmp/devcorp-reasoning-single.json +ssh fe@172.24.63.178 'curl -fsG http://127.0.0.1:19112/api/v1/query --data-urlencode "query=sum by (token_ref,estimation_method) (iop_openai_reasoning_estimated_tokens_total{token_ref=\"devcorp-smoke-usage\"})"' +``` + +Do not paste `${SMOKE_TOKEN}` value into review output. + +#### 수정 파일 및 체크리스트 + +- [ ] `/v1/models` with smoke token returns 200 +- [ ] single `/v1/chat/completions` returns 200 +- [ ] Prometheus query shows `token_ref="devcorp-smoke-usage"` and positive estimated token value +- [ ] `iop_openai_usage_tokens_total{token_type="reasoning"}` remains absent or unchanged when provider did not report reasoning tokens + +#### 테스트 작성 + +skip source tests. This item is live smoke verification. + +#### 중간 검증 + +```bash +ssh fe@172.24.63.178 'curl -fsG http://127.0.0.1:19112/api/v1/query --data-urlencode "query=sum by (token_ref,estimation_method) (iop_openai_reasoning_estimated_tokens_total{token_ref=\"devcorp-smoke-usage\"})"' +``` + +Expected: query succeeds and returns a positive value after scrape. + +### [API-3] concurrent smoke and Prometheus accounting + +#### 문제 + +Single-call smoke does not prove concurrent accounting. The original user flow requires usage measurement under real runtime calls, and earlier dev-corp smokes used concurrent requests to catch routing/accounting misses. + +#### 해결 방법 + +Run at least 6 concurrent requests with the same smoke token. Then query requests and estimated reasoning metric deltas by `token_ref`. If Prometheus scrape lags, wait one scrape interval and rerun only the query. + +Commands: + +```bash +SMOKE_TOKEN="$(ssh toki@iop.ai.kr 'cat /Users/toki/agent-work/iop-dev-corp/build/dev-corp-runtime/.secrets/openai_usage_metering_smoke_token')" +rm -f /tmp/devcorp-reasoning-concurrent-*.json +seq 1 6 | xargs -P6 -I{} sh -c 'curl -fsS https://digitalplatform.iop.ai.kr/v1/chat/completions -H "Authorization: Bearer '"${SMOKE_TOKEN}"'" -H "Content-Type: application/json" -d "{\"model\":\"ornith:35b\",\"messages\":[{\"role\":\"user\",\"content\":\"요청 {}: 한 문장으로 답해.\"}],\"max_tokens\":64,\"include_reasoning\":true}" > /tmp/devcorp-reasoning-concurrent-{}.json' +ssh fe@172.24.63.178 'curl -fsG http://127.0.0.1:19112/api/v1/query --data-urlencode "query=sum by (token_ref,status) (iop_openai_requests_total{token_ref=\"devcorp-smoke-usage\"})"' +ssh fe@172.24.63.178 'curl -fsG http://127.0.0.1:19112/api/v1/query --data-urlencode "query=sum by (token_ref,estimation_method) (iop_openai_reasoning_estimated_tokens_total{token_ref=\"devcorp-smoke-usage\"})"' +``` + +#### 수정 파일 및 체크리스트 + +- [ ] 6 concurrent requests all return HTTP 200 +- [ ] `iop_openai_requests_total{token_ref="devcorp-smoke-usage",status="success"}` increases by at least 6 from the pre-concurrency snapshot +- [ ] `iop_openai_reasoning_estimated_tokens_total{token_ref="devcorp-smoke-usage"}` increases from the pre-concurrency snapshot +- [ ] no raw token appears in terminal output copied into review stub + +#### 테스트 작성 + +skip source tests. This item is live concurrency verification. + +#### 중간 검증 + +```bash +test "$(ls /tmp/devcorp-reasoning-concurrent-*.json | wc -l | tr -d ' ')" -ge 6 +``` + +Expected: at least 6 response files exist and each curl command succeeded. + +## 수정 파일 요약 + +| 파일 | 항목 | +|------|------| +| none | API-1, API-2, API-3 | + +## 최종 검증 + +```bash +test -f agent-task/openai_reasoning_estimated_usage/01_metric_core/complete.log +ssh fe@172.24.63.178 'cd /Users/fe/agent-work/iop-dev-corp && git status --short --branch && git rev-parse HEAD && (/opt/homebrew/bin/go version || go version)' +ssh fe@172.24.63.178 'curl -fsS http://127.0.0.1:19112/-/ready' +ssh toki@iop.ai.kr 'cd /Users/toki/agent-work/iop-dev-corp && test -f build/dev-corp-runtime/edge.yaml && pgrep -af "iop-edge.*serve" && lsof -nP -iTCP:18086 -sTCP:LISTEN && lsof -nP -iTCP:19105 -sTCP:LISTEN' +ssh toki@iop.ai.kr 'curl -fsS http://127.0.0.1:18086/healthz && curl -fsS http://127.0.0.1:19105/metrics | grep -F "iop_openai_reasoning_estimated_tokens_total"' +ssh fe@172.24.63.178 'curl -fsG http://127.0.0.1:19112/api/v1/query --data-urlencode "query=sum by (token_ref,estimation_method) (iop_openai_reasoning_estimated_tokens_total{token_ref=\"devcorp-smoke-usage\"})"' +ssh fe@172.24.63.178 'curl -fsG http://127.0.0.1:19112/api/v1/query --data-urlencode "query=sum by (token_ref,status) (iop_openai_requests_total{token_ref=\"devcorp-smoke-usage\"})"' +git diff --check -- agent-task/openai_reasoning_estimated_usage +``` + +Expected: predecessor complete.log exists, preflight succeeds, new metric is exposed, Prometheus returns positive estimated reasoning token values for smoke token, request success count includes concurrent calls, and task artifact diff has no whitespace errors. + +모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다. diff --git a/agent-test/dev-corp/control-plane-smoke.md b/agent-test/dev-corp/control-plane-smoke.md index e20edd0..fc08f3c 100644 --- a/agent-test/dev-corp/control-plane-smoke.md +++ b/agent-test/dev-corp/control-plane-smoke.md @@ -3,7 +3,7 @@ test_env: dev-corp test_profile: control-plane-smoke domain: control-plane verification_type: smoke -last_rule_updated_at: 2026-07-02 +last_rule_updated_at: 2026-07-14 --- # control-plane-smoke dev-corp 테스트 @@ -75,7 +75,7 @@ last_rule_updated_at: 2026-07-02 - mac-mini runner에서 compose project `iop-dev-corp-agent`로 `postgres`, `redis`, `control-plane`, `prometheus`, `grafana`가 기동됐다. - Control Plane은 host `18002/19004/19005`를 container `18000/19080/19081`로 publish하고, `/healthz`, `/readyz`, Client WebSocket upgrade가 통과했다. -- Prometheus는 `configs/prometheus/prometheus.dev-corp.yml`을 사용하며 `control-plane:9093`, `host.docker.internal:19102`, `prometheus:9090` target이 `up`이다. +- Prometheus는 `configs/prometheus/prometheus.dev-corp.yml`을 사용하며 `control-plane:9093`, `iop.ai.kr:19105`, `prometheus:9090` target이 `up`이다. - Grafana health는 runner-local `127.0.0.1:19122/api/health`에서 통과했다. - `web` service는 runner Flutter가 Dart `3.10.0`이고 앱 요구 조건이 `^3.11.3`인 점, `agent-shell`/`nexo` sibling path dependency가 없는 점 때문에 별도 차단 상태다. - 외부 public `13002`, `18002`, `19004`는 연결 거부 상태로 유지한다. @@ -83,7 +83,7 @@ last_rule_updated_at: 2026-07-02 ## 판정 기준 - control-plane service가 dev-corp stack으로 기동되고 health endpoint가 성공 응답을 반환한다. -- Prometheus와 Grafana가 같은 dev-corp compose project에서 올라오며, Prometheus는 dev-corp 설정에서 `control-plane:9093`과 native Edge metrics `host.docker.internal:19102`를 scrape한다. +- Prometheus와 Grafana가 같은 dev-corp compose project에서 올라오며, Prometheus는 dev-corp 설정에서 `control-plane:9093`과 native Edge metrics `iop.ai.kr:19105`를 scrape한다. - wire endpoint는 Control Plane 브라우저/앱 경계로 유지되고 Node 직접 연결/스케줄링 경로로 확장되지 않는다. - dev-corp stack은 local/test/dev stack의 host publish 포트를 점유하지 않는다. diff --git a/agent-test/dev-corp/edge-smoke.md b/agent-test/dev-corp/edge-smoke.md index 740f40e..967856b 100644 --- a/agent-test/dev-corp/edge-smoke.md +++ b/agent-test/dev-corp/edge-smoke.md @@ -3,7 +3,7 @@ test_env: dev-corp test_profile: edge-smoke domain: edge verification_type: smoke -last_rule_updated_at: 2026-07-13 +last_rule_updated_at: 2026-07-14 --- # edge-smoke dev-corp 테스트 @@ -35,7 +35,7 @@ last_rule_updated_at: 2026-07-13 - host: local checkout. provider pool과 source/build evidence가 필요하면 mac-mini `ssh fe@172.24.63.178`의 `/Users/fe/agent-work/iop-dev-corp` checkout을 사용한다. - Edge runtime evidence: public `iop.ai.kr` host를 기준으로 한다. -- port: compose Edge-Node TCP transport `19006`; native provider-pool Edge-Node TCP 후보 `18087`; artifact/bootstrap HTTP 후보 `18085`; Edge OpenAI-compatible HTTP 후보 `18086`; Edge metrics 후보 `19102`; admin 후보 `19094`. +- port: compose Edge-Node TCP transport `19006`; native provider-pool Edge-Node TCP 후보 `18087`; artifact/bootstrap HTTP 후보 `18085`; Edge OpenAI-compatible HTTP 후보 `18086`; Edge metrics 후보 `19105`; admin 후보 `19094`. - runtime: Go `1.24` - package manager: Go modules / Makefile - docker: unit/smoke quick check는 Docker를 요구하지 않는다. compose dev-corp 검증은 mac-mini에서 `.env.dev-corp.example` 기준으로 수행한다. diff --git a/agent-test/dev-corp/inventory.yaml b/agent-test/dev-corp/inventory.yaml index d5da82c..5c65323 100644 --- a/agent-test/dev-corp/inventory.yaml +++ b/agent-test/dev-corp/inventory.yaml @@ -1,6 +1,6 @@ test_env: dev-corp profile: dev-corp-provider-pool -last_updated_at: "2026-07-13" +last_updated_at: "2026-07-14" source: remote_runner: @@ -40,7 +40,7 @@ compose: edge_node_tcp_compose: 19006 postgres: 15402 redis: 16302 - edge_metrics: 19102 + edge_metrics: 19105 control_plane_metrics: 19104 prometheus: 19112 grafana: 19122 diff --git a/agent-test/dev-corp/rules.md b/agent-test/dev-corp/rules.md index 5e35cd2..6be2e8c 100644 --- a/agent-test/dev-corp/rules.md +++ b/agent-test/dev-corp/rules.md @@ -1,6 +1,6 @@ --- test_env: dev-corp -last_rule_updated_at: 2026-07-13 +last_rule_updated_at: 2026-07-14 --- # dev-corp 테스트 규칙 @@ -36,7 +36,7 @@ last_rule_updated_at: 2026-07-13 - compose identity: `COMPOSE_PROJECT_NAME=iop-dev-corp-agent`, `IOP_COMPOSE_NETWORK=iop-dev-corp-agent-net`, `IOP_COMPOSE_SUBNET=10.89.2.0/24`. - port: web/dev preview `13002`, Control Plane HTTP `18002`, Client WS `19004`, CP-Edge wire `19005`, compose Edge-Node TCP `19006`, native Edge-Node TCP 후보 `18087`, Edge admin 후보 `19094`, Control Plane metrics `19104`, Prometheus `19112`, Grafana `19122`. - operator notice: 기존 dev-corp 사용자-facing 접속점(`13002`, `18002`, Edge OpenAI-compatible 후보 `18086`, native Edge 후보 `18087`)은 변경하지 않는다. 2026-07-13 기준 mac-mini runner에는 compose Control Plane/observability 그룹이 배포되어 있으나 public `13002`/`18002`/`19004`는 외부에서 닫힌 상태로 유지한다. -- optional field ports: artifact/bootstrap HTTP `18085`, Edge OpenAI-compatible HTTP `18086`, Edge metrics `19102`. +- optional field ports: artifact/bootstrap HTTP `18085`, Edge OpenAI-compatible HTTP `18086`, Edge metrics `19105`. - runtime: Go quick check는 local toolchain을 우선한다. Docker/Flutter/client/field/bootstrap/provider-pool evidence는 mac-mini와 내부 provider nodes를 사용한다. - package manager: Go modules / Makefile / Flutter pub - docker: 현재 작업 컨테이너에서는 Docker-in-Docker를 사용하지 않는다. Docker compose 검증은 mac-mini checkout에서 수행한다. @@ -55,7 +55,7 @@ last_rule_updated_at: 2026-07-13 | Edge-Node TCP | `19090` | `19003` | `19006` | | Edge artifact/bootstrap | `18080` | `18082` | `18085` | | Edge OpenAI-compatible | `18081` | `18083` | `18086` | -| Edge metrics | `19092` | `19101` | `19102` | +| Edge metrics | `19092` | `19101` | `19105` | | Control Plane metrics | `19100` | `19103` | `19104` | | Prometheus UI/API | `19110` | `19111` | `19112` | | Grafana UI | `19120` | `19121` | `19122` | @@ -71,7 +71,7 @@ dev-corp native/provider-pool profile은 compose Edge-Node TCP `19006`이 아니 - public Edge evidence: Edge runtime evidence와 OpenAI-compatible smoke는 public `iop.ai.kr` 경로로 판정한다. - compose dev-corp stack: `.env.dev-corp.example`, `COMPOSE_PROJECT_NAME=iop-dev-corp-agent`, `IOP_COMPOSE_NETWORK=iop-dev-corp-agent-net`, `IOP_COMPOSE_SUBNET=10.89.2.0/24`, Edge-Node TCP `19006`, Control Plane metrics `19104`, Prometheus `19112`, Grafana `19122`를 사용한다. - compose observability: dev-corp Control Plane 관측 그룹은 mac-mini runner의 compose project `iop-dev-corp-agent` 기준으로 검증한다. Prometheus는 dev-corp 전용 설정 `configs/prometheus/prometheus.dev-corp.yml`로 compose Control Plane metrics와 native Edge metrics를 함께 scrape한다. -- Edge direct dev-corp profile: artifact/bootstrap `18085`, OpenAI-compatible `18086`, metrics `19102`, admin `19094`를 사용한다. +- Edge direct dev-corp profile: artifact/bootstrap `18085`, OpenAI-compatible `18086`, metrics `19105`, admin `19094`를 사용한다. - dev-corp provider pool: 기본 public Edge-Node TCP `iop.ai.kr:18087`을 기준으로 한다. `/Users/fe/agent-work/iop-dev-corp/build/dev-corp-runtime/edge.yaml`은 mac-mini runner의 보조 산출물일 뿐 dev-corp Edge source of truth로 판정하지 않는다. retired reverse tunnel route는 Edge addr로 사용하지 않는다. 3-node/provider 세부는 `agent-test/dev-corp/edge-smoke.md`와 `agent-test/dev-corp/node-smoke.md`를 따른다. - current native Control Plane: 2026-07-02 기준 provider-pool runtime에서 `build/dev-corp-runtime/bin/control-plane`이 `18002/19004/19005`를 listen하고, Edge id `dev-corp-edge`가 `127.0.0.1:19005`로 연결된다. provider snapshot status 기준은 `http://127.0.0.1:18002/edges/dev-corp-edge/status`다. - external provider field: DGX Spark 01/02와 Mac Studio는 mac-mini에서 내부 SSH로 접속한다. 직접 접속 경로와 provider endpoint는 `agent-test/dev-corp/inventory.yaml`을 따른다. diff --git a/configs/prometheus/prometheus.dev-corp.yml b/configs/prometheus/prometheus.dev-corp.yml index 8925a54..36d012f 100644 --- a/configs/prometheus/prometheus.dev-corp.yml +++ b/configs/prometheus/prometheus.dev-corp.yml @@ -20,7 +20,7 @@ scrape_configs: metrics_path: /metrics static_configs: - targets: - - host.docker.internal:19102 + - iop.ai.kr:19105 labels: component: edge runtime: native-dev-corp diff --git a/docs/openai-usage-grafana.md b/docs/openai-usage-grafana.md index 47161ec..0362b6f 100644 --- a/docs/openai-usage-grafana.md +++ b/docs/openai-usage-grafana.md @@ -393,6 +393,6 @@ Host publish 기본 포트: - local/test: Prometheus `19110`, Grafana `19120`, Control Plane metrics `19100`, Edge metrics `19092` - dev: Prometheus `19111`, Grafana `19121`, Control Plane metrics `19103`, Edge metrics `19101` -- dev-corp compose: Prometheus `19112`, Grafana `19122`, Control Plane metrics `19104`, Edge metrics `19102` +- dev-corp compose: Prometheus `19112`, Grafana `19122`, Control Plane metrics `19104`, Edge metrics `19105` 이 관측 스택은 기존 테스트 사용자-facing endpoint를 변경하지 않는다. dev 기준 기존 UI/Control Plane/Edge OpenAI-compatible 접속점은 유지하며, Grafana/Prometheus/metrics 포트만 운영자용으로 추가한다. dev Grafana/Prometheus는 기본적으로 원격 runner 내부 또는 SSH 터널 접근 기준이다. dev-corp는 현재 접근 가능한 runner가 없을 때 문서 정의만 갱신하고 실제 compose 세팅은 별도 세션에서 수행한다. From f8978dbf703c5c8f6f29ab074de860420469685c Mon Sep 17 00:00:00 2001 From: toki Date: Tue, 14 Jul 2026 18:12:01 +0900 Subject: [PATCH 2/2] docs: add repetition guard milestone detail --- ...ai-compatible-output-validation-filters.md | 15 ++++--- .../SDD.md | 39 ++++++++++++------- 2 files changed, 34 insertions(+), 20 deletions(-) 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 8601963..a514e29 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 @@ -7,8 +7,8 @@ ## 목표 -OpenAI-compatible Chat Completions provider 경로에서 모델 출력 이상을 요청 단위 필터로 감지하고, 사용자 경험을 해치지 않는 방식으로 중단/재시도/검증한다. -반복 출력 루프는 streaming passthrough를 유지한 채 upstream만 교체해 continuation repair로 이어 쓰고, `metadata.scheme` JSON 출력 계약은 검증 전 downstream content streaming을 막는 `contract_schema` 경로로 처리한다. +OpenAI-compatible Chat Completions provider 경로에서 모델 출력/행동 이상을 요청 및 짧은 session 단위 필터로 감지하고, 사용자 경험을 해치지 않는 방식으로 중단/재시도/검증한다. +반복 루프는 content streaming 반복과 동일 tool/action 반복을 모두 포함한다. content 반복은 streaming passthrough를 유지한 채 upstream만 교체해 continuation repair로 이어 쓰고, tool/action 반복은 `tool name + normalized args` fingerprint와 동일/no-progress 결과를 기준으로 감지해 side-effect 안전성이 없으면 repair 대신 안전 중단한다. `metadata.scheme` JSON 출력 계약은 검증 전 downstream content streaming을 막는 `contract_schema` 경로로 처리한다. ## 상태 @@ -35,7 +35,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 제한 +- 반복 출력 루프 감지용 single-stream rolling inspector, cross-request tool/action fingerprint inspector, tool call delta hold/release 판정, upstream abort, continuation repair, 1회 repair 제한, side-effect 구간 안전 중단 - `metadata.scheme` JSON schema 계약 수신, 마지막 user message prompt append, buffered validation, schema 위반 시 bounded retry - `passthrough`, `passthrough_guarded`, `contract_schema` 내부 response path 구분과 실행 로그/관측 기준. 이 이름들은 caller가 지정하는 공개 request field가 아니라 IOP 내부 경로/로그 기준이다. - normalized 실행 경로와 CLI adapter 경로를 OpenAI-compatible provider 출력 검증 경로와 분리하는 책임 경계 @@ -48,10 +48,10 @@ 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 구간 차단을 확인한다. +- [ ] [filter-policy] 각 출력 검증 filter는 공통 interface/registry를 통해 enable/disable 정책을 평가하고, environment(`dev`, `dev-corp`)와 model group/model/provider별로 반복루프 guard, schema contract 같은 기능을 독립적으로 켜고 끌 수 있다. 검증: qwen/gemma/ornith fixture 기반 config/handler tests에서 필터별 활성/비활성, 정책 우선순위, disabled filter observation이 통과한다. +- [ ] [repeat-guard] content 반복은 단일 provider stream의 rolling window로 감지하고, tool/action 반복은 incoming `messages`의 이전 assistant tool call 및 tool result history와 현재 provider tool call delta를 결합해 감지한다. content 반복은 downstream SSE를 닫지 않고 upstream provider request만 abort한 뒤 emitted safe prefix와 bad tail summary로 continuation repair 요청을 이어 붙인다. tool/action 반복은 완성 전 tool call delta를 짧게 hold해 full call fingerprint를 만든 뒤 release/block을 결정하고, `tool name + normalized args` fingerprint, 짧은 TTL/session state, 동일/no-progress tool result를 함께 본다. 이미 downstream으로 tool call이 나갔거나 side effect 가능성이 있으면 자동 repair하지 않고 guard terminal error 또는 안전 중단으로 끝낸다. 검증: 반복 chunk fixture와 반복 action fixture가 1회 repair, safe prefix 보존, `[DONE]` 단일 종료, tool call delta hold/release, side-effect 구간 차단, `dev-corp-iop-ornith`/`ornith:35b` 유형의 동일 toolUse 루프 감지를 확인한다. - [ ] [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 축과 함께 관찰된다. +- [ ] [ops-evidence] 출력 필터 결과가 요청 실행 로그와 smoke에서 원인 축을 구분할 수 있게 남는다. 검증: dev-corp Pi TUI smoke와 Ornith 반복 toolUse fixture에서 반복루프 중단/재요청/안전 중단 또는 schema validation 결과가 model/provider/IOP/CLI 축과 함께 관찰된다. ## 완료 리뷰 @@ -78,6 +78,9 @@ OpenAI-compatible provider 응답을 사용자에게 노출하기 전에 필터 - 관련 경로: `apps/edge/internal/openai`, `apps/edge/internal/service`, `apps/node/internal/adapters/openai_compat`, `apps/node/internal/runtime`, `packages/go/config`, [openai-compatible-api.md](../../../../agent-contract/outer/openai-compatible-api.md) - 표준선(선택): 반복루프 필터는 streaming passthrough UX를 유지하는 `passthrough_guarded` 경로이며, 이미 흘린 정상 prefix를 버리지 않고 continuation repair로 이어 쓴다. +- 표준선(선택): 반복루프 필터는 텍스트 n-gram/문단 반복뿐 아니라 tool/action 반복도 본다. action fingerprint는 `tool name + normalized args`를 안정적으로 정규화해 만들고, 로그/metric label에는 raw args나 secret 가능 문자열을 넣지 않고 hash/redacted summary만 남긴다. +- 표준선(선택): tool/action 반복은 단일 stream 안에서만 판단하지 않는다. 이전 request `messages`의 assistant tool call과 tool result, 현재 provider tool call delta, consecutive 동일 fingerprint, 동일/no-progress tool result, threshold, 짧은 TTL/session state를 함께 만족해야 한다. action guard가 활성화된 tool call delta는 full fingerprint 판정 전까지 해당 tool call fragment만 hold하며 전체 content stream을 buffer하지 않는다. `git status`, `ls`, polling처럼 정상적으로 반복될 수 있는 read-only action은 model/provider/tool별 threshold/allow policy로 조정하고, dev-corp 초기 적용은 observe-only evidence를 먼저 남긴 뒤 guard mode로 승격한다. +- 표준선(선택): Pi agent는 실제 local tool invocation을 보므로 별도 보조 guard를 둘 수 있지만, IOP 필터는 Pi 구현에 의존하지 않고 provider stream의 tool call delta와 request/message history에서 감지 가능한 action 반복을 우선 방어한다. - 표준선(선택): 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에서 일관되게 평가한다. 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 c7391aa..f3b5401 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 @@ -18,7 +18,7 @@ ## 문제 / 비목표 -- 문제: provider stream에서 반복 출력 루프가 발생하면 Pi TUI 같은 client는 이미 열린 SSE를 계속 받으므로, IOP가 모델/provider 축의 이상을 감지해 upstream만 중단하고 같은 downstream stream에 안전하게 repair 출력을 이어 붙여야 한다. 동시에 `metadata.scheme` JSON 출력 계약은 전체 결과 검증 없이는 성공 여부를 알 수 없어 streaming passthrough와 다른 gated 경로가 필요하다. +- 문제: provider stream에서 반복 출력 루프가 발생하면 Pi TUI 같은 client는 이미 열린 SSE를 계속 받으므로, IOP가 모델/provider 축의 이상을 감지해 upstream만 중단하고 같은 downstream stream에 안전하게 repair 출력을 이어 붙여야 한다. 또한 `dev-corp-iop-ornith`/`ornith:35b` 유형처럼 모델이 같은 tool/action을 동일하거나 진전 없는 결과 뒤에 반복 생성하면 단일 응답 content 반복만으로는 감지하기 어렵기 때문에, tool call delta와 request/message history에서 action fingerprint 반복을 별도로 감시해야 한다. 동시에 `metadata.scheme` JSON 출력 계약은 전체 결과 검증 없이는 성공 여부를 알 수 없어 streaming passthrough와 다른 gated 경로가 필요하다. - 비목표: - normalized 실행 경로를 OpenAI-compatible provider 출력 검증 경로와 합친다. - CLI adapter protocol을 이 Milestone에서 변경한다. @@ -34,20 +34,21 @@ | 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 방향, 모델/환경별 filter enable/disable와 공통 interface/base 계층을 확정했다. | +| User Decision | 현재 사용자 요청 | 사용자가 `metadata.scheme` 단일 계약, normalized CLI-only 경계, streaming guard 방향, 동일 tool/action 반복 guard, 모델/환경별 filter enable/disable와 공통 interface/base 계층을 확정했다. | ## State Machine | 상태 | 진입 조건 | 다음 상태 | 근거 | |------|-----------|-----------|------| -| `ingress` | `/v1/chat/completions` 요청 수신 | `passthrough`, `guarded_stream`, `contract_schema`, `policy_rejected` | request metadata, environment/model/provider별 internal filter path policy | +| `ingress` | `/v1/chat/completions` 요청 수신 | `repeat_guard_preflight`, `passthrough`, `guarded_stream`, `contract_schema`, `policy_rejected` | request metadata, environment/model/provider별 internal filter path policy | +| `repeat_guard_preflight` | incoming `messages`가 이미 repeated action threshold를 넘겼거나 짧은 session state와 일치한다 | `guard_error`, `guarded_stream` | action fingerprint history event | | `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 응답 | +| `guarded_stream` | streaming 응답에 content 반복루프 또는 tool/action 반복 online filter 적용 | `repairing`, `done`, `guard_error` | rolling inspector 또는 action fingerprint event | +| `repairing` | content 반복루프 감지 후 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 | +| `guard_error` | repair 불가, retry exhausted, tool side-effect 구간 진입, side-effect 가능 action 반복 감지 | `done` | OpenAI-compatible error or terminal SSE error | | `schema_error` | retry 후에도 JSON/schema 불일치 | `done` | OpenAI-compatible validation error | | `done` | 성공 응답 또는 terminal error 전송 | 없음 | final chunk/body | @@ -57,11 +58,11 @@ - 입력: - `metadata.scheme`: optional JSON schema object. 있으면 IOP는 요청 출력 계약으로 간주한다. - `stream`: caller 요청값. `metadata.scheme`이 있으면 downstream content streaming보다 schema validation이 우선한다. - - `messages`: 마지막 user message에 IOP output scheme instruction을 append할 대상이다. - - `tools`: tool call side effect 안전성 판단에 사용한다. + - `messages`: 마지막 user message에 IOP output scheme instruction을 append할 대상이며, 명시 session id가 없을 때 이전 assistant tool call과 tool result를 이용해 action 반복 context를 추정하는 요청 history 기준이다. + - `tools`: tool name/argument 구조와 side effect 안전성 판단에 사용한다. - 출력: - `passthrough`: 기존 provider-compatible 응답을 유지한다. - - `passthrough_guarded`: downstream SSE 연결을 유지하되 IOP가 upstream stream을 감시하고 필요 시 upstream만 abort/retry한다. 내부 실행/로그 path이며 provider-original byte-identical로 표시하지 않는다. + - `passthrough_guarded`: downstream SSE 연결을 유지하되 IOP가 upstream content delta와 tool call delta를 감시하고 필요 시 upstream만 abort/retry하거나 안전 중단한다. action guard가 활성화된 tool call delta는 full call fingerprint 판정 전까지 해당 tool call fragment만 hold할 수 있다. 내부 실행/로그 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: @@ -71,6 +72,10 @@ - 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`로 종료한다. + - action 반복 guard는 `tool name + normalized args`를 안정 fingerprint로 만들고, 이전 request history의 tool call/result, 현재 provider tool call delta, consecutive 동일 fingerprint, 동일/no-progress tool result, threshold, 짧은 TTL/session state를 함께 만족할 때만 repeated action으로 판정한다. + - tool call delta hold는 full call fingerprint 판정에 필요한 최소 fragment에만 적용하며, content delta 전체 buffering으로 확장하지 않는다. 판정이 safe이면 held tool call delta를 release하고, repeated action이면 downstream으로 release하지 않는다. + - 명시 session id가 없고 request history만으로 반복 여부를 안정적으로 판정할 수 없으면 guard mode로 단정하지 않고 observe-only decision을 남긴다. + - action 반복 observation은 filter id, feature name, model/provider, fingerprint hash, repeat count, threshold, decision, redacted summary를 남기되 Prometheus label은 낮은 cardinality 값으로 제한한다. - 내부 path 주의: - `passthrough_guarded`와 `contract_schema`는 caller가 지정하는 공개 request field가 아니라 IOP 내부 실행/로그 path 이름이다. - 공개 OpenAI-compatible 경로 선택은 [계약 원문](../../../../agent-contract/outer/openai-compatible-api.md)에 따라 request `model`이 가리키는 provider capability로 결정한다. @@ -80,6 +85,8 @@ - normalized path를 CLI 외 OpenAI-compatible provider 출력 검증에 사용하지 않는다. - 반복루프 repair 안내 문구를 assistant content chunk로 주입하지 않는다. - 이미 downstream으로 tool call을 보내 실행 side effect 가능성이 생긴 뒤 자동 continuation repair를 수행하지 않는다. + - tool/action fingerprint raw args나 secret 가능 문자열을 metric label, request id, provider id 같은 장기 식별자에 넣지 않는다. + - side-effect 가능 tool/action을 자동 replay하거나 continuation repair로 재실행하지 않는다. ## Acceptance Scenarios @@ -88,11 +95,12 @@ | S01 | `contract-doc` | OpenAI-compatible Chat Completions 계약 문서와 구현 타입 | `metadata.scheme` 및 내부 response path 경계가 추가된다 | 계약 문서, request 타입, handler 정책이 wrapper 없는 `metadata.scheme`과 normalized CLI-only 경계를 동일하게 설명한다 | | S02 | `filter-pipeline` | provider route 요청이 들어온다 | request metadata와 filter 정책을 평가한다 | pure passthrough, guarded stream, contract schema 중 하나의 내부 path로 결정되고 unsupported 조합은 400으로 거부된다 | | S03 | `repeat-guard` | provider SSE가 같은 문장/단락을 반복 출력한다 | rolling inspector가 threshold를 넘긴다 | downstream SSE는 유지되고 upstream만 abort되며 safe prefix 보존과 bad tail summary 기반 continuation repair가 1회 이어진다 | -| S04 | `repeat-guard` | tool call delta가 이미 downstream으로 흘렀거나 tool side effect 가능 구간이다 | 반복루프가 감지된다 | 자동 repair를 수행하지 않고 guard terminal error 또는 안전한 중단으로 끝낸다 | +| S04 | `repeat-guard` | tool call delta가 이미 downstream으로 흘렀거나 tool side effect 가능 구간이다 | content 반복 또는 action fingerprint 반복이 감지된다 | 자동 repair를 수행하지 않고 guard terminal error 또는 안전한 중단으로 끝낸다 | | 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으로 거부한다 | +| S08 | `filter-policy` | dev/dev-corp에서 qwen/gemma/ornith model/provider별 반복루프 guard와 schema-contract enablement가 다르게 설정되어 있다 | 요청이 선택된 route로 들어온다 | filter registry가 가장 구체적인 정책을 적용하고, optional disabled filter는 skip하며, required disabled schema-contract는 unsupported/400으로 거부한다 | +| S09 | `repeat-guard` | `dev-corp-iop-ornith`/`ornith:35b` 유형 요청 history에 같은 tool/action fingerprint와 동일/no-progress tool result가 누적되고 현재 provider 응답이 같은 tool call delta를 다시 생성한다 | action repetition threshold를 넘긴다 | IOP가 held tool call delta를 downstream으로 release하지 않고 repeated_action observation을 남기며, side-effect 가능 구간에서는 자동 repair 없이 guard terminal error 또는 안전 중단으로 무한 toolUse 루프를 끊는다 | ## Evidence Map @@ -101,11 +109,12 @@ | S01 | 계약 문서 diff, Go request/metadata type tests | `agent-task/m-openai-compatible-output-validation-filters/...` | `Roadmap Completion`에 `contract-doc`, `go test ./apps/edge/internal/openai -count=1` 결과 | | S02 | mode selection unit tests, unsupported 조합 tests | `agent-task/m-openai-compatible-output-validation-filters/...` | `Roadmap Completion`에 `filter-pipeline`, handler test 결과 | | S03 | 반복 chunk fixture stream test | `agent-task/m-openai-compatible-output-validation-filters/...` | `Roadmap Completion`에 `repeat-guard`, upstream abort/repair/single `[DONE]` assertion | -| S04 | tool side-effect guard fixture | `agent-task/m-openai-compatible-output-validation-filters/...` | `Roadmap Completion`에 `repeat-guard`, automatic repair block assertion | +| S04 | tool side-effect/action repeat guard fixture | `agent-task/m-openai-compatible-output-validation-filters/...` | `Roadmap Completion`에 `repeat-guard`, automatic repair block assertion | | 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 | +| S08 | qwen/gemma/ornith 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 | +| S09 | cross-request repeated tool/action fingerprint fixture, Ornith 반복 toolUse 로그 재현 | `agent-task/m-openai-compatible-output-validation-filters/...` | `Roadmap Completion`에 `repeat-guard`, tool call delta hold/block/release assertion, repeated_action observation, no-progress threshold, observe-only 불확실성 처리, side-effect safe stop assertion | ## Cross-repo Dependencies @@ -125,5 +134,7 @@ ## 작업 컨텍스트 - 표준선: 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`로 평가한다. +- 표준선: filter 확장은 공통 interface, registry, shared policy/base helper를 통해 추가한다. enable/disable는 `dev`/`dev-corp`, qwen/gemma/ornith 같은 model group/model/provider, filter feature 단위로 선언하고 handler는 같은 `FilterContext`로 평가한다. +- 표준선: action 반복 guard는 단일 응답 텍스트 반복과 별도 축이다. Edge는 provider stream의 tool call delta와 request/message history를 이용해 `tool name + normalized args` fingerprint 반복을 감시하고, Pi agent 쪽 local tool invocation guard는 보조 방어로 둘 수 있지만 IOP 필터의 필수 의존성으로 두지 않는다. +- 표준선: action 반복 guard는 false positive를 줄이기 위해 observe-only evidence 수집을 먼저 허용하고, consecutive 동일 fingerprint와 동일/no-progress 결과가 threshold를 넘을 때 guard mode에서 중단한다. read-only 반복 action은 model/provider/tool별 threshold 또는 allow policy로 조정한다. - 후속 SDD: 없음