# SDD: OpenAI-compatible 출력 검증 필터

## 위치

- Milestone: [Milestone 문서](../../../phase/knowledge-tool-optimization-extension/milestones/openai-compatible-output-validation-filters.md)
- Phase: [PHASE.md](../../../phase/knowledge-tool-optimization-extension/PHASE.md)

## 상태

[검토중]

## SDD 잠금

- 상태: 잠금
- 사용자 리뷰: [USER_REVIEW.md](USER_REVIEW.md)
- 잠금 항목:
  - [ ] [D01] assistant history anchor의 preflight history sanitation, live reasoning dedupe, no-progress repair 승격 정책
  - [ ] [D02] 이번 Milestone의 endpoint 범위를 `/v1/chat/completions`로 유지할지 `/v1/responses`까지 확장할지
  - [ ] [D03] 기존 assembled content/reasoning 운영 로그의 raw 보존과 기본 redaction 정책

## 문제 / 비목표

- 문제: OpenAI-compatible caller가 provider stream에서 반복 출력을 받으면 이미 열린 SSE가 계속 유지되므로, IOP가 caller 제품명과 무관하게 request history와 provider response에서 이상을 감지하고 안전하게 관찰/보정/중단해야 한다. 동일 tool/action을 동일하거나 진전 없는 결과 뒤에 반복 생성하는 루프는 단일 응답 content 반복만으로 감지하기 어려워 tool call delta와 request history의 action fingerprint를 별도로 감시해야 한다. 2026-07-16 Pi/Ornith incident는 user가 입력하지 않은 assistant `content` anchor가 이후 assistant `reasoning_content`/`reasoning` history에 누적되어 모델이 user 발화처럼 재인용한 caller 사례다. 마지막 중단 요청 직전 generic Chat Completions payload 재구성에서는 같은 anchor를 가진 이전 message 11개가 모두 `role=assistant`였고 user occurrence는 0이었다. 이 사례를 Pi 전용 규칙으로 처리하지 않고 raw HTTP/OpenAI SDK를 포함한 동일 protocol fixture로 일반화해야 한다. 동시에 `metadata.scheme` JSON 출력 계약은 전체 결과 검증 없이는 성공 여부를 알 수 없어 streaming passthrough와 다른 gated 경로가 필요하다.
- 비목표:
  - normalized 실행 경로를 OpenAI-compatible provider 출력 검증 경로와 합친다.
  - CLI adapter protocol을 이 Milestone에서 변경한다.
  - 전체 streaming 응답을 기본적으로 buffer해 사용자 경험을 늦춘다.
  - schema 계약이 있는 요청에서 검증 전 partial content를 성공 출력으로 노출한다.
  - validator 모델로 애매한 자연어/tool-call 후보를 판정한다.
  - Pi session JSONL, Pi SDK 내부 type, Pi local tool invocation을 IOP filter runtime 입력으로 사용한다.
  - 명시적 conversation identity 없이 caller/model 조합이나 history hash를 대화 식별자로 간주해 caller 간 TTL state를 공유한다.
  - D02 결정 전에 `/v1/responses`에 Chat Completions용 history mutation/repair 정책을 그대로 적용한다.

## Source of Truth

| 영역 | 기준 | 메모 |
|------|------|------|
| Roadmap | [Milestone 문서](../../../phase/knowledge-tool-optimization-extension/milestones/openai-compatible-output-validation-filters.md) | 범위, Task, 완료 evidence 기준 |
| 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 정책 원문 |
| Incident Evidence | [2026-07-16 Pi/Ornith cross-request history anchor](evidence/2026-07-16-pi-ornith-cross-request-history-anchor.log) | host Pi session과 IOP dev Edge 로그에서 추출한 sanitized 회귀 기준. raw prompt/tool args/result는 포함하지 않는다. |
| External Provider | OpenAI-compatible provider pool | provider 원본 요청/응답은 IOP 필터 정책에 따라 upstream abort/retry 대상이 된다. |
| User Decision | 현재 사용자 요청 및 [USER_REVIEW.md](USER_REVIEW.md) | caller-neutral OpenAI-compatible filter와 별도 sanitized evidence log 요구는 확정됐다. history mutation/repair(D01), endpoint 범위(D02), raw 로그 보존(D03)은 사용자 결정 대기다. |

## State Machine

