feat: model route queue policy alignment phase added and edge service updates

- Add model-route-queue-policy-alignment milestone to inference provider extension phase
- Add SDD documentation for inference provider extension
- Update edge chat/responses handlers for OpenAI compatible API
- Update edge config with new queue policy settings
- Add config tests for queue policy support
- Update task tracking for model route queue policy alignment
This commit is contained in:
toki 2026-06-16 22:30:36 +09:00
parent d276c74700
commit 957700c2d6
15 changed files with 1140 additions and 34 deletions

View file

@ -32,6 +32,10 @@ Ollama 경로가 안정화된 뒤, 그 결과를 기준선으로 삼아 Lemonade
- 경로: `agent-roadmap/archive/phase/inference-provider-extension/milestones/edge-model-group-queue-scheduling.md`
- 요약: OpenAI-compatible 요청의 `model` 값을 queue group key로 사용하고, Node-local admission queue를 Edge-owned model-group FIFO와 node dispatch scheduler로 전환한다.
- [진행중] Model route Queue 정책 정합화
- 경로: `agent-roadmap/phase/inference-provider-extension/milestones/model-route-queue-policy-alignment.md`
- 요약: Edge-owned model group queue의 정책 원천을 `openai.model_routes[]`의 model alias로 정리하고, Node/provider instance에는 per-node capacity 기준만 남긴다.
- [진행중] vLLM provider 서빙 경로 추가
- 경로: `agent-roadmap/phase/inference-provider-extension/milestones/vllm-provider-serving-validation.md`
- 요약: Ollama 경로와 Node 단일 통로 멀티 타겟 기준선을 바탕으로 vLLM OpenAI-compatible provider를 붙이고, 모델 조회와 non-streaming/streaming chat을 검증한다.

View file

@ -0,0 +1,81 @@
# Milestone: Model route Queue 정책 정합화
## 위치
- Roadmap: `agent-roadmap/ROADMAP.md`
- Phase: `agent-roadmap/phase/inference-provider-extension/PHASE.md`
## 목표
Edge-owned model group queue의 정책 원천을 OpenAI-compatible `model_routes`의 model alias로 정리한다.
`max_queue``queue_timeout_ms`는 model alias queue 정책으로 두고, Node/provider instance의 `capacity`는 per-node 동시 실행 slot 기준으로 남긴다.
기존 adapter instance queue 설정은 호환 fallback으로만 유지해, 다중 Node/다중 provider pool에서 queue owner와 설정 위치가 어긋나지 않게 만든다.
## 상태
[진행중]
## 승격 조건
- 없음
## 구현 잠금
- 상태: 해제
- SDD: 필요
- SDD 문서: `agent-roadmap/sdd/inference-provider-extension/model-route-queue-policy-alignment/SDD.md`
- SDD 사유: OpenAI route config schema와 Edge queue dispatch 계약을 바꾸는 작업이다.
- 잠금 해제 조건:
- [x] SDD 잠금이 해제되어 있다
- [x] SDD 사용자 리뷰가 없거나 승인/해결되었다
- [x] Acceptance Scenario가 Milestone 기능 Task와 연결되어 있다
- [x] Evidence Map이 plan의 `Spec Targets`와 완료 시 `Spec Completion`으로 검증 가능하게 연결되어 있다
- 결정 필요: 없음
## 범위
- `openai.model_routes[]`에 Edge-owned model group queue 정책 필드를 추가한다.
- route-level `max_queue``queue_timeout_ms`가 있으면 Edge queue group policy에서 우선 사용한다.
- route-level queue policy가 없으면 기존 Node/provider adapter instance의 queue 설정을 호환 fallback으로 유지한다.
- Node/provider instance의 `capacity`는 각 Node가 해당 adapter instance로 동시에 받을 수 있는 요청 slot으로 유지한다.
- `/v1/chat/completions``/v1/responses` 모두 같은 route-level queue policy 전달 경로를 사용한다.
- 설정 예시와 테스트에서 model alias queue policy와 per-node capacity의 책임 경계를 명확히 한다.
## 기능
### Epic: [route-policy] Model Route Queue Policy
OpenAI-compatible model alias가 Edge queue group의 정책 원천이 되도록 config, dispatch DTO, queue admission 경로를 정리한다.
- [ ] [config-schema] `openai.model_routes[]``max_queue`, `queue_timeout_ms`를 받을 수 있고 validation/default/fallback 문서가 정리되어 있다. 검증: `go test -count=1 ./packages/go/config ./apps/edge/internal/openai`
- [ ] [dispatch-contract] OpenAI Chat Completions와 Responses route resolution이 route-level queue policy를 `SubmitRunRequest`로 전달한다. 검증: `go test -count=1 ./apps/edge/internal/openai`
- [ ] [queue-policy] Edge queue admission이 route-level queue policy를 adapter instance fallback보다 우선하고, route policy가 없을 때 기존 fallback을 유지한다. 검증: `go test -count=1 ./apps/edge/internal/service`
- [ ] [docs-example] `configs/edge.yaml` 예시가 model alias queue policy와 Node/provider `capacity`의 책임 경계를 보여준다.
## 완료 리뷰
- 상태: 없음
- 요청일: 없음
- 완료 근거: 모든 기능 Task가 아직 충족되지 않았다.
- 리뷰 필요:
- [ ] 사용자가 완료 결과를 확인했다
- [ ] archive 이동을 승인했다
- 리뷰 코멘트: 없음
## 범위 제외
- GPU/CPU/VRAM 실측 기반 자동 capacity 조정
- cross-Edge 부하 라우팅, cloud fallback, 품질/비용 기반 모델 선택
- `model_routes` 하나에서 provider별로 서로 다른 target을 선언하는 route 후보 목록
- vLLM/Lemonade/Ollama provider 자체의 실서비스 smoke 완료 판정
- Node bootstrap, Control Plane, Client UI 변경
## 작업 컨텍스트
- 관련 경로: `packages/go/config`, `configs/edge.yaml`, `apps/edge/internal/openai`, `apps/edge/internal/service`, `agent-test/local/edge-smoke.md`, `agent-test/local/platform-common-smoke.md`
- 표준선(선택): queue owner는 Edge이며, OpenAI-compatible `model` 값은 model group key다.
- 표준선(선택): `model_routes`는 외부 model alias와 Edge queue policy를 소유하고, Node/provider instance는 adapter endpoint와 per-node `capacity`를 소유한다.
- 표준선(선택): 기존 adapter instance `max_queue`/`queue_timeout_ms`는 route-level queue policy가 없는 설정의 호환 fallback으로 유지한다.
- 선행 작업: Edge 모델 그룹 Queue 스케줄링 전환
- 후속 작업: vLLM provider 서빙 경로 추가, SGLang provider 서빙 경로 추가, provider catalog/device status에서 queue policy 표시 확장
- 확인 필요: 없음

