<!-- task=openai_passthrough_model_groups plan=0 tag=API -->

# OpenAI-Compatible Model Groups Passthrough Plan

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

`CODE_REVIEW-local-G06.md`의 구현 에이전트 소유 섹션을 채우는 것이 구현의 마지막 단계다. 코드를 수정하고 검증을 실행한 뒤 실제 변경 내용, 설계 결정, stdout/stderr를 review stub에 기록하고 active 파일을 그대로 둔 채 review ready로 보고한다. 최종 판정, `.log` rename, `complete.log`, task archive 이동은 code-review-skill 전용이다. 선택된 Milestone `구현 잠금 > 결정 필요` 항목이 실구현을 막을 때만 review stub의 `사용자 리뷰 요청` 섹션을 채우고 멈춘다. 직접 사용자에게 질문하거나 선택지를 제시하거나 `request_user_input`, `USER_REVIEW.md`, `complete.log`, archive 이동을 수행하지 않는다. 환경/secret/service blocker, 일반 범위 조정, 검증 증거 공백은 사용자 리뷰 요청이 아니라 검증 결과나 후속 plan으로 처리한다.

## 배경

OpenAI-compatible provider model group은 계약상 provider raw tunnel passthrough가 기본 경로여야 한다. 현재 Chat Completions는 provider-pool 모델에서도 `metadata.iop_response_mode="transformed"`가 들어오면 normalized `SubmitRun` 경로로 들어가고, `/v1/responses` provider-pool 모델은 raw parity가 없어서 항상 normalized path를 탄다. 배포 설정이 `models[]`/`nodes[].providers[]`로 OpenAI-compatible model group을 묶는 순간, 이 연결고리를 끊지 않으면 기존 passthrough 운영이 다시 normalized 경로로 회귀할 수 있다.

## 사용자 리뷰 요청 흐름

구현 중 사용자 리뷰 요청은 선택된 Milestone lock 결정이 실제 구현을 차단할 때만 `CODE_REVIEW-local-G06.md`의 `사용자 리뷰 요청` 섹션에 기록한다. 직접 채팅 질문은 금지하며, code-review가 정당성을 검증한 뒤 필요한 경우에만 `USER_REVIEW.md`를 작성한다.

## 분석 결과

### 읽은 파일

- `agent-ops/rules/project/rules.md`
- `agent-ops/rules/common/rules-roadmap.md`
- `agent-ops/skills/common/router.md`
- `agent-ops/skills/common/plan/SKILL.md`
- `agent-ops/skills/common/_templates/implementation-user-review-request-section.md`
- `agent-ops/rules/project/domain/edge/rules.md`
- `agent-ops/rules/project/domain/node/rules.md`
- `agent-ops/rules/project/domain/platform-common/rules.md`
- `agent-ops/rules/project/domain/testing/rules.md`
- `agent-test/local/rules.md`
- `agent-test/local/edge-smoke.md`
- `agent-test/local/node-smoke.md`
- `agent-test/local/platform-common-smoke.md`
- `agent-roadmap/current.md`
- `agent-roadmap/ROADMAP.md`
- `agent-roadmap/phase/routing-policy-model-orchestration/PHASE.md`
- `agent-roadmap/phase/routing-policy-model-orchestration/milestones/openai-compatible-hybrid-routing-context-optimization.md`
- `agent-contract/index.md`
- `agent-contract/outer/openai-compatible-api.md`
- `agent-contract/inner/edge-config-runtime-refresh.md`
- `agent-contract/inner/edge-node-runtime-wire.md`
- `apps/edge/internal/openai/chat_handler.go`
- `apps/edge/internal/openai/responses_handler.go`
- `apps/edge/internal/openai/stream.go`
- `apps/edge/internal/openai/server.go`
- `apps/edge/internal/openai/routes.go`
- `apps/edge/internal/openai/run_result.go`
- `apps/edge/internal/openai/server_test.go`
- `apps/edge/internal/service/run_dispatch.go`
- `apps/edge/internal/service/service.go`
- `apps/edge/internal/service/service_test.go`
- `apps/edge/internal/service/model_queue.go`
- `packages/go/config/config.go`
- `configs/edge.yaml`
- `scripts/e2e-openai-ollama.sh`
- `Makefile`

### SDD 기준