| 상태 | 진입 조건 | 다음 상태 | 근거 |
|------|-----------|-----------|------|
| `ingress` | 선택된 OpenAI-compatible endpoint 요청 수신. 현재 기준선은 `/v1/chat/completions`이며 D02에서 범위를 확정한다 | `repeat_guard_preflight`, `passthrough`, `guarded_stream`, `contract_schema`, `policy_rejected` | request metadata, endpoint, environment/model/provider별 internal filter path policy |
| `repeat_guard_preflight` | 현재 incoming `messages`가 repeated action이나 assistant-only history anchor 후보를 가진다 | `history_sanitizing`, `guard_error`, `guarded_stream` | role-separated text/action fingerprint history event |
| `history_sanitizing` | D01에서 preflight mutation이 승인됐고 plain assistant reasoning history가 mutation threshold를 넘겼다 | `guarded_stream`, `guard_error` | signed/encrypted/tool/content를 제외한 bounded request-history sanitation |
| `passthrough` | `metadata.scheme` 없음, guarded filter 비활성 또는 적용 대상 아님 | `done`, `provider_error` | provider SSE/non-stream 응답 |
| `guarded_stream` | streaming 응답에 single-stream content, assistant history anchor 또는 tool/action 반복 online filter 적용 | `observe_continue`, `dedupe_continue`, `repairing`, `done`, `guard_error` | rolling inspector, assistant history anchor 또는 action fingerprint event |
| `observe_continue` | guard가 후보를 감지했지만 D01이 observe-only이거나 판정 근거가 부족하다 | `guarded_stream`, `done`, `guard_error` | response mutation 없는 observation |
| `dedupe_continue` | D01에서 live dedupe가 승인됐고 assistant-only anchor가 plain non-final reasoning에 재등장했다 | `guarded_stream`, `done`, `guard_error` | bounded candidate fragment suppression과 `assistant_history_anchor` observation |
| `repairing` | single-stream content 반복 또는 D01에서 승인된 무진전 assistant-history 반복 감지 후 upstream provider request abort 성공 | `guarded_stream`, `guard_error` | sanitized history 기반 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 구간 진입, 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 |

## Interface Contract

- 계약 원문: [openai-compatible-api.md](../../../../agent-contract/outer/openai-compatible-api.md)
- 입력:
  - `metadata.scheme`: optional JSON schema object. 있으면 IOP는 요청 출력 계약으로 간주한다.
  - `stream`: caller 요청값. `metadata.scheme`이 있으면 downstream content streaming보다 schema validation이 우선한다.
  - `messages`: 마지막 user message에 IOP output scheme instruction을 append할 대상이며, role별 user/assistant text provenance와 이전 assistant tool call/tool result를 이용해 history anchor 및 action 반복 context를 판정하는 현재 request source of truth다.
  - `tools`: tool name/argument 구조와 side effect 안전성 판단에 사용한다.