View file

@ -0,0 +1,100 @@
# SDD: Model route Queue 정책 정합화
## 위치
- Milestone: `agent-roadmap/phase/inference-provider-extension/milestones/model-route-queue-policy-alignment.md`
- Phase: `agent-roadmap/phase/inference-provider-extension/PHASE.md`
## 상태
[승인됨]
## SDD 잠금
- 상태: 해제
- 사용자 리뷰: 없음
- 잠금 항목:
- 없음
## 문제 / 비목표
- 문제: Edge-owned model group queue는 OpenAI `model` alias를 group key로 쓰지만, 현재 queue policy의 설정 원천은 Node/provider adapter instance에 남아 있어 다중 Node/다중 provider pool에서 어느 값이 alias queue 정책인지 불명확하다.
- 비목표:
- GPU/CPU/VRAM metric 기반 자동 capacity 산정
- cross-Edge 라우팅, cloud fallback, 품질/비용 기반 모델 선택
- provider별 target 후보 목록을 `model_routes` 하나에 직접 여러 개 선언하는 schema
## Source of Truth
| 영역 | 기준 | 메모 |
|------|------|------|
| Roadmap | `agent-roadmap/phase/inference-provider-extension/milestones/model-route-queue-policy-alignment.md` | Milestone 기능 Task와 완료 반영 기준 |
| Code | `packages/go/config/config.go`, `apps/edge/internal/openai`, `apps/edge/internal/service` | route schema, OpenAI dispatch, Edge queue policy 구현 원천 |
| External Provider | 없음 | provider endpoint 자체의 동작 변경은 범위 밖 |
| User Decision | 없음 | 사용자가 queue policy는 model alias, capacity는 Node/provider instance에 둔다는 방향을 확정했다 |
## State Machine
| 상태 | 진입 조건 | 다음 상태 | 근거 |
|------|-----------|-----------|------|
| route-resolved | OpenAI request `model``model_routes[]` 또는 fallback route로 해석된다 | queue-admission | `resolveRouteDispatch` 결과와 `SubmitRunRequest.ModelGroupKey` |
| queue-admission | `ModelGroupKey`가 있고 Edge queue manager가 후보 Node와 policy를 받는다 | dispatched 또는 queued 또는 rejected | `modelQueueManager.admit` |
| queued | 모든 후보 Node의 `in_flight >= capacity`이고 group queue 여유가 있다 | dispatched 또는 timeout/cancelled | terminal run event, queue timeout, request context |
| dispatched | 후보 Node의 `in_flight < capacity` slot이 예약되어 Node로 run request를 보냈다 | released | run `complete`, `error`, `cancelled`, node disconnect |
| released | terminal event 또는 disconnect로 slot이 해제됐다 | queue-admission | 다음 queued item dispatch 시도 |
## Interface Contract
- 계약 원문: 없음
- 입력:
- `openai.model_routes[].model`: OpenAI-compatible 외부 model alias이며 Edge model group queue key다.
- `openai.model_routes[].adapter`: 내부 adapter 또는 adapter instance key다.
- `openai.model_routes[].target`: 내부 provider/model target이다.
- `openai.model_routes[].max_queue`: 해당 model alias queue의 최대 대기 요청 수다. 0이면 fallback/default를 쓴다.
- `openai.model_routes[].queue_timeout_ms`: 해당 model alias queue의 대기 timeout이다. 0이면 fallback/default를 쓴다.
- `nodes[].adapters.*_instances[].capacity`: 해당 Node/provider instance가 동시에 받을 수 있는 실행 slot 수다.
- 출력:
- Edge queue admission은 route-level policy를 우선 적용하고, 없으면 기존 adapter instance queue policy 또는 기본값을 사용한다.
- provider snapshot의 `capacity`, `in_flight`, `queued`는 Edge-owned queue 상태와 per-node capacity 기준으로 표현된다.
- 금지:
- GPU utilization만으로 Edge queue admission 가능 여부를 판단하지 않는다.
- route-level queue policy가 있는데 adapter instance `max_queue`가 이를 덮어쓰지 않는다.
- `model_routes`의 중복 `model` alias를 허용하지 않는다.
## Acceptance Scenarios
| ID | Milestone Task | Given | When | Then |
|----|----------------|-------|------|------|
| S01 | `config-schema` | `openai.model_routes[]``max_queue`, `queue_timeout_ms`가 있다 | Edge config를 load/validate한다 | route entry에 값이 보존되고 중복/빈 model 검증은 유지된다 |
| S02 | `dispatch-contract` | Chat/Responses 요청이 route catalog model alias로 들어온다 | OpenAI handler가 service request를 만든다 | `ModelGroupKey`와 route queue policy가 함께 전달된다 |
| S03 | `queue-policy` | route-level queue policy와 adapter fallback policy가 모두 있다 | Edge queue group을 생성한다 | route-level `max_queue`, `queue_timeout_ms`가 우선 적용된다 |
| S04 | `docs-example` | 사용자가 다중 Node/provider pool 예시를 본다 | `configs/edge.yaml`을 읽는다 | queue policy는 `model_routes`, capacity는 Node/provider instance에 둔다는 책임 경계가 드러난다 |
## Evidence Map
| Scenario | Required Evidence | `agent-task` 연결 | `Spec Completion` 기대 |
|----------|-------------------|------------------|---------------------------|
| S01 | `go test -count=1 ./packages/go/config ./apps/edge/internal/openai` | `agent-task/m-model-route-queue-policy-alignment/01_config_route_policy` | `config-schema` PASS와 S01 evidence |
| S02 | `go test -count=1 ./apps/edge/internal/openai` | `agent-task/m-model-route-queue-policy-alignment/01_config_route_policy` | `dispatch-contract` PASS와 S02 evidence |
| S03 | `go test -count=1 ./apps/edge/internal/service` | `agent-task/m-model-route-queue-policy-alignment/02+01_service_queue_policy` | `queue-policy` PASS와 S03 evidence |
| S04 | `git diff --check``configs/edge.yaml` 예시 변경 리뷰 | `agent-task/m-model-route-queue-policy-alignment/01_config_route_policy` | `docs-example` PASS와 S04 evidence |
## Cross-repo Dependencies
- 없음
## Drift Check
- [x] Milestone 기능 Task와 Acceptance Scenario가 일치한다.
- [x] Evidence Map이 plan/code-review/complete.log에서 검증 가능하다.
- [x] agent-contract를 쓰는 경우 SDD에 계약 원문을 복제하지 않았다.
- [x] 사용자 리뷰가 필요한 항목은 `USER_REVIEW.md`에만 남겼다.
## 사용자 리뷰 이력
- 없음
## 작업 컨텍스트
- 표준선: Edge queue owner 원칙, OpenAI `model` 값 기반 group key, 내부 `adapter + target` 실행 계약을 유지한다.
- 후속 SDD: 없음