not applicable. 활성 Milestone `agent-roadmap/phase/routing-policy-model-orchestration/milestones/openai-compatible-hybrid-routing-context-optimization.md`는 `[스케치]`이고 `구현 잠금: 잠금`이라 Roadmap Targets로 선택하지 않는다. 이 plan은 해당 Milestone 기능 Task 완료가 아니라 OpenAI-compatible provider passthrough 회귀 차단 hotfix다.

### 테스트 환경 규칙

- test_env: `local`
- 읽은 env rule: `agent-test/local/rules.md`
- 매칭 profile: `agent-test/local/edge-smoke.md`, `agent-test/local/platform-common-smoke.md`
- 참고 profile: `agent-test/local/node-smoke.md`
- 적용 명령: 변경 패키지 `go test`, Edge OpenAI-compatible 입력 표면 검증, 가능한 local full-cycle smoke.
- fallback source: `Makefile`, `scripts/e2e-openai-ollama.sh`
- 사전 확인:
  - runner: local shell
  - repo root/workdir: `/config/workspace/iop`
  - branch/HEAD: `main`, `e3053b1`
  - dirty state: 분석 시점 변경 없음
  - source sync: `main...origin/main`
  - Go: `/config/.local/bin/go`, `go version go1.26.2 linux/arm64`
  - Make: `/config/.local/bin/make`, `GNU Make 4.3`
  - local smoke: `make test-openai-ollama`는 temp dir과 fake Ollama HTTP server, random local ports를 사용하며 외부 credential이 필요 없다.

### 테스트 커버리지 공백

- Chat Completions provider-pool omitted/passthrough mode가 tunnel을 쓰는 테스트는 있다: `apps/edge/internal/openai/server_test.go:4041`.
- 현재 `TestChatCompletionsResponseModeRouting`은 provider-pool `transformed`가 normalized `SubmitRun`을 타는 것을 기대한다: `apps/edge/internal/openai/server_test.go:4108`. 요구사항과 반대라 회귀 테스트로 바꿔야 한다.
- Chat provider-pool generation policy 테스트는 `transformed` normalized input을 검사한다: `apps/edge/internal/openai/server_test.go:5305`. passthrough tunnel body rewrite 검사로 바꿔야 한다.
- `/v1/responses` provider-pool 테스트는 normalized `SubmitRun`을 성공 경로로 검증한다: `apps/edge/internal/openai/server_test.go:5438`. provider route에서는 raw parity 전까지 거부하는 테스트로 바꿔야 한다.
- dev-corp live runtime에서 Edge-only passthrough 전환은 Node runtime이 같은 ProviderTunnelRequest 계약을 처리하지 못하면 Chat Completions 요청이 provider response frame 없이 대기할 수 있다. local fake tunnel tests만으로는 stale Node artifact나 Edge/Node ref mismatch를 검증하지 못하므로 배포 전 runtime compatibility gate가 별도로 필요하다.

### 심볼 참조

none. 이름 변경이나 제거는 계획하지 않는다.

### 분할 판단

split decision policy를 먼저 평가했다. API contract 문구, OpenAI handler guard, 기존 OpenAI server tests가 하나의 외부 API behavior를 고정하므로 단일 plan이 더 안전하다. service provider tunnel queue는 이미 `SubmitProviderTunnel` provider-pool admission을 공유하므로 foundation/API split이 필요 없다. node adapter/proto/config schema 변경도 없어서 별도 ownership split이 없다.

### 범위 결정 근거

- 포함: Chat Completions provider tunnel route에서 explicit `transformed` normalized path 차단, `/v1/responses` provider tunnel route normalized dispatch 차단, 계약 문서와 OpenAI server tests 갱신.
- 포함: dev-corp 배포는 Edge-only 금지로 기록하고, Edge/Node ProviderTunnel 계약을 같은 source ref로 맞추는 후속 rollout plan 필요성을 명시한다.
- 제외: provider raw tunnel wire/proto 변경, Node adapter 구현, provider-pool admission/queue 변경, `/v1/responses` raw passthrough parity 구현, 하이브리드 라우팅 Milestone 구현, dev-corp Edge/Node 실제 배포.
- legacy CLI/normal adapter 경로의 normalized `SubmitRun`은 유지한다.
- `ollama` legacy route의 `/v1/responses` normalized 경로는 이 plan에서 바꾸지 않는다. 현재 tunnel provider route 판정은 `ProviderPool`, `openai_compat`, `vllm`에 한정되어 있다.

### 빌드 등급

`local-G06`: 외부 API behavior와 계약을 바꾸지만 변경 지점은 OpenAI handler/tests/contract로 좁고 local fake-provider smoke로 검증 가능하다.

