<!-- task=m-model-route-queue-policy-alignment/01_config_route_policy plan=0 tag=API -->

# Plan - API route queue policy contract

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

구현 완료 후 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채우는 것이 필수 최종 단계입니다. 아래 체크리스트와 검증 명령을 실제로 실행하고, 구현 내용과 stdout/stderr를 review stub에 붙이세요. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 충돌로 막히면 review stub의 `사용자 리뷰 요청`에 정확한 근거를 남기고 멈춥니다. 구현 에이전트는 사용자에게 직접 묻거나 선택지를 제시하거나 `USER_REVIEW.md`, `complete.log`, archive 로그를 만들지 않습니다. 명령 재실행이나 산출물 수집으로 메울 수 있는 증거 공백은 사용자 리뷰 요청이 아닙니다.

## 배경

Edge queue owner 원칙은 이미 있으나, group queue policy는 Node/provider adapter instance에서 파생되고 있다. model alias queue 정책은 `openai.model_routes[]`가 소유하고, Node/provider instance는 per-node `capacity`만 소유해야 다중 Node/provider pool에서 설정 위치가 명확하다. 이 선행 작업은 route config schema와 OpenAI request dispatch DTO를 먼저 확장한다.

## 사용자 리뷰 요청 흐름

구현 중 차단은 active review stub의 `사용자 리뷰 요청` 섹션에 `agent-ops/skills/common/_templates/implementation-user-review-request-section.md` 형식으로 기록한다. 직접 사용자 프롬프트는 금지되며, code-review가 USER_REVIEW 작성 여부를 판단한다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/inference-provider-extension/milestones/model-route-queue-policy-alignment.md`
- Task ids:
  - `config-schema`: `openai.model_routes[]`가 `max_queue`, `queue_timeout_ms`를 받을 수 있고 validation/default/fallback 문서가 정리되어 있다.
  - `dispatch-contract`: OpenAI Chat Completions와 Responses route resolution이 route-level queue policy를 `SubmitRunRequest`로 전달한다.
  - `docs-example`: `configs/edge.yaml` 예시가 model alias queue policy와 Node/provider `capacity`의 책임 경계를 보여준다.
- Completion mode: check-on-pass

## Spec Targets

- SDD: `agent-roadmap/sdd/inference-provider-extension/model-route-queue-policy-alignment/SDD.md`
- Acceptance scenarios:
  - `S01`: task=`config-schema`; evidence=`go test -count=1 ./packages/go/config ./apps/edge/internal/openai`
  - `S02`: task=`dispatch-contract`; evidence=`go test -count=1 ./apps/edge/internal/openai`
  - `S04`: task=`docs-example`; evidence=`git diff --check`와 `configs/edge.yaml` 예시 변경 리뷰
- Completion mode: spec-check-on-pass

## 분석 결과

### 읽은 파일

- `agent-roadmap/current.md`
- `agent-roadmap/phase/inference-provider-extension/PHASE.md`
- `agent-roadmap/phase/inference-provider-extension/milestones/model-route-queue-policy-alignment.md`
- `agent-roadmap/sdd/inference-provider-extension/model-route-queue-policy-alignment/SDD.md`
- `agent-test/local/rules.md`
- `agent-test/local/edge-smoke.md`
- `agent-test/local/platform-common-smoke.md`
- `agent-ops/rules/project/domain/edge/rules.md`
- `agent-ops/rules/project/domain/platform-common/rules.md`
- `packages/go/config/config.go`
- `packages/go/config/config_test.go`
- `configs/edge.yaml`
- `apps/edge/internal/openai/chat_handler.go`
- `apps/edge/internal/openai/responses_handler.go`
- `apps/edge/internal/openai/server_test.go`
- `apps/edge/internal/service/run_dispatch.go`

### 테스트 환경 규칙

- test_env: local.
- `agent-test/local/rules.md` present/read.
- Matched profiles read: `agent-test/local/edge-smoke.md`, `agent-test/local/platform-common-smoke.md`.
- 적용 명령: `go test -count=1 ./packages/go/config ./apps/edge/internal/openai`, `git diff --check`.
- config 계약 변경이므로 `packages/go/config` struct/default와 `configs/edge.yaml` 예시 일치를 확인한다. OpenAI-compatible 입력 표면 변경은 focused handler tests로 검증한다.

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

- `OpenAIRouteEntry`는 `model`, `node`, `adapter`, `target`, `session_id`, `timeout_sec`, `workspace_required`만 테스트한다. `max_queue`, `queue_timeout_ms` load/negative validation test가 없다.
- Chat/Responses route catalog tests는 adapter/target/model group key만 확인한다. route queue policy가 `SubmitRunRequest`로 전달되는 assertion이 없다.
- `configs/edge.yaml` 예시는 route queue policy와 provider capacity 책임 경계를 보여주지 않는다.

### 심볼 참조

- 제거/rename 없음.
- 추가 예정: `config.OpenAIRouteEntry.MaxQueue`, `config.OpenAIRouteEntry.QueueTimeoutMS`; `edgeservice.SubmitRunRequest.MaxQueue`, `edgeservice.SubmitRunRequest.QueueTimeoutMS`; `openai.routeDispatch.MaxQueue`, `openai.routeDispatch.QueueTimeoutMS`.
- call site: `apps/edge/internal/openai/chat_handler.go`, `apps/edge/internal/openai/responses_handler.go`, `apps/edge/internal/service/run_dispatch.go`.

### 분할 판단

- split decision policy evaluated. Multi-plan 선택.
- shared task group: `agent-task/m-model-route-queue-policy-alignment`.
- `01_config_route_policy`: config schema, route DTO, OpenAI handler propagation, docs example. 선행 없음.
- `02+01_service_queue_policy`: Edge service queue policy application. Depends on `01_config_route_policy` because service request fields must exist before behavior can consume them.

### 범위 결정 근거

- 포함: `packages/go/config`, `configs/edge.yaml`, `apps/edge/internal/openai`, service request DTO field addition only.
- 제외: `apps/edge/internal/service` queue behavior 변경은 sibling `02+01_service_queue_policy`가 담당한다.
- 제외: Node/provider runtime, proto, Control Plane, Client UI. 이번 계약은 Edge config와 OpenAI handler-local DTO 확장으로 충분하다.

### 빌드 등급

- build lane/grade: `local-G07`. Config schema + OpenAI surface DTO change이며 focused tests로 검증 가능하지만 public config contract와 roadmap SDD target을 포함해 중간 이상 review가 필요하다.

## 구현 체크리스트

- [ ] `OpenAIRouteEntry`에 `max_queue`, `queue_timeout_ms` 필드를 추가하고 non-negative validation을 붙인다.
- [ ] OpenAI route dispatch DTO와 Chat/Responses submit path가 route-level queue policy를 `SubmitRunRequest`로 전달한다.
- [ ] `configs/edge.yaml` 예시가 route-level queue policy와 Node/provider `capacity` 책임 경계를 보여준다.
- [ ] Focused config/OpenAI tests와 `git diff --check`를 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [API-1] Route config schema에 queue policy 필드 추가

#### 문제

`packages/go/config/config.go:90`의 `OpenAIRouteEntry`에는 route-level queue policy 필드가 없다.

```go
type OpenAIRouteEntry struct {
    Model             string `mapstructure:"model"              yaml:"model"`
    NodeRef           string `mapstructure:"node"               yaml:"node,omitempty"`
    Adapter           string `mapstructure:"adapter"            yaml:"adapter,omitempty"`
    Target            string `mapstructure:"target"             yaml:"target"`
    SessionID         string `mapstructure:"session_id"         yaml:"session_id,omitempty"`
    TimeoutSec        int    `mapstructure:"timeout_sec"        yaml:"timeout_sec,omitempty"`
    WorkspaceRequired bool   `mapstructure:"workspace_required" yaml:"workspace_required,omitempty"`
}
```

#### 해결 방법

`OpenAIRouteEntry`에 `MaxQueue`, `QueueTimeoutMS`를 추가한다. `validateOpenAIRoutes`에서 음수 값을 거부한다.

```go
type OpenAIRouteEntry struct {
    Model             string `mapstructure:"model"              yaml:"model"`
    NodeRef           string `mapstructure:"node"               yaml:"node,omitempty"`
    Adapter           string `mapstructure:"adapter"            yaml:"adapter,omitempty"`
    Target            string `mapstructure:"target"             yaml:"target"`
    SessionID         string `mapstructure:"session_id"         yaml:"session_id,omitempty"`
    TimeoutSec        int    `mapstructure:"timeout_sec"        yaml:"timeout_sec,omitempty"`
    MaxQueue          int    `mapstructure:"max_queue"          yaml:"max_queue,omitempty"`
    QueueTimeoutMS    int    `mapstructure:"queue_timeout_ms"   yaml:"queue_timeout_ms,omitempty"`
    WorkspaceRequired bool   `mapstructure:"workspace_required" yaml:"workspace_required,omitempty"`
}
```

Validation은 `openai.model_routes[i].max_queue must be non-negative`, `openai.model_routes[i].queue_timeout_ms must be non-negative`처럼 필드명을 포함하게 한다.

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

- [ ] `packages/go/config/config.go`: struct field 추가, route validation 보강.
- [ ] `packages/go/config/config_test.go`: 기존 route catalog load test에 값 보존 assertion 추가.
- [ ] `packages/go/config/config_test.go`: negative max_queue/queue_timeout_ms reject tests 추가.

#### 테스트 작성

작성.
- `TestLoadEdge_OpenAIRouteCatalog`에 `max_queue`, `queue_timeout_ms` 값을 추가하고 보존 여부를 검증한다.
- `TestLoadEdge_OpenAIRouteCatalogQueuePolicyRejectsNegative`를 추가해 두 음수 필드가 거부되는지 검증한다.

#### 중간 검증

```bash
go test -count=1 ./packages/go/config
```

기대 결과: config package tests PASS. Go test cache 출력은 허용하지 않는다.

### [API-2] OpenAI route dispatch가 queue policy를 전달

#### 문제

`apps/edge/internal/openai/chat_handler.go:163`의 `routeDispatch`는 queue policy를 담을 수 없고, `chat_handler.go:73`과 `responses_handler.go:81`의 `SubmitRunRequest`도 route policy를 넘기지 않는다.

#### 해결 방법

`routeDispatch`와 `edgeservice.SubmitRunRequest`에 같은 queue policy 필드를 추가하고, route catalog match 시 route 값을 복사한다. fallback route에서는 0 값을 유지해 기존 adapter fallback을 보존한다.

```go
type routeDispatch struct {
    NodeRef           string
    Adapter           string
    Target            string
    SessionID         string
    TimeoutSec        int
    MaxQueue          int
    QueueTimeoutMS    int
    WorkspaceRequired bool
}
```

`SubmitRunRequest`에는 exported field로 `MaxQueue int`, `QueueTimeoutMS int`를 추가한다. Chat/Responses submit path는 `dispatch.MaxQueue`, `dispatch.QueueTimeoutMS`를 전달한다.

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

- [ ] `apps/edge/internal/openai/chat_handler.go`: `routeDispatch` 필드와 route resolution 복사 추가.
- [ ] `apps/edge/internal/openai/chat_handler.go`: chat submit request에 queue policy 전달.
- [ ] `apps/edge/internal/openai/responses_handler.go`: responses submit request에 queue policy 전달.
- [ ] `apps/edge/internal/service/run_dispatch.go`: `SubmitRunRequest` 필드 추가만 수행. Behavior consumption은 sibling task에서 처리.
- [ ] `apps/edge/internal/openai/server_test.go`: Chat/Responses route catalog tests에 `MaxQueue`, `QueueTimeoutMS` assertion 추가.

#### 테스트 작성

작성.
- `TestChatCompletionsRouteCatalogDispatchesModelB`: route에 `MaxQueue`, `QueueTimeoutMS`를 넣고 `fake.req`가 같은 값을 받는지 검증한다.
- `TestResponsesRouteCatalogTakesPriorityOverMetadataTarget`: route queue policy도 함께 전달되는지 검증한다.
- fallback route test는 값이 0인지 확인해 adapter fallback 여지를 보존한다.

#### 중간 검증

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

기대 결과: OpenAI handler tests PASS. Go test cache 출력은 허용하지 않는다.

### [API-3] edge.yaml 예시에 책임 경계 반영

#### 문제

`configs/edge.yaml:56`의 `model_routes` 예시는 adapter/target/node만 보여주고, queue policy가 model alias에 붙는다는 점을 설명하지 않는다. `configs/edge.yaml:165` 이후 Node adapter examples는 `capacity`, `max_queue`, `queue_timeout_ms`가 provider instance에 있어 queue owner가 Node처럼 보인다.

#### 해결 방법

`model_routes` 예시에 `max_queue`, `queue_timeout_ms`를 추가하고, provider instance 예시 주석은 `capacity`가 per-node slot이라는 점을 명시한다. 기존 adapter instance `max_queue`/`queue_timeout_ms`는 호환 fallback이라는 설명을 추가하거나 route 예시와 충돌하지 않게 줄인다.

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

- [ ] `configs/edge.yaml`: `model_routes` 주석과 예시 갱신.
- [ ] `configs/edge.yaml`: adapter instance `capacity` 설명을 per-node slot으로 명시.

#### 테스트 작성

별도 테스트 작성 없음. YAML example은 config load fixture가 아니며, schema 보존과 handler propagation은 API-1/API-2 tests가 검증한다.

#### 중간 검증

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

기대 결과: whitespace error 없음.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `packages/go/config/config.go` | API-1 |
| `packages/go/config/config_test.go` | API-1 |
| `apps/edge/internal/service/run_dispatch.go` | API-2 |
| `apps/edge/internal/openai/chat_handler.go` | API-2 |
| `apps/edge/internal/openai/responses_handler.go` | API-2 |
| `apps/edge/internal/openai/server_test.go` | API-2 |
| `configs/edge.yaml` | API-3 |

## 의존 관계 및 구현 순서

1. API-1로 config schema와 validation을 먼저 고정한다.
2. API-2로 OpenAI handler에서 service DTO까지 route policy를 전달한다.
3. API-3으로 예시를 새 책임 경계에 맞춘다.

## 최종 검증

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

기대 결과: 모든 명령 PASS. Go test cache 출력은 허용하지 않는다.

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