- 출력:
  - `passthrough`: 기존 provider-compatible 응답을 유지한다.
  - `passthrough_guarded`: downstream SSE 연결을 유지하되 IOP가 upstream `content`/provider reasoning delta와 tool call delta를 감시하고 D01에서 승인된 policy에 따라 observe, bounded dedupe, upstream abort/retry 또는 안전 중단한다. history anchor 후보 text와 action guard가 활성화된 tool call delta는 판정에 필요한 최소 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:
  - 구현은 Go class 상속이 아니라 공통 filter interface와 공유 policy/base helper를 기준으로 한다.
  - 이번 stream 처리 모듈 적용 범위는 `/v1/chat/completions` provider SSE다. 모듈은 chunk 조립, bounded look-behind/tail 보류, terminal/tool event 판정, safe release와 상위 filter/policy 계층이 전달한 replacement 적용만 담당한다. 반복 보정, schema validation, workflow routing 같은 정책과 replacement 생성은 상위 계층이 담당하며 외부 API caller는 replacement를 지정하지 않는다.
  - 확정된 선행 event는 즉시 release하고 terminal/tool 판정에 필요한 tail만 bounded hold한다. 한도 초과 시 선택된 response path 정책에 따라 release 또는 명시 오류로 끝내며 전체 응답 buffering으로 확대하지 않는다.
  - `/v1/responses`, Claude stream, agent-family별 codec은 이번 모듈 구현과 S14 검증 범위에 포함하지 않는다. 후속 workflow는 event mechanics와 interface를 재사용할 수 있지만 endpoint/provider codec은 D02 또는 후속 Milestone에서 별도로 정한다.
  - filter input과 decision에는 caller/agent 제품명을 넣지 않는다. 동일한 OpenAI-compatible payload와 provider capability는 raw HTTP, OpenAI SDK, Pi 등 caller가 달라도 같은 판정을 내린다.
  - 모든 filter는 filter id, feature name, 지원 response path, 기본 활성값, request 적용 여부, 실행/검증 결과를 같은 interface로 노출한다.
  - `FilterContext`는 최소한 environment, model group/model alias, provider id/type, endpoint, stream 여부, request-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`로 종료한다.
  - action 반복 guard는 `tool name + normalized args`를 안정 fingerprint로 만들고, 현재 incoming `messages` 안의 tool call/result, 현재 provider tool call delta, consecutive 동일 fingerprint, 동일/no-progress tool result, threshold를 함께 만족할 때만 repeated action으로 판정한다. 명시적 conversation identity가 추가되기 전에는 request 외 TTL/session state를 판정에 사용하지 않는다.
  - tool call delta hold는 full call fingerprint 판정에 필요한 최소 fragment에만 적용하며, content delta 전체 buffering으로 확장하지 않는다. 판정이 safe이면 held tool call delta를 release하고, repeated action이면 downstream으로 release하지 않는다.
  - 현재 계약에는 caller-neutral conversation identity가 없으므로 MVP는 incoming `messages`와 current response만 사용한다. request history만으로 반복 여부를 안정적으로 판정할 수 없으면 guard mode로 단정하지 않고 current-stream 검사 또는 observe-only로 낮춘다.
  - action 반복 observation은 filter id, feature name, model/provider, fingerprint hash, repeat count, threshold, decision, redacted summary를 남기되 Prometheus label은 낮은 cardinality 값으로 제한한다.
  - assistant history anchor inspector는 raw Chat Completions payload의 `content`, `reasoning_content`, `reasoning`, `reasoning_text`를 provider reasoning alias registry로 읽되 role과 channel을 별도 보존한다. whitespace/control normalization 뒤 fingerprint를 만들고, 같은 fingerprint가 user 입력에는 없으며 current `messages`의 assistant history에 configurable N회 누적됐을 때 후보로 판정한다.
  - caller가 reasoning history를 재전송하지 않으면 해당 channel의 과거 occurrence를 0으로 본다. 명시 conversation identity 계약이 없는 상태에서 history-lineage hash나 configured OpenAI session id로 누락 occurrence를 보충하지 않는다. provider 전환은 현재 request history에 들어 있는 channel alias와 current response를 통해서만 관찰한다.
  - progress는 validator 모델 없이 incoming history에서 완료된 이전 tool/action fingerprint, tool result/error hash 변화, terminal 상태만으로 판정한다. current response의 tool result는 아직 존재하지 않으므로 progress 근거가 아니며, 서로 다른 action 자체만으로 진전을 단정하지 않는다.
  - history sanitation은 D01 승인 시 plain string reasoning의 반복 fragment에만 적용한다. assistant final `content`, tool call, signed/encrypted reasoning, 알 수 없는 provider field는 변경하지 않으며, repair request도 같은 sanitized history를 사용해야 한다.
  - live reasoning dedupe와 no-progress repair는 D01 승인 결과를 따른다. tool call이 이미 release됐거나 side effect 가능 구간이면 자동 repair하지 않고 안전 중단 정책을 따른다.
  - history anchor observation은 fingerprint hash, redacted preview, first-seen role/channel, current role/channel, request-history count, provider transition, completed-progress signal, decision만 남긴다. raw message/tool payload 보존은 D03 결정에 따르며 기본 filter metric label에는 넣지 않는다.
  - `/v1/responses` 적용 여부는 D02 결정 전까지 확정하지 않는다. Chat Completions와 다른 item/reasoning/tool 계약을 같은 parser로 가정하지 않는다.
- 내부 path 주의:
  - `passthrough_guarded`와 `contract_schema`는 caller가 지정하는 공개 request field가 아니라 IOP 내부 실행/로그 path 이름이다.
  - 공개 OpenAI-compatible 경로 선택은 [계약 원문](../../../../agent-contract/outer/openai-compatible-api.md)에 따라 request `model`이 가리키는 provider capability로 결정한다.
- 금지:
  - `metadata.scheme`을 `iop_output_contract` 같은 wrapper로 감싸지 않는다.
  - schema 계약 요청을 normalized path로 보내지 않는다.
  - 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로 재실행하지 않는다.
  - assistant history anchor 증거를 위해 raw prompt, raw tool args/result, 전체 reasoning/content를 장기 로그에 복제하지 않는다.
  - 진행 신호가 있는 서로 다른 tool/action을 assistant history anchor 반복만으로 차단하지 않는다.
  - assistant final `content`를 history anchor fingerprint만으로 조용히 삭제하지 않는다.
  - caller/agent 제품명으로 filter enablement, threshold, state key, repair path를 분기하지 않는다.
  - Pi session 파일이나 caller SDK 내부 message object를 읽어 runtime 판정을 보강하지 않는다.

## Acceptance Scenarios

| ID | Milestone Task | Given | When | Then |
|----|----------------|-------|------|------|
| S01 | `contract-doc` | OpenAI-compatible 계약 문서와 구현 타입 | `metadata.scheme`, caller-neutral guard 및 D02 endpoint 범위가 추가된다 | 계약 문서, request 타입, handler 정책이 wrapper 없는 `metadata.scheme`, normalized CLI-only 경계, conversation identity 부재 시 degraded behavior와 선택된 endpoint 범위를 동일하게 설명한다 |
| 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 가능 구간이다 | 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` | generic raw HTTP/OpenAI SDK에서 반복루프 또는 schema 계약 smoke를 실행하고 Pi TUI는 선택적 field smoke로 추가한다 | sanitized evidence와 D03 로그 정책을 확인한다 | caller 제품명과 무관하게 문제 축이 model/provider, IOP guard, protocol 경계로 구분되고 role preservation, raw 보존 정책, 최종 stream/body 비오염이 확인된다 |
| 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 루프를 끊는다 |
| S10 | `repeat-guard` | generic Chat Completions request history에서 user occurrence 0인 assistant anchor가 `content` 1회, `reasoning_content` 6회, `reasoning` 4회 누적되고 완료된 tool history는 서로 다른 action/result를 가진다 | assistant-history threshold를 넘긴다 | IOP가 Pi session 없이 role/channel provenance와 completed-progress를 판정하고, D01 정책에 따라 observe 또는 plain reasoning history sanitation/live dedupe를 수행하며 서로 다른 action을 anchor만으로 차단하지 않는다 |
| S11 | `repeat-guard` | assistant-only anchor가 request history에 반복되고 완료된 action/result가 동일 실패 또는 deterministic no-progress/churn을 보인다 | history anchor와 no-progress threshold를 함께 넘긴다 | D01에서 repair가 승인되고 tool call release 전이면 sanitized history로 upstream repair를 최대 1회 수행하며, 미승인 또는 release/side-effect 구간이면 observe/안전 중단하고 final `content`를 조용히 삭제하지 않는다 |
| S12 | `repeat-guard` | caller가 이전 assistant reasoning을 `messages`에 재전송하지 않고 caller-neutral conversation identity도 제공하지 않는다 | 새 request가 들어온다 | IOP는 누락된 history occurrence나 TTL lineage를 추정하지 않고 incoming content/tool history와 current stream에서 관찰 가능한 guard만 적용한다 |
| S13 | `filter-policy` | 의미와 provider capability가 같은 payload를 raw HTTP, OpenAI SDK, Pi caller가 각각 전송한다 | filter path와 decision을 평가한다 | caller/agent 제품명과 무관하게 같은 path, threshold, observation/decision을 내리고 Pi는 선택적 field evidence로만 남는다 |
| S14 | `stream-tail-module` | `/v1/chat/completions` SSE event가 여러 chunk로 나뉘고 terminal marker 또는 tool-call fragment가 tail에 걸쳐 들어온다 | stream 처리 모듈이 bounded tail을 조립하고 상위 filter/policy 계층이 release 또는 replacement를 전달한다 | 확정된 선행 event는 즉시 전달되고 terminal/tool event만 정확히 판정되며 전체 응답 buffering 없이 원본 release 또는 replacement event sequence가 한 번만 출력된다 |