## 구현 체크리스트

- [ ] [API-1] Chat Completions provider tunnel route에서 `metadata.iop_response_mode="transformed"`가 normalized `SubmitRun`으로 들어가지 못하게 거부하고, passthrough/sideband tunnel 동작과 generation policy body rewrite 테스트를 갱신한다.
- [ ] [API-2] `/v1/responses` provider tunnel route가 raw parity 없이 normalized `SubmitRun`으로 들어가지 못하게 거부하고, 계약 문서와 provider-pool Responses 테스트를 갱신한다.
- [ ] [API-3] dev-corp 배포는 Edge-only로 진행하지 않도록 제한을 기록하고, Edge/Node ProviderTunnel 계약 동기화와 coordinated rollout smoke를 별도 후속 plan으로 남긴다.
- [ ] 최종 검증 명령을 실행하고 결과를 `CODE_REVIEW-local-G06.md`에 실제 stdout/stderr로 기록한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [API-1] Chat Completions Provider Groups Must Not Use Transformed

#### 문제

`apps/edge/internal/openai/chat_handler.go:77`의 switch는 passthrough와 sideband일 때만 tunnel로 보내고, `apps/edge/internal/openai/chat_handler.go:116`의 transformed branch는 provider-pool route도 normalized `SubmitRun`으로 보낸다. 기존 테스트도 `apps/edge/internal/openai/server_test.go:4108`에서 이 동작을 기대한다.

Before:

```go
// apps/edge/internal/openai/chat_handler.go:77
switch {
case responseMode == responseModePassthrough && routeUsesProviderTunnel(dispatch):
	estimate := s.estimateChatInputTokens(basePrompt, runMeta, req.Tools, req.ToolChoice)
	contextClass := classifyContext(estimate, s.longContextThreshold())
	s.tunnelChatCompletionPassthrough(w, r, req, dispatch, runMeta, rawBody, estimate, contextClass)
	return
case responseMode == responseModePassthroughSideband && routeUsesProviderTunnel(dispatch):
	estimate := s.estimateChatInputTokens(basePrompt, runMeta, req.Tools, req.ToolChoice)
	contextClass := classifyContext(estimate, s.longContextThreshold())
	s.tunnelChatCompletionPassthroughSideband(w, r, req, dispatch, runMeta, rawBody, estimate, contextClass)
	return
}
```

#### 해결 방법

provider tunnel route는 `passthrough`와 `passthrough+sideband`만 허용한다. `transformed`가 명시되면 400으로 거부하고 `SubmitRun`과 `SubmitProviderTunnel` 모두 호출하지 않는다.

After:

```go
if routeUsesProviderTunnel(dispatch) && responseMode == responseModeTransformed {
	writeError(w, http.StatusBadRequest, "invalid_request_error", "metadata.iop_response_mode=transformed is not supported for OpenAI-compatible provider model groups")
	return
}
switch {
case responseMode == responseModePassthrough && routeUsesProviderTunnel(dispatch):
	estimate := s.estimateChatInputTokens(basePrompt, runMeta, req.Tools, req.ToolChoice)
	contextClass := classifyContext(estimate, s.longContextThreshold())
	s.tunnelChatCompletionPassthrough(w, r, req, dispatch, runMeta, rawBody, estimate, contextClass)
	return
case responseMode == responseModePassthroughSideband && routeUsesProviderTunnel(dispatch):
	estimate := s.estimateChatInputTokens(basePrompt, runMeta, req.Tools, req.ToolChoice)
	contextClass := classifyContext(estimate, s.longContextThreshold())
	s.tunnelChatCompletionPassthroughSideband(w, r, req, dispatch, runMeta, rawBody, estimate, contextClass)
	return
}
```

Generation policy는 tunnel `BuildBody` 전에 계속 적용한다. `apps/edge/internal/openai/stream.go:363`의 `rewriteChatCompletionModel(rawBody, target, req)`가 mutated `req`의 `max_tokens`, `think`, `thinking_token_budget`를 raw provider body에 반영해야 한다.

#### 수정 파일 및 체크리스트

- [ ] `apps/edge/internal/openai/chat_handler.go`: provider tunnel route의 transformed mode 400 guard 추가.
- [ ] `apps/edge/internal/openai/server_test.go`: `TestChatCompletionsResponseModeRouting`의 transformed case를 reject/no tunnel/no run으로 변경.
- [ ] `apps/edge/internal/openai/server_test.go`: provider-pool generation policy tests를 normalized input 검사에서 tunnel body 검사로 변경.
- [ ] `apps/edge/internal/openai/server_test.go`: legacy `openai_compat` route도 explicit transformed를 normalized로 보내지 않는 회귀 case 추가.