View file

@ -0,0 +1,182 @@
<!-- task=m-model-route-queue-policy-alignment/01_config_route_policy plan=0 tag=API -->
# Code Review Reference - API
> **[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 user-only decision, user-owned external environment prerequisite, or scope conflict, fill `사용자 리뷰 요청` with evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`. Evidence gaps that a follow-up agent can close by rerunning commands or collecting artifacts 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 the needed decision 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-06-16
task=m-model-route-queue-policy-alignment/01_config_route_policy, plan=0, tag=API
## 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
## 이 파일을 읽는 리뷰 에이전트에게
> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다.
1. 판정을 append한다.
2. `CODE_REVIEW-local-G07.md` -> `code_review_local_G07_N.log`, `PLAN-local-G07.md` -> `plan_local_G07_M.log`로 아카이브한다.
3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-model-route-queue-policy-alignment/01_config_route_policy/`로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다. `USER_REVIEW.md`가 사용자 결정으로 완료/PASS 해소되면 code-review가 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log` 작성 후 archive 이동한다.
4. PASS이고 task group이 `m-<milestone-slug>`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다.
5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다.
---
## 구현 항목별 완료 여부
| 항목 | 완료 여부 |
|------|---------|
| [API-1] Route config schema에 queue policy 필드 추가 | [x] |
| [API-2] OpenAI route dispatch가 queue policy를 전달 | [x] |
| [API-3] edge.yaml 예시에 책임 경계 반영 | [x] |
## 구현 체크리스트
- [x] `OpenAIRouteEntry`에 `max_queue`, `queue_timeout_ms` 필드를 추가하고 non-negative validation을 붙인다.
- [x] OpenAI route dispatch DTO와 Chat/Responses submit path가 route-level queue policy를 `SubmitRunRequest`로 전달한다.
- [x] `configs/edge.yaml` 예시가 route-level queue policy와 Node/provider `capacity` 책임 경계를 보여준다.
- [x] Focused config/OpenAI tests와 `git diff --check`를 실행한다.
- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
## 코드리뷰 전용 체크리스트
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다.
- [x] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
- [x] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
- [x] active `CODE_REVIEW-*-G??.md`를 `code_review_local_G07_N.log`로 아카이브한다.
- [x] active `PLAN-*-G??.md`를 `plan_local_G07_M.log`로 아카이브한다.
- [x] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다.
- [x] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
- [x] PASS이면 active task 디렉터리 `agent-task/m-model-route-queue-policy-alignment/01_config_route_policy/`를 `agent-task/archive/YYYY/MM/m-model-route-queue-policy-alignment/01_config_route_policy/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
- [x] PASS이고 task group이 `m-<milestone-slug>`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
- [x] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-model-route-queue-policy-alignment/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다.
- [ ] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-local-G07.md`와 `CODE_REVIEW-local-G07.md`를 작성하고 `complete.log`를 작성하지 않는다.
- [ ] USER_REVIEW이면 `agent-ops/skills/common/code-review/templates/user-review-template.md` 기준으로 `USER_REVIEW.md`를 작성하고 active `PLAN-*.md`, `CODE_REVIEW-*.md`, `complete.log`를 남기지 않는다.
- [ ] USER_REVIEW가 사용자 결정으로 완료/PASS 해소되면 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log`를 작성한 뒤 task directory를 archive로 이동한다.
## 계획 대비 변경 사항
- 없음. 계획된 모든 기능 요구 사항과 검증이 차질 없이 구현 및 통과되었습니다.
## 주요 설계 결정
- `config.OpenAIRouteEntry`에 `max_queue`, `queue_timeout_ms` 필드를 노출하고, `validateOpenAIRoutes`에서 음수 값 여부를 검증하게 하였습니다.
- OpenAI Chat Completions와 Responses에서 routeCatalog 매칭 시 `MaxQueue`와 `QueueTimeoutMS`를 `SubmitRunRequest`로 정상 전달하도록 handlers 및 service DTO를 수정하였습니다.
- `configs/edge.yaml`에서 route-level queue policy와 node capacity의 책임 경계에 대한 설명 주석을 추가하고 형식을 보강하였습니다.
## 사용자 리뷰 요청
- 상태: 없음
- 사유 유형: 없음
- 결정 필요: 없음
- 차단 근거: 없음
- 실행한 검증/명령: 없음
- 자동 후속 불가 이유: 없음
- 재개 조건: 없음
## 리뷰어를 위한 체크포인트
- Route-level queue policy fields are loaded, validated non-negative, and do not break duplicate/empty model validation.
- Chat and Responses both pass route-level queue policy to `SubmitRunRequest`.
- `configs/edge.yaml` presents queue policy on model aliases and capacity on provider instances without implying GPU metric scheduling.
## 검증 결과
필수 규칙:
- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다.
- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다.
- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다.
- 사용자 리뷰 요청으로 명령을 끝까지 실행하지 못했다면 `사용자 리뷰 요청`에 실행한 명령, 실제 출력, 미실행 명령의 사유를 기록한다.
### API-1 중간 검증
```text
$ go test -count=1 ./packages/go/config
ok iop/packages/go/config 0.016s
```
### API-2 중간 검증
```text
$ go test -count=1 ./apps/edge/internal/openai
ok iop/apps/edge/internal/openai 1.508s
```
### API-3 중간 검증
```text
$ git diff --check
(출력 없음 - 성공)
```
### 최종 검증
```text
$ go test -count=1 ./packages/go/config ./apps/edge/internal/openai
ok iop/packages/go/config 0.016s
ok iop/apps/edge/internal/openai 1.508s
$ git diff --check
(출력 없음 - 성공)
```
---
> **[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.
## Section Ownership
| Section | Owner | Note |
|---------|-------|------|
| Header comment, 개요, 리뷰 에이전트 지시 | Fixed at stub creation | Implementing agent must not modify or execute these |
| Roadmap Targets | Fixed at stub creation from plan | Implementing agent must not modify |
| Spec Targets | Fixed at stub creation from plan | Implementing agent must not modify |
| 구현 항목별 완료 여부 | Implementing agent checks boxes only | Item names fixed |
| 구현 체크리스트 | Implementing agent checks boxes only | Text/order fixed |
| 코드리뷰 전용 체크리스트 | Review agent only | Implementing agent must not modify |
| 계획 대비 변경 사항, 주요 설계 결정 | Implementing agent | Replace placeholder text |
| 사용자 리뷰 요청 | Implementing agent | Keep `상태: 없음` unless blocked |
| 리뷰어를 위한 체크포인트 | Fixed at stub creation | Review focus |
| 검증 결과 | Implementing agent fills command output | Command changes require `계획 대비 변경 사항` |
## 코드리뷰 결과
- 종합 판정: PASS
- 차원별 평가:
- correctness: Pass
- completeness: Pass
- test coverage: Pass
- API contract: Pass
- code quality: Pass
- plan deviation: Pass
- verification trust: Pass
- spec conformance: Pass
- 발견된 문제: 없음
- 다음 단계: PASS이므로 active plan/review를 로그로 아카이브하고 `complete.log` 작성 후 task directory를 `agent-task/archive/2026/06/m-model-route-queue-policy-alignment/01_config_route_policy/`로 이동한다. `m-model-route-queue-policy-alignment` 완료 이벤트 메타데이터를 보고하고 roadmap 수정은 런타임에 맡긴다.

View file

@ -0,0 +1,55 @@
# Complete - m-model-route-queue-policy-alignment/01_config_route_policy
## 완료 일시
2026-06-16
## 요약
Route-level OpenAI queue policy config/dispatch/docs contract review completed in loop 0 with final verdict PASS.
## 루프 이력
| Plan | Review | Verdict | 메모 |
|------|--------|---------|------|
| `plan_local_G07_0.log` | `code_review_local_G07_0.log` | PASS | `OpenAIRouteEntry` queue policy schema, OpenAI Chat/Responses `SubmitRunRequest` propagation, and `configs/edge.yaml` responsibility-boundary example passed review. |
## 구현/정리 내용
- Added `max_queue` and `queue_timeout_ms` to OpenAI route config with non-negative validation and focused config tests.
- Propagated route-level queue policy through OpenAI Chat/Responses dispatch into `SubmitRunRequest`.
- Updated `configs/edge.yaml` comments/examples to show model-alias queue policy and per-node capacity responsibilities.
- Review cleanup: ran `gofmt` on `apps/edge/internal/service/run_dispatch.go` for formatting only.
## 최종 검증
- `go test -count=1 ./packages/go/config ./apps/edge/internal/openai` - PASS; `ok iop/packages/go/config 0.021s`, `ok iop/apps/edge/internal/openai 1.508s`.
- `git diff --check` - PASS; no output.
- `go test -count=1 ./apps/edge/...` - PASS; all edge packages passed, including `apps/edge/internal/openai` and `apps/edge/internal/service`.
- `go test -count=1 ./packages/go/...` - PASS; all packages with tests passed and no-test packages reported normally.
## Roadmap Completion
- Milestone: `agent-roadmap/phase/inference-provider-extension/milestones/model-route-queue-policy-alignment.md`
- Completed task ids:
- `config-schema`: PASS; evidence=`agent-task/archive/2026/06/m-model-route-queue-policy-alignment/01_config_route_policy/plan_local_G07_0.log`, `agent-task/archive/2026/06/m-model-route-queue-policy-alignment/01_config_route_policy/code_review_local_G07_0.log`; verification=`go test -count=1 ./packages/go/config ./apps/edge/internal/openai`
- `dispatch-contract`: PASS; evidence=`agent-task/archive/2026/06/m-model-route-queue-policy-alignment/01_config_route_policy/plan_local_G07_0.log`, `agent-task/archive/2026/06/m-model-route-queue-policy-alignment/01_config_route_policy/code_review_local_G07_0.log`; verification=`go test -count=1 ./apps/edge/internal/openai`
- `docs-example`: PASS; evidence=`agent-task/archive/2026/06/m-model-route-queue-policy-alignment/01_config_route_policy/plan_local_G07_0.log`, `agent-task/archive/2026/06/m-model-route-queue-policy-alignment/01_config_route_policy/code_review_local_G07_0.log`; verification=`git diff --check` and reviewed `configs/edge.yaml`
- Not completed task ids: 없음
## Spec Completion
- SDD: `agent-roadmap/sdd/inference-provider-extension/model-route-queue-policy-alignment/SDD.md`
- Completed scenario ids:
- `S01`: PASS; task=`config-schema`; evidence=`agent-task/archive/2026/06/m-model-route-queue-policy-alignment/01_config_route_policy/plan_local_G07_0.log`, `agent-task/archive/2026/06/m-model-route-queue-policy-alignment/01_config_route_policy/code_review_local_G07_0.log`; verification=`go test -count=1 ./packages/go/config ./apps/edge/internal/openai`
- `S02`: PASS; task=`dispatch-contract`; evidence=`agent-task/archive/2026/06/m-model-route-queue-policy-alignment/01_config_route_policy/plan_local_G07_0.log`, `agent-task/archive/2026/06/m-model-route-queue-policy-alignment/01_config_route_policy/code_review_local_G07_0.log`; verification=`go test -count=1 ./apps/edge/internal/openai`
- `S04`: PASS; task=`docs-example`; evidence=`agent-task/archive/2026/06/m-model-route-queue-policy-alignment/01_config_route_policy/plan_local_G07_0.log`, `agent-task/archive/2026/06/m-model-route-queue-policy-alignment/01_config_route_policy/code_review_local_G07_0.log`; verification=`git diff --check` and reviewed `configs/edge.yaml`
- Not completed scenario ids: 없음
## 잔여 Nit
- 없음
## 후속 작업
- 없음

View file

@ -0,0 +1,261 @@
<!-- 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`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.

View file

@ -0,0 +1,152 @@
<!-- task=m-model-route-queue-policy-alignment/02+01_service_queue_policy plan=0 tag=API -->
# Code Review Reference - API
> **[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 user-only decision, user-owned external environment prerequisite, or scope conflict, fill `사용자 리뷰 요청` with evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`. Evidence gaps that a follow-up agent can close by rerunning commands or collecting artifacts 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 the needed decision 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-06-16
task=m-model-route-queue-policy-alignment/02+01_service_queue_policy, plan=0, tag=API
## Roadmap Targets
- Milestone: `agent-roadmap/phase/inference-provider-extension/milestones/model-route-queue-policy-alignment.md`
- Task ids:
- `queue-policy`: Edge queue admission이 route-level queue policy를 adapter instance fallback보다 우선하고, route policy가 없을 때 기존 fallback을 유지한다.
- Completion mode: check-on-pass
## Spec Targets
- SDD: `agent-roadmap/sdd/inference-provider-extension/model-route-queue-policy-alignment/SDD.md`
- Acceptance scenarios:
- `S03`: task=`queue-policy`; evidence=`go test -count=1 ./apps/edge/internal/service`
- Completion mode: spec-check-on-pass
## 이 파일을 읽는 리뷰 에이전트에게
> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다.
1. 판정을 append한다.
2. `CODE_REVIEW-local-G07.md` -> `code_review_local_G07_N.log`, `PLAN-local-G07.md` -> `plan_local_G07_M.log`로 아카이브한다.
3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-model-route-queue-policy-alignment/02+01_service_queue_policy/`로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다.
4. PASS이고 task group이 `m-<milestone-slug>`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다.
5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다.
---
## 구현 항목별 완료 여부
| 항목 | 완료 여부 |
|------|---------|
| [API-1] Route policy 우선순위 적용 | [ ] |
| [API-2] Queue timeout route policy 검증 | [ ] |
## 구현 체크리스트
- [ ] `SubmitRunRequest`의 route-level queue policy가 있으면 Edge group policy에서 adapter fallback보다 우선 사용한다.
- [ ] route policy가 0이면 기존 adapter instance `max_queue`/`queue_timeout_ms` fallback과 default policy가 유지된다.
- [ ] route policy override와 fallback regression tests를 추가/갱신한다.
- [ ] Focused service tests와 `git diff --check`를 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
## 코드리뷰 전용 체크리스트
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다.
- [ ] `코드리뷰 결과``PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
- [ ] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
- [ ] active `CODE_REVIEW-*-G??.md``code_review_local_G07_N.log`로 아카이브한다.
- [ ] active `PLAN-*-G??.md``plan_local_G07_M.log`로 아카이브한다.
- [ ] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md``agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다.
- [ ] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
- [ ] PASS이면 active task 디렉터리 `agent-task/m-model-route-queue-policy-alignment/02+01_service_queue_policy/``agent-task/archive/YYYY/MM/m-model-route-queue-policy-alignment/02+01_service_queue_policy/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
- [ ] PASS이고 task group이 `m-<milestone-slug>`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-model-route-queue-policy-alignment/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다.
- [ ] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-local-G07.md``CODE_REVIEW-local-G07.md`를 작성하고 `complete.log`를 작성하지 않는다.
- [ ] USER_REVIEW이면 `agent-ops/skills/common/code-review/templates/user-review-template.md` 기준으로 `USER_REVIEW.md`를 작성하고 active `PLAN-*.md`, `CODE_REVIEW-*.md`, `complete.log`를 남기지 않는다.
- [ ] USER_REVIEW가 사용자 결정으로 완료/PASS 해소되면 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log`를 작성한 뒤 task directory를 archive로 이동한다.
## 계획 대비 변경 사항
_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._
## 주요 설계 결정
_구현 에이전트가 주요 설계 결정 사항을 기록한다._
## 사용자 리뷰 요청
_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. 구현 에이전트는 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백만으로는 사용자 리뷰 요청을 작성하지 않는다._
- 상태: 없음
- 사유 유형: 없음
- 결정 필요: 없음
- 차단 근거: 없음
- 실행한 검증/명령: 없음
- 자동 후속 불가 이유: 없음
- 재개 조건: 없음
## 리뷰어를 위한 체크포인트
- Route-level queue policy wins over adapter instance fallback.
- Existing adapter instance fallback behavior is preserved when request policy fields are zero.
- Queue timeout and max queue semantics still use Edge-owned model queue, not Node-local queue ownership.
## 검증 결과
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
필수 규칙:
- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다.
- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다.
- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다.
- 사용자 리뷰 요청으로 명령을 끝까지 실행하지 못했다면 `사용자 리뷰 요청`에 실행한 명령, 실제 출력, 미실행 명령의 사유를 기록한다.
### API-1 중간 검증
```text
$ go test -count=1 ./apps/edge/internal/service
```
### API-2 중간 검증
```text
$ go test -count=1 ./apps/edge/internal/service -run 'TestSubmitRunModelQueue'
```
### 최종 검증
```text
$ go test -count=1 ./apps/edge/internal/service
$ git diff --check
```
---
> **[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.
## Section Ownership
| Section | Owner | Note |
|---------|-------|------|
| Header comment, 개요, 리뷰 에이전트 지시 | Fixed at stub creation | Implementing agent must not modify or execute these |
| Roadmap Targets | Fixed at stub creation from plan | Implementing agent must not modify |
| Spec Targets | Fixed at stub creation from plan | Implementing agent must not modify |
| 구현 항목별 완료 여부 | Implementing agent checks boxes only | Item names fixed |
| 구현 체크리스트 | Implementing agent checks boxes only | Text/order fixed |
| 코드리뷰 전용 체크리스트 | Review agent only | Implementing agent must not modify |
| 계획 대비 변경 사항, 주요 설계 결정 | Implementing agent | Replace placeholder text |
| 사용자 리뷰 요청 | Implementing agent | Keep `상태: 없음` unless blocked |
| 리뷰어를 위한 체크포인트 | Fixed at stub creation | Review focus |
| 검증 결과 | Implementing agent fills command output | Command changes require `계획 대비 변경 사항` |

View file

@ -0,0 +1,184 @@
<!-- task=m-model-route-queue-policy-alignment/02+01_service_queue_policy plan=0 tag=API -->
# Plan - API service queue policy priority
## 이 파일을 읽는 구현 에이전트에게
구현 완료 후 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채우는 것이 필수 최종 단계입니다. 이 task는 `01_config_route_policy` 완료 후 진행해야 합니다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 충돌로 막히면 review stub의 `사용자 리뷰 요청`에 정확한 근거를 남기고 멈춥니다. 구현 에이전트는 사용자에게 직접 묻거나 `USER_REVIEW.md`, `complete.log`, archive 로그를 만들지 않습니다.
## 배경
선행 task가 route-level queue policy를 `SubmitRunRequest`에 실어 보낸다. 이 후속 작업은 Edge service가 그 값을 실제 model queue group policy로 우선 적용하게 만든다. route policy가 없으면 기존 Node/provider adapter instance fallback을 유지해야 기존 설정을 깨지 않는다.
## 사용자 리뷰 요청 흐름
구현 중 차단은 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:
- `queue-policy`: Edge queue admission이 route-level queue policy를 adapter instance fallback보다 우선하고, route policy가 없을 때 기존 fallback을 유지한다.
- Completion mode: check-on-pass
## Spec Targets
- SDD: `agent-roadmap/sdd/inference-provider-extension/model-route-queue-policy-alignment/SDD.md`
- Acceptance scenarios:
- `S03`: task=`queue-policy`; evidence=`go test -count=1 ./apps/edge/internal/service`
- Completion mode: spec-check-on-pass
## 분석 결과
### 읽은 파일
- `agent-roadmap/current.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-ops/rules/project/domain/edge/rules.md`
- `apps/edge/internal/service/run_dispatch.go`
- `apps/edge/internal/service/model_queue.go`
- `apps/edge/internal/service/service_test.go`
### 테스트 환경 규칙
- test_env: local.
- `agent-test/local/rules.md` present/read.
- Matched profile read: `agent-test/local/edge-smoke.md`.
- 적용 명령: `go test -count=1 ./apps/edge/internal/service`, `git diff --check`.
- Edge service dispatch 변경이므로 focused service tests가 필수다. 전체 edge smoke는 queue policy 자체보다 실행 경로 생존 확인 보조로 후속 review가 필요하다고 판단할 때만 확장한다.
### 테스트 커버리지 공백
- `resolveQueueCandidates`는 group policy가 adapter config에서 온다는 전제를 갖고 있다.
- `TestSubmitRunModelQueueUsesProviderInstancePolicy`는 adapter instance `MaxQueue` fallback만 검증한다.
- route-level policy가 adapter fallback보다 우선하는 regression test가 없다.
### 심볼 참조
- 제거/rename 없음.
- 선행 task가 추가할 `SubmitRunRequest.MaxQueue`, `SubmitRunRequest.QueueTimeoutMS`를 소비한다.
- call site: `submitRunQueued`, `resolveQueueCandidates`, `groupPolicyFromStore`, service tests.
### 분할 판단
- split decision policy evaluated. 이 plan은 `02+01_service_queue_policy`이며 predecessor `01_config_route_policy``complete.log`가 필요하다.
- 현재 작성 시점에는 predecessor active task가 생성되었지만 완료되지 않았다. 구현 runtime은 `01_config_route_policy` 완료 후 이 task를 실행해야 한다.
### 범위 결정 근거
- 포함: Edge service queue policy derivation and tests.
- 제외: config schema/OpenAI handler/docs 변경은 `01_config_route_policy` 담당.
- 제외: Node/provider admission behavior, GPU metric detection, cross-Edge routing.
### 빌드 등급
- build lane/grade: `local-G07`. Concurrency/queue policy path지만 focused service tests로 deterministic하게 검증 가능하다.
## 구현 체크리스트
- [ ] `SubmitRunRequest`의 route-level queue policy가 있으면 Edge group policy에서 adapter fallback보다 우선 사용한다.
- [ ] route policy가 0이면 기존 adapter instance `max_queue`/`queue_timeout_ms` fallback과 default policy가 유지된다.
- [ ] route policy override와 fallback regression tests를 추가/갱신한다.
- [ ] Focused service tests와 `git diff --check`를 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
### [API-1] Route policy 우선순위 적용
#### 문제
`apps/edge/internal/service/run_dispatch.go:187`의 comment와 구현은 group policy를 adapter config에서만 파생한다. `groupPolicyFromStore``run_dispatch.go:397`에서 후보 Node adapter instance의 `maxQueue`, `queueTimeoutMS`를 먼저 만나면 반환한다.
```go
policy := groupPolicyFromStore(s.nodeStore, entries, req.Adapter, req.Target)
```
#### 해결 방법
`SubmitRunRequest`의 route-level policy를 먼저 `groupPolicy`로 변환하는 helper를 추가한다.
```go
func groupPolicyFromRequestOrStore(req SubmitRunRequest, store *edgenode.NodeStore, entries []*edgenode.NodeEntry) groupPolicy {
if req.MaxQueue > 0 || req.QueueTimeoutMS > 0 {
return groupPolicy{
maxQueue: positiveOr(req.MaxQueue, defaultGroupMaxQueue),
queueTimeout: queueTimeoutOrDefault(req.QueueTimeoutMS),
}
}
return groupPolicyFromStore(store, entries, req.Adapter, req.Target)
}
```
`resolveQueueCandidates`의 explicit-node path와 all-candidates path 모두 이 helper를 사용한다. 음수는 config validation에서 막지만 service direct caller 방어를 위해 `<=0`은 fallback/default로만 해석한다.
#### 수정 파일 및 체크리스트
- [ ] `apps/edge/internal/service/run_dispatch.go`: route policy 우선 helper 추가.
- [ ] `apps/edge/internal/service/run_dispatch.go`: explicit NodeRef path와 all-candidates path 모두 helper 사용.
#### 테스트 작성
작성.
- `TestSubmitRunModelQueueUsesRoutePolicyBeforeProviderInstancePolicy`: provider instance `MaxQueue`가 큰 값이어도 request `MaxQueue: 1`이면 두 번째 queued request 후 세 번째 대기 request가 queue full로 거부되는지 검증한다.
- `TestSubmitRunModelQueueFallsBackToProviderInstancePolicy`: request policy가 0이면 기존 `TestSubmitRunModelQueueUsesProviderInstancePolicy` 기대가 유지되는지 검증한다. 기존 test를 이름 유지한 채 보강하거나 새 test로 분리한다.
#### 중간 검증
```bash
go test -count=1 ./apps/edge/internal/service
```
기대 결과: service package tests PASS. Go test cache 출력은 허용하지 않는다.
### [API-2] Queue timeout route policy 검증
#### 문제
`model_queue.go:183``group.policy.queueTimeout`으로 queued item deadline을 만든다. route-level `queue_timeout_ms`가 적용되지 않으면 model alias queue의 timeout 정책을 독립적으로 설정할 수 없다.
#### 해결 방법
API-1의 helper에서 `QueueTimeoutMS`가 있으면 `time.Duration(req.QueueTimeoutMS) * time.Millisecond`를 우선 사용한다. `MaxQueue`만 있고 timeout이 없으면 default timeout을 사용한다. `QueueTimeoutMS`만 있고 max queue가 없으면 default max queue를 사용한다.
#### 수정 파일 및 체크리스트
- [ ] `apps/edge/internal/service/run_dispatch.go`: timeout conversion helper 작성.
- [ ] `apps/edge/internal/service/service_test.go`: timeout override test 추가 또는 max_queue override test 안에 assertion 포함.
#### 테스트 작성
작성.
- `TestSubmitRunModelQueueUsesRouteQueueTimeout`: capacity를 채우고 queued request context보다 route timeout을 짧게 설정해 `errQueueTimeout` path가 route timeout 기준으로 발생하는지 검증한다.
#### 중간 검증
```bash
go test -count=1 ./apps/edge/internal/service -run 'TestSubmitRunModelQueue'
```
기대 결과: model queue 관련 service tests PASS. Go test cache 출력은 허용하지 않는다.
## 수정 파일 요약
| 파일 | 항목 |
|------|------|
| `apps/edge/internal/service/run_dispatch.go` | API-1, API-2 |
| `apps/edge/internal/service/service_test.go` | API-1, API-2 |
## 의존 관계 및 구현 순서
이 task directory name `02+01_service_queue_policy`가 runtime dependency source of truth다.
Predecessor `01_config_route_policy` must produce `agent-task/m-model-route-queue-policy-alignment/01_config_route_policy/complete.log` or matching archived complete log before implementation starts.
## 최종 검증
```bash
go test -count=1 ./apps/edge/internal/service
git diff --check
```
기대 결과: 모든 명령 PASS. Go test cache 출력은 허용하지 않는다.
모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.

View file

@ -71,16 +71,18 @@ func (s *Server) handleChatCompletions(w http.ResponseWriter, r *http.Request) {
)
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,
Metadata: chatRunMetadata(runMeta, req, outputPolicy),
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: chatRunMetadata(runMeta, req, outputPolicy),
})
if err != nil {
writeError(w, http.StatusBadGateway, "node_dispatch_error", err.Error())
@ -166,6 +168,8 @@ type routeDispatch struct {
Target string
SessionID string
TimeoutSec int
MaxQueue int
QueueTimeoutMS int
WorkspaceRequired bool
}
@ -213,6 +217,8 @@ func (s *Server) resolveRouteDispatch(model, metadataTarget string) (routeDispat
Target: route.Target,
SessionID: sessionID,
TimeoutSec: timeoutSec,
MaxQueue: route.MaxQueue,
QueueTimeoutMS: route.QueueTimeoutMS,
WorkspaceRequired: route.WorkspaceRequired,
}, true
}

View file

@ -79,16 +79,18 @@ func (s *Server) handleResponses(w http.ResponseWriter, r *http.Request) {
)
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,
Metadata: runMeta,
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,
})
if err != nil {
writeError(w, http.StatusBadGateway, "node_dispatch_error", err.Error())

View file

@ -1022,7 +1022,7 @@ func TestChatCompletionsRouteCatalogDispatchesModelB(t *testing.T) {
srv := NewServer(config.EdgeOpenAIConf{
ModelRoutes: []config.OpenAIRouteEntry{
{Model: "model-a", Adapter: "ollama", Target: "llama3"},
{Model: "model-b", Adapter: "vllm", Target: "qwen"},
{Model: "model-b", Adapter: "vllm", Target: "qwen", MaxQueue: 5, QueueTimeoutMS: 2000},
},
}, fake, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
@ -1041,6 +1041,9 @@ func TestChatCompletionsRouteCatalogDispatchesModelB(t *testing.T) {
if fake.req.ModelGroupKey != "model-b" {
t.Fatalf("model group key: got %q, want model-b", fake.req.ModelGroupKey)
}
if fake.req.MaxQueue != 5 || fake.req.QueueTimeoutMS != 2000 {
t.Fatalf("queue policy mismatch: MaxQueue=%d, QueueTimeoutMS=%d", fake.req.MaxQueue, fake.req.QueueTimeoutMS)
}
}
func TestChatCompletionsCatalogMissFallsToLegacyTarget(t *testing.T) {
@ -1118,7 +1121,7 @@ func TestResponsesRouteCatalogTakesPriorityOverMetadataTarget(t *testing.T) {
srv := NewServer(config.EdgeOpenAIConf{
ModelRoutes: []config.OpenAIRouteEntry{
{Model: "model-a", Adapter: "ollama", Target: "route-target"},
{Model: "model-a", Adapter: "ollama", Target: "route-target", MaxQueue: 3, QueueTimeoutMS: 1500},
},
}, fake, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{
@ -1138,6 +1141,9 @@ func TestResponsesRouteCatalogTakesPriorityOverMetadataTarget(t *testing.T) {
if fake.req.ModelGroupKey != "model-a" {
t.Fatalf("model group key: got %q, want model-a", fake.req.ModelGroupKey)
}
if fake.req.MaxQueue != 3 || fake.req.QueueTimeoutMS != 1500 {
t.Fatalf("queue policy mismatch: MaxQueue=%d, QueueTimeoutMS=%d", fake.req.MaxQueue, fake.req.QueueTimeoutMS)
}
}
func TestChatCompletionsEdgePassesNumCtxForAdapterEnforcement(t *testing.T) {

View file

@ -14,18 +14,20 @@ import (
)
type SubmitRunRequest struct {
NodeRef string
RunID string
ModelGroupKey string
Adapter string
Target string
SessionID string
Workspace string
Prompt string
Input map[string]any
Background bool
TimeoutSec int
Metadata map[string]string
NodeRef string
RunID string
ModelGroupKey string
Adapter string
Target string
SessionID string
Workspace string
Prompt string
Input map[string]any
Background bool
TimeoutSec int
MaxQueue int
QueueTimeoutMS int
Metadata map[string]string
}
// RunDispatch describes a dispatched run in surface-neutral terms. It is the

View file

@ -57,6 +57,7 @@ openai:
# When set, /v1/models exposes these ids and /v1/chat/completions + /v1/responses
# resolve adapter/target per-entry. Entries not matched fall back to target/adapter above.
# workspace_required: true marks CLI agent routes that require a workspace path from the caller.
# max_queue and queue_timeout_ms configure the Edge-owned queue policy for the model alias.
# model_routes:
# - model: "codex"
# adapter: "cli"
@ -65,6 +66,8 @@ openai:
# - model: "llama3"
# adapter: "ollama"
# target: "llama3:8b"
# max_queue: 10
# queue_timeout_ms: 30000
# - model: "qwen3"
# adapter: "vllm"
# target: "qwen3-72b"
@ -100,7 +103,9 @@ nodes:
enabled: false
base_url: ""
context_size: 0
# capacity is the per-node concurrent execution slot limit.
capacity: 0
# max_queue and queue_timeout_ms are fallbacks used only if not specified in route-level queue policy.
max_queue: 0
queue_timeout_ms: 0
request_timeout_ms: 0
@ -162,7 +167,9 @@ nodes:
# enabled: true
# base_url: "http://127.0.0.1:11434"
# context_size: 131072
# # capacity is the per-node concurrent execution slot limit.
# capacity: 4
# # max_queue and queue_timeout_ms are fallbacks used only if not specified in route-level queue policy.
# max_queue: 16
# queue_timeout_ms: 30000
# request_timeout_ms: 300000

View file

@ -94,6 +94,8 @@ type OpenAIRouteEntry struct {
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"`
}
@ -526,6 +528,12 @@ func validateOpenAIRoutes(routes []OpenAIRouteEntry) error {
return fmt.Errorf("openai.model_routes: duplicate model %q", model)
}
seen[model] = struct{}{}
if r.MaxQueue < 0 {
return fmt.Errorf("openai.model_routes[%d].max_queue must be non-negative", i)
}
if r.QueueTimeoutMS < 0 {
return fmt.Errorf("openai.model_routes[%d].queue_timeout_ms must be non-negative", i)
}
}
return nil
}

