<!-- task=02+01_edge_dispatch plan=0 tag=API -->
# Plan - API

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

구현이 끝났다고 보고하기 전에 반드시 `CODE_REVIEW-local-G07.md`의 구현 에이전트 소유 섹션을 실제 변경 내용과 검증 출력으로 채운다. 검증 명령을 실행하고 실제 stdout/stderr를 남기며, active plan/review 파일은 그대로 둔 상태에서 review-ready만 보고한다. 선택된 SDD 결정이나 선택된 Milestone `구현 잠금 > 결정 필요` 항목이 구현을 차단할 때만 review stub의 `사용자 리뷰 요청` 섹션을 채우고 중단한다. 구현 에이전트는 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하거나 `USER_REVIEW.md`, `complete.log`, archive log를 작성하지 않는다. 환경/secret/서비스 준비, 일반 범위 조정, 검증 증거 공백은 그 자체로 사용자 리뷰 요청이 아니다.

## 배경

Provider pool schema가 준비되면 Edge dispatch는 canonical model alias를 유지한 채 provider 후보를 고르고, 선택된 provider가 제공하는 실제 served model로 target을 rewrite해야 한다. 현재 queue는 `adapter + target`이 같은 첫 available 후보를 고르는 구조라 provider id별 load ratio 비교와 deterministic tie-break를 표현하지 못한다. 이 계획은 schema foundation 이후 runtime dispatch policy만 다룬다.

## 사용자 리뷰 요청 흐름