#### 테스트 작성

테스트 작성/수정 필수. Bug fix이므로 회귀 테스트가 필요하다. 기존 `TestChatCompletionsResponseModeRouting`, `TestChatCompletionsProviderPoolAppliesGenerationPolicy`, `TestChatCompletionsProviderPoolThinkingPolicyOverridesStrictOutputDisable`, `TestChatCompletionsProviderPoolPreservesLargerGenerationPolicyValues`, `TestChatCompletionsPassthroughLegacyProviderRouteUsesTunnel`를 갱신한다.

#### 중간 검증

```bash
go test ./apps/edge/internal/openai -run 'TestChatCompletions(ResponseModeRouting|ProviderPoolDispatch|PassthroughLegacyProviderRouteUsesTunnel|ProviderPoolAppliesGenerationPolicy|ProviderPoolThinkingPolicyOverridesStrictOutputDisable|ProviderPoolPreservesLargerGenerationPolicyValues)' -count=1
```

기대 결과: PASS. provider-pool/legacy provider route에서 transformed case는 normalized SubmitRun을 호출하지 않는다.

### [API-2] Responses Provider Groups Must Not Use Normalized Until Raw Parity Exists

#### 문제

`/v1/responses`는 raw passthrough parity가 별도 구현 대상이라고 계약에 적혀 있지만, `apps/edge/internal/openai/responses_handler.go:102`에서 provider-pool dispatch도 그대로 normalized `SubmitRun`을 호출한다. 기존 tests도 `apps/edge/internal/openai/server_test.go:5438`부터 provider-pool Responses normalized 성공 경로를 기대한다.

Before:

```go
// apps/edge/internal/openai/responses_handler.go:102
handle, err := s.service.SubmitRun(r.Context(), edgeservice.SubmitRunRequest{
	NodeRef:              dispatch.NodeRef,
	ModelGroupKey:        strings.TrimSpace(req.Model),
	Adapter:              dispatch.Adapter,
	Target:               dispatch.Target,
	SessionID:            dispatch.SessionID,
	Workspace:            workspace,
	Prompt:               prompt,
	Input:                input,
	TimeoutSec:           dispatch.TimeoutSec,
	MaxQueue:             dispatch.MaxQueue,
	QueueTimeoutMS:       dispatch.QueueTimeoutMS,
	Metadata:             runMeta,
	EstimatedInputTokens: estimate,
	ContextClass:         contextClass,
	ProviderPool:         dispatch.ProviderPool,
})
```

#### 해결 방법

Responses endpoint에서 `routeUsesProviderTunnel(dispatch)`가 true이면 raw parity 구현 전까지 400으로 거부한다. 이렇게 하면 OpenAI-compatible provider model group이 `/v1/responses`로 들어와도 normalized path에 연결되지 않는다. legacy CLI/일반 adapter Responses 경로는 유지한다.

After:

```go
if routeUsesProviderTunnel(dispatch) {
	writeError(w, http.StatusBadRequest, "invalid_request_error", "/v1/responses is not supported for OpenAI-compatible provider model groups until raw passthrough parity is implemented")
	return
}
```

이 guard는 `dispatch`/workspace validation 직후, provider-pool generation policy와 `SubmitRun` 구성 전에 둔다.

#### 수정 파일 및 체크리스트

- [ ] `apps/edge/internal/openai/responses_handler.go`: provider tunnel route guard 추가.
- [ ] `apps/edge/internal/openai/server_test.go`: provider-pool Responses dispatch/generation policy tests를 rejection/no SubmitRun 테스트로 갱신.
- [ ] `agent-contract/outer/openai-compatible-api.md`: Chat provider route에서 `transformed`는 더 이상 provider model group에 허용되지 않고, `/v1/responses` provider model group은 raw parity 전까지 거부됨을 명시.

#### 테스트 작성

테스트 작성/수정 필수. `TestResponsesProviderPoolDispatch`, `TestResponsesProviderPoolAppliesGenerationPolicy`, `TestResponsesProviderPoolThinkingPolicyOverridesStrictOutputDisable`를 provider route rejection 기준으로 갱신한다. legacy non-provider Responses tests는 유지한다.

#### 중간 검증

