<!-- task=m-openai-compatible-provider-passthrough-contract-sync/02+01_model_route_dispatch plan=2 tag=REVIEW_REVIEW_API -->

# Plan - Review Follow-up for Responses Provider-Pool Dispatch

## 이 파일을 읽는 구현 에이전트에게

이 follow-up은 이전 리뷰의 Required 이슈 3개만 고친다. 구현 완료 전에는 반드시 같은 디렉터리의 `CODE_REVIEW-local-G06.md`에서 구현 에이전트 소유 섹션을 실제 변경 내용과 검증 출력으로 채운다.

선택된 Milestone `구현 잠금 > 결정 필요` 항목이 실구현을 막는 경우에만 review stub의 `사용자 리뷰 요청` 섹션을 정확한 근거와 함께 채우고 멈춘다. 구현 중 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 외부 환경, secret, 서비스 준비, 일반 범위 조정, 검증 증거 공백은 사용자 리뷰 요청이 아니며 `검증 결과` 또는 `계획 대비 변경 사항`에 기록한다. `USER_REVIEW.md`, `complete.log`, archive 이동은 code-review-skill 전용이다.

## 배경

이전 loop는 Responses provider-pool을 selected-after-prepare 구조로 옮겼고 targeted Go tests는 통과했다. 하지만 리뷰에서 production debug/panic, normalized Responses strict-output policy 손실, provider-pool tunnel metadata/estimate/context 손실이 확인되었다. 이 follow-up은 model-route Task를 계속 닫기 위한 좁은 repair다.

## 사용자 리뷰 요청 흐름

구현 중 새 결정이 필요해 보여도 직접 질문하지 않는다. 선택된 Milestone lock decision이 실제 구현을 차단할 때만 `CODE_REVIEW-local-G06.md`의 `사용자 리뷰 요청` 섹션에 `agent-ops/skills/common/_templates/implementation-user-review-request-section.md` 형식으로 기록하고 code-review가 `USER_REVIEW.md` 작성 여부를 판단한다.

## Archive Evidence Snapshot

- Current archived plan: `agent-task/m-openai-compatible-provider-passthrough-contract-sync/02+01_model_route_dispatch/plan_local_G06_1.log`
- Current archived review: `agent-task/m-openai-compatible-provider-passthrough-contract-sync/02+01_model_route_dispatch/code_review_local_G06_1.log`
- Verdict: FAIL
- Required summary:
  - `apps/edge/internal/openai/responses_handler.go:373`: production handler still logs `DEBUG handleResponsesProviderPool` and keeps a nil `PrepareRun` panic guard.
  - `apps/edge/internal/openai/responses_handler.go:432`: normalized provider-pool Responses discards the prepared `responsesRequest`/strict output policy and calls `completeResponse` with `s.resolveOutputPolicy("")`.
  - `apps/edge/internal/openai/responses_handler.go:249`: provider-pool base `Run`/`Tunnel` omit metadata, `EstimatedInputTokens`, and `ContextClass`, so tunnel dispatch loses observability/long-context context.
- Affected files:
  - `apps/edge/internal/openai/responses_handler.go`
  - `apps/edge/internal/openai/server_test.go`
  - `apps/edge/internal/openai/chat_handler.go` only if the identity `PrepareRun` was added solely for the fake-service panic guard and can be safely removed.
  - `apps/edge/internal/service/run_dispatch.go` only if service-level copy semantics need a focused regression test.
- Verification evidence from failed loop:
  - `GOCACHE=/tmp/iop-go-cache go test ./apps/edge/internal/openai -run 'TestResponsesProviderPool.*(Unknown|Streaming|Tunnel|Ollama|Mixed)|TestChatCompletions.*ProviderPool' -count=1` PASS.
  - `GOCACHE=/tmp/iop-go-cache go test ./apps/edge/internal/service -run 'TestResolveProviderPoolCandidates.*ExecutionPath|TestResolveProviderPoolCandidatesKeepsMixedProviderTypes' -count=1` PASS.
  - `GOCACHE=/tmp/iop-go-cache go test ./apps/edge/internal/openai ./apps/edge/internal/service ./apps/node/internal/adapters/openai_compat -count=1` PASS.
  - `git diff --check` PASS.
- Roadmap carryover:
  - Milestone task remains `model-route`.
  - SDD target scenarios remain S02/S03/S09.
  - Predecessor satisfied by `agent-task/archive/2026/07/m-openai-compatible-provider-passthrough-contract-sync/01_selector_metadata_surface/complete.log`.