선택된 SDD 결정 또는 선택된 Milestone lock 결정이 실제 구현을 차단할 때만 `CODE_REVIEW-local-G07.md`의 `사용자 리뷰 요청` 섹션에 `agent-ops/skills/common/_templates/implementation-user-review-request-section.md` 형식으로 근거를 채운다. 구현 중 직접 사용자에게 묻거나 채팅 선택지를 만들지 않는다. code-review 단계가 사용자 리뷰 요청의 타당성을 검증하고 실제 `USER_REVIEW.md` 작성 여부를 결정한다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/operational-observability-provider-management/milestones/provider-catalog-device-status.md`
- Task ids:
  - `target-rewrite`: Edge가 선택 provider의 served model을 concrete target으로 rewrite해 Node에 전달
  - `selection-policy`: available provider 중 최저 `in_flight / capacity` load ratio 우선 선택, deterministic tie-break, queue timeout 적용
- Completion mode: check-on-pass

## Spec Targets

- SDD: `agent-roadmap/sdd/operational-observability-provider-management/provider-catalog-device-status/SDD.md`
- Acceptance Scenarios:
  - `S03`: Edge-owned target rewrite
  - `S04`: load-ratio provider selection policy
- Evidence Map:
  - `S03`, `S04`는 이 task directory의 `complete.log`에 Roadmap Completion과 Spec Completion 근거가 있어야 한다.

## 분석 결과

- 읽은 규칙/계약: `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/roadmap-sdd/SKILL.md`, `agent-ops/skills/common/update-roadmap/SKILL.md`, `agent-contract/index.md`, `agent-contract/provided/openai-compatible-api.md`.
- 읽은 Roadmap/SDD: `agent-roadmap/current.md`, active Phase, active Milestone, 승인된 SDD. SDD는 `[승인됨]`, SDD 잠금은 `해제`, active `USER_REVIEW.md`는 없다.
- 읽은 domain/test rules: edge, platform-common, testing, `agent-test/local/rules.md`, `agent-test/local/edge-smoke.md`, `agent-test/local/platform-common-smoke.md`.
- 읽은 source: `packages/go/config/config.go`, `apps/edge/internal/openai/chat_handler.go`, `apps/edge/internal/openai/responses_handler.go`, `apps/edge/internal/openai/routes.go`, `apps/edge/internal/openai/server.go`, `apps/edge/internal/service/run_dispatch.go`, `apps/edge/internal/service/model_queue.go`, `apps/edge/internal/service/status_provider.go`, `proto/iop/runtime.proto`, `configs/edge.yaml`, `apps/edge/README.md`.
- 읽은 tests: `packages/go/config/config_test.go`, `apps/edge/internal/openai/server_test.go`, `apps/edge/internal/service/model_queue_test.go`, `apps/edge/internal/service/service_internal_test.go`, `apps/edge/internal/service/service_test.go`, `apps/edge/internal/service/status_provider_test.go`.
- 기존 커버리지: OpenAI handler가 client model을 `ModelGroupKey`로 유지하는 경로, route별 adapter/target dispatch, queue timeout/admission/release는 테스트가 있다. Provider id 후보 선택, selected provider target rewrite, load-ratio ranking, tie-break ordering은 테스트가 없다.
- 분할 판단: 이 subtask는 `02+01_edge_dispatch`로 predecessor `01_pool_schema`에 의존한다. 현재 같은 task group 안에 `agent-task/m-provider-catalog-device-status/01_pool_schema/PLAN-local-G06.md`와 `CODE_REVIEW-local-G06.md`가 active로 존재하므로 predecessor completion은 미충족이다. 구현은 `01_pool_schema/complete.log`가 PASS로 생성된 뒤 시작한다.
- 의존 관계 및 구현 순서: `01_pool_schema`가 `models[]`, `nodes[].providers[]`, provider snapshot/status field를 확정한 다음, 이 plan이 그 type을 소비한다. predecessor가 plan 대비 다른 field 이름을 채택했다면 이 plan의 변경 지점을 그 실제 type에 맞추고 `계획 대비 변경 사항`에 기록한다.
- 심볼 참조: 기존 symbol rename/remove는 계획하지 않는다. `SubmitRunRequest`, `candidateNode`, `modelQueueGroup`, `resolveQueueCandidates`, `resolveRouteDispatch`, `BuildRunRequest`에는 field 추가와 selection path 분기가 필요하다.
- dependency manifest: 새 외부 package는 추가하지 않는다.
- pre-check compile: queue candidate에 provider id와 served target을 싣고, 선택 시점 이후 `RunRequest.Target`에 concrete served model을 넣어야 기존 Node adapter boundary를 유지할 수 있다.
- build grade: local-G07. Edge OpenAI input, service dispatch, queue policy, status를 함께 변경하지만 local Go tests로 반복 검증할 수 있다.

## 구현 체크리스트

- [ ] `apps/edge/internal/openai` handler/route resolution에서 canonical client model alias를 유지하고 provider pool 대상인지 service layer에 전달한다.
- [ ] `apps/edge/internal/service/run_dispatch.go`의 candidate resolution이 `models[].providers`와 `nodes[].providers[]`를 기준으로 provider 후보를 만들도록 확장한다.
- [ ] Queue candidate/model group에 provider id, node id, concrete served target, capacity/in-flight/queued 상태를 보존하고 선택 후 Node 요청은 selected provider의 target으로 rewrite한다.
- [ ] `apps/edge/internal/service/model_queue.go`의 available candidate selection을 lowest `in_flight / capacity` 우선, deterministic tie-break 순서로 바꾸고 queue timeout 동작을 유지한다.
- [ ] status/provider snapshot에 선택 결과가 반영되도록 in-flight/queued/load-ratio 업데이트 경로를 보강한다.
- [ ] OpenAI/service/model_queue/status tests에 provider pool dispatch, target rewrite, load-ratio ranking, deterministic tie-break, timeout 회귀를 추가한다.
- [ ] 중간 및 최종 검증 명령을 실행하고 결과를 `CODE_REVIEW-local-G07.md`에 붙여 넣는다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## API-1 - Provider pool route resolution

### 문제

`apps/edge/internal/openai/chat_handler.go`와 `responses_handler.go`는 request model을 `ModelGroupKey`로 유지하지만, `resolveRouteDispatch`는 legacy route의 단일 target만 반환한다. Provider pool에서는 외부 alias와 concrete served target이 분리되어야 한다.

### 변경

Provider pool model alias가 존재하면 OpenAI handler는 client model alias를 canonical key로 유지하고 service layer에는 provider-pool resolution을 요청한다. Legacy `openai.model_routes`는 provider pool alias가 없을 때 fallback으로 유지한다. `/v1/models`는 provider pool `models[].id`를 우선 노출하고, 없으면 legacy catalog를 노출한다.

### 중간 검증

```bash
go test ./apps/edge/internal/openai -run 'TestOpenAI.*Model|TestOpenAI.*Route|TestOpenAI.*Provider' -count=1
```

기대 결과: 기존 route tests와 새 provider pool catalog/dispatch tests가 모두 통과한다.

## API-2 - Candidate selection and target rewrite

### 문제

`resolveQueueCandidates`는 adapter/target 일치 여부로 후보를 좁히고, `model_queue.go`는 available 후보 중 첫 번째 후보를 선택한다. 이 구조는 provider id별 served model rewrite, provider capacity comparison, deterministic tie-break를 표현하지 못한다.

### 변경

`candidateNode`에 provider id, provider category/type, served target, load metric source를 추가한다. Provider pool request는 `models[].providers` 순서와 provider id reference를 기준으로 후보를 만들고, queue admission은 available 후보의 `in_flight / capacity`가 가장 낮은 provider를 선택한다. 동률은 provider id, node id, adapter/name 같은 stable key로 정렬해 deterministic하게 처리한다. 선택 후 `BuildRunRequest` 직전 `req.Target` 또는 선택 결과 target을 concrete served model name으로 바꾼다.

### 중간 검증

```bash
go test ./apps/edge/internal/service -run 'TestModelQueue.*Provider|TestSubmitRun.*Provider|TestService.*Provider|TestBuildRunRequest' -count=1
```

기대 결과: provider pool request가 lowest load-ratio provider를 선택하고, Node RunRequest target이 selected provider served model로 rewrite된다.

## API-3 - Queue timeout and status projection

### 문제

Queue timeout과 provider snapshot은 legacy adapter/target group 기준으로 검증되어 있다. Provider pool selection이 들어가도 queued/in-flight accounting과 timeout이 alias-level queue key와 provider-level status에 일관되게 반영되어야 한다.

### 변경

Queue group key는 `models[].id` canonical alias를 사용하고, provider-level in-flight/queued/load ratio projection은 선택 provider와 queued group 상태에서 계산한다. 기존 adapter snapshot field는 compatibility를 위해 유지한다.

### 중간 검증

```bash
go test ./apps/edge/internal/service -run 'TestModelQueue.*Timeout|TestProviderStatus|TestStatusProvider' -count=1
```

기대 결과: timeout tests가 유지되고 provider status에 in-flight/queued/load-ratio가 반영된다.

## 최종 검증

```bash
go test ./apps/edge/internal/openai ./apps/edge/internal/service -count=1
git diff --check
```

기대 결과: OpenAI input/service queue/status 관련 Go tests가 통과하고 whitespace error가 없다.

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