<!-- task=m-model-group-mixed-provider-dispatch/04_custom_field_preservation plan=0 tag=SURFACE_FIELD -->

# Code Review Reference - SURFACE_FIELD

> **[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-12
task=m-model-group-mixed-provider-dispatch/04_custom_field_preservation, plan=0, tag=SURFACE_FIELD

## Roadmap Targets

- Milestone: `agent-roadmap/phase/routing-policy-model-orchestration/milestones/model-group-mixed-provider-dispatch.md`
- Milestone link: [Milestone 문서](agent-roadmap/phase/routing-policy-model-orchestration/milestones/model-group-mixed-provider-dispatch.md)
- Task ids:
  - `custom-field-preservation`: Provider-pool Chat route raw body/envelope 분리와 unknown/Codex/provider-specific field 보존
- Completion mode: check-on-pass

## 이 파일을 읽는 리뷰 에이전트에게

> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.

각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
리뷰 완료는 판정 append, active file log archive, PASS 시 `complete.log` 작성과 task archive 이동까지 끝난 상태를 의미합니다. PASS이고 task group이 `m-<milestone-slug>`이면 완료 이벤트 메타데이터를 보고하고 roadmap 수정은 런타임 책임입니다.

---

## 구현 항목별 완료 여부

| 항목 | 완료 여부 |
|------|---------|  
| [SURFACE_FIELD-1] envelope-first Chat provider-pool decode | 완료 |
| [SURFACE_FIELD-2] provider tunnel unknown field preservation test | 완료 |
| [SURFACE_FIELD-3] normalized provider field policy test | 완료 |

## 구현 체크리스트

- [x] [SURFACE_FIELD-1] Chat Completions provider-pool route가 strict unknown-field decode 전에 routing envelope를 읽고, model group selector 거부와 route resolution을 envelope 기준으로 수행하게 한다.
- [x] [SURFACE_FIELD-2] OpenAI-compatible provider 선택 시 `rewriteChatCompletionModel`이 model rewrite와 catalog generation policy 외에는 caller raw body unknown/Codex/provider-specific field를 보존함을 regression test로 검증한다. 검증: `GOCACHE=/tmp/iop-go-cache go test ./apps/edge/internal/openai -run 'ProviderPool.*Unknown|ProviderPoolRejectsResponseMode|MixedProviderPool' -count=1`.
- [x] [SURFACE_FIELD-3] normalized-provider 선택 시 standard supported field만 normalized `RunRequest.Input`에 반영되고 unknown provider-specific field가 native request에 임의 전파되지 않음을 regression test로 검증한다. 검증: `GOCACHE=/tmp/iop-go-cache go test ./apps/edge/internal/openai -run 'ProviderPool.*Unknown|ProviderPool.*Normalized' -count=1`.
- [x] [SURFACE_FIELD-4] 최종 OpenAI handler 패키지 검증을 실행한다. 검증: `GOCACHE=/tmp/iop-go-cache go test ./apps/edge/internal/openai -count=1`.
- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## 코드리뷰 전용 체크리스트

> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다.

- [x] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
- [x] active `CODE_REVIEW-*-G??.md`와 `PLAN-*-G??.md`를 `.log`로 아카이브한다.
- [x] `.gitignore`의 Agent-Ops 관리 block이 task artifact를 unignore하는지 확인한다.
- [x] PASS이면 `complete.log`를 작성하고 active task 디렉터리를 archive로 이동한다.
- [x] PASS이고 task group이 `m-<milestone-slug>`이면 완료 이벤트 메타데이터를 보고하고 roadmap 수정은 하지 않는다.
- [ ] WARN/FAIL이면 다음 active plan/review 또는 USER_REVIEW gate를 처리한다.

## 계획 대비 변경 사항

- `decodeChatCompletionEnvelope` 구현 시 초기에 `json.Unmarshal`을 사용했으나, 기존 테스트 body(`TestToolValidationRetryDispatchFailureEmitsErrorMetric`)가 루트 JSON 객체 이후 후행 트리머(`}\n\t}`)를 포함하고 있어 `json.Unmarshal`은 전체 입력 유효성을 요구하여 실패했다. `json.Decoder.Decode`는 첫 번째 JSON 값만 읽고 후행 트리머를 허용하므로, Responses handler의 `decodeResponsesEnvelope`와 동일한挙동으로 맞추기 위해 `json.NewDecoder(bytes.NewReader(rawBody)).Decode(&env)` 방식으로 변경했다.
- tunnel body 검증 시 `tunnelReqsSnapshot()`의 `Body` 필드가 `BuildBody` 적용 이전의 빈 값을 유지하므로, 실제 tunnel 전송 body를 확인하려면 `tunnelBodiesSnapshot()`을 사용해야 한다. 테스트에서 `fake.tunnelBodiesSnapshot()`으로 전환했다.
- PLAN의 `심볼 참조` 섹션에서 두 번째 후보로 언급된 `decodeChatCompletionRequest(..., strictUnknown bool)` 대신, 새 함수 `decodeChatCompletionRequestLenient`를 별도로 만들었다. 기존 `decodeChatCompletionRequest`를 수정하면 non-provider 경로에도 영향을 줄 수 있으므로, provider-pool 경로 전용 lenient decode를 명확히 분리하는 것이 안전하다고 판단했다.

## 주요 설계 결정

- **Envelope-first 구조**: `decodeChatCompletionRequest` 호출 전 `decodeChatCompletionEnvelope`로 routing envelope(model, metadata, stream)를 먼저 lenient decode한다. provider-pool 경로에서만 `decodeChatCompletionRequestLenient`를 사용하고, non-provider/legacy 경로는 기존 strict `decodeChatCompletionRequest`를 유지한다.
- **Lenient decode 검증 재사용**: `decodeChatCompletionRequestLenient`는 known-field allowlist 검증과 value validation(max_tokens, temperature 등)은 유지하되 unknown top-level field를 거부하지 않는다.
- **함수 분리 선택**: PLAN에서 두 번째 후보로 언급된 `decodeChatCompletionRequest(..., strictUnknown bool)` 대신 `decodeChatCompletionRequestLenient`를 별도 함수로 만들었다. 기존 strict decode를 수정하면 non-provider 경로에도 영향을 줄 수 있으므로, provider-pool 경로 전용 lenient decode를 명확히 분리했다.
- **ResponseMode selector 위치 유지**: envelope decode → route resolution → provider-pool responseMode 거부 순서가 provider-pool dispatch 전에 유지된다.
- **테스트 isolation**: tunnel path 테스트는 `tunnelServedTarget: "served-model"`을 설정하여 model rewrite 검증 가능. normalized path 테스트는 `poolDispatchPath: "normalized"`로 Ollama 경로 선택.

## 사용자 리뷰 요청

_기본값은 `없음`이다. 구현 중 새 결정이 필요해 보여도 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 이 섹션은 선택된 Milestone `구현 잠금 > 결정 필요` 항목이 실구현을 차단할 때만 채운다. 외부 환경/secret/서비스 준비, 검증 증거 공백, 반복 실패, 일반 범위 조정은 사용자 리뷰 요청이 아니며 `검증 결과`, `계획 대비 변경 사항`, 또는 code-review의 일반 follow-up plan으로 처리한다._

- 상태: 없음
- 사유 유형: 없음
- 연결 대상: 없음
- 결정 필요: 없음
- 차단 근거: 없음
- 실행한 검증/명령: 없음
- 자동 후속 불가 이유: 없음
- 재개 조건: 없음

## 리뷰어를 위한 체크포인트

- provider-pool Chat route만 lenient unknown-field decode를 사용하고 legacy/non-provider strict decode는 유지되는가.
- tunnel path의 raw body는 model rewrite와 catalog policy 외 unknown field를 보존하는가.
- normalized path는 unknown provider-specific field를 native `RunRequest.Input`에 임의 전파하지 않는가.

## 검증 결과

_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._

### SURFACE_FIELD-1 중간 검증
```bash
$ GOCACHE=/tmp/iop-go-cache go test ./apps/edge/internal/openai -run 'ProviderPool.*Unknown|ProviderPoolRejectsResponseMode' -count=1
ok  	ip/apps/edge/internal/openai	0.008s
```

### SURFACE_FIELD-2/3 중간 검증 (새 테스트)
```bash
$ GOCACHE=/tmp/iop-go-cache go test ./apps/edge/internal/openai -run 'ProviderPoolTunnelPreservesUnknownFields|ProviderPoolNormalizedIgnoresUnknownFields' -count=1 -v
=== RUN   TestChatCompletionsProviderPoolTunnelPreservesUnknownFields
--- PASS: TestChatCompletionsProviderPoolTunnelPreservesUnknownFields (0.00s)
=== RUN   TestChatCompletionsProviderPoolNormalizedIgnoresUnknownFields
--- PASS: TestChatCompletionsProviderPoolNormalizedIgnoresUnknownFields (0.00s)
PASS
ok  	ip/apps/edge/internal/openai	0.007s
```

### 최종 검증
```bash
$ GOCACHE=/tmp/iop-go-cache go test ./apps/edge/internal/openai -run 'ProviderPool.*Unknown|ProviderPoolRejectsResponseMode|MixedProviderPool' -count=1
ok  	ip/apps/edge/internal/openai	0.009s

$ GOCACHE=/tmp/iop-go-cache go test ./apps/edge/internal/openai -count=1
ok  	ip/apps/edge/internal/openai	6.783s
```

전체 100+ 테스트 통과. 새 regression test 2개 포함.

---

> **[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.

## 코드리뷰 결과

- 종합 판정: PASS
- 차원별 평가:
  - correctness: Pass
  - completeness: Pass
  - test coverage: Pass
  - API contract: Pass
  - code quality: Pass
  - implementation deviation: Pass
  - verification trust: Pass
  - spec conformance: Pass
- 발견된 문제: 없음
- 리뷰 검증:
  - `GOCACHE=/tmp/iop-go-cache go test ./apps/edge/internal/openai -run 'ProviderPool.*Unknown|ProviderPoolRejectsResponseMode|MixedProviderPool' -count=1` - PASS; `ok iop/apps/edge/internal/openai 0.009s`
  - `GOCACHE=/tmp/iop-go-cache go test ./apps/edge/internal/openai -run 'ProviderPoolTunnelPreservesUnknownFields|ProviderPoolNormalizedIgnoresUnknownFields' -count=1 -v` - PASS; 두 신규 regression test 통과, `ok iop/apps/edge/internal/openai 0.008s`
  - `GOCACHE=/tmp/iop-go-cache go test ./apps/edge/internal/openai -count=1` - PASS; `ok iop/apps/edge/internal/openai 6.781s`
- 다음 단계: PASS - active plan/review를 `.log`로 아카이브하고 `complete.log` 작성 후 task directory를 archive로 이동한다.