## Evidence Map

| Scenario | Required Evidence | `agent-task` 연결 | 완료 Evidence 기대 |
|----------|-------------------|------------------|---------------------------|
| 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/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 | generic raw HTTP/OpenAI SDK smoke, optional Pi field smoke, Edge provider log, D03 raw/redacted logging tests | `agent-task/m-openai-compatible-output-validation-filters/...` | `Roadmap Completion`에 `ops-evidence`, generic smoke evidence, optional caller field evidence, role preservation/raw logging policy 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 |
| S10 | [2026-07-16 sanitized incident evidence](evidence/2026-07-16-pi-ornith-cross-request-history-anchor.log)에서 파생한 caller-neutral content/reasoning alias fixture, completed distinct action/result fixture | `agent-task/m-openai-compatible-output-validation-filters/...` | `Roadmap Completion`에 `repeat-guard`, assistant-only provenance, incoming-history threshold, D01 decision assertion, distinct action non-block assertion |
| S11 | generic assistant anchor + completed no-progress/churn fixture, released/unreleased tool call fixture | `agent-task/m-openai-compatible-output-validation-filters/...` | `Roadmap Completion`에 `repeat-guard`, D01 정책, sanitized repair history, 1회 budget, single `[DONE]`, final content non-suppression, side-effect safe stop assertion |
| S12 | reasoning history omitted fixture, no conversation identity fixture | `agent-task/m-openai-compatible-output-validation-filters/...` | `Roadmap Completion`에 `repeat-guard`, no inferred TTL/lineage와 current-request degradation assertion |
| S13 | raw HTTP/OpenAI SDK/Pi equivalent-payload table test | `agent-task/m-openai-compatible-output-validation-filters/...` | `Roadmap Completion`에 `filter-policy`, caller-name-independent path/threshold/decision assertion |
| S14 | Chat Completions SSE split chunk, terminal marker, tool-call fragment, buffer limit, pass-through release, terminal replacement table tests | `agent-task/m-openai-compatible-output-validation-filters/...` | `Roadmap Completion`에 `stream-tail-module`, bounded memory, early release, single terminal/replacement sequence assertion |