- Specific archive files above may be read if exact prior context is needed. Do not search `agent-task/archive/**` broadly.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/routing-policy-model-orchestration/milestones/openai-compatible-provider-passthrough-contract-sync.md`
- Milestone link: [Milestone 문서](agent-roadmap/phase/routing-policy-model-orchestration/milestones/openai-compatible-provider-passthrough-contract-sync.md)
- Task ids:
  - `model-route`: Chat Completions와 Responses에서 `model` -> route/provider capability -> passthrough 또는 normalized 경로를 사용
- Completion mode: check-on-pass

## 분석 결과

### SDD 기준

- SDD: `agent-roadmap/sdd/routing-policy-model-orchestration/openai-compatible-provider-passthrough-contract-sync/SDD.md`
- Status: `[승인됨]`, SDD lock 해제.
- Targeted Acceptance Scenarios:
  - `S02` / `model-route`: OpenAI-compatible provider 선택 시 provider tunnel passthrough.
  - `S03` / `model-route`: CLI/Ollama/native provider 선택 시 normalized `RunRequest`.
  - `S09` / `go-tests`: 관련 Go test evidence.

### 범위 결정 근거

- 포함: Responses provider-pool handler repair, fake service/test cleanup, strict-output normalized Responses regression, tunnel metadata/context propagation tests.
- 제외: provider scoring/queue policy 변경, Node adapter runtime 변경, contract 문서 갱신, dev-corp live provider smoke.
- Follow-up route: `local-G06`. 변경 범위가 bounded이고 deterministic Go tests로 검증 가능하다.

## 구현 체크리스트

- [ ] [REVIEW_REVIEW_API-1] production/test debug-only artifacts를 제거하고 fake service의 optional `PrepareRun` 동작을 실제 service 계약과 맞춘다.
- [ ] [REVIEW_REVIEW_API-2] Responses provider-pool normalized branch가 `PrepareRun`에서 준비한 request와 strict output policy를 `completeResponse`에 전달하게 한다.
- [ ] [REVIEW_REVIEW_API-3] Responses provider-pool tunnel branch가 metadata, `EstimatedInputTokens`, `ContextClass`를 pool request와 tunnel dispatch에 보존하게 한다.
- [ ] [REVIEW_REVIEW_API-4] focused regression tests와 최종 package 검증 명령을 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## 의존 관계 및 구현 순서

`02+01_model_route_dispatch`는 predecessor `01_selector_metadata_surface` 완료 후 진행한다. 해당 predecessor는 archive complete log로 충족되어 있으므로 이 follow-up 구현을 진행할 수 있다.

### [REVIEW_REVIEW_API-1] Debug Artifact Cleanup

#### 문제

`handleResponsesProviderPool`에 production debug log와 panic guard가 남아 있다. 또한 `server_test.go`에는 Debug 전용 테스트와 fake service panic guard가 남아 있어 optional `PrepareRun` 계약을 테스트 더블이 과하게 제한한다.

#### 해결 방법

- `apps/edge/internal/openai/responses_handler.go`: `DEBUG handleResponsesProviderPool` 로그와 nil `PrepareRun` panic guard를 제거한다.
- `apps/edge/internal/openai/server_test.go`: `TestResponsesProviderPoolDebugOllamaNormalizeCheck`, `TestResponsesProviderPoolDebugHandleResponses`, debug `t.Logf`, direct mutex value 확인을 제거하거나 의미 있는 regression test로 병합한다.
- `apps/edge/internal/openai/server_test.go`: fake `SubmitProviderPool`에서 `req.PrepareRun == nil`이면 panic하는 guard를 제거한다. `PrepareRun`은 service 계약상 optional이며 normalized path에서 hook이 있으면 호출되고 tunnel path에서는 호출되지 않는 동작만 필요한 테스트에서 assertion한다.
- `apps/edge/internal/openai/chat_handler.go`: identity `PrepareRun`이 fake panic을 피하기 위한 코드였는지 재검토한다. 실제 retry 흐름이나 symmetry를 위해 유지해도 되지만, fake guard 때문에 production 코드를 추가한 상태로 두지 않는다.

#### 중간 검증

```bash
rg --sort path 'DEBUG handleResponsesProviderPool|TestResponsesProviderPoolDebug|panic\("fake SubmitProviderPool|panic\("poolReq.PrepareRun' apps/edge/internal/openai
```

예상 결과: 관련 debug/panic artifact 출력 없음.

### [REVIEW_REVIEW_API-2] Prepared Responses Output Policy

#### 문제

Normalized provider-pool Responses는 `PrepareRun` 안에서 strict decode, prompt build, `outputPolicy := s.resolveOutputPolicy(prompt)`를 수행하지만, 결과 조립 시 `responsesRequest{Model: env.Model}`와 `s.resolveOutputPolicy("")`를 넘긴다. XML completion contract가 있는 strict-output prompt에서는 non-provider normalized Responses와 달리 response wrapping/normalization이 깨질 수 있다.

#### 해결 방법

- `handleResponsesProviderPool` request-local scope에 prepared `responsesRequest`와 `strictOutputPolicy`를 저장한다.
- `PrepareRun`이 decode/prompt/policy 준비를 성공하면 해당 값을 채우고, normalized result branch에서 이 값을 `completeResponse(w, r, preparedReq, handle, preparedOutputPolicy)`로 전달한다.
- normalized result인데 `PrepareRun`이 성공적으로 실행되지 않은 불일치 상태는 500 `run_error`로 명확히 처리한다.
- `TestResponsesStrictOutputNormalizesAgentResponse`와 유사한 provider-pool normalized regression test를 추가한다. provider-pool selected path가 normalized일 때 prompt에 XML completion contract가 있으면 response `output_text`가 expected XML block으로 wrapping되는지 확인한다.

#### 중간 검증

```bash
GOCACHE=/tmp/iop-go-cache go test ./apps/edge/internal/openai -run 'TestResponsesProviderPool.*StrictOutput|TestResponsesStrictOutputNormalizesAgentResponse' -count=1
```

예상 결과: provider-pool normalized strict-output regression과 기존 non-provider strict-output test PASS.

### [REVIEW_REVIEW_API-3] Responses Pool Tunnel Metadata And Context

#### 문제

새 provider-pool Responses path는 selection 전에 `baseRun`/`baseTunnel`을 만들지만 metadata, estimate, context class를 채우지 않는다. Real `SubmitProviderPool`은 `req.Run.ContextClass`로 long admission을 판단하고 tunnel branch의 dispatch info에 `req.Run.EstimatedInputTokens`/`ContextClass`를 복사한다. 현재 상태에서는 selected tunnel path가 기존 direct tunnel path의 metadata/observability/long-context 문맥을 잃는다.

#### 해결 방법

- `handleResponsesProviderPool`에서 `runMeta`와 principal metadata를 만든 뒤, raw body 기준 `estimate := estimateInputTokens(string(rawBody), runMeta, nil, nil)`와 `contextClass := classifyContext(estimate, s.longContextThreshold())`를 계산한다.
- tunnel/base metadata에 `openai_model`, `openai_stream`, `estimated_input_tokens`, `context_class`를 추가한다.
- `baseRun`과 `baseTunnel`에 `Metadata`, `EstimatedInputTokens`, `ContextClass`를 모두 채운다. Normalized branch는 `PrepareRun`에서 decoded prompt 기준으로 `runReq.Metadata`, `EstimatedInputTokens`, `ContextClass`를 다시 채울 수 있다.
- test double이 tunnel `SubmitProviderTunnelRequest` snapshot에서 metadata/context를 검증할 수 있게 유지한다.
- regression test를 추가해 Responses provider-pool tunnel selection에서 `tunnelReq.Metadata["openai_model"]`, principal metadata, `estimated_input_tokens`, `context_class`, typed `EstimatedInputTokens`, `ContextClass`가 비어 있지 않음을 확인한다.

#### 중간 검증

```bash
GOCACHE=/tmp/iop-go-cache go test ./apps/edge/internal/openai -run 'TestResponsesProviderPool.*(Metadata|Context|TunnelUnknown|Streaming|Ollama)' -count=1
```

예상 결과: tunnel metadata/context regression과 기존 tunnel/normalized mixed path tests PASS.

### [REVIEW_REVIEW_API-4] Final Verification

#### 문제

이 follow-up은 handler, service surface contract, fake service fidelity를 함께 건드리므로 focused와 package-level 검증이 필요하다.

#### 해결 방법

아래 명령을 fresh test로 실행하고 stdout/stderr를 review stub에 붙인다.

#### 중간 검증

```bash
test -f agent-task/archive/2026/07/m-openai-compatible-provider-passthrough-contract-sync/01_selector_metadata_surface/complete.log
```

```bash
GOCACHE=/tmp/iop-go-cache go test ./apps/edge/internal/openai -run 'TestResponsesProviderPool.*(Unknown|Streaming|Tunnel|Ollama|Mixed|Metadata|Context|StrictOutput)|TestResponsesStrictOutputNormalizesAgentResponse|TestChatCompletions.*ProviderPool' -count=1
```

```bash
GOCACHE=/tmp/iop-go-cache go test ./apps/edge/internal/service -run 'TestResolveProviderPoolCandidates.*ExecutionPath|TestResolveProviderPoolCandidatesKeepsMixedProviderTypes' -count=1
```

```bash
GOCACHE=/tmp/iop-go-cache go test ./apps/edge/internal/openai ./apps/edge/internal/service ./apps/node/internal/adapters/openai_compat -count=1
```

```bash
git diff --check
```

예상 결과: 모두 PASS. full-cycle local Edge/Node smoke는 이 follow-up의 deterministic unit regression 범위 밖이면 실행하지 않은 이유와 남은 위험을 `검증 결과`에 명시한다.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `apps/edge/internal/openai/responses_handler.go` | REVIEW_REVIEW_API-1, REVIEW_REVIEW_API-2, REVIEW_REVIEW_API-3 |
| `apps/edge/internal/openai/server_test.go` | REVIEW_REVIEW_API-1, REVIEW_REVIEW_API-2, REVIEW_REVIEW_API-3 |
| `apps/edge/internal/openai/chat_handler.go` | REVIEW_REVIEW_API-1 only if fake-guard workaround cleanup requires it |
| `apps/edge/internal/service/run_dispatch.go` | REVIEW_REVIEW_API-3 only if service-level test/contract assertion is added |

## 최종 검증

구현 완료 후 `REVIEW_REVIEW_API-4`의 모든 명령 결과를 `CODE_REVIEW-local-G06.md`에 붙인다.
