From 957700c2d6d6a51945f2dd34c5576b335f6a4d71 Mon Sep 17 00:00:00 2001 From: toki Date: Tue, 16 Jun 2026 22:30:36 +0900 Subject: [PATCH] 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 --- .../inference-provider-extension/PHASE.md | 4 + .../model-route-queue-policy-alignment.md | 81 ++++++ .../model-route-queue-policy-alignment/SDD.md | 100 +++++++ .../code_review_local_G07_0.log | 182 ++++++++++++ .../01_config_route_policy/complete.log | 55 ++++ .../plan_local_G07_0.log | 261 ++++++++++++++++++ .../CODE_REVIEW-local-G07.md | 152 ++++++++++ .../PLAN-local-G07.md | 184 ++++++++++++ apps/edge/internal/openai/chat_handler.go | 26 +- .../edge/internal/openai/responses_handler.go | 22 +- apps/edge/internal/openai/server_test.go | 10 +- apps/edge/internal/service/run_dispatch.go | 26 +- configs/edge.yaml | 7 + packages/go/config/config.go | 8 + packages/go/config/config_test.go | 56 ++++ 15 files changed, 1140 insertions(+), 34 deletions(-) create mode 100644 agent-roadmap/phase/inference-provider-extension/milestones/model-route-queue-policy-alignment.md create mode 100644 agent-roadmap/sdd/inference-provider-extension/model-route-queue-policy-alignment/SDD.md create mode 100644 agent-task/archive/2026/06/m-model-route-queue-policy-alignment/01_config_route_policy/code_review_local_G07_0.log create mode 100644 agent-task/archive/2026/06/m-model-route-queue-policy-alignment/01_config_route_policy/complete.log create mode 100644 agent-task/archive/2026/06/m-model-route-queue-policy-alignment/01_config_route_policy/plan_local_G07_0.log create mode 100644 agent-task/m-model-route-queue-policy-alignment/02+01_service_queue_policy/CODE_REVIEW-local-G07.md create mode 100644 agent-task/m-model-route-queue-policy-alignment/02+01_service_queue_policy/PLAN-local-G07.md diff --git a/agent-roadmap/phase/inference-provider-extension/PHASE.md b/agent-roadmap/phase/inference-provider-extension/PHASE.md index bfd37c7..a06582b 100644 --- a/agent-roadmap/phase/inference-provider-extension/PHASE.md +++ b/agent-roadmap/phase/inference-provider-extension/PHASE.md @@ -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을 검증한다. diff --git a/agent-roadmap/phase/inference-provider-extension/milestones/model-route-queue-policy-alignment.md b/agent-roadmap/phase/inference-provider-extension/milestones/model-route-queue-policy-alignment.md new file mode 100644 index 0000000..1aba99f --- /dev/null +++ b/agent-roadmap/phase/inference-provider-extension/milestones/model-route-queue-policy-alignment.md @@ -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 표시 확장 +- 확인 필요: 없음 diff --git a/agent-roadmap/sdd/inference-provider-extension/model-route-queue-policy-alignment/SDD.md b/agent-roadmap/sdd/inference-provider-extension/model-route-queue-policy-alignment/SDD.md new file mode 100644 index 0000000..92f5f5d --- /dev/null +++ b/agent-roadmap/sdd/inference-provider-extension/model-route-queue-policy-alignment/SDD.md @@ -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: 없음 diff --git a/agent-task/archive/2026/06/m-model-route-queue-policy-alignment/01_config_route_policy/code_review_local_G07_0.log b/agent-task/archive/2026/06/m-model-route-queue-policy-alignment/01_config_route_policy/code_review_local_G07_0.log new file mode 100644 index 0000000..3c2b6b3 --- /dev/null +++ b/agent-task/archive/2026/06/m-model-route-queue-policy-alignment/01_config_route_policy/code_review_local_G07_0.log @@ -0,0 +1,182 @@ + + +# 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-`이면 완료 이벤트 메타데이터를 보고한다. 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-`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, 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 수정은 런타임에 맡긴다. diff --git a/agent-task/archive/2026/06/m-model-route-queue-policy-alignment/01_config_route_policy/complete.log b/agent-task/archive/2026/06/m-model-route-queue-policy-alignment/01_config_route_policy/complete.log new file mode 100644 index 0000000..85e45b9 --- /dev/null +++ b/agent-task/archive/2026/06/m-model-route-queue-policy-alignment/01_config_route_policy/complete.log @@ -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 + +- 없음 + +## 후속 작업 + +- 없음 diff --git a/agent-task/archive/2026/06/m-model-route-queue-policy-alignment/01_config_route_policy/plan_local_G07_0.log b/agent-task/archive/2026/06/m-model-route-queue-policy-alignment/01_config_route_policy/plan_local_G07_0.log new file mode 100644 index 0000000..b5c5766 --- /dev/null +++ b/agent-task/archive/2026/06/m-model-route-queue-policy-alignment/01_config_route_policy/plan_local_G07_0.log @@ -0,0 +1,261 @@ + + +# 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`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다. diff --git a/agent-task/m-model-route-queue-policy-alignment/02+01_service_queue_policy/CODE_REVIEW-local-G07.md b/agent-task/m-model-route-queue-policy-alignment/02+01_service_queue_policy/CODE_REVIEW-local-G07.md new file mode 100644 index 0000000..4386381 --- /dev/null +++ b/agent-task/m-model-route-queue-policy-alignment/02+01_service_queue_policy/CODE_REVIEW-local-G07.md @@ -0,0 +1,152 @@ + + +# 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-`이면 완료 이벤트 메타데이터를 보고한다. 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-`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, 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 `계획 대비 변경 사항` | diff --git a/agent-task/m-model-route-queue-policy-alignment/02+01_service_queue_policy/PLAN-local-G07.md b/agent-task/m-model-route-queue-policy-alignment/02+01_service_queue_policy/PLAN-local-G07.md new file mode 100644 index 0000000..9a75b8f --- /dev/null +++ b/agent-task/m-model-route-queue-policy-alignment/02+01_service_queue_policy/PLAN-local-G07.md @@ -0,0 +1,184 @@ + + +# 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`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다. diff --git a/apps/edge/internal/openai/chat_handler.go b/apps/edge/internal/openai/chat_handler.go index c51e6e8..efe3412 100644 --- a/apps/edge/internal/openai/chat_handler.go +++ b/apps/edge/internal/openai/chat_handler.go @@ -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 } diff --git a/apps/edge/internal/openai/responses_handler.go b/apps/edge/internal/openai/responses_handler.go index b29710c..ced5083 100644 --- a/apps/edge/internal/openai/responses_handler.go +++ b/apps/edge/internal/openai/responses_handler.go @@ -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()) diff --git a/apps/edge/internal/openai/server_test.go b/apps/edge/internal/openai/server_test.go index d841f5b..b41d117 100644 --- a/apps/edge/internal/openai/server_test.go +++ b/apps/edge/internal/openai/server_test.go @@ -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) { diff --git a/apps/edge/internal/service/run_dispatch.go b/apps/edge/internal/service/run_dispatch.go index 0950c7e..98a9aa5 100644 --- a/apps/edge/internal/service/run_dispatch.go +++ b/apps/edge/internal/service/run_dispatch.go @@ -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 diff --git a/configs/edge.yaml b/configs/edge.yaml index fe51f23..8d2976f 100644 --- a/configs/edge.yaml +++ b/configs/edge.yaml @@ -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 diff --git a/packages/go/config/config.go b/packages/go/config/config.go index be65e20..c7b0c03 100644 --- a/packages/go/config/config.go +++ b/packages/go/config/config.go @@ -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 } diff --git a/packages/go/config/config_test.go b/packages/go/config/config_test.go index d9db290..cc7c459 100644 --- a/packages/go/config/config_test.go +++ b/packages/go/config/config_test.go @@ -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) {