## Cross-repo Dependencies

- 없음

## Drift Check

- [x] Milestone 기능 Task와 Acceptance Scenario가 일치한다.
- [x] Evidence Map이 code-review/complete.log에서 검증 가능하다.
- [x] agent-contract를 쓰는 경우 SDD에 계약 원문을 복제하지 않았다.
- [x] 사용자 리뷰가 필요한 항목은 [USER_REVIEW.md](USER_REVIEW.md)에만 남겼다.

## 사용자 리뷰 이력

- 2026-07-16: caller-neutral 경계 보강 후 D01-D03 사용자 리뷰 요청. 해결 전 SDD 잠금 유지.

## 작업 컨텍스트

- 표준선: OpenAI-compatible provider 출력 검증은 Edge OpenAI handler/stream bridge에 두고, normalized는 CLI 전용으로 유지한다. 반복루프는 streaming repair filter, `metadata.scheme`은 buffered contract validation으로 분리한다.
- 표준선: Chat Completions stream chunk 조립과 bounded terminal tail 처리는 별도 모듈로 두고, 모듈은 event mechanics만 제공한다. 현재 출력 보정 정책은 이 모듈을 사용하고, 후속 agent workflow terminal hook은 interface 재사용 후보로만 둔다. 다른 endpoint/provider codec과 각자의 replacement 및 다음 단계 결정은 모듈 밖에서 수행한다.
- 표준선: 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로 조정한다.
- 표준선: assistant history anchor는 single-stream content 반복과 identical action 반복 사이의 별도 축이다. provider/caller가 바뀌었다고 가정하지 않고 현재 incoming history의 role/channel provenance만 source of truth로 사용한다. mutation/repair 승격은 D01에서 확정한다.
- 표준선: generic raw HTTP/OpenAI SDK fixture가 완료 기준이며 Pi는 2026-07-16 incident evidence와 선택적 field smoke다. 원본 host session과 dev Edge 로그는 장기 문서에 복제하지 않고 SDD 옆 sanitized evidence에는 비민감 anchor 예시, 시간/role/channel/count/hash, provider 전환, action/result 집계, adapter payload 재구성 경계만 남긴다.
- 후속 SDD: 없음