```bash
go test ./apps/edge/internal/openai -run 'TestResponsesProviderPool|TestResponsesDispatchesNonStreamingRequest|TestResponsesReturnsOpenAICompatibleShape' -count=1
```

기대 결과: PASS. provider-pool Responses는 400을 반환하고 `SubmitRun`을 호출하지 않으며, legacy Responses 경로는 계속 동작한다.

### [API-3] Dev-Corp Rollout Must Be Edge/Node Coordinated

#### 문제

dev-corp live runtime에서 Edge만 최신 passthrough 코드로 교체하면 `openai chat completion passthrough dispatch`까지는 들어가도, 연결된 Node runtime이 같은 ProviderTunnelRequest/ProviderTunnelFrame 계약을 처리하지 못하는 경우 Chat Completions 응답이 provider body 없이 대기할 수 있다. 이 plan의 local fake-provider tests는 handler 계약 회귀는 잡지만, 원격 Node artifact가 같은 source ref에서 빌드되었는지와 live Node가 tunnel frames를 반환하는지는 검증하지 못한다.

#### 해결 방법

G06 구현 완료 후 dev-corp 배포는 Edge-only로 진행하지 않는다. 별도 follow-up plan에서 같은 source ref 기준 Edge binary, macOS Node binary, Linux ARM64 Node binary를 함께 빌드/배포하고, live smoke로 omitted-mode passthrough, explicit sideband, explicit transformed 400, Node reconnect, provider snapshots 회복을 검증한다.

Follow-up plan 최소 범위:

```text
task_group: dev_corp_passthrough_rollout
scope:
- Edge/Node binaries built from the same G06 source ref
- dev-corp runtime artifact replacement for Edge and connected provider Nodes
- /v1/chat/completions omitted mode passthrough smoke
- metadata.iop_response_mode=passthrough+sideband smoke
- metadata.iop_response_mode=transformed rejection smoke
- Control Plane provider snapshots in_flight=0 queued=0 recovery check
```

#### 수정 파일 및 체크리스트

- [ ] `agent-task/openai_passthrough_model_groups/PLAN-local-G06.md`: Edge-only 배포 금지와 follow-up rollout 범위를 기록한다.
- [ ] `agent-task/openai_passthrough_model_groups/CODE_REVIEW-local-G06.md`: 구현 에이전트 체크리스트와 리뷰 포커스에 runtime compatibility gate를 반영한다.
- [ ] G06 구현 에이전트는 dev-corp 배포 요청이 이어질 경우 `dev_corp_passthrough_rollout` 후속 plan을 작성하거나, 이미 작성된 후속 plan 경로를 `CODE_REVIEW-local-G06.md`의 `계획 대비 변경 사항`에 기록한다.

#### 테스트 작성

테스트 코드는 작성하지 않는다. 이 항목은 local unit test로 검증 가능한 handler behavior가 아니라 live runtime rollout gate다. G06 code change 검증은 API-1/API-2 테스트가 담당하고, dev-corp runtime 검증은 후속 rollout plan의 smoke가 담당한다.

#### 중간 검증

```bash
git diff -- agent-task/openai_passthrough_model_groups/PLAN-local-G06.md agent-task/openai_passthrough_model_groups/CODE_REVIEW-local-G06.md
```

기대 결과: Edge-only 배포 금지, Edge/Node coordinated rollout 필요성, 후속 plan 최소 smoke 범위가 active plan/review에 명시되어 있다.

## 수정 파일 요약

| 파일 | 항목 |
|---|---|
| `apps/edge/internal/openai/chat_handler.go` | API-1 |
| `apps/edge/internal/openai/responses_handler.go` | API-2 |
| `apps/edge/internal/openai/server_test.go` | API-1, API-2 |
| `agent-contract/outer/openai-compatible-api.md` | API-2 |
| `agent-task/openai_passthrough_model_groups/PLAN-local-G06.md` | API-3 |
| `agent-task/openai_passthrough_model_groups/CODE_REVIEW-local-G06.md` | API-3 |

## 최종 검증

```bash
go test ./apps/edge/internal/openai -count=1
```

기대 결과: PASS. Go test cache는 허용하지 않으므로 `-count=1`을 사용한다.

```bash
go test ./apps/edge/... -count=1
```

기대 결과: PASS. Edge OpenAI-compatible 입력 표면과 인접 service/bootstrap test가 통과한다.

```bash
make test-openai-ollama
```

기대 결과: PASS. local fake Ollama 기반 OpenAI-compatible smoke가 통과하고 legacy Ollama 경로가 깨지지 않는다.

모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
