<!-- task=m-node-multi-target-serving-foundation/03+01_route_catalog plan=1 tag=REVIEW_API -->

# Plan - REVIEW_API

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

이 plan은 `code_review_cloud_G08_0.log`의 Required 이슈만 해결한다. 구현 완료 전 `CODE_REVIEW-cloud-G08.md`의 구현 에이전트 소유 섹션을 실제 변경 내용과 검증 출력으로 채운다. 사용자 전용 blocker는 review stub의 `사용자 리뷰 요청`에 기록하고, 직접 질문이나 `USER_REVIEW.md` 작성은 하지 않는다.

## 배경

1차 구현은 OpenAI route catalog의 정상 dispatch와 legacy fallback을 추가했지만, 중복 external model id가 first-match로 조용히 처리되고, `target`이 없는 route가 `/v1/models`에 노출될 수 있다. 이는 route catalog가 Edge-owned source of truth라는 계약과 계획의 ambiguous route 테스트 요구를 충족하지 못한다.

## 사용자 리뷰 요청 흐름

구현 중 차단은 active review stub에 기록한다. code-review가 blocker 타당성과 후속 상태를 결정한다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/inference-provider-extension/milestones/node-multi-target-serving-foundation.md`
- Task ids:
  - `route-catalog`: Edge OpenAI-compatible model catalog가 외부 `model`을 Edge-owned route table로 해석하고, 고정 `openai.target` 단일 override 없이 내부 `adapter + target`으로 변환한다.
- Completion mode: check-on-pass

## 분석 결과

### 읽은 파일

- `agent-task/m-node-multi-target-serving-foundation/03+01_route_catalog/code_review_cloud_G08_0.log`
- `agent-task/m-node-multi-target-serving-foundation/03+01_route_catalog/plan_local_G08_0.log`
- `packages/go/config/config.go`
- `packages/go/config/config_test.go`
- `apps/edge/internal/openai/routes.go`
- `apps/edge/internal/openai/chat_handler.go`
- `apps/edge/internal/openai/server_test.go`
- `configs/edge.yaml`

### 테스트 환경 규칙

- test_env: `local`
- 변경 범위는 `packages/go/config`와 `apps/edge/internal/openai`이다. 변경 패키지 테스트를 필수로 실행하고, 전체 edge 범위는 기존 service/transport 실패가 남을 수 있으므로 실제 출력과 함께 기록한다.

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

- duplicate `openai.model_routes[*].model`가 config load 또는 dispatch 단계에서 실패하는 테스트가 없다.
- `target`이 빈 route entry가 `/v1/models`에 노출되지 않거나 config load에서 거부되는 테스트가 없다.

### 심볼 참조

- `OpenAIRouteEntry`, `EdgeOpenAIConf.ModelRoutes`, `LoadEdge`, `handleModels`, `resolveRoute`, `resolveRouteDispatch`.

### 범위 결정 근거

- 이번 follow-up은 route catalog 계약 보강만 다룬다. provider별 profile, service dispatch 구조 변경, live field smoke는 후속 milestone task 범위로 남긴다.

### 빌드 등급

- build: `local-G08`, review: `cloud-G08`. 이슈는 deterministic config/API contract 보강이지만, OpenAI-compatible route catalog 계약을 닫는 리뷰이므로 기존 review lane을 유지한다.

## 구현 체크리스트

- [ ] duplicate external model id가 ambiguous route로 조용히 처리되지 않도록 검증 또는 오류 처리를 추가한다.
- [ ] target이 없는 route entry가 `/v1/models`와 dispatch 계약을 어긋나게 만들지 않도록 검증 또는 필터링 기준을 일치시킨다.
- [ ] ambiguous route와 invalid route 테스트를 추가한다.
- [ ] 최종 검증 명령을 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [REVIEW_API-1] Ambiguous Route Guard

- 문제: `apps/edge/internal/openai/chat_handler.go:154`의 `resolveRoute`는 duplicate model route를 first-match로 선택하고, `apps/edge/internal/openai/routes.go:29`는 duplicate model id를 그대로 노출할 수 있다.
- 해결 방법: 권장안은 `packages/go/config.LoadEdge`에서 trim한 `openai.model_routes[*].model`이 비어 있거나 중복이면 오류를 반환하는 것이다. HTTP handler 단계에서 처리한다면 resolver가 ambiguity를 오류로 반환하고 chat/responses 양쪽이 같은 오류 응답을 쓰게 한다.
- 수정 파일 및 체크리스트:
  - [ ] `packages/go/config/config.go`: route catalog model id 중복 검증 추가.
  - [ ] `packages/go/config/config_test.go`: duplicate model route load 실패 테스트 추가.
  - [ ] `apps/edge/internal/openai/server_test.go`: 필요한 경우 duplicate route가 노출/dispatch되지 않는 OpenAI surface 테스트 추가.
- 테스트 작성: 추가한다. 최소한 `LoadEdge`가 duplicate `model_routes` model id를 실패시키는 테스트가 필요하다.
- 중간 검증:
  - `go test -count=1 ./packages/go/config`

### [REVIEW_API-2] Invalid Route Target Guard

- 문제: `apps/edge/internal/openai/routes.go:29`는 `model`만 있으면 catalog model로 광고하지만, `apps/edge/internal/openai/chat_handler.go:161`는 `target`이 빈 route를 skip한다. 광고된 catalog model이 catalog route로 실행되지 않는 불일치가 생긴다.
- 해결 방법: 권장안은 `packages/go/config.LoadEdge`에서 `model_routes[*].target` 빈 값을 오류로 거부하는 것이다. config에서 거부하지 않는 방식을 택한다면 `/v1/models`와 resolver가 동일한 유효성 기준을 공유하고, target 없는 route가 광고되지 않음을 테스트한다.
- 수정 파일 및 체크리스트:
  - [ ] `packages/go/config/config.go`: route catalog target 필수 검증 추가.
  - [ ] `packages/go/config/config_test.go`: empty target route load 실패 테스트 추가.
  - [ ] `apps/edge/internal/openai/routes.go` 또는 `chat_handler.go`: config 검증만으로 부족한 runtime 불일치가 없도록 정리.
  - [ ] `apps/edge/internal/openai/server_test.go`: 필요한 경우 `/v1/models`와 dispatch 유효성 기준 일치 테스트 추가.
- 테스트 작성: 추가한다. 최소한 `LoadEdge`가 empty `model_routes[*].target`을 실패시키는 테스트가 필요하다.
- 중간 검증:
  - `go test -count=1 ./packages/go/config ./apps/edge/internal/openai`

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `packages/go/config/config.go` | REVIEW_API-1, REVIEW_API-2 |
| `packages/go/config/config_test.go` | REVIEW_API-1, REVIEW_API-2 |
| `apps/edge/internal/openai/routes.go` | REVIEW_API-2 |
| `apps/edge/internal/openai/chat_handler.go` | REVIEW_API-1, REVIEW_API-2 |
| `apps/edge/internal/openai/server_test.go` | REVIEW_API-1, REVIEW_API-2 |

## 최종 검증

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

전체 edge 범위에서 기존 service/transport 실패가 남으면 정확한 실패 테스트 이름을 검증 결과에 기록하고, 변경 패키지 테스트가 통과했는지 별도로 명시한다.