View file

@ -1146,6 +1146,8 @@ openai:
node: "node-01"
session_id: "sess-a"
timeout_sec: 30
max_queue: 10
queue_timeout_ms: 5000
- model: "model-b"
adapter: "vllm"
target: "qwen"
@ -1167,6 +1169,9 @@ openai:
if r0.NodeRef != "node-01" || r0.SessionID != "sess-a" || r0.TimeoutSec != 30 {
t.Errorf("route[0] optional fields mismatch: %+v", r0)
}
if r0.MaxQueue != 10 || r0.QueueTimeoutMS != 5000 {
t.Errorf("route[0] queue policy mismatch: max_queue=%d, queue_timeout_ms=%d", r0.MaxQueue, r0.QueueTimeoutMS)
}
if r0.WorkspaceRequired {
t.Errorf("route[0] workspace_required should default to false: %+v", r0)
}
@ -1174,6 +1179,9 @@ openai:
if r1.Model != "model-b" || r1.Adapter != "vllm" || r1.Target != "qwen" {
t.Errorf("route[1] mismatch: %+v", r1)
}
if r1.MaxQueue != 0 || r1.QueueTimeoutMS != 0 {
t.Errorf("route[1] queue policy should default to 0: max_queue=%d, queue_timeout_ms=%d", r1.MaxQueue, r1.QueueTimeoutMS)
}
if r1.WorkspaceRequired {
t.Errorf("route[1] workspace_required should default to false: %+v", r1)
}
@ -1272,6 +1280,54 @@ openai:
}
}
func TestLoadEdge_OpenAIRouteCatalogQueuePolicyRejectsNegative(t *testing.T) {
dir := t.TempDir()
// Test negative max_queue
f1 := filepath.Join(dir, "edge_neg_max.yaml")
yaml1 := `
server:
listen: "0.0.0.0:9090"
openai:
model_routes:
- model: "model-a"
target: "llama3"
max_queue: -1
`
if err := os.WriteFile(f1, []byte(yaml1), 0o600); err != nil {
t.Fatalf("write yaml: %v", err)
}
_, err := config.LoadEdge(f1)
if err == nil {
t.Fatal("expected error for negative max_queue")
}
if !strings.Contains(err.Error(), "max_queue must be non-negative") {
t.Fatalf("expected error mentioning max_queue must be non-negative, got %v", err)
}
// Test negative queue_timeout_ms
f2 := filepath.Join(dir, "edge_neg_timeout.yaml")
yaml2 := `
server:
listen: "0.0.0.0:9090"
openai:
model_routes:
- model: "model-a"
target: "llama3"
queue_timeout_ms: -500
`
if err := os.WriteFile(f2, []byte(yaml2), 0o600); err != nil {
t.Fatalf("write yaml: %v", err)
}
_, err = config.LoadEdge(f2)
if err == nil {
t.Fatal("expected error for negative queue_timeout_ms")
}
if !strings.Contains(err.Error(), "queue_timeout_ms must be non-negative") {
t.Fatalf("expected error mentioning queue_timeout_ms must be non-negative, got %v", err)
}
}
// NormalizeAdapters regression tests
func TestNormalizeAdapters_OllamaIdempotent(t *testing.T) {