feat: node multi-target serving foundation

- Add inference provider extension phase and milestones for node multi-target serving
- Update edge cmd config and node mapper for multi-target support
- Add Ollama adapter updates and transport client changes
- Update config package and edge.yaml for new settings
- Extend runtime proto with multi-target serving fields
- Add agent task archives and plans for adapter registry, route catalog,
  engine profiles, edge options, commands/caps, runtime concurrency,
  and field smoke tests
This commit is contained in:
toki 2026-06-11 04:09:48 +09:00
parent 918cfed90c
commit 3624ff0f01
34 changed files with 2738 additions and 27 deletions

View file

@ -7,13 +7,17 @@
## 목표
Ollama 경로가 안정화된 뒤, 그 결과를 기준선으로 삼아 SGLang 같은 추가 추론 서버 provider를 붙인다.
이 Phase는 여러 provider의 adapter/config/target/model 매핑을 성급히 일반화하지 않고, Ollama에서 검증된 serving 기준을 기준선으로 삼아 추가 provider를 하나씩 검증한다.
이 Phase는 하나의 Node transport 위에서 여러 CLI profile, terminal gateway, 추론 엔진, model target을 함께 쓰는 구조를 기준선으로 삼고, provider별 adapter/config/target/model 매핑을 단계적으로 검증한다.
## Milestone 흐름
완료된 Milestone은 archive 경로를 가리키고, 검토중, 진행중, 계획 또는 보류 Milestone은 이 Phase 하위 `milestones/` 경로를 가리킨다.
완료, 검토중, 진행중, 계획 순서로 두어 아래로 갈수록 미래 작업에 가까워지게 정렬한다.
- [계획] Node 단일 통로 멀티 타겟 서빙 기반
- 경로: `agent-roadmap/phase/inference-provider-extension/milestones/node-multi-target-serving-foundation.md`
- 요약: 하나의 Node 연결이 여러 CLI profile, terminal gateway, Ollama/vLLM/SGLang 같은 추론 엔진 연결, 여러 model target을 동시에 제공할 수 있도록 config/proto/routing/runtime 기준선을 정리한다.
- [계획] SGLang provider 서빙 경로 추가
- 경로: `agent-roadmap/phase/inference-provider-extension/milestones/sglang-provider-serving-validation.md`
- 요약: Ollama 경로가 안정화된 뒤 추가 추론 서버 provider로 SGLang을 붙이고, 같은 Edge OpenAI-compatible 입력 표면에서 모델 조회와 non-streaming/streaming chat을 검증한다.
@ -22,5 +26,5 @@ Ollama 경로가 안정화된 뒤, 그 결과를 기준선으로 삼아 SGLang
- 이 Phase는 Ollama 경로 안정화가 선행된 뒤 시작한다.
- 진행중인 Ollama 실테스트와 후속 안정화 작업을 이 Phase로 흡수하지 않는다.
- 여러 추론 서버 provider 표준화는 Ollama 경로가 안정화된 결과를 기준선으로 삼아 진행한다.
- 여러 추론 서버 provider 표준화는 Ollama 경로가 안정화된 결과와 Node 단일 통로 멀티 타겟 기준선을 함께 기준으로 삼아 진행한다.
- cloud fallback, 자동 부하 라우팅, 품질 평가 feedback과 후속 최적화 계층은 이 Phase에서 다루지 않는다.

View file

@ -0,0 +1,89 @@
# Milestone: Node 단일 통로 멀티 타겟 서빙 기반
## 위치
- Roadmap: `agent-roadmap/ROADMAP.md`
- Phase: `agent-roadmap/phase/inference-provider-extension/PHASE.md`
## 목표
하나의 Node proto-socket 연결 위에서 여러 CLI profile, terminal gateway, 추론 엔진 연결, model target을 함께 사용할 수 있는 기준선을 만든다.
Edge는 Node별 adapter/profile/engine/model 라우팅과 Edge-owned runtime option을 소유하고, Node는 그 단일 통로에서 여러 실행 target을 안정적으로 처리한다.
## 상태
[계획]
## 승격 조건
- 없음
## 구현 잠금
- 상태: 해제
- 결정 필요: 없음
## 범위
- Node 하나가 단일 TCP/proto-socket 연결로 여러 adapter와 target을 동시에 제공하는 구조를 공식 기준선으로 정리한다.
- CLI profile map, terminal gateway, Ollama/vLLM/SGLang 같은 추론 엔진 연결을 같은 `adapter + target` 실행 모델에서 표현한다.
- Edge config와 proto payload가 adapter type별 단일 endpoint에 갇히지 않고 여러 engine/profile instance와 model route를 표현할 수 있게 한다.
- OpenAI-compatible `/v1/models`, `/v1/chat/completions`, `/v1/responses`가 Edge-owned model catalog와 route table을 통해 내부 `adapter + target`으로 변환되게 한다.
- 모델별 context window, runner sizing, keep-alive 같은 load/unload 영향 옵션은 agent request가 아니라 Edge 설정이 소유한다.
- Node runtime concurrency와 adapter capability가 멀티 타겟 동시 실행에서 실제로 적용되는지 검증한다.
## 기능
### Epic: [multi-target-node] Single Node Multi Target Serving
- [ ] [config-topology] Edge `nodes[]` 설정과 `NodeConfigPayload`가 하나의 Node 안에 여러 CLI profile, terminal gateway, 추론 engine/profile instance, model target route를 표현한다.
- [ ] [adapter-registry] Node adapter registry가 동일 adapter type의 여러 engine/profile instance를 덮어쓰지 않고 라우팅 가능한 target namespace로 관리한다.
- [ ] [route-catalog] Edge OpenAI-compatible model catalog가 외부 `model`을 Edge-owned route table로 해석하고, 고정 `openai.target` 단일 override 없이 내부 `adapter + target`으로 변환한다. 검증: `/v1/models`가 여러 route를 노출하고 각 model 요청이 기대 adapter/target으로 dispatch되는 단위 테스트를 통과한다.
- [ ] [engine-profiles] Ollama는 여러 `base_url`/model target과 target별 `context_size`를 지원하고, vLLM/SGLang 같은 OpenAI-compatible inference engine 연결을 같은 Node 통로에서 확장할 수 있는 provider/profile 기준이 정리되어 있다.
- [ ] [edge-owned-options] context window와 runner sizing 옵션은 Edge config가 최종 소유하며, 외부 agent request의 `options.num_ctx` 같은 값이 Edge-owned 값을 변경하지 못한다. 검증: 요청 option override가 route/profile 설정으로 덮이는 테스트를 통과한다.
- [ ] [commands-caps] `CAPABILITIES`, `SESSION_LIST`, `USAGE_STATUS`, provider passthrough command가 여러 adapter/profile/model target에서 모호하지 않은 결과를 반환한다.
- [ ] [runtime-concurrency] `runtime.concurrency`와 adapter capability가 Node 멀티 호출에서 실제 제한 또는 스케줄링 정책으로 적용된다. 검증: 동시 run 테스트에서 제한 초과 요청이 queue/reject/timeout 중 정의된 정책대로 동작한다.
- [ ] [field-smoke] split-host smoke에서 하나의 연결된 Node가 최소 두 개의 inference model target과 하나의 CLI target을 같은 Edge-Node 연결로 호출할 수 있음을 확인한다.
## 작업 분할
- 상태: 첫 Epic의 각 기능 Task는 config/proto, Edge routing, Node registry, provider profile, runtime concurrency, field smoke에 걸치는 구조 변경이라 작은 즉시 코드 변경으로 처리하지 않는다.
- Active task group: `agent-task/m-node-multi-target-serving-foundation/`
- 분할:
- `01_config_topology`: `[config-topology]`
- `02+01_adapter_registry`: `[adapter-registry]`
- `03+01_route_catalog`: `[route-catalog]`
- `04+01,02_engine_profiles`: `[engine-profiles]`
- `05+03,04_edge_owned_options`: `[edge-owned-options]`
- `06+02,04_commands_caps`: `[commands-caps]`
- `07+02_runtime_concurrency`: `[runtime-concurrency]`
- `08+03,04,05,06,07_field_smoke`: `[field-smoke]`
- 적용 원칙: 작은 보정이 각 subtask 구현 중 발견되면 해당 plan 범위 안에서 바로 처리하고, 구조 변경 또는 검증 실패는 plan/code-review loop로 이어간다.
## 완료 리뷰
- 상태: 없음
- 요청일: 없음
- 완료 근거: 모든 기능 Task와 Task 안에 명시된 검증이 아직 충족되지 않았다.
- 리뷰 필요:
- [ ] 사용자가 완료 결과를 확인했다
- [ ] archive 이동을 승인했다
- 리뷰 코멘트: 없음
## 범위 제외
- Control Plane이 Node에 직접 연결하거나 Node를 직접 스케줄링하는 구조
- OpenAI-compatible/A2A payload에 terminal 제어 기능을 섞는 구조
- cloud fallback, 품질 평가 feedback, 자동 비용/품질 최적화
- RAG, context 압축, web search, MCP/tool policy 같은 후속 최적화 계층
- 모든 provider의 완전한 production hardening을 한 번에 끝내는 작업
## 작업 컨텍스트
- 관련 경로: `packages/go/config`, `proto/iop`, `apps/edge/internal/node`, `apps/edge/internal/service`, `apps/edge/internal/openai`, `apps/node/internal/adapters`, `apps/node/internal/router`, `apps/node/internal/node`, `configs`, `docs`
- 표준선(선택): Node transport는 node id당 하나의 TCP/proto-socket 연결을 유지하고, 그 연결 위에서 `adapter + target + session_id` 기준으로 여러 실행을 multiplex한다.
- 표준선(선택): Edge는 Node registry, adapter/profile configuration, model catalog, routing, runtime option의 원본 소유자다.
- 표준선(선택): CLI adapter의 profile map과 `(target, session_id)` session key는 멀티 타겟 구조의 기존 기준선으로 유지한다.
- 선행 작업: Ollama 서빙 안정화 기반, proto-socket 멀티 호출 안정화
- 후속 작업: SGLang provider 서빙 경로 추가, 지식과 도구 최적화 확장
- 확인 필요: 구현 중 발견되는 provider별 세부 옵션은 Edge-owned route/profile 설정에 남기고, agent request가 runner sizing을 직접 바꾸는 경로는 허용하지 않는다.

View file

@ -66,6 +66,6 @@ Ollama 실테스트에서 안정화한 모델 조회, non-streaming/streaming ch
- 관련 경로: `apps/edge`, `apps/node`, `packages/go/config`, `proto/iop`, `configs`, `bin/edge.sh`, `bin/node.sh`, `agent-test/local/edge-smoke.md`, `agent-test/local/node-smoke.md`, `agent-test/local/platform-common-smoke.md`
- 표준선(선택): 내부 실행 계약은 `adapter + target`을 유지하고, OpenAI-compatible API는 외부 호환 입력 표면으로 둔다.
- 표준선(선택): Ollama provider에서 안정화한 field smoke 기준을 SGLang provider에도 그대로 적용한다.
- 선행 작업: Ollama 실테스트와 후속 안정화
- 선행 작업: Ollama 실테스트와 후속 안정화, Node 단일 통로 멀티 타겟 서빙 기반
- 후속 작업: SGLang provider 실테스트에서 확인된 serving 경로 안정화 보완
- 확인 필요: SGLang adapter 경계, 실테스트 endpoint/model, 인증/헤더 필요 여부, streaming 기대 동작

View file

@ -0,0 +1,143 @@
<!-- task=m-node-multi-target-serving-foundation/01_config_topology 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`.
> Do not ask the user directly, present choices in chat, or call `request_user_input` during implementation.
> Finalization is review-agent-only.
## 개요
date=2026-06-10
task=m-node-multi-target-serving-foundation/01_config_topology, plan=0, tag=API
## Roadmap Targets
- Milestone: `agent-roadmap/phase/inference-provider-extension/milestones/node-multi-target-serving-foundation.md`
- Task ids:
- `config-topology`: Edge `nodes[]` 설정과 `NodeConfigPayload`가 하나의 Node 안에 여러 CLI profile, terminal gateway, 추론 engine/profile instance, model target route를 표현한다.
- Completion mode: check-on-pass
## 이 파일을 읽는 리뷰 에이전트에게
> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.
구현을 실제 소스와 대조하고 검증 출력이 코드와 일치하는지 확인한다. PASS이면 active 파일을 log로 아카이브하고 `complete.log`를 작성한 뒤 task directory를 archive로 이동한다. WARN/FAIL이면 user-review gate를 확인하고 후속 plan/review 또는 `USER_REVIEW.md`를 작성한다.
## 구현 항목별 완료 여부
| 항목 | 완료 여부 |
|------|---------|
| [API-1] Multi Target Topology Contract | [x] |
## 구현 체크리스트
- [x] Edge config와 proto payload가 multi adapter/profile/model route topology를 표현하고 legacy 단일 설정을 호환한다.
- [x] config/proto/mapper/edgecmd 테스트가 멀티 topology와 legacy compat를 검증한다.
- [x] `make proto` 필요 여부를 확인하고 생성물 및 import 깨짐을 정리한다.
- [x] 최종 검증 명령을 실행한다.
- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
## 코드리뷰 전용 체크리스트
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
- [x] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
- [x] active `CODE_REVIEW-cloud-G08.md`를 `code_review_cloud_G08_N.log`로 아카이브한다.
- [x] active `PLAN-local-G08.md`를 `plan_local_G08_M.log`로 아카이브한다.
- [ ] PASS이면 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
- [ ] PASS이면 active task 디렉터리를 archive로 이동한다.
- [ ] PASS이고 task group이 `m-node-multi-target-serving-foundation`이면 런타임 완료 이벤트 메타데이터를 보고한다.
- [x] WARN/FAIL이면 후속 active plan/review 또는 `USER_REVIEW.md`를 작성한다.
## 계획 대비 변경 사항
- **`edgecmd/node_register.go`**: plan에서 "register/config 생성 경로 보강"을 언급했으나, `node_register.go`의 `--adapter` 플래그 흐름은 단일 ollama/cli 진입 경로로 남겨뒀다. Multi-instance 등록은 edge.yaml 직접 편집으로 처리하는 것이 현 UX 방향과 맞고, CLI 플래그로 다수의 named instance를 표현하는 인터페이스 설계는 후속 task 범위로 판단해 이번 변경에서 제외했다. `validateEdgeConfig`에서 `ollama_instances`/`vllm_instances` 유효성 검증은 추가했다.
- **`normalizeAdapters`의 legacy 이름 고정**: legacy `Ollama.Enabled` 단일 필드는 `OllamaInstances`로 promote 시 name을 `"ollama"`로 고정한다. 이는 기존 단일 인스턴스 시나리오에서 instance identity를 예측 가능하게 만들기 위함이다.
- **기존 `TestBuildConfigPayload_OllamaEnabled` 수정**: mapper가 `OllamaInstances`만 읽도록 변경되었으므로, LoadEdge를 통하지 않고 NodeRecord를 직접 구성하는 기존 테스트도 `OllamaInstances`를 세팅하도록 수정했다. 이것은 "mapper는 post-normalisation 상태를 받는다"는 계약을 테스트에 명시하는 것이다.
## 주요 설계 결정
- **AdapterConfig.name 필드 (proto field 8)**: `AdapterConfig`에 `name` 문자열 필드를 추가해 동일 type의 여러 instance를 구분하는 stable identity로 사용한다. 빈 값은 legacy single-instance로 간주한다.
- **AdapterConfig.target 필드 (proto field 9)**: 선택적 default target/route 힌트. 이번 task에서는 proto contract 확보 목적으로 추가만 하고 routing 로직은 후속 task에서 사용한다.
- **`OllamaInstances`/`VllmInstances` slice를 `AdaptersConf`에 추가**: 동일 type 여러 engine instance를 표현한다. legacy `Ollama`/`Vllm` 단일 필드는 `LoadEdge` 내 `normalizeAdapters`에서 슬라이스 앞에 promote되어 하위 코드가 슬라이스만 읽으면 된다.
- **중복 name 검증**: `normalizeAdapters`(config load)와 `BuildConfigPayload`(mapper) 양쪽에서 중복 name을 에러로 처리해 키 충돌을 load 시점에 잡는다.
## 사용자 리뷰 요청
_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. 구현 에이전트는 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백만으로는 사용자 리뷰 요청을 작성하지 않는다._
- 상태: 없음
- 사유 유형: 없음
- 결정 필요: 없음
- 차단 근거: 없음
- 실행한 검증/명령: 없음
- 자동 후속 불가 이유: 없음
- 재개 조건: 없음
## 리뷰어를 위한 체크포인트
- legacy 단일 `ollama`/`vllm`/`cli` 설정이 깨지지 않았는지 확인한다.
- proto 변경 시 생성물과 call site가 함께 반영됐는지 확인한다.
- 동일 adapter type 여러 instance의 key 충돌 정책이 테스트로 고정됐는지 확인한다.
## 검증 결과
### API-1 중간 검증
```bash
$ go test -count=1 ./packages/go/... ./apps/edge/internal/node/... ./apps/edge/internal/edgecmd/...
ok iop/packages/go/audit 0.002s
ok iop/packages/go/config 0.013s
ok iop/packages/go/hostsetup 0.004s
ok iop/packages/go/observability 0.015s
ok iop/apps/edge/internal/node 0.005s
ok iop/apps/edge/internal/edgecmd 0.007s
```
### 최종 검증
```bash
$ go test -count=1 ./packages/go/... ./proto/gen/... ./apps/edge/internal/node/... ./apps/edge/internal/edgecmd/...
ok iop/packages/go/audit 0.002s
ok iop/packages/go/config 0.014s
ok iop/packages/go/hostsetup 0.006s
ok iop/packages/go/observability 0.038s
? iop/proto/gen/iop [no test files]
ok iop/apps/edge/internal/node 0.004s
ok iop/apps/edge/internal/edgecmd 0.011s
```
```bash
$ make proto
protoc \
--go_out=. \
--go_opt=module=iop \
--proto_path=. \
proto/iop/runtime.proto \
proto/iop/node.proto \
proto/iop/control.proto \
proto/iop/job.proto
```
proto 변경 반영됨: `AdapterConfig`에 `name`(field 8), `target`(field 9) 추가. `proto/gen/iop/runtime.pb.go` 갱신 확인.
## 코드리뷰 결과
- 종합 판정: `WARN`
- 차원별 평가:
- correctness: Warn
- completeness: Pass
- test coverage: Warn
- API contract: Pass
- code quality: Pass
- plan deviation: Pass
- verification trust: Pass
- 발견된 문제:
- Suggested: `apps/edge/internal/edgecmd/config.go:143`의 `hasOllama`/`hasVllm` 판정이 `len(instance) > 0`만 확인해, 모든 instance가 `enabled: false`여도 node가 adapter를 가진 것으로 검증 통과할 수 있다. 활성 instance 기준으로 판정하고 이 회귀를 edgecmd 테스트로 고정한다.
- Suggested: `apps/edge/internal/edgecmd/config.go:151` 주변 검증이 `ollama_instances[].base_url`만 확인하고 `vllm_instances[].endpoint`는 확인하지 않는다. `enabled: true`인 vLLM instance의 빈 endpoint를 거부하고 테스트를 추가한다.
- 다음 단계: WARN/FAIL 후속 plan/review를 생성한다.

View file

@ -0,0 +1,167 @@
<!-- task=m-node-multi-target-serving-foundation/01_config_topology plan=1 tag=REVIEW_API -->
# Code Review Reference - REVIEW_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-10
task=m-node-multi-target-serving-foundation/01_config_topology, plan=1, tag=REVIEW_API
## Roadmap Targets
- Milestone: `agent-roadmap/phase/inference-provider-extension/milestones/node-multi-target-serving-foundation.md`
- Task ids:
- `config-topology`: Edge `nodes[]` 설정과 `NodeConfigPayload`가 하나의 Node 안에 여러 CLI profile, terminal gateway, 추론 engine/profile instance, model target route를 표현한다.
- Completion mode: check-on-pass
## 이 파일을 읽는 리뷰 에이전트에게
> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다.
1. 판정을 append한다.
2. `CODE_REVIEW-cloud-G08.md` -> `code_review_cloud_G08_N.log`, `PLAN-local-G08.md` -> `plan_local_G08_M.log`로 아카이브한다.
3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-node-multi-target-serving-foundation/01_config_topology/`로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다.
4. PASS이고 task group이 `m-node-multi-target-serving-foundation`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다.
5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다.
---
## 구현 항목별 완료 여부
| 항목 | 완료 여부 |
|------|---------|
| [REVIEW_API-1] Edge Config Validation Follow-Up | [x] |
## 구현 체크리스트
- [x] `validateEdgeConfig`가 legacy adapter와 multi-instance adapter 모두 실제 enabled 상태 기준으로 "at least one adapter"를 판단한다.
- [x] `validateEdgeConfig`가 enabled vLLM legacy/instance endpoint 빈 값을 거부하고 edgecmd 테스트가 이를 검증한다.
- [x] 최종 검증 명령을 실행한다.
- [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_cloud_G08_N.log`로 아카이브한다.
- [x] active `PLAN-*-G??.md`를 `plan_local_G08_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-node-multi-target-serving-foundation/01_config_topology/`를 `agent-task/archive/YYYY/MM/m-node-multi-target-serving-foundation/01_config_topology/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
- [x] PASS이고 task group이 `m-node-multi-target-serving-foundation`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
- [x] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-node-multi-target-serving-foundation/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다.
- [ ] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-local-G08.md`와 `CODE_REVIEW-cloud-G08.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로 이동한다.
## 계획 대비 변경 사항
없음. plan이 지정한 두 항목(enabled 기준 at-least-one, vLLM endpoint 빈 값 거부)을 그대로 구현했다.
## 주요 설계 결정
- **`anyOllamaEnabled` / `anyVllmEnabled` 헬퍼 분리**: `validateEdgeConfig` 내부에서 legacy 단일 필드와 instance slice 양쪽을 순회해 실제 enabled 인스턴스가 하나라도 있는지 판단한다. 이전 `len(OllamaInstances) > 0` 방식은 `Enabled: false` 인스턴스만 있는 경우에도 true를 반환하는 문제가 있었다.
- **vLLM 검증 일관성**: legacy `Vllm.Enabled && Endpoint == ""` 와 `VllmInstances[]` enabled + empty endpoint 를 동일 패턴으로 거부한다. Ollama의 base_url 검증과 대칭을 맞췄다.
## 사용자 리뷰 요청
_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. 구현 에이전트는 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백만으로는 사용자 리뷰 요청을 작성하지 않는다._
- 상태: 없음
- 사유 유형: 없음
- 결정 필요: 없음
- 차단 근거: 없음
- 실행한 검증/명령: 없음
- 자동 후속 불가 이유: 없음
- 재개 조건: 없음
## 리뷰어를 위한 체크포인트
- `config check`가 실제 enabled adapter가 없는 node를 거부하는지 확인한다.
- enabled legacy `vllm`과 `vllm_instances[]`의 빈 endpoint가 거부되는지 확인한다.
- 기존 legacy `ollama`/`cli` 경로가 불필요하게 깨지지 않았는지 확인한다.
## 검증 결과
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
필수 규칙:
- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다.
- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다.
- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다.
- 사용자 리뷰 요청으로 명령을 끝까지 실행하지 못했다면 `사용자 리뷰 요청`에 실행한 명령, 실제 출력, 미실행 명령의 사유를 기록한다.
- mobile/UI hang, timeout, 또는 2분 무진행은 blind retry를 중단하고 focused rerun 명령과 screenshot/window/UI-tree evidence path를 남기며, 불가능하면 정확한 사유를 남긴다.
### REVIEW_API-1 중간 검증
```bash
$ go test -count=1 ./apps/edge/internal/edgecmd/...
ok iop/apps/edge/internal/edgecmd 0.007s
```
### 최종 검증
```bash
$ go test -count=1 ./packages/go/... ./proto/gen/... ./apps/edge/internal/node/... ./apps/edge/internal/edgecmd/...
ok iop/packages/go/audit 0.002s
? iop/packages/go/auth [no test files]
ok iop/packages/go/config 0.013s
? iop/packages/go/events [no test files]
ok iop/packages/go/hostsetup 0.004s
? iop/packages/go/jobs [no test files]
? iop/packages/go/metadata [no test files]
ok iop/packages/go/observability 0.012s
? iop/packages/go/policy [no test files]
? iop/packages/go/version [no test files]
? iop/proto/gen/iop [no test files]
ok iop/apps/edge/internal/node 0.005s
ok iop/apps/edge/internal/edgecmd 0.006s
```
---
> **[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.
Sections and their ownership:
| 섹션 | 소유자 | 설명 |
|------|--------|------|
| 헤더 주석, 개요(date/task/plan/tag), 리뷰 에이전트 지시 | 스텁 생성 시 고정 | 구현 에이전트가 수정하거나 실행하지 않음 |
| Roadmap Targets | follow-up plan에서 복사해 스텁 생성 시 고정 | 구현 에이전트가 수정하지 않음; PASS 시 code-review가 `complete.log`의 `Roadmap Completion`으로 복사 |
| 구현 항목별 완료 여부 (항목명) | 스텁 생성 시 고정 | `[ ]` -> `[x]` 체크만 구현 에이전트가 수행 |
| 구현 체크리스트 (항목 텍스트/순서) | follow-up plan에서 복사해 스텁 생성 시 고정 | 구현 에이전트가 `[ ]` -> `[x]` 체크만 수행; 마지막 체크박스는 저장 전 필수 |
| 코드리뷰 전용 체크리스트 | Review agent only | Implementing agent must not modify or check this section |
| 계획 대비 변경 사항, 주요 설계 결정 | 구현 에이전트가 채움 | placeholder 텍스트를 실제 내용으로 교체 |
| 사용자 리뷰 요청 | 구현 에이전트가 채움 | 진행에 사용자 입력이 필요하지 않으면 `상태: 없음` 유지; 구현 중 직접 질문은 금지; 필요하면 결정 항목, 근거, 명령 출력, 자동 후속 불가 이유, 재개 조건을 기록 |
| 리뷰어를 위한 체크포인트 | 스텁 생성 시 고정 | 계획에서 추출한 리뷰 포인트 |
| 검증 결과 (섹션 제목 + 명령) | 스텁 생성 시 고정 | 실행 출력만 구현 에이전트가 채움; 명령 변경은 `계획 대비 변경 사항`에 기록 |
| 코드리뷰 결과 | 리뷰 에이전트가 append | 스텁에 포함하지 않음 |
## 코드리뷰 결과
- 종합 판정: `PASS`
- 차원별 평가:
- correctness: Pass
- completeness: Pass
- test coverage: Pass
- API contract: Pass
- code quality: Pass
- plan deviation: Pass
- verification trust: Pass
- 발견된 문제: 없음
- 다음 단계: PASS 종결 처리로 `complete.log`를 작성하고 task directory를 archive로 이동한다.

View file

@ -0,0 +1,41 @@
# Complete - m-node-multi-target-serving-foundation/01_config_topology
## 완료 일시
2026-06-10
## 요약
Edge config multi-target topology foundation review loop completed after 2 reviews; final verdict PASS.
## 루프 이력
| Plan | Review | Verdict | 메모 |
|------|--------|---------|------|
| `plan_local_G08_0.log` | `code_review_cloud_G08_0.log` | WARN | Multi topology contract was judgeable, but `config check` needed enabled-instance and vLLM endpoint validation follow-up. |
| `plan_local_G08_1.log` | `code_review_cloud_G08_1.log` | PASS | Follow-up validation gaps were fixed and covered by edgecmd tests. |
## 구현/정리 내용
- Edge config validation now counts enabled legacy adapters and enabled multi-instance adapters when enforcing "at least one adapter".
- Enabled legacy vLLM and `vllm_instances[]` entries now reject empty endpoints.
- Edgecmd tests cover disabled instance-only rejection, vLLM empty endpoint rejection, and enabled Ollama instance acceptance.
## 최종 검증
- `go test -count=1 ./packages/go/... ./proto/gen/... ./apps/edge/internal/node/... ./apps/edge/internal/edgecmd/...` - PASS; reviewer rerun succeeded across package/config/proto-gen/edge node/edgecmd targets.
## Roadmap Completion
- Milestone: `agent-roadmap/phase/inference-provider-extension/milestones/node-multi-target-serving-foundation.md`
- Completed task ids:
- `config-topology`: PASS; evidence=`plan_local_G08_0.log`, `code_review_cloud_G08_0.log`, `plan_local_G08_1.log`, `code_review_cloud_G08_1.log`; verification=`go test -count=1 ./packages/go/... ./proto/gen/... ./apps/edge/internal/node/... ./apps/edge/internal/edgecmd/...`
- Not completed task ids: 없음
## 잔여 Nit
- 없음
## 후속 작업
- 없음

View file

@ -0,0 +1,124 @@
<!-- task=m-node-multi-target-serving-foundation/01_config_topology plan=0 tag=API -->
# Plan - API
## 이 파일을 읽는 구현 에이전트에게
`CODE_REVIEW-cloud-G08.md`의 구현 에이전트 소유 섹션 작성까지가 구현의 일부다. 코드 변경 후 검증을 실행하고 실제 변경 내용, 검증 출력, 계획 대비 변경 사항을 채운 뒤 active 파일을 그대로 두고 리뷰 준비를 보고한다. 사용자 결정, 사용자 소유 외부 환경, 또는 범위 충돌 없이는 진행할 수 없으면 review stub의 `사용자 리뷰 요청`에 근거를 남기고 중단한다. 구현 중 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`, `USER_REVIEW.md`, archive log, `complete.log`를 만들지 않는다.
## 배경
현재 Edge `nodes[]`와 `NodeConfigPayload`는 adapter type별 단일 endpoint를 표현하는 구조다. 하나의 Node 연결에서 여러 CLI profile, terminal gateway, 여러 inference engine/profile, model route를 함께 쓰려면 config와 proto payload가 먼저 멀티 타겟 topology를 표현해야 한다.
## 사용자 리뷰 요청 흐름
구현 중 blocker는 active `CODE_REVIEW-cloud-G08.md`의 `사용자 리뷰 요청` 섹션에 기록한다. 구현 에이전트는 직접 사용자 프롬프트를 만들지 않으며, code-review가 요청 타당성과 실제 `USER_REVIEW.md` 작성 여부를 판단한다.
## Roadmap Targets
- Milestone: `agent-roadmap/phase/inference-provider-extension/milestones/node-multi-target-serving-foundation.md`
- Task ids:
- `config-topology`: Edge `nodes[]` 설정과 `NodeConfigPayload`가 하나의 Node 안에 여러 CLI profile, terminal gateway, 추론 engine/profile instance, model target route를 표현한다.
- Completion mode: check-on-pass
## 분석 결과
### 읽은 파일
- `agent-test/local/rules.md`
- `agent-test/local/node-smoke.md`
- `agent-test/local/edge-smoke.md`
- `agent-test/local/platform-common-smoke.md`
- `packages/go/config/config.go`
- `packages/go/config/config_test.go`
- `proto/iop/runtime.proto`
- `configs/edge.yaml`
- `apps/edge/internal/node/mapper.go`
- `apps/edge/internal/node/mapper_test.go`
- `apps/edge/internal/edgecmd/config.go`
- `apps/edge/internal/edgecmd/node_register.go`
- `agent-roadmap/phase/inference-provider-extension/milestones/node-multi-target-serving-foundation.md`
### 테스트 환경 규칙
- test_env: `local`
- local rules/profile을 읽었다. 적용 명령은 `go test ./packages/go/... ./proto/gen/...`, `go test ./apps/edge/internal/node/...`, `go test ./apps/edge/internal/edgecmd/...`이며 proto schema 변경 시 `make proto` 후 생성물 차이를 확인한다.
- `platform-common-smoke`, `edge-smoke`, `node-smoke` 기준상 사용자 실행 경로가 바뀌면 최소 edge/node config round-trip smoke 증거를 남겨야 한다.
### 테스트 커버리지 공백
- `EdgeConfig.Nodes[].Adapters`가 단일 `OllamaConf`/`VllmConf`만 갖는 구조는 `config_test.go`의 단일 context size 테스트로만 덮인다. 멀티 engine/profile decode 테스트가 없다.
- `NodeConfigPayload`는 typed oneof가 단일 adapter instance를 전송하는 테스트만 있다. 동일 type 여러 instance와 route/profile id 전달 테스트가 없다.
- `edgecmd node register`는 단일 ollama/cli 플래그 중심이라 여러 profile/engine 입력 테스트가 없다.
### 심볼 참조
- 변경 전 rename/remove 예정 심볼: 없음. 새 config/proto 필드를 추가하고 legacy 필드는 compat path로 유지한다.
- 관련 참조: `AdaptersConf`, `OllamaConf`, `VllmConf`, `CLIConf`, `BuildConfigPayload`, `NodeConfigPayload`, `AdapterConfig`.
### 분할 판단
- split 정책을 적용했다. 공유 API/proto/config foundation이 먼저 필요하므로 task group은 `m-node-multi-target-serving-foundation`이고 이 작업은 `01_config_topology` 독립 선행 작업이다.
- sibling 작업: `02+01_adapter_registry`, `03+01_route_catalog`, `04+01,02_engine_profiles`, `05+03,04_edge_owned_options`, `06+02,04_commands_caps`, `07+02_runtime_concurrency`, `08+03,04,05,06,07_field_smoke`.
- predecessor: 없음.
### 범위 결정 근거
- Edge OpenAI route 해석, Node adapter registry, provider 실행 구현은 후속 sibling task로 분리한다.
- SGLang 완성 구현, cloud fallback, RAG/tool policy는 milestone 범위 제외에 남긴다.
### 빌드 등급
- build: `local-G08`, review: `cloud-G08`. 프로토콜/schema와 config/call-site가 함께 움직이지만 범위가 foundation 파일로 제한된다.
## 구현 체크리스트
- [ ] Edge config와 proto payload가 multi adapter/profile/model route topology를 표현하고 legacy 단일 설정을 호환한다.
- [ ] config/proto/mapper/edgecmd 테스트가 멀티 topology와 legacy compat를 검증한다.
- [ ] `make proto` 필요 여부를 확인하고 생성물 및 import 깨짐을 정리한다.
- [ ] 최종 검증 명령을 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
### [API-1] Multi Target Topology Contract
- 문제: `AdaptersConf`는 `Ollama`, `Vllm`, `CLI` 단일 필드만 제공해 동일 adapter type 여러 engine/profile instance를 표현하지 못한다 (`packages/go/config/config.go:168`). `NodeConfigPayload`와 `AdapterConfig`도 instance identity나 route metadata 없이 `type` 중심이다 (`proto/iop/runtime.proto:147`, `proto/iop/runtime.proto:153`). `BuildConfigPayload`는 ollama/vllm을 각각 한 번만 append한다 (`apps/edge/internal/node/mapper.go:24`, `apps/edge/internal/node/mapper.go:36`).
- 해결 방법: `NodeConf`/`AdaptersConf`에 typed profile collection을 추가하고 기존 단일 필드는 legacy alias로 유지한다. `AdapterConfig`에는 stable `name` 또는 `id`, optional target route/profile metadata를 추가한다. Mapper는 legacy 단일 필드와 신규 collection을 모두 payload로 변환하되 중복 key는 명확한 에러로 처리한다.
- 수정 파일 및 체크리스트:
- [ ] `packages/go/config/config.go`: multi profile structs와 defaults/legacy normalization 추가.
- [ ] `proto/iop/runtime.proto`: adapter instance identity와 route/profile payload 필드 추가.
- [ ] `apps/edge/internal/node/mapper.go`: 신규 topology를 `NodeConfigPayload`로 변환.
- [ ] `apps/edge/internal/edgecmd/node_register.go`: register/config 생성 경로가 신규 topology를 만들 수 있게 보강.
- [ ] `configs/edge.yaml`: 사람용 예시를 multi target 기준으로 갱신.
- 테스트 작성: 추가한다. `packages/go/config/config_test.go`에 멀티 Ollama/vLLM/CLI profile decode와 legacy 단일 decode 테스트, `apps/edge/internal/node/mapper_test.go`에 동일 adapter type 여러 instance payload 테스트, `apps/edge/internal/edgecmd` 테스트에 register 출력 topology 테스트를 추가한다.
- 중간 검증:
- `go test -count=1 ./packages/go/... ./apps/edge/internal/node/... ./apps/edge/internal/edgecmd/...`
- proto 변경 시 `make proto && go test -count=1 ./proto/gen/...`
## 수정 파일 요약
| 파일 | 항목 |
|------|------|
| `packages/go/config/config.go` | API-1 |
| `packages/go/config/config_test.go` | API-1 |
| `proto/iop/runtime.proto` | API-1 |
| `proto/gen/**` | API-1 |
| `apps/edge/internal/node/mapper.go` | API-1 |
| `apps/edge/internal/node/mapper_test.go` | API-1 |
| `apps/edge/internal/edgecmd/config.go` | API-1 |
| `apps/edge/internal/edgecmd/node_register.go` | API-1 |
| `configs/edge.yaml` | API-1 |
## 최종 검증
```bash
go test -count=1 ./packages/go/... ./proto/gen/... ./apps/edge/internal/node/... ./apps/edge/internal/edgecmd/...
```
proto 파일을 수정했으면 아래도 실행한다.
```bash
make proto
git diff -- proto/gen
```
모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.

View file

@ -0,0 +1,78 @@
<!-- task=m-node-multi-target-serving-foundation/01_config_topology plan=1 tag=REVIEW_API -->
# Plan - REVIEW_API
## 이 파일을 읽는 구현 에이전트에게
`CODE_REVIEW-cloud-G08.md`의 구현 에이전트 소유 섹션 작성까지가 구현의 일부다. 코드 변경 후 검증을 실행하고 실제 변경 내용, 검증 출력, 계획 대비 변경 사항을 채운 뒤 active 파일을 그대로 두고 리뷰 준비를 보고한다. 사용자 결정, 사용자 소유 외부 환경, 또는 범위 충돌 없이는 진행할 수 없으면 review stub의 `사용자 리뷰 요청`에 근거를 남기고 중단한다. 구현 중 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`, `USER_REVIEW.md`, archive log, `complete.log`를 만들지 않는다.
## 배경
1차 구현은 Edge config와 `NodeConfigPayload`가 multi adapter/profile topology를 표현하도록 확장했다. 코드리뷰 결과, 핵심 contract 자체는 judgeable하지만 `iop-edge config check`의 adapter 활성 판정과 vLLM endpoint 검증에 빈틈이 있어 후속 보강이 필요하다.
## 사용자 리뷰 요청 흐름
구현 중 blocker는 active `CODE_REVIEW-cloud-G08.md`의 `사용자 리뷰 요청` 섹션에 기록한다. 구현 에이전트는 직접 사용자 프롬프트를 만들지 않으며, code-review가 요청 타당성과 실제 `USER_REVIEW.md` 작성 여부를 판단한다.
## Roadmap Targets
- Milestone: `agent-roadmap/phase/inference-provider-extension/milestones/node-multi-target-serving-foundation.md`
- Task ids:
- `config-topology`: Edge `nodes[]` 설정과 `NodeConfigPayload`가 하나의 Node 안에 여러 CLI profile, terminal gateway, 추론 engine/profile instance, model target route를 표현한다.
- Completion mode: check-on-pass
## 분석 결과
### 이전 루프
- Archived plan: `agent-task/m-node-multi-target-serving-foundation/01_config_topology/plan_local_G08_0.log`
- Archived review: `agent-task/m-node-multi-target-serving-foundation/01_config_topology/code_review_cloud_G08_0.log`
- Verdict: `WARN`
### 리뷰 발견 사항
- `apps/edge/internal/edgecmd/config.go:143`의 `hasOllama`/`hasVllm` 판정이 instance slice 길이만 확인한다. 모든 instance가 `enabled: false`여도 adapter가 있다고 판단할 수 있다.
- `apps/edge/internal/edgecmd/config.go:151` 주변 검증은 `ollama_instances[].base_url`만 확인한다. `enabled: true`인 legacy `vllm.endpoint` 및 `vllm_instances[].endpoint`가 빈 값인 경우를 거부하지 않는다.
### 범위 결정 근거
- 기존 multi topology contract, proto field, mapper 구조는 유지한다.
- 후속 범위는 `config check` 검증 보강과 관련 edgecmd 테스트 추가에 한정한다.
- Node runtime routing, OpenAI route catalog, adapter registry 구현은 sibling task 범위이므로 다루지 않는다.
### 빌드 등급
- build: `local-G08`, review: `cloud-G08`. 수정 범위는 deterministic config validation과 테스트에 한정되지만, milestone-linked API foundation의 후속 검토라 기존 review route를 유지한다.
## 구현 체크리스트
- [ ] `validateEdgeConfig`가 legacy adapter와 multi-instance adapter 모두 실제 enabled 상태 기준으로 "at least one adapter"를 판단한다.
- [ ] `validateEdgeConfig`가 enabled vLLM legacy/instance endpoint 빈 값을 거부하고 edgecmd 테스트가 이를 검증한다.
- [ ] 최종 검증 명령을 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
### [REVIEW_API-1] Edge Config Validation Follow-Up
- 문제: 비활성 instance만 존재하는 node가 `config check`에서 통과할 수 있고, enabled vLLM endpoint 빈 값이 검증되지 않는다.
- 해결 방법: `validateEdgeConfig`에서 enabled instance 존재 여부를 helper 또는 명확한 loop로 계산한다. legacy `vllm.enabled`와 `vllm_instances[].enabled`가 true이면 endpoint가 비어 있지 않아야 한다. 기존 Ollama 검증은 유지하되 multi-instance 판정도 enabled 기준으로 정리한다.
- 수정 파일 및 체크리스트:
- [ ] `apps/edge/internal/edgecmd/config.go`: enabled adapter 판정과 vLLM endpoint 검증 보강.
- [ ] `apps/edge/internal/edgecmd/edgecmd_test.go`: 비활성 instance-only node 거부, enabled vLLM endpoint 빈 값 거부 테스트 추가.
- 테스트 작성: 추가한다. `edgecmd` 테스트에서 `config check` command를 실행해 실제 사용자-facing validation 결과를 고정한다.
- 중간 검증:
- `go test -count=1 ./apps/edge/internal/edgecmd/...`
## 수정 파일 요약
| 파일 | 항목 |
|------|------|
| `apps/edge/internal/edgecmd/config.go` | REVIEW_API-1 |
| `apps/edge/internal/edgecmd/edgecmd_test.go` | REVIEW_API-1 |
## 최종 검증
```bash
go test -count=1 ./packages/go/... ./proto/gen/... ./apps/edge/internal/node/... ./apps/edge/internal/edgecmd/...
```
모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.

View file

@ -0,0 +1,96 @@
<!-- task=m-node-multi-target-serving-foundation/02+01_adapter_registry plan=0 tag=REFACTOR -->
# Code Review Reference - REFACTOR
> **[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.
> Do not ask the user directly, present choices in chat, or call `request_user_input` during implementation.
> Finalization is review-agent-only.
## 개요
date=2026-06-10
task=m-node-multi-target-serving-foundation/02+01_adapter_registry, plan=0, tag=REFACTOR
## Roadmap Targets
- Milestone: `agent-roadmap/phase/inference-provider-extension/milestones/node-multi-target-serving-foundation.md`
- Task ids:
- `adapter-registry`: Node adapter registry가 동일 adapter type의 여러 engine/profile instance를 덮어쓰지 않고 라우팅 가능한 target namespace로 관리한다.
- Completion mode: check-on-pass
## 이 파일을 읽는 리뷰 에이전트에게
> **[REVIEW AGENT ONLY]** 종결 절차는 코드리뷰 에이전트 전용이다.
구현을 plan과 대조하고, registry/router namespace가 legacy 단일 요청과 multi instance 요청을 모두 안전하게 처리하는지 검토한다.
## 구현 항목별 완료 여부
| 항목 | 완료 여부 |
|------|---------|
| [REFACTOR-1] Registry Namespace | [ ] |
## 구현 체크리스트
- [ ] Registry가 adapter type과 instance key를 보존하고 동일 adapter type 여러 instance를 덮어쓰지 않는다.
- [ ] Factory가 `NodeConfigPayload`의 multi adapter instance를 registry namespace로 등록한다.
- [ ] Router가 request의 adapter/target을 새 namespace로 해석하고 legacy 단일 adapter 요청을 호환한다.
- [ ] registry/factory/router/node 단위 테스트를 추가 또는 갱신한다.
- [ ] 최종 검증 명령을 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
## 코드리뷰 전용 체크리스트
- [ ] `코드리뷰 결과``PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
- [ ] active `CODE_REVIEW-cloud-G07.md``code_review_cloud_G07_N.log`로 아카이브한다.
- [ ] active `PLAN-local-G08.md``plan_local_G08_M.log`로 아카이브한다.
- [ ] PASS이면 `complete.log` 작성 후 archive 이동한다.
- [ ] WARN/FAIL이면 후속 active plan/review 또는 `USER_REVIEW.md`를 작성한다.
## 계획 대비 변경 사항
_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._
## 주요 설계 결정
_구현 에이전트가 주요 설계 결정 사항을 기록한다._
## 사용자 리뷰 요청
_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. 구현 에이전트는 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백만으로는 사용자 리뷰 요청을 작성하지 않는다._
- 상태: 없음
- 사유 유형: 없음
- 결정 필요: 없음
- 차단 근거: 없음
- 실행한 검증/명령: 없음
- 자동 후속 불가 이유: 없음
- 재개 조건: 없음
## 리뷰어를 위한 체크포인트
- 동일 adapter type 여러 instance가 등록되어도 overwrite가 없는지 확인한다.
- ambiguous legacy adapter 요청이 조용히 임의 target으로 가지 않는지 확인한다.
- lifecycle `Start`/`Stop` 순서와 `All()` 진단 출력이 새 key 기준으로 안정적인지 확인한다.
## 검증 결과
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
### REFACTOR-1 중간 검증
```bash
$ go test -count=1 ./apps/node/internal/adapters/... ./apps/node/internal/router/...
(output)
```
### 최종 검증
```bash
$ go test -count=1 ./apps/node/...
(output)
```

View file

@ -0,0 +1,113 @@
<!-- task=m-node-multi-target-serving-foundation/02+01_adapter_registry plan=0 tag=REFACTOR -->
# Plan - REFACTOR
## 이 파일을 읽는 구현 에이전트에게
구현 완료의 마지막 단계는 `CODE_REVIEW-cloud-G07.md`의 구현 에이전트 소유 섹션 작성이다. 검증을 실행하고 실제 출력과 설계 결정을 기록한 뒤 active 파일을 유지하고 리뷰 준비를 보고한다. 사용자 전용 결정이나 외부 환경 prerequisite 없이는 진행할 수 없을 때만 `사용자 리뷰 요청`을 채우고 멈춘다.
## 배경
Node registry는 adapter name을 map key로 쓰기 때문에 같은 adapter type의 여러 engine/profile instance를 등록하면 덮어쓴다. 멀티 타겟 Node에서는 adapter type과 route target을 분리해 라우팅 가능한 namespace가 필요하다.
## 사용자 리뷰 요청 흐름
구현 중 차단은 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. 구현 에이전트는 직접 질문하지 않으며, code-review가 user-review stop 여부를 판단한다.
## Roadmap Targets
- Milestone: `agent-roadmap/phase/inference-provider-extension/milestones/node-multi-target-serving-foundation.md`
- Task ids:
- `adapter-registry`: Node adapter registry가 동일 adapter type의 여러 engine/profile instance를 덮어쓰지 않고 라우팅 가능한 target namespace로 관리한다.
- Completion mode: check-on-pass
## 분석 결과
### 읽은 파일
- `agent-test/local/rules.md`
- `agent-test/local/node-smoke.md`
- `apps/node/internal/adapters/registry.go`
- `apps/node/internal/adapters/factory.go`
- `apps/node/internal/adapters/adapters_blackbox_test.go`
- `apps/node/internal/router/router.go`
- `apps/node/internal/router/router_test.go`
- `apps/node/internal/runtime/types.go`
- `apps/node/internal/node/node.go`
- `apps/node/internal/node/node_test.go`
- `proto/iop/runtime.proto`
### 테스트 환경 규칙
- test_env: `local`
- `node-smoke` 기준 명령은 `go test ./apps/node/...`와 필요 시 edge-node command/run round-trip smoke다. 이 plan은 registry/router 단위 변경이므로 `go test -count=1 ./apps/node/...`를 최종 계약으로 둔다.
### 테스트 커버리지 공백
- registry는 `map[string]runtime.Adapter`로 name 중복을 덮어쓰는 현재 동작을 명시적으로 방어하는 테스트가 부족하다.
- factory는 payload 내 동일 type 여러 adapter instance 등록을 허용하지 않는 구조다.
- router는 `adapter` 단일 string으로 adapter instance와 adapter type을 구분하지 않는다.
### 심볼 참조
- rename/remove 예정 심볼: 없음. 단, `Registry.Get`, `Registry.Register`, `Router.ResolveAdapter` call site는 signature 변경 가능성이 있으므로 전체 grep 후 갱신한다.
### 분할 판단
- predecessor `01`: `01_config_topology`가 active plan으로 존재하며 아직 `complete.log`가 없다. 구현은 `agent-task/m-node-multi-target-serving-foundation/01_config_topology/complete.log`가 생긴 뒤 시작한다.
- 이 작업은 registry/router 내부 변경만 다루고 provider 실행/route catalog는 sibling task로 분리한다.
### 범위 결정 근거
- OpenAI route table과 engine profile별 endpoint 실행은 후속 task에서 처리한다.
- concurrency scheduling은 `07+02_runtime_concurrency`에 둔다.
### 빌드 등급
- build: `local-G08`, review: `cloud-G07`. 내부 refactor지만 Node routing contract라 review는 더 넓은 맥락을 본다.
## 의존 관계 및 구현 순서
- `02+01_adapter_registry``01_config_topology` 완료 후 구현한다. 디렉터리 이름의 `+01` 외 추가 dependency를 만들지 않는다.
## 구현 체크리스트
- [ ] Registry가 adapter type과 instance key를 보존하고 동일 adapter type 여러 instance를 덮어쓰지 않는다.
- [ ] Factory가 `NodeConfigPayload`의 multi adapter instance를 registry namespace로 등록한다.
- [ ] Router가 request의 adapter/target을 새 namespace로 해석하고 legacy 단일 adapter 요청을 호환한다.
- [ ] registry/factory/router/node 단위 테스트를 추가 또는 갱신한다.
- [ ] 최종 검증 명령을 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
### [REFACTOR-1] Registry Namespace
- 문제: `Registry.Register``a.Name()`만 key로 쓰고 기존 key를 덮어쓴다 (`apps/node/internal/adapters/registry.go:26`). `BuildFromPayload``"ollama"` 하나만 등록한다 (`apps/node/internal/adapters/factory.go:29`). 이 상태에서는 `ollama@local`, `ollama@dgx`, `vllm@fast` 같은 여러 instance가 공존할 수 없다.
- 해결 방법: adapter registry entry에 stable instance id/type/targets를 둔다. `Get`/`All`/lifecycle 순서는 새 key 기준으로 유지하고, legacy `"ollama"` 단일 요청은 단일 후보일 때만 호환한다. ambiguous legacy 요청은 명확한 error를 반환한다.
- 수정 파일 및 체크리스트:
- [ ] `apps/node/internal/adapters/registry.go`: key model과 duplicate policy 구현.
- [ ] `apps/node/internal/adapters/factory.go`: payload instance id를 registry key로 등록.
- [ ] `apps/node/internal/router/router.go`: request resolution 업데이트.
- [ ] `apps/node/internal/runtime/types.go`: 필요한 routing metadata 최소 추가.
- 테스트 작성: 추가한다. `adapters_blackbox_test.go` 또는 신규 registry test에 duplicate type multi instance 등록, lifecycle ordering, ambiguous legacy lookup 테스트를 둔다. `router_test.go`에 adapter instance/target 해석 테스트를 둔다.
- 중간 검증:
- `go test -count=1 ./apps/node/internal/adapters/... ./apps/node/internal/router/...`
## 수정 파일 요약
| 파일 | 항목 |
|------|------|
| `apps/node/internal/adapters/registry.go` | REFACTOR-1 |
| `apps/node/internal/adapters/factory.go` | REFACTOR-1 |
| `apps/node/internal/adapters/adapters_blackbox_test.go` | REFACTOR-1 |
| `apps/node/internal/router/router.go` | REFACTOR-1 |
| `apps/node/internal/router/router_test.go` | REFACTOR-1 |
| `apps/node/internal/runtime/types.go` | REFACTOR-1 |
| `apps/node/internal/node/node_test.go` | REFACTOR-1 |
## 최종 검증
```bash
go test -count=1 ./apps/node/...
```
모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.

View file

@ -0,0 +1,91 @@
<!-- task=m-node-multi-target-serving-foundation/03+01_route_catalog plan=0 tag=API -->
# Code Review Reference - API
> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.**
> Complete every implementation-owned section, paste real verification output, then stop with active files in place.
> If blocked by a user-only decision or user-owned external prerequisite, fill `사용자 리뷰 요청`; do not ask the user directly.
> Finalization is review-agent-only.
## 개요
date=2026-06-10
task=m-node-multi-target-serving-foundation/03+01_route_catalog, plan=0, tag=API
## Roadmap Targets
- Milestone: `agent-roadmap/phase/inference-provider-extension/milestones/node-multi-target-serving-foundation.md`
- Task ids:
- `route-catalog`: Edge OpenAI-compatible model catalog가 외부 `model`을 Edge-owned route table로 해석하고, 고정 `openai.target` 단일 override 없이 내부 `adapter + target`으로 변환한다.
- Completion mode: check-on-pass
## 이 파일을 읽는 리뷰 에이전트에게
> **[REVIEW AGENT ONLY]** 종결 절차는 코드리뷰 에이전트 전용이다.
route catalog가 legacy fallback과 multi model dispatch를 모두 보존하는지 실제 테스트와 대조한다.
## 구현 항목별 완료 여부
| 항목 | 완료 여부 |
|------|---------|
| [API-1] OpenAI Route Catalog | [ ] |
## 구현 체크리스트
- [ ] Edge config에 external model catalog와 route table을 추가하고 legacy `openai.target` 동작을 호환한다.
- [ ] `/v1/models`가 route table의 여러 model id를 노출한다.
- [ ] `/v1/chat/completions``/v1/responses`가 외부 model을 내부 adapter/target/node/profile로 변환한다.
- [ ] route miss, ambiguous route, legacy fallback 테스트를 추가한다.
- [ ] 최종 검증 명령을 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
## 코드리뷰 전용 체크리스트
- [ ] `코드리뷰 결과`에 판정을 append한다.
- [ ] active `CODE_REVIEW-cloud-G08.md``code_review_cloud_G08_N.log`로 아카이브한다.
- [ ] active `PLAN-local-G08.md``plan_local_G08_M.log`로 아카이브한다.
- [ ] PASS이면 `complete.log` 작성 후 archive 이동한다.
- [ ] WARN/FAIL이면 후속 상태를 작성한다.
## 계획 대비 변경 사항
_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._
## 주요 설계 결정
_구현 에이전트가 주요 설계 결정 사항을 기록한다._
## 사용자 리뷰 요청
_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. 구현 에이전트는 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백만으로는 사용자 리뷰 요청을 작성하지 않는다._
- 상태: 없음
- 사유 유형: 없음
- 결정 필요: 없음
- 차단 근거: 없음
- 실행한 검증/명령: 없음
- 자동 후속 불가 이유: 없음
- 재개 조건: 없음
## 리뷰어를 위한 체크포인트
- `/v1/models`가 route catalog source of truth를 사용한다.
- chat/responses가 같은 resolver를 공유하거나 동일한 우선순위로 동작한다.
- legacy `openai.target` 사용자가 갑자기 깨지지 않는다.
## 검증 결과
### API-1 중간 검증
```bash
$ go test -count=1 ./packages/go/... ./apps/edge/internal/openai/... ./apps/edge/internal/service/...
(output)
```
### 최종 검증
```bash
$ go test -count=1 ./packages/go/... ./apps/edge/...
(output)
```

View file

@ -0,0 +1,120 @@
<!-- task=m-node-multi-target-serving-foundation/03+01_route_catalog plan=0 tag=API -->
# Plan - API
## 이 파일을 읽는 구현 에이전트에게
구현 완료 전 `CODE_REVIEW-cloud-G08.md`의 구현 에이전트 소유 섹션을 실제 변경 내용과 검증 출력으로 채운다. 사용자 전용 blocker는 review stub의 `사용자 리뷰 요청`에 기록하고, 직접 질문이나 `USER_REVIEW.md` 작성은 하지 않는다.
## 배경
현재 OpenAI-compatible surface는 `openai.target` 단일 override 또는 요청 model 값을 그대로 내부 target으로 사용한다. Edge가 model catalog와 route table을 소유해야 외부 `model`을 내부 `adapter + target + profile`로 안정적으로 변환할 수 있다.
## 사용자 리뷰 요청 흐름
구현 중 차단은 active review stub에 기록한다. code-review가 blocker 타당성과 후속 상태를 결정한다.
## Roadmap Targets
- Milestone: `agent-roadmap/phase/inference-provider-extension/milestones/node-multi-target-serving-foundation.md`
- Task ids:
- `route-catalog`: Edge OpenAI-compatible model catalog가 외부 `model`을 Edge-owned route table로 해석하고, 고정 `openai.target` 단일 override 없이 내부 `adapter + target`으로 변환한다.
- Completion mode: check-on-pass
## 분석 결과
### 읽은 파일
- `agent-test/local/rules.md`
- `agent-test/local/edge-smoke.md`
- `packages/go/config/config.go`
- `packages/go/config/config_test.go`
- `apps/edge/internal/openai/routes.go`
- `apps/edge/internal/openai/types.go`
- `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`
- `apps/edge/internal/service/service_test.go`
- `configs/edge.yaml`
### 테스트 환경 규칙
- test_env: `local`
- edge smoke 기준: config/API 변경은 `go test ./apps/edge/...`, OpenAI surface 변경은 `/healthz`, `/v1/models`, `/v1/chat/completions` 또는 `/v1/responses` smoke 증거가 필요하다. 이 plan은 단위 테스트를 계약으로 두고 live smoke는 field-smoke task로 넘긴다.
### 테스트 커버리지 공백
- `/v1/models``cfg.Models` 또는 단일 `cfg.Target`만 노출한다 (`apps/edge/internal/openai/routes.go:28`).
- chat/responses는 `s.cfg.Target`이 있으면 요청 model과 metadata override를 무시한다 (`apps/edge/internal/openai/chat_handler.go:132`).
- 여러 외부 model이 서로 다른 adapter/target으로 dispatch되는 테스트가 없다.
### 심볼 참조
- rename/remove 예정 심볼: 없음. `EdgeOpenAIConf.Target`, `Models`, `Adapter`, `NodeRef`는 legacy compat로 유지한다.
- route 해석 관련 참조: `handleModels`, `resolveAdapter`, `resolveTargetWithOverride`, `SubmitRunRequest`.
### 분할 판단
- predecessor `01`: `01_config_topology` active plan이 있으며 아직 완료 전이다. 구현은 `01_config_topology/complete.log` 이후 시작한다.
- route catalog는 Edge API surface 변경이므로 Node registry/provider 작업과 분리했다.
### 범위 결정 근거
- 실제 provider별 endpoint/context 실행은 `04+01,02_engine_profiles``05+03,04_edge_owned_options`에서 처리한다.
- live split-host 검증은 `08+03,04,05,06,07_field_smoke`에서 수행한다.
### 빌드 등급
- build: `local-G08`, review: `cloud-G08`. OpenAI-compatible API contract와 Edge routing이 함께 바뀌어 높은 검토가 필요하다.
## 의존 관계 및 구현 순서
- `03+01_route_catalog``01_config_topology` 완료 후 구현한다. 디렉터리 이름에 없는 `02` 의존성은 추가하지 않는다.
## 구현 체크리스트
- [ ] Edge config에 external model catalog와 route table을 추가하고 legacy `openai.target` 동작을 호환한다.
- [ ] `/v1/models`가 route table의 여러 model id를 노출한다.
- [ ] `/v1/chat/completions``/v1/responses`가 외부 model을 내부 adapter/target/node/profile로 변환한다.
- [ ] route miss, ambiguous route, legacy fallback 테스트를 추가한다.
- [ ] 최종 검증 명령을 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
### [API-1] OpenAI Route Catalog
- 문제: `handleModels`는 route table 없이 문자열 목록만 반환한다 (`apps/edge/internal/openai/routes.go:23`). `resolveTargetWithOverride``cfg.Target`을 전역 override로 우선해 모델별 라우팅을 막는다 (`apps/edge/internal/openai/chat_handler.go:132`). `/v1/responses`도 같은 해석을 사용한다 (`apps/edge/internal/openai/responses_handler.go:51`).
- 해결 방법: `EdgeOpenAIConf``routes` 또는 `model_routes`를 추가해 external model id, node ref, adapter instance, target, session/timeout/profile option reference를 표현한다. handler는 request model을 route catalog로 해석하고, 없을 때만 legacy target/model fallback을 사용한다.
- 수정 파일 및 체크리스트:
- [ ] `packages/go/config/config.go`: route catalog config type 추가.
- [ ] `apps/edge/internal/openai/types.go`: request/response model handling 보존.
- [ ] `apps/edge/internal/openai/routes.go`: `/v1/models` catalog 기반 출력.
- [ ] `apps/edge/internal/openai/chat_handler.go`: route resolver 적용.
- [ ] `apps/edge/internal/openai/responses_handler.go`: route resolver 적용.
- [ ] `apps/edge/internal/service/run_dispatch.go`: 필요한 route metadata 전달.
- 테스트 작성: 추가한다. `server_test.go``/v1/models` multi route, chat dispatch model A/B, responses metadata override와 route 우선순위, missing model 400/404 테스트를 둔다. `config_test.go`에 route catalog decode를 추가한다.
- 중간 검증:
- `go test -count=1 ./packages/go/... ./apps/edge/internal/openai/... ./apps/edge/internal/service/...`
## 수정 파일 요약
| 파일 | 항목 |
|------|------|
| `packages/go/config/config.go` | API-1 |
| `packages/go/config/config_test.go` | API-1 |
| `apps/edge/internal/openai/routes.go` | API-1 |
| `apps/edge/internal/openai/types.go` | API-1 |
| `apps/edge/internal/openai/chat_handler.go` | API-1 |
| `apps/edge/internal/openai/responses_handler.go` | API-1 |
| `apps/edge/internal/openai/server_test.go` | API-1 |
| `apps/edge/internal/service/run_dispatch.go` | API-1 |
| `apps/edge/internal/service/service_test.go` | API-1 |
| `configs/edge.yaml` | API-1 |
## 최종 검증
```bash
go test -count=1 ./packages/go/... ./apps/edge/...
```
모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.

View file

@ -0,0 +1,91 @@
<!-- task=m-node-multi-target-serving-foundation/04+01,02_engine_profiles plan=0 tag=API -->
# Code Review Reference - API
> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.**
> Fill implementation-owned sections and verification output, then stop with active files in place.
> Do not ask the user directly; record user-only blockers in `사용자 리뷰 요청`.
> Finalization is review-agent-only.
## 개요
date=2026-06-10
task=m-node-multi-target-serving-foundation/04+01,02_engine_profiles, plan=0, tag=API
## Roadmap Targets
- Milestone: `agent-roadmap/phase/inference-provider-extension/milestones/node-multi-target-serving-foundation.md`
- Task ids:
- `engine-profiles`: Ollama는 여러 `base_url`/model target과 target별 `context_size`를 지원하고, vLLM/SGLang 같은 OpenAI-compatible inference engine 연결을 같은 Node 통로에서 확장할 수 있는 provider/profile 기준이 정리되어 있다.
- Completion mode: check-on-pass
## 이 파일을 읽는 리뷰 에이전트에게
> **[REVIEW AGENT ONLY]** 종결 절차는 코드리뷰 에이전트 전용이다.
provider profile 선택이 target별 endpoint/context를 정확히 고정하고, vLLM/SGLang 확장 기준이 과도하게 넓어지지 않았는지 확인한다.
## 구현 항목별 완료 여부
| 항목 | 완료 여부 |
|------|---------|
| [API-1] Provider Profile Execution | [ ] |
## 구현 체크리스트
- [ ] Ollama multi profile이 endpoint/model/context를 target별로 선택한다.
- [ ] vLLM 또는 공통 OpenAI-compatible provider/profile baseline이 registry namespace 위에서 확장 가능하게 정리된다.
- [ ] provider capabilities가 profile/target 식별자를 모호하지 않게 반환한다.
- [ ] Ollama/vLLM/factory 테스트가 multi endpoint/profile 선택을 검증한다.
- [ ] 최종 검증 명령을 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
## 코드리뷰 전용 체크리스트
- [ ] `코드리뷰 결과`에 판정을 append한다.
- [ ] active `CODE_REVIEW-cloud-G08.md``code_review_cloud_G08_N.log`로 아카이브한다.
- [ ] active `PLAN-cloud-G07.md``plan_cloud_G07_M.log`로 아카이브한다.
- [ ] PASS이면 `complete.log` 작성 후 archive 이동한다.
- [ ] WARN/FAIL이면 후속 상태를 작성한다.
## 계획 대비 변경 사항
_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._
## 주요 설계 결정
_구현 에이전트가 주요 설계 결정 사항을 기록한다._
## 사용자 리뷰 요청
_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. 구현 에이전트는 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백만으로는 사용자 리뷰 요청을 작성하지 않는다._
- 상태: 없음
- 사유 유형: 없음
- 결정 필요: 없음
- 차단 근거: 없음
- 실행한 검증/명령: 없음
- 자동 후속 불가 이유: 없음
- 재개 조건: 없음
## 리뷰어를 위한 체크포인트
- Ollama request가 target별 fake server로 정확히 나뉘는지 확인한다.
- target별 `context_size`가 request option으로 반영되는지 확인한다.
- vLLM/SGLang 기준선이 disabled dead-end가 아니라 후속 provider 추가 경로를 만든다.
## 검증 결과
### API-1 중간 검증
```bash
$ go test -count=1 ./apps/node/internal/adapters/ollama/... ./apps/node/internal/adapters/vllm/... ./apps/node/internal/adapters/...
(output)
```
### 최종 검증
```bash
$ go test -count=1 ./apps/node/...
(output)
```

View file

@ -0,0 +1,113 @@
<!-- task=m-node-multi-target-serving-foundation/04+01,02_engine_profiles plan=0 tag=API -->
# Plan - API
## 이 파일을 읽는 구현 에이전트에게
구현 마지막 단계는 `CODE_REVIEW-cloud-G08.md`에 실제 변경 내용과 검증 출력 기록이다. 사용자 전용 결정 없이 막히면 직접 질문하지 말고 `사용자 리뷰 요청`에 증거를 남긴다.
## 배경
Ollama adapter는 하나의 `baseURL``contextSize`만 가진다. 같은 Node 연결에서 여러 Ollama endpoint/model, vLLM/SGLang류 OpenAI-compatible engine profile을 함께 쓰려면 provider/profile 기준을 Node 쪽 실행 단위에 반영해야 한다.
## 사용자 리뷰 요청 흐름
구현 중 blocker는 active review stub에 기록한다. code-review가 user-review stop 여부를 판단한다.
## Roadmap Targets
- Milestone: `agent-roadmap/phase/inference-provider-extension/milestones/node-multi-target-serving-foundation.md`
- Task ids:
- `engine-profiles`: Ollama는 여러 `base_url`/model target과 target별 `context_size`를 지원하고, vLLM/SGLang 같은 OpenAI-compatible inference engine 연결을 같은 Node 통로에서 확장할 수 있는 provider/profile 기준이 정리되어 있다.
- Completion mode: check-on-pass
## 분석 결과
### 읽은 파일
- `agent-test/local/rules.md`
- `agent-test/local/node-smoke.md`
- `packages/go/config/config.go`
- `proto/iop/runtime.proto`
- `apps/node/internal/adapters/ollama/ollama.go`
- `apps/node/internal/adapters/ollama/ollama_test.go`
- `apps/node/internal/adapters/vllm/vllm.go`
- `apps/node/internal/adapters/vllm/vllm_test.go`
- `apps/node/internal/adapters/factory.go`
- `apps/node/internal/runtime/types.go`
### 테스트 환경 규칙
- test_env: `local`
- Node profile 기준 검증은 `go test ./apps/node/...`다. OpenAI-compatible provider 실행은 unit/fake HTTP server로 deterministic하게 검증한다.
### 테스트 커버리지 공백
- Ollama는 adapter instance 하나가 `baseURL` 하나만 가진다 (`apps/node/internal/adapters/ollama/ollama.go:23`).
- `Capabilities`가 endpoint별 target/source를 구분하지 않는다 (`apps/node/internal/adapters/ollama/ollama.go:45`).
- vLLM은 `Execute`가 비활성 에러를 반환한다 (`apps/node/internal/adapters/vllm/vllm.go:47`).
- multiple profile selection과 target별 `context_size` 테스트가 없다.
### 심볼 참조
- rename/remove 예정 심볼: 없음. `ollama.New`, `vllm.New`, `runtime.Capabilities`, `ExecutionSpec.Target/Input` 확장 가능성이 있다.
### 분할 판단
- predecessor `01`: config/proto topology 완료 필요. 현재 active plan, complete 없음.
- predecessor `02`: registry namespace 완료 필요. 현재 active plan, complete 없음.
- provider 실행은 route catalog와 독립적이지만 registry namespace가 있어야 같은 type 여러 instance를 안전하게 검증할 수 있다.
### 범위 결정 근거
- SGLang 전용 production hardening은 후속 SGLang milestone로 넘긴다.
- Edge route catalog와 OpenAI request option ownership은 별도 sibling task에서 다룬다.
### 빌드 등급
- build: `cloud-G07`, review: `cloud-G08`. provider 실행, fake HTTP server, config/proto 결과가 함께 얽혀 있어 cloud lane으로 둔다.
## 의존 관계 및 구현 순서
- `04+01,02_engine_profiles``01_config_topology``02+01_adapter_registry` 완료 후 구현한다. 디렉터리 이름의 `+01,02` 외 추가 dependency를 만들지 않는다.
## 구현 체크리스트
- [ ] Ollama multi profile이 endpoint/model/context를 target별로 선택한다.
- [ ] vLLM 또는 공통 OpenAI-compatible provider/profile baseline이 registry namespace 위에서 확장 가능하게 정리된다.
- [ ] provider capabilities가 profile/target 식별자를 모호하지 않게 반환한다.
- [ ] Ollama/vLLM/factory 테스트가 multi endpoint/profile 선택을 검증한다.
- [ ] 최종 검증 명령을 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
### [API-1] Provider Profile Execution
- 문제: `Ollama``baseURL``contextSize`를 adapter instance 필드 하나로 보관한다 (`apps/node/internal/adapters/ollama/ollama.go:23`). `Execute``spec.Target`을 model로만 해석한다 (`apps/node/internal/adapters/ollama/ollama.go:54`). vLLM은 target 조회만 일부 있고 실행은 disabled다 (`apps/node/internal/adapters/vllm/vllm.go:47`).
- 해결 방법: registry instance 또는 provider profile이 endpoint/context/model mapping을 가진다. `ExecutionSpec`가 route/profile metadata를 받거나 target namespace를 parsing 없이 안정적으로 해석한다. vLLM/SGLang류는 공통 OpenAI-compatible provider interface를 두되 완성 범위는 baseline과 fake-server 테스트로 제한한다.
- 수정 파일 및 체크리스트:
- [ ] `apps/node/internal/adapters/ollama/ollama.go`: profile-aware endpoint/model/context selection.
- [ ] `apps/node/internal/adapters/vllm/vllm.go`: OpenAI-compatible provider baseline 또는 명확한 extension point.
- [ ] `apps/node/internal/adapters/factory.go`: profile config를 provider instance로 생성.
- [ ] `apps/node/internal/runtime/types.go`: 필요한 profile route metadata 최소 추가.
- 테스트 작성: 추가한다. `ollama_test.go`에 두 fake server가 서로 다른 model/context를 받는 테스트를 두고, `vllm_test.go`에는 `/v1/models` 및 실행 baseline contract를 고정한다.
- 중간 검증:
- `go test -count=1 ./apps/node/internal/adapters/ollama/... ./apps/node/internal/adapters/vllm/... ./apps/node/internal/adapters/...`
## 수정 파일 요약
| 파일 | 항목 |
|------|------|
| `apps/node/internal/adapters/ollama/ollama.go` | API-1 |
| `apps/node/internal/adapters/ollama/ollama_test.go` | API-1 |
| `apps/node/internal/adapters/vllm/vllm.go` | API-1 |
| `apps/node/internal/adapters/vllm/vllm_test.go` | API-1 |
| `apps/node/internal/adapters/factory.go` | API-1 |
| `apps/node/internal/runtime/types.go` | API-1 |
## 최종 검증
```bash
go test -count=1 ./apps/node/...
```
모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.

View file

@ -0,0 +1,89 @@
<!-- task=m-node-multi-target-serving-foundation/05+03,04_edge_owned_options plan=0 tag=API -->
# Code Review Reference - API
> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.**
> Fill implementation-owned sections and real verification output before reporting ready for review.
> Do not ask the user directly; record user-only blockers in `사용자 리뷰 요청`.
## 개요
date=2026-06-10
task=m-node-multi-target-serving-foundation/05+03,04_edge_owned_options, plan=0, tag=API
## Roadmap Targets
- Milestone: `agent-roadmap/phase/inference-provider-extension/milestones/node-multi-target-serving-foundation.md`
- Task ids:
- `edge-owned-options`: context window와 runner sizing 옵션은 Edge config가 최종 소유하며, 외부 agent request의 `options.num_ctx` 같은 값이 Edge-owned 값을 변경하지 못한다.
- Completion mode: check-on-pass
## 이 파일을 읽는 리뷰 에이전트에게
> **[REVIEW AGENT ONLY]** 종결 절차는 코드리뷰 에이전트 전용이다.
request option이 route/profile owned option을 덮지 못하는지 테스트와 구현을 같이 확인한다.
## 구현 항목별 완료 여부
| 항목 | 완료 여부 |
|------|---------|
| [API-1] Edge-Owned Runtime Options | [ ] |
## 구현 체크리스트
- [ ] Edge route/profile config의 context/runner sizing 옵션이 request option보다 우선한다.
- [ ] OpenAI chat/responses에서 request `options.num_ctx` 등 runner sizing override가 내부 profile 값을 바꾸지 못한다.
- [ ] provider별 option merge 정책을 테스트로 고정한다.
- [ ] 최종 검증 명령을 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
## 코드리뷰 전용 체크리스트
- [ ] `코드리뷰 결과`에 판정을 append한다.
- [ ] active `CODE_REVIEW-local-G07.md``code_review_local_G07_N.log`로 아카이브한다.
- [ ] active `PLAN-local-G07.md``plan_local_G07_M.log`로 아카이브한다.
- [ ] PASS이면 `complete.log` 작성 후 archive 이동한다.
- [ ] WARN/FAIL이면 후속 상태를 작성한다.
## 계획 대비 변경 사항
_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._
## 주요 설계 결정
_구현 에이전트가 주요 설계 결정 사항을 기록한다._
## 사용자 리뷰 요청
_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. 구현 에이전트는 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백만으로는 사용자 리뷰 요청을 작성하지 않는다._
- 상태: 없음
- 사유 유형: 없음
- 결정 필요: 없음
- 차단 근거: 없음
- 실행한 검증/명령: 없음
- 자동 후속 불가 이유: 없음
- 재개 조건: 없음
## 리뷰어를 위한 체크포인트
- request `options.num_ctx`가 profile context size보다 우선하지 않는다.
- chat/responses 양쪽 경로가 같은 정책을 따른다.
- allow-pass-through option과 forced-owned option이 코드에서 구분된다.
## 검증 결과
### API-1 중간 검증
```bash
$ go test -count=1 ./apps/edge/internal/openai/... ./apps/node/internal/adapters/ollama/...
(output)
```
### 최종 검증
```bash
$ go test -count=1 ./apps/edge/... ./apps/node/...
(output)
```

View file

@ -0,0 +1,110 @@
<!-- task=m-node-multi-target-serving-foundation/05+03,04_edge_owned_options plan=0 tag=API -->
# Plan - API
## 이 파일을 읽는 구현 에이전트에게
구현 완료 전 `CODE_REVIEW-local-G07.md`를 실제 구현 내용과 검증 출력으로 채운다. 사용자 전용 blocker는 review stub에 기록하고 직접 질문하지 않는다.
## 배경
사용자는 context window/runner sizing이 agent request마다 달라져 모델 로딩/언로딩이 흔들리는 것을 원하지 않는다. 현재 Ollama 단일 adapter에는 `num_ctx` override 방어가 들어갔지만, route/profile 기반 멀티 타겟 구조에서도 Edge-owned option이 최종 소유권을 가져야 한다.
## 사용자 리뷰 요청 흐름
구현 중 차단은 active review stub의 `사용자 리뷰 요청`에 기록한다. code-review가 실제 user-review stop 여부를 판단한다.
## Roadmap Targets
- Milestone: `agent-roadmap/phase/inference-provider-extension/milestones/node-multi-target-serving-foundation.md`
- Task ids:
- `edge-owned-options`: context window와 runner sizing 옵션은 Edge config가 최종 소유하며, 외부 agent request의 `options.num_ctx` 같은 값이 Edge-owned 값을 변경하지 못한다.
- Completion mode: check-on-pass
## 분석 결과
### 읽은 파일
- `agent-test/local/rules.md`
- `agent-test/local/edge-smoke.md`
- `agent-test/local/node-smoke.md`
- `apps/edge/internal/openai/chat_handler.go`
- `apps/edge/internal/openai/responses_handler.go`
- `apps/edge/internal/openai/server_test.go`
- `apps/node/internal/adapters/ollama/ollama.go`
- `apps/node/internal/adapters/ollama/ollama_test.go`
- `packages/go/config/config.go`
### 테스트 환경 규칙
- test_env: `local`
- unit contract는 `go test ./apps/edge/... ./apps/node/...`다. live agent request override smoke는 field-smoke task에서 처리한다.
### 테스트 커버리지 공백
- 현재 `ollamaOptionsFromInput`은 단일 `contextSize``num_ctx`로 강제한다 (`apps/node/internal/adapters/ollama/ollama.go:298`).
- route/profile별 context ownership과 OpenAI handler에서 request option이 어떻게 전달되는지에 대한 end-to-end 단위 테스트가 부족하다.
- `keep_alive`나 runner sizing 성격의 추가 옵션 소유권 기준이 명문화되어 있지 않다.
### 심볼 참조
- rename/remove 예정 심볼: 없음. `ollamaOptionsFromInput`, OpenAI request option pass-through, route/profile config 필드 참조를 grep한다.
### 분할 판단
- predecessor `03`: route catalog 완료 필요. 현재 active plan, complete 없음.
- predecessor `04`: engine profile 완료 필요. 현재 active plan, complete 없음.
- 이 작업은 옵션 소유권 정책만 다루며 provider profile 구조 자체는 수정하지 않는다.
### 범위 결정 근거
- 새로운 provider 추가나 route catalog schema 변경은 선행 task 범위다.
- 실제 원격 모델 로딩/언로딩 관찰은 field-smoke로 넘긴다.
### 빌드 등급
- build: `local-G07`, review: `local-G07`. 정책은 중요하지만 단위 테스트로 검출 가능하고 범위가 명확하다.
## 의존 관계 및 구현 순서
- `05+03,04_edge_owned_options``03+01_route_catalog``04+01,02_engine_profiles` 완료 후 구현한다.
## 구현 체크리스트
- [ ] Edge route/profile config의 context/runner sizing 옵션이 request option보다 우선한다.
- [ ] OpenAI chat/responses에서 request `options.num_ctx` 등 runner sizing override가 내부 profile 값을 바꾸지 못한다.
- [ ] provider별 option merge 정책을 테스트로 고정한다.
- [ ] 최종 검증 명령을 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
### [API-1] Edge-Owned Runtime Options
- 문제: 단일 Ollama adapter에는 `num_ctx` 방어가 있지만 (`apps/node/internal/adapters/ollama/ollama.go:305`), route/profile 기반에서는 외부 request option과 Edge-owned profile option의 merge 우선순위가 다시 불명확해질 수 있다.
- 해결 방법: Edge config route/profile의 runner sizing option을 canonical source로 정의하고, handler 또는 provider adapter에서 request options를 allow/pass-through와 forced-owned option으로 분리한다. `num_ctx`처럼 runner sizing에 영향이 큰 옵션은 Edge-owned 값이 있을 때 항상 덮어쓴다.
- 수정 파일 및 체크리스트:
- [ ] `packages/go/config/config.go`: route/profile option ownership 필드 확인 또는 보강.
- [ ] `apps/edge/internal/openai/chat_handler.go`: request option 전달 시 route/profile metadata 유지.
- [ ] `apps/edge/internal/openai/responses_handler.go`: responses path도 같은 정책 적용.
- [ ] `apps/node/internal/adapters/ollama/ollama.go`: profile별 forced option 적용.
- 테스트 작성: 추가한다. `server_test.go``ollama_test.go`에 request `options.num_ctx=8192`가 route/profile `context_size=262144`로 덮이는 테스트를 둔다. provider별 runner sizing option 목록은 최소 `num_ctx`로 시작하고 확장 가능하게 한다.
- 중간 검증:
- `go test -count=1 ./apps/edge/internal/openai/... ./apps/node/internal/adapters/ollama/...`
## 수정 파일 요약
| 파일 | 항목 |
|------|------|
| `packages/go/config/config.go` | API-1 |
| `apps/edge/internal/openai/chat_handler.go` | API-1 |
| `apps/edge/internal/openai/responses_handler.go` | API-1 |
| `apps/edge/internal/openai/server_test.go` | API-1 |
| `apps/node/internal/adapters/ollama/ollama.go` | API-1 |
| `apps/node/internal/adapters/ollama/ollama_test.go` | API-1 |
## 최종 검증
```bash
go test -count=1 ./apps/edge/... ./apps/node/...
```
모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.

View file

@ -0,0 +1,89 @@
<!-- task=m-node-multi-target-serving-foundation/06+02,04_commands_caps plan=0 tag=API -->
# Code Review Reference - API
> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.**
> Fill implementation-owned sections and verification output, then report ready for review.
> Do not ask the user directly; record user-only blockers in `사용자 리뷰 요청`.
## 개요
date=2026-06-10
task=m-node-multi-target-serving-foundation/06+02,04_commands_caps, plan=0, tag=API
## Roadmap Targets
- Milestone: `agent-roadmap/phase/inference-provider-extension/milestones/node-multi-target-serving-foundation.md`
- Task ids:
- `commands-caps`: `CAPABILITIES`, `SESSION_LIST`, `USAGE_STATUS`, provider passthrough command가 여러 adapter/profile/model target에서 모호하지 않은 결과를 반환한다.
- Completion mode: check-on-pass
## 이 파일을 읽는 리뷰 에이전트에게
> **[REVIEW AGENT ONLY]** 종결 절차는 코드리뷰 에이전트 전용이다.
command/capability 결과가 adapter instance와 target namespace를 잃지 않는지 확인한다.
## 구현 항목별 완료 여부
| 항목 | 완료 여부 |
|------|---------|
| [API-1] Command Identity Contract | [ ] |
## 구현 체크리스트
- [ ] capabilities 응답이 adapter instance/profile/model target identity를 모호하지 않게 포함한다.
- [ ] session/usage/provider command가 target namespace를 정확히 보존한다.
- [ ] Edge service command DTO와 Node command handling 테스트를 갱신한다.
- [ ] 최종 검증 명령을 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
## 코드리뷰 전용 체크리스트
- [ ] `코드리뷰 결과`에 판정을 append한다.
- [ ] active `CODE_REVIEW-local-G07.md``code_review_local_G07_N.log`로 아카이브한다.
- [ ] active `PLAN-local-G07.md``plan_local_G07_M.log`로 아카이브한다.
- [ ] PASS이면 `complete.log` 작성 후 archive 이동한다.
- [ ] WARN/FAIL이면 후속 상태를 작성한다.
## 계획 대비 변경 사항
_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._
## 주요 설계 결정
_구현 에이전트가 주요 설계 결정 사항을 기록한다._
## 사용자 리뷰 요청
_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. 구현 에이전트는 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백만으로는 사용자 리뷰 요청을 작성하지 않는다._
- 상태: 없음
- 사유 유형: 없음
- 결정 필요: 없음
- 차단 근거: 없음
- 실행한 검증/명령: 없음
- 자동 후속 불가 이유: 없음
- 재개 조건: 없음
## 리뷰어를 위한 체크포인트
- capabilities/session/usage가 동일 target 이름 충돌에서 모호하지 않다.
- Edge command DTO와 proto/runtime DTO가 같은 identity를 전달한다.
- long command 처리와 concurrency scheduling 범위가 이 plan을 넘어 확장되지 않았다.
## 검증 결과
### API-1 중간 검증
```bash
$ go test -count=1 ./apps/node/... ./apps/edge/internal/service/...
(output)
```
### 최종 검증
```bash
$ go test -count=1 ./apps/node/... ./apps/edge/internal/service/...
(output)
```

View file

@ -0,0 +1,116 @@
<!-- task=m-node-multi-target-serving-foundation/06+02,04_commands_caps plan=0 tag=API -->
# Plan - API
## 이 파일을 읽는 구현 에이전트에게
구현 완료 전 `CODE_REVIEW-local-G07.md`를 실제 변경 사항과 검증 출력으로 채운다. 사용자 전용 blocker는 review stub에 기록한다.
## 배경
멀티 adapter/profile/model target 환경에서는 `CAPABILITIES`, `SESSION_LIST`, `USAGE_STATUS`, provider passthrough command가 어떤 target에 대한 응답인지 명확해야 한다. 현재 command path는 adapter/target 문자열만 전달해 multi profile 결과가 모호해질 수 있다.
## 사용자 리뷰 요청 흐름
구현 중 차단은 active review stub에 기록한다. code-review가 후속 상태를 결정한다.
## Roadmap Targets
- Milestone: `agent-roadmap/phase/inference-provider-extension/milestones/node-multi-target-serving-foundation.md`
- Task ids:
- `commands-caps`: `CAPABILITIES`, `SESSION_LIST`, `USAGE_STATUS`, provider passthrough command가 여러 adapter/profile/model target에서 모호하지 않은 결과를 반환한다.
- Completion mode: check-on-pass
## 분석 결과
### 읽은 파일
- `agent-test/local/rules.md`
- `agent-test/local/node-smoke.md`
- `agent-test/local/edge-smoke.md`
- `apps/edge/internal/service/node_command.go`
- `apps/edge/internal/service/status_provider.go`
- `apps/edge/internal/service/service_test.go`
- `apps/node/internal/node/node.go`
- `apps/node/internal/node/node_test.go`
- `apps/node/internal/adapters/cli/cli.go`
- `apps/node/internal/runtime/types.go`
- `apps/node/internal/transport/session.go`
- `proto/iop/runtime.proto`
### 테스트 환경 규칙
- test_env: `local`
- command path는 node/edge 양쪽 단위 테스트와 최소 proto-socket command round-trip smoke가 필요하다. 이 plan은 `go test -count=1 ./apps/node/... ./apps/edge/internal/service/...`를 계약으로 둔다.
### 테스트 커버리지 공백
- CLI capabilities는 profile target list만 반환한다 (`apps/node/internal/adapters/cli/cli.go:182`).
- session list는 label 문자열 중심이라 adapter instance/profile identity가 확장될 때 모호해질 수 있다 (`apps/node/internal/adapters/cli/cli.go:356`).
- `NodeCommandRequest` listener는 typed request를 동기 처리하므로 long command가 command path를 막을 수 있다 (`apps/node/internal/transport/session.go:69`).
### 심볼 참조
- rename/remove 예정 심볼: 없음. `NodeCommandRequest`, `runtime.CommandRequest`, `Capabilities`, CLI `HandleCommand` 참조를 확인한다.
### 분할 판단
- predecessor `02`: adapter registry namespace 완료 필요. 현재 active plan, complete 없음.
- predecessor `04`: provider profile baseline 완료 필요. 현재 active plan, complete 없음.
- runtime concurrency scheduling은 `07`로 분리하고, 이 plan은 command result identity와 passthrough ambiguity만 다룬다.
### 범위 결정 근거
- OpenAI request routing과 provider execution은 선행 task에 있다.
- terminal gateway protocol 자체 변경은 필요할 때 별도 task로 분리한다.
### 빌드 등급
- build: `local-G07`, review: `local-G07`. command contract는 명확하고 단위 테스트로 방어 가능하다.
## 의존 관계 및 구현 순서
- `06+02,04_commands_caps``02+01_adapter_registry``04+01,02_engine_profiles` 완료 후 구현한다.
## 구현 체크리스트
- [ ] capabilities 응답이 adapter instance/profile/model target identity를 모호하지 않게 포함한다.
- [ ] session/usage/provider command가 target namespace를 정확히 보존한다.
- [ ] Edge service command DTO와 Node command handling 테스트를 갱신한다.
- [ ] 최종 검증 명령을 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
### [API-1] Command Identity Contract
- 문제: command request/response는 adapter/target/session 문자열을 전달하지만 multi profile에서 이 값이 adapter instance인지 model target인지 모호할 수 있다. CLI session list는 label 문자열만 기본 출력으로 제공한다 (`apps/node/internal/adapters/cli/cli.go:366`).
- 해결 방법: command request/response와 capabilities result에 route/profile namespace를 명확히 포함한다. Edge service DTO는 old fields를 유지하되 new identity fields를 채운다. CLI/session/provider command는 target namespace를 잃지 않고 반환한다.
- 수정 파일 및 체크리스트:
- [ ] `proto/iop/runtime.proto`: command/capability identity 필드 보강 여부 확인.
- [ ] `apps/node/internal/runtime/types.go`: command response/capability DTO 갱신.
- [ ] `apps/node/internal/node/node.go`: command dispatch/result mapping 갱신.
- [ ] `apps/node/internal/adapters/cli/cli.go`: session/usage result identity 보존.
- [ ] `apps/edge/internal/service/node_command.go`: Edge command DTO 매핑 갱신.
- 테스트 작성: 추가한다. `node_test.go`에 multi adapter/profile command routing, `service_test.go`에 command request/response DTO, CLI 테스트에 session list identity 검증을 둔다.
- 중간 검증:
- `go test -count=1 ./apps/node/... ./apps/edge/internal/service/...`
## 수정 파일 요약
| 파일 | 항목 |
|------|------|
| `proto/iop/runtime.proto` | API-1 |
| `apps/node/internal/runtime/types.go` | API-1 |
| `apps/node/internal/node/node.go` | API-1 |
| `apps/node/internal/node/node_test.go` | API-1 |
| `apps/node/internal/adapters/cli/cli.go` | API-1 |
| `apps/edge/internal/service/node_command.go` | API-1 |
| `apps/edge/internal/service/status_provider.go` | API-1 |
| `apps/edge/internal/service/service_test.go` | API-1 |
## 최종 검증
```bash
go test -count=1 ./apps/node/... ./apps/edge/internal/service/...
```
모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.

View file

@ -0,0 +1,90 @@
<!-- task=m-node-multi-target-serving-foundation/07+02_runtime_concurrency plan=0 tag=REFACTOR -->
# Code Review Reference - REFACTOR
> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.**
> Fill implementation-owned sections and actual verification output, then stop with active files in place.
> Do not ask the user directly; record user-only blockers in `사용자 리뷰 요청`.
## 개요
date=2026-06-10
task=m-node-multi-target-serving-foundation/07+02_runtime_concurrency, plan=0, tag=REFACTOR
## Roadmap Targets
- Milestone: `agent-roadmap/phase/inference-provider-extension/milestones/node-multi-target-serving-foundation.md`
- Task ids:
- `runtime-concurrency`: `runtime.concurrency`와 adapter capability가 Node 멀티 호출에서 실제 제한 또는 스케줄링 정책으로 적용된다.
- Completion mode: check-on-pass
## 이 파일을 읽는 리뷰 에이전트에게
> **[REVIEW AGENT ONLY]** 종결 절차는 코드리뷰 에이전트 전용이다.
concurrency policy가 deterministic하고 foreground/background 양쪽에서 같은지 검토한다.
## 구현 항목별 완료 여부
| 항목 | 완료 여부 |
|------|---------|
| [REFACTOR-1] Runtime Concurrency Gate | [ ] |
## 구현 체크리스트
- [ ] Node runtime global concurrency와 adapter/profile concurrency 중 적용 우선순위를 정의한다.
- [ ] Foreground/background run 모두 동일한 scheduling/limit 정책을 통과한다.
- [ ] 제한 초과 요청의 queue/reject/timeout 중 하나를 선택하고 error/event/store 상태를 테스트로 고정한다.
- [ ] race/order test를 추가하고 최종 검증을 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
## 코드리뷰 전용 체크리스트
- [ ] `코드리뷰 결과`에 판정을 append한다.
- [ ] active `CODE_REVIEW-cloud-G08.md``code_review_cloud_G08_N.log`로 아카이브한다.
- [ ] active `PLAN-cloud-G07.md``plan_cloud_G07_M.log`로 아카이브한다.
- [ ] PASS이면 `complete.log` 작성 후 archive 이동한다.
- [ ] WARN/FAIL이면 후속 상태를 작성한다.
## 계획 대비 변경 사항
_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._
## 주요 설계 결정
_구현 에이전트가 주요 설계 결정 사항을 기록한다._
## 사용자 리뷰 요청
_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. 구현 에이전트는 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백만으로는 사용자 리뷰 요청을 작성하지 않는다._
- 상태: 없음
- 사유 유형: 없음
- 결정 필요: 없음
- 차단 근거: 없음
- 실행한 검증/명령: 없음
- 자동 후속 불가 이유: 없음
- 재개 조건: 없음
## 리뷰어를 위한 체크포인트
- permit acquire/release가 adapter error, timeout, cancellation, background run에서 누수되지 않는다.
- 선택한 제한 초과 정책이 테스트와 문서에 일치한다.
- proto-socket transport 변경으로 문제를 우회하지 않았다.
## 검증 결과
### REFACTOR-1 중간 검증
```bash
$ go test -race -count=1 ./apps/node/internal/node/...
(output)
```
### 최종 검증
```bash
$ go test -race -count=1 ./apps/node/internal/node/...
$ go test -count=1 ./apps/node/...
(output)
```

View file

@ -0,0 +1,109 @@
<!-- task=m-node-multi-target-serving-foundation/07+02_runtime_concurrency plan=0 tag=REFACTOR -->
# Plan - REFACTOR
## 이 파일을 읽는 구현 에이전트에게
구현 완료 전 `CODE_REVIEW-cloud-G08.md`의 구현 에이전트 소유 섹션을 실제 변경 내용과 검증 출력으로 채운다. 사용자 전용 blocker는 review stub에 기록하고 직접 질문하지 않는다.
## 배경
proto-socket은 nonce/pending request로 멀티 호출을 실을 수 있지만, Node runtime이 실제 동시 실행 제한과 queue/reject/timeout 정책을 적용해야 안정적이다. 현재 `runtime.concurrency`와 adapter capability의 `MaxConcurrency`는 실행 path에서 실질적인 gate로 보이지 않는다.
## 사용자 리뷰 요청 흐름
구현 중 차단은 active review stub에 기록한다. code-review가 후속 상태를 결정한다.
## Roadmap Targets
- Milestone: `agent-roadmap/phase/inference-provider-extension/milestones/node-multi-target-serving-foundation.md`
- Task ids:
- `runtime-concurrency`: `runtime.concurrency`와 adapter capability가 Node 멀티 호출에서 실제 제한 또는 스케줄링 정책으로 적용된다.
- Completion mode: check-on-pass
## 분석 결과
### 읽은 파일
- `agent-test/local/rules.md`
- `agent-test/local/node-smoke.md`
- `apps/node/internal/node/node.go`
- `apps/node/internal/node/run_manager.go`
- `apps/node/internal/node/node_test.go`
- `apps/node/internal/runtime/types.go`
- `apps/node/internal/transport/session.go`
- `apps/node/internal/adapters/registry.go`
- `proto/iop/runtime.proto`
### 테스트 환경 규칙
- test_env: `local`
- concurrency logic은 `go test -race` 또는 deterministic ordering test가 권장된다. 최종 계약은 `go test -race -count=1 ./apps/node/internal/node/...``go test -count=1 ./apps/node/...`다.
### 테스트 커버리지 공백
- `Node.OnRunRequest`는 adapter 실행 전 concurrency permit을 획득하지 않는다 (`apps/node/internal/node/node.go:122`).
- `run_manager`는 active run tracking/cancel 중심이고 scheduling/limit 정책이 없다.
- background run은 goroutine으로 바로 실행되어 global 또는 adapter별 limit을 우회할 수 있다 (`apps/node/internal/node/node.go:132`).
### 심볼 참조
- rename/remove 예정 심볼: 없음. `NodeRuntimeConfig.Concurrency`, `runtime.Capabilities.MaxConcurrency`, `RunRequest.Background`, `runManager` 참조를 확인한다.
### 분할 판단
- predecessor `02`: adapter registry namespace 완료 필요. 현재 active plan, complete 없음.
- route catalog/provider profile과 독립적으로 Node runtime gate를 구현할 수 있어 `03/04/05/06`에는 의존하지 않는다.
### 범위 결정 근거
- proto-socket 자체의 멀티 요청 구현을 다시 작성하지 않는다. transport는 요청을 실어 나르는 계층으로 두고 Node runtime scheduling만 담당한다.
- policy는 하나만 선택한다. queue/reject/timeout 중 여러 정책을 동시에 노출하지 않는다.
### 빌드 등급
- build: `cloud-G07`, review: `cloud-G08`. concurrency는 hard-to-review failure mode가 있어 cloud lane으로 둔다.
## 의존 관계 및 구현 순서
- `07+02_runtime_concurrency``02+01_adapter_registry` 완료 후 구현한다.
## 구현 체크리스트
- [ ] Node runtime global concurrency와 adapter/profile concurrency 중 적용 우선순위를 정의한다.
- [ ] Foreground/background run 모두 동일한 scheduling/limit 정책을 통과한다.
- [ ] 제한 초과 요청의 queue/reject/timeout 중 하나를 선택하고 error/event/store 상태를 테스트로 고정한다.
- [ ] race/order test를 추가하고 최종 검증을 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
### [REFACTOR-1] Runtime Concurrency Gate
- 문제: `Node.OnRunRequest`는 store insert 후 바로 adapter `Execute`를 호출한다 (`apps/node/internal/node/node.go:127`). runtime concurrency config와 adapter capability `MaxConcurrency`가 실행 제한으로 연결되어 있지 않다.
- 해결 방법: Node에 scheduler/permit manager를 두고 global runtime limit과 adapter/profile limit을 계산한다. policy는 우선 `reject` 또는 bounded wait timeout 중 하나로 고정하고, background/foreground 모두 같은 permit path를 탄다. cancellation과 store completion status가 permit 획득 전/후에서 일관되게 남도록 한다.
- 수정 파일 및 체크리스트:
- [ ] `apps/node/internal/node/node.go`: run execution을 scheduler permit으로 감싼다.
- [ ] `apps/node/internal/node/run_manager.go`: active run tracking과 scheduling 책임을 분리하거나 확장한다.
- [ ] `apps/node/internal/runtime/types.go`: capability/concurrency metadata 확인.
- [ ] `proto/iop/runtime.proto`: 필요한 config 필드가 이미 충분한지 확인.
- 테스트 작성: 추가한다. `node_test.go`에 blocking adapter 두 개를 이용해 limit 1에서 두 번째 요청이 정의된 정책대로 reject/timeout/queue 되는지, background run도 gate를 통과하는지 검증한다. race test가 통과해야 한다.
- 중간 검증:
- `go test -race -count=1 ./apps/node/internal/node/...`
## 수정 파일 요약
| 파일 | 항목 |
|------|------|
| `apps/node/internal/node/node.go` | REFACTOR-1 |
| `apps/node/internal/node/run_manager.go` | REFACTOR-1 |
| `apps/node/internal/node/node_test.go` | REFACTOR-1 |
| `apps/node/internal/runtime/types.go` | REFACTOR-1 |
| `proto/iop/runtime.proto` | REFACTOR-1 |
## 최종 검증
```bash
go test -race -count=1 ./apps/node/internal/node/...
go test -count=1 ./apps/node/...
```
모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.

View file

@ -0,0 +1,112 @@
<!-- task=m-node-multi-target-serving-foundation/08+03,04,05,06,07_field_smoke plan=0 tag=TEST -->
# Code Review Reference - TEST
> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.**
> Record the exact remote/local environment, commands, stdout/stderr, and any artifact paths.
> If blocked by user-owned server, node, token, secret, or model endpoint preparation, fill `사용자 리뷰 요청` and stop.
> Do not ask the user directly.
## 개요
date=2026-06-10
task=m-node-multi-target-serving-foundation/08+03,04,05,06,07_field_smoke, plan=0, tag=TEST
## Roadmap Targets
- Milestone: `agent-roadmap/phase/inference-provider-extension/milestones/node-multi-target-serving-foundation.md`
- Task ids:
- `field-smoke`: split-host smoke에서 하나의 연결된 Node가 최소 두 개의 inference model target과 하나의 CLI target을 같은 Edge-Node 연결로 호출할 수 있음을 확인한다.
- Completion mode: check-on-pass
## 이 파일을 읽는 리뷰 에이전트에게
> **[REVIEW AGENT ONLY]** 종결 절차는 코드리뷰 에이전트 전용이다.
smoke 증거가 같은 Edge-Node 연결과 같은 Node id를 가리키는지 확인한다. 환경 blocker가 repo-owned인지 user-owned인지 구분한다.
## 구현 항목별 완료 여부
| 항목 | 완료 여부 |
|------|---------|
| [TEST-1] Split Host Multi Target Smoke | [ ] |
## 구현 체크리스트
- [ ] split-host smoke 사전 조건과 실제 실행 config를 기록한다.
- [ ] Edge `/v1/models`가 두 inference model target을 노출하는 증거를 수집한다.
- [ ] 같은 연결된 Node를 통해 inference model target 두 개를 각각 호출하는 증거를 수집한다.
- [ ] 같은 연결된 Node를 통해 CLI target 하나를 호출하는 증거를 수집한다.
- [ ] 실패 시 repo-owned fix와 user-owned prerequisite을 구분해 기록한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
## 코드리뷰 전용 체크리스트
- [ ] `코드리뷰 결과`에 판정을 append한다.
- [ ] active `CODE_REVIEW-cloud-G07.md``code_review_cloud_G07_N.log`로 아카이브한다.
- [ ] active `PLAN-cloud-G07.md``plan_cloud_G07_M.log`로 아카이브한다.
- [ ] PASS이면 `complete.log` 작성 후 archive 이동한다.
- [ ] WARN/FAIL이면 후속 상태 또는 `USER_REVIEW.md`를 작성한다.
## 계획 대비 변경 사항
_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._
## 주요 설계 결정
_구현 에이전트가 주요 설계 결정 사항을 기록한다._
## 사용자 리뷰 요청
_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. 구현 에이전트는 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백만으로는 사용자 리뷰 요청을 작성하지 않는다._
- 상태: 없음
- 사유 유형: 없음
- 결정 필요: 없음
- 차단 근거: 없음
- 실행한 검증/명령: 없음
- 자동 후속 불가 이유: 없음
- 재개 조건: 없음
## 리뷰어를 위한 체크포인트
- `/v1/models`에 두 inference route가 실제로 보인다.
- 두 inference 호출과 CLI 호출이 같은 connected Node id/alias를 사용한다.
- secret/token 값이 문서에 노출되지 않았다.
- 실패가 있으면 repo-owned follow-up과 user-owned blocker가 분리되어 있다.
## 검증 결과
### TEST-1 중간 검증
```bash
$ go test -count=1 ./...
(output)
```
```bash
$ curl -fsS http://<edge-openai>/healthz
(output)
```
```bash
$ curl -fsS http://<edge-openai>/v1/models
(output)
```
### 최종 검증
```bash
$ curl -fsS http://<edge-openai>/v1/chat/completions -H 'Content-Type: application/json' -d '{"model":"<model-a>","messages":[{"role":"user","content":"ping"}]}'
(output)
```
```bash
$ curl -fsS http://<edge-openai>/v1/chat/completions -H 'Content-Type: application/json' -d '{"model":"<model-b>","messages":[{"role":"user","content":"ping"}]}'
(output)
```
```bash
$ <edge-cli-command-for-cli-target>
(output)
```

View file

@ -0,0 +1,118 @@
<!-- task=m-node-multi-target-serving-foundation/08+03,04,05,06,07_field_smoke plan=0 tag=TEST -->
# Plan - TEST
## 이 파일을 읽는 구현 에이전트에게
이 작업은 field smoke 증거 수집용이다. 코드 변경이 필요하면 최소 범위로 제한하고, `CODE_REVIEW-cloud-G07.md`에 실행 환경, 명령, 실제 출력, 실패 시 원인을 기록한다. 사용자 소유 원격 서버/노드/secret 준비가 필요하면 직접 묻지 말고 `사용자 리뷰 요청`에 재개 조건을 남긴다.
## 배경
첫 Epic의 최종 확인은 하나의 Edge-Node proto-socket 연결에서 최소 두 inference model target과 하나의 CLI target을 호출하는 것이다. 단위 테스트가 통과해도 split-host 연결, bootstrap, edge API, CLI target을 함께 확인해야 실제 목표가 닫힌다.
## 사용자 리뷰 요청 흐름
원격 서버, 노드, secret, 외부 모델 endpoint처럼 사용자 소유 prerequisite이 없으면 진행할 수 없는 경우 active review stub의 `사용자 리뷰 요청`에 기록한다. code-review가 user-review stop 여부를 결정한다.
## Roadmap Targets
- Milestone: `agent-roadmap/phase/inference-provider-extension/milestones/node-multi-target-serving-foundation.md`
- Task ids:
- `field-smoke`: split-host smoke에서 하나의 연결된 Node가 최소 두 개의 inference model target과 하나의 CLI target을 같은 Edge-Node 연결로 호출할 수 있음을 확인한다.
- Completion mode: check-on-pass
## 분석 결과
### 읽은 파일
- `agent-test/local/rules.md`
- `agent-test/local/edge-smoke.md`
- `agent-test/local/node-smoke.md`
- `docs`
- `configs/edge.yaml`
- `apps/edge/internal/openai/routes.go`
- `apps/edge/internal/service/service.go`
- `apps/node/internal/transport/client.go`
- `apps/node/internal/transport/session.go`
### 테스트 환경 규칙
- test_env: `local`
- field smoke는 local unit test가 아니라 split-host 실제 실행 증거가 핵심이다. 그래도 사전 회귀는 `go test -count=1 ./...` 또는 변경 범위별 테스트를 실행한다.
- 원격 endpoint, node token, 모델 endpoint secret은 사용자 소유 환경일 수 있으므로 없으면 `사용자 리뷰 요청` 사유가 된다.
### 테스트 커버리지 공백
- 단위 테스트는 하나의 연결에서 두 inference model target과 CLI target을 모두 호출하는 실제 split-host 경로를 증명하지 않는다.
- `/v1/models`, `/v1/chat/completions`, `/v1/responses`, CLI command path를 같은 Node connection으로 묶은 증거가 없다.
### 심볼 참조
- rename/remove 예정 심볼: 없음. smoke script나 docs를 추가할 경우 기존 command entrypoint와 config path를 확인한다.
### 분할 판단
- predecessor `03`: route catalog 완료 필요. 현재 active plan, complete 없음.
- predecessor `04`: engine profiles 완료 필요. 현재 active plan, complete 없음.
- predecessor `05`: edge-owned options 완료 필요. 현재 active plan, complete 없음.
- predecessor `06`: commands/caps 완료 필요. 현재 active plan, complete 없음.
- predecessor `07`: runtime concurrency 완료 필요. 현재 active plan, complete 없음.
- field smoke는 모든 기능 구현 후 실행해야 하므로 최종 dependent task로 둔다.
### 범위 결정 근거
- smoke 실패로 발견한 작은 config/docs 보정은 이 task에서 처리할 수 있다.
- 구조적 코드 결함은 새 follow-up plan으로 분리한다. 원격 secret이나 서버 준비는 repo가 해결하지 않는다.
### 빌드 등급
- build: `cloud-G07`, review: `cloud-G07`. 원격/터미널 실행 증거와 장시간 command 진단이 중심이다.
## 의존 관계 및 구현 순서
- `08+03,04,05,06,07_field_smoke``03`, `04`, `05`, `06`, `07` predecessor subtask의 `complete.log`가 모두 생긴 뒤 실행한다.
## 구현 체크리스트
- [ ] split-host smoke 사전 조건과 실제 실행 config를 기록한다.
- [ ] Edge `/v1/models`가 두 inference model target을 노출하는 증거를 수집한다.
- [ ] 같은 연결된 Node를 통해 inference model target 두 개를 각각 호출하는 증거를 수집한다.
- [ ] 같은 연결된 Node를 통해 CLI target 하나를 호출하는 증거를 수집한다.
- [ ] 실패 시 repo-owned fix와 user-owned prerequisite을 구분해 기록한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
### [TEST-1] Split Host Multi Target Smoke
- 문제: milestone 목표는 단위 테스트만으로 완료됐다고 볼 수 없다. 한 Node connection에서 multiple inference + CLI target을 함께 쓰는 실제 경로가 검증되어야 한다.
- 해결 방법: 빌드 산출물, Edge config, Node bootstrap config, node token, model route ids, CLI target id를 기록하고, `/healthz`, `/v1/models`, inference call 2개, CLI command/run 1개를 같은 Node id로 확인한다. 출력에는 request model, internal target/node id, status, 최소 응답 본문을 남긴다.
- 수정 파일 및 체크리스트:
- [ ] 필요 시 `docs/` 또는 smoke 문서에 최신 명령을 보정한다.
- [ ] 필요 시 `configs/edge.yaml` 예시를 실제 multi target smoke와 맞춘다.
- [ ] 코드 변경은 smoke를 막는 작은 config/docs 보정으로 제한한다.
- 테스트 작성: 신규 단위 테스트는 이 task의 주목적이 아니다. 필요한 경우 smoke script/문서 검증 명령만 추가한다.
- 중간 검증:
- `go test -count=1 ./...`
- `curl -fsS http://<edge-openai>/healthz`
- `curl -fsS http://<edge-openai>/v1/models`
## 수정 파일 요약
| 파일 | 항목 |
|------|------|
| `docs/**` | TEST-1 |
| `configs/edge.yaml` | TEST-1 |
| `agent-task/m-node-multi-target-serving-foundation/08+03,04,05,06,07_field_smoke/CODE_REVIEW-cloud-G07.md` | TEST-1 |
## 최종 검증
```bash
go test -count=1 ./...
curl -fsS http://<edge-openai>/healthz
curl -fsS http://<edge-openai>/v1/models
curl -fsS http://<edge-openai>/v1/chat/completions -H 'Content-Type: application/json' -d '{"model":"<model-a>","messages":[{"role":"user","content":"ping"}]}'
curl -fsS http://<edge-openai>/v1/chat/completions -H 'Content-Type: application/json' -d '{"model":"<model-b>","messages":[{"role":"user","content":"ping"}]}'
```
CLI target 호출은 프로젝트의 최신 Edge command endpoint 또는 `iop-edge` command로 실행하고, 실제 명령과 출력을 `CODE_REVIEW-cloud-G07.md`에 기록한다.
모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.

View file

@ -125,6 +125,30 @@ func configCheckCmd() *cobra.Command {
}
}
func anyOllamaEnabled(a config.AdaptersConf) bool {
if a.Ollama.Enabled {
return true
}
for _, inst := range a.OllamaInstances {
if inst.Enabled {
return true
}
}
return false
}
func anyVllmEnabled(a config.AdaptersConf) bool {
if a.Vllm.Enabled {
return true
}
for _, inst := range a.VllmInstances {
if inst.Enabled {
return true
}
}
return false
}
func validateEdgeConfig(cfg *config.EdgeConfig) error {
_, err := node.LoadFromConfig(cfg.Nodes)
if err != nil {
@ -140,12 +164,27 @@ func validateEdgeConfig(cfg *config.EdgeConfig) error {
name = fmt.Sprintf("index %d", i)
}
if !n.Adapters.Ollama.Enabled && !n.Adapters.CLI.Enabled && !n.Adapters.Vllm.Enabled {
hasOllama := anyOllamaEnabled(n.Adapters)
hasVllm := anyVllmEnabled(n.Adapters)
if !hasOllama && !n.Adapters.CLI.Enabled && !hasVllm {
return fmt.Errorf("node %q: at least one adapter must be enabled", name)
}
if n.Adapters.Ollama.Enabled && n.Adapters.Ollama.BaseURL == "" {
return fmt.Errorf("node %q: ollama adapter base_url must not be empty", name)
}
for j, inst := range n.Adapters.OllamaInstances {
if inst.Enabled && inst.BaseURL == "" {
return fmt.Errorf("node %q: ollama_instances[%d] %q: base_url must not be empty", name, j, inst.Name)
}
}
if n.Adapters.Vllm.Enabled && n.Adapters.Vllm.Endpoint == "" {
return fmt.Errorf("node %q: vllm adapter endpoint must not be empty", name)
}
for j, inst := range n.Adapters.VllmInstances {
if inst.Enabled && inst.Endpoint == "" {
return fmt.Errorf("node %q: vllm_instances[%d] %q: endpoint must not be empty", name, j, inst.Name)
}
}
if n.Runtime.Concurrency < 0 {
return fmt.Errorf("node %q: runtime concurrency must be non-negative", name)
}

View file

@ -216,3 +216,90 @@ bootstrap:
t.Errorf("artifact_base_url = %q", doc.Bootstrap.ArtifactBaseURL)
}
}
func TestValidateEdgeConfig_VllmLegacyEmptyEndpointRejected(t *testing.T) {
cfg := &config.EdgeConfig{
Nodes: []config.NodeDefinition{
{
ID: "node-1",
Alias: "test",
Token: "tok",
Adapters: config.AdaptersConf{
Vllm: config.VllmConf{Enabled: true, Endpoint: ""},
},
},
},
}
err := validateEdgeConfig(cfg)
if err == nil {
t.Fatal("expected error for enabled vllm with empty endpoint")
}
}
func TestValidateEdgeConfig_VllmInstanceEmptyEndpointRejected(t *testing.T) {
cfg := &config.EdgeConfig{
Nodes: []config.NodeDefinition{
{
ID: "node-1",
Alias: "test",
Token: "tok",
Adapters: config.AdaptersConf{
VllmInstances: []config.VllmInstanceConf{
{Name: "a100", Enabled: true, Endpoint: ""},
},
},
},
},
}
err := validateEdgeConfig(cfg)
if err == nil {
t.Fatal("expected error for enabled vllm instance with empty endpoint")
}
}
func TestValidateEdgeConfig_AllAdaptersDisabledRejected(t *testing.T) {
cfg := &config.EdgeConfig{
Nodes: []config.NodeDefinition{
{
ID: "node-1",
Alias: "test",
Token: "tok",
Adapters: config.AdaptersConf{
Ollama: config.OllamaConf{Enabled: false},
Vllm: config.VllmConf{Enabled: false},
CLI: config.CLIConf{Enabled: false},
OllamaInstances: []config.OllamaInstanceConf{
{Name: "off", Enabled: false, BaseURL: "http://127.0.0.1:11434"},
},
VllmInstances: []config.VllmInstanceConf{
{Name: "off", Enabled: false, Endpoint: "http://10.0.0.5:8000"},
},
},
},
},
}
err := validateEdgeConfig(cfg)
if err == nil {
t.Fatal("expected error when all adapters are disabled")
}
}
func TestValidateEdgeConfig_OllamaInstanceEnabledAccepted(t *testing.T) {
cfg := &config.EdgeConfig{
Nodes: []config.NodeDefinition{
{
ID: "node-1",
Alias: "test",
Token: "tok",
Adapters: config.AdaptersConf{
OllamaInstances: []config.OllamaInstanceConf{
{Name: "local", Enabled: true, BaseURL: "http://127.0.0.1:11434"},
},
},
},
},
}
if err := validateEdgeConfig(cfg); err != nil {
t.Fatalf("unexpected error: %v", err)
}
}

View file

@ -1,12 +1,18 @@
package node
import (
"fmt"
"iop/packages/go/config"
iop "iop/proto/gen/iop"
)
// BuildConfigPayload converts a NodeRecord's adapter config to the proto payload
// sent to node during registration.
//
// After config normalisation, OllamaInstances and VllmInstances already contain
// the promoted legacy single-instance entries, so only the slice fields are
// iterated here. Duplicate instance names produce an error.
func BuildConfigPayload(rec *NodeRecord) (*iop.NodeConfigPayload, error) {
payload := &iop.NodeConfigPayload{
Runtime: &iop.NodeRuntimeConfig{
@ -21,27 +27,49 @@ func BuildConfigPayload(rec *NodeRecord) (*iop.NodeConfigPayload, error) {
Config: &iop.AdapterConfig_Mock{Mock: &iop.MockAdapterConfig{}},
})
if rec.Adapters.Ollama.Enabled {
seen := make(map[string]struct{})
for _, inst := range rec.Adapters.OllamaInstances {
if !inst.Enabled {
continue
}
key := "ollama:" + inst.Name
if _, dup := seen[key]; dup {
return nil, fmt.Errorf("duplicate ollama instance name %q", inst.Name)
}
seen[key] = struct{}{}
payload.Adapters = append(payload.Adapters, &iop.AdapterConfig{
Type: "ollama",
Enabled: true,
Name: inst.Name,
Config: &iop.AdapterConfig_Ollama{
Ollama: &iop.OllamaAdapterConfig{
BaseUrl: rec.Adapters.Ollama.BaseURL,
ContextSize: int32(rec.Adapters.Ollama.ContextSize),
BaseUrl: inst.BaseURL,
ContextSize: int32(inst.ContextSize),
},
},
})
}
if rec.Adapters.Vllm.Enabled {
for _, inst := range rec.Adapters.VllmInstances {
if !inst.Enabled {
continue
}
key := "vllm:" + inst.Name
if _, dup := seen[key]; dup {
return nil, fmt.Errorf("duplicate vllm instance name %q", inst.Name)
}
seen[key] = struct{}{}
payload.Adapters = append(payload.Adapters, &iop.AdapterConfig{
Type: "vllm",
Enabled: true,
Name: inst.Name,
Config: &iop.AdapterConfig_Vllm{
Vllm: &iop.VllmAdapterConfig{Endpoint: rec.Adapters.Vllm.Endpoint},
Vllm: &iop.VllmAdapterConfig{Endpoint: inst.Endpoint},
},
})
}
if rec.Adapters.CLI.Enabled {
profiles := make(map[string]*iop.CLIProfileConfig, len(rec.Adapters.CLI.Profiles))
for name, p := range rec.Adapters.CLI.Profiles {

View file

@ -9,15 +9,16 @@ import (
)
func TestBuildConfigPayload_OllamaEnabled(t *testing.T) {
// Simulates the post-normalisation state produced by LoadEdge for a legacy
// single-instance ollama config: OllamaInstances is populated by LoadEdge,
// so callers that bypass LoadEdge must populate it manually.
rec := &edgenode.NodeRecord{
ID: "node-1",
Alias: "test",
Token: "token",
Adapters: config.AdaptersConf{
Ollama: config.OllamaConf{
Enabled: true,
BaseURL: "http://localhost:11434",
ContextSize: 262144,
OllamaInstances: []config.OllamaInstanceConf{
{Name: "ollama", Enabled: true, BaseURL: "http://localhost:11434", ContextSize: 262144},
},
},
Runtime: config.RuntimeConf{
@ -243,6 +244,139 @@ func TestBuildConfigPayload_MockAlwaysPresent(t *testing.T) {
}
}
func TestBuildConfigPayload_MultiOllamaInstances(t *testing.T) {
rec := &edgenode.NodeRecord{
ID: "node-1",
Alias: "test",
Token: "token",
Adapters: config.AdaptersConf{
OllamaInstances: []config.OllamaInstanceConf{
{Name: "local", Enabled: true, BaseURL: "http://127.0.0.1:11434", ContextSize: 131072},
{Name: "dgx", Enabled: true, BaseURL: "http://192.168.0.91:11434", ContextSize: 262144},
},
},
Runtime: config.RuntimeConf{WorkspaceRoot: "/tmp/ws"},
}
payload, err := edgenode.BuildConfigPayload(rec)
if err != nil {
t.Fatalf("BuildConfigPayload failed: %v", err)
}
var ollamaAdapters []*iop.AdapterConfig
for _, a := range payload.Adapters {
if a.Type == "ollama" {
ollamaAdapters = append(ollamaAdapters, a)
}
}
if len(ollamaAdapters) != 2 {
t.Fatalf("expected 2 ollama adapters, got %d", len(ollamaAdapters))
}
names := map[string]bool{}
for _, a := range ollamaAdapters {
names[a.Name] = true
}
if !names["local"] || !names["dgx"] {
t.Errorf("expected instance names local and dgx, got %v", names)
}
}
func TestBuildConfigPayload_MultiVllmInstances(t *testing.T) {
rec := &edgenode.NodeRecord{
ID: "node-1",
Alias: "test",
Token: "token",
Adapters: config.AdaptersConf{
VllmInstances: []config.VllmInstanceConf{
{Name: "a100", Enabled: true, Endpoint: "http://10.0.0.5:8000"},
{Name: "h100", Enabled: true, Endpoint: "http://10.0.0.6:8000"},
},
},
Runtime: config.RuntimeConf{WorkspaceRoot: "/tmp/ws"},
}
payload, err := edgenode.BuildConfigPayload(rec)
if err != nil {
t.Fatalf("BuildConfigPayload failed: %v", err)
}
var vllmAdapters []*iop.AdapterConfig
for _, a := range payload.Adapters {
if a.Type == "vllm" {
vllmAdapters = append(vllmAdapters, a)
}
}
if len(vllmAdapters) != 2 {
t.Fatalf("expected 2 vllm adapters, got %d", len(vllmAdapters))
}
names := map[string]bool{}
for _, a := range vllmAdapters {
names[a.Name] = true
if a.GetVllm() == nil {
t.Errorf("instance %q: expected vllm typed config", a.Name)
}
}
if !names["a100"] || !names["h100"] {
t.Errorf("expected instance names a100 and h100, got %v", names)
}
}
func TestBuildConfigPayload_LegacyOllamaViaInstances(t *testing.T) {
rec := &edgenode.NodeRecord{
ID: "node-1",
Alias: "test",
Token: "token",
Adapters: config.AdaptersConf{
// Simulate what LoadEdge produces after legacy normalisation.
OllamaInstances: []config.OllamaInstanceConf{
{Name: "ollama", Enabled: true, BaseURL: "http://localhost:11434", ContextSize: 4096},
},
},
Runtime: config.RuntimeConf{WorkspaceRoot: "/tmp/ws"},
}
payload, err := edgenode.BuildConfigPayload(rec)
if err != nil {
t.Fatalf("BuildConfigPayload failed: %v", err)
}
var found *iop.AdapterConfig
for _, a := range payload.Adapters {
if a.Type == "ollama" {
found = a
break
}
}
if found == nil {
t.Fatal("expected ollama adapter in payload")
}
if found.Name != "ollama" {
t.Errorf("expected name %q, got %q", "ollama", found.Name)
}
if found.GetOllama().GetBaseUrl() != "http://localhost:11434" {
t.Errorf("unexpected base_url: %q", found.GetOllama().GetBaseUrl())
}
}
func TestBuildConfigPayload_DuplicateOllamaNameError(t *testing.T) {
rec := &edgenode.NodeRecord{
ID: "node-1",
Alias: "test",
Token: "token",
Adapters: config.AdaptersConf{
OllamaInstances: []config.OllamaInstanceConf{
{Name: "dup", Enabled: true, BaseURL: "http://127.0.0.1:11434"},
{Name: "dup", Enabled: true, BaseURL: "http://127.0.0.2:11434"},
},
},
}
_, err := edgenode.BuildConfigPayload(rec)
if err == nil {
t.Fatal("expected error for duplicate ollama instance name")
}
}
func TestBuildConfigPayload_MockUsesTypedConfig(t *testing.T) {
rec := &edgenode.NodeRecord{
ID: "node-1",

View file

@ -303,9 +303,8 @@ func ollamaOptionsFromInput(input map[string]any, contextSize int) map[string]an
}
}
if contextSize > 0 {
if _, ok := options["num_ctx"]; !ok {
options["num_ctx"] = contextSize
}
// num_ctx affects Ollama runner sizing, so keep it owned by Edge config.
options["num_ctx"] = contextSize
}
if len(options) == 0 {
return nil

View file

@ -115,8 +115,17 @@ func TestOllamaExecutePassesOptionsAndTopLevelFields(t *testing.T) {
if req.Options["temperature"].(float64) != 0.2 || req.Options["top_p"].(float64) != 0.9 || req.Options["num_predict"].(float64) != 32 {
t.Fatalf("unexpected options: %+v", req.Options)
}
if req.Options["num_ctx"].(float64) != 4096 {
t.Fatalf("request num_ctx should override config context size: %+v", req.Options)
switch v := req.Options["num_ctx"].(type) {
case float64:
if int(v) != 262144 {
t.Fatalf("config context size should override request num_ctx: %+v", req.Options)
}
case int:
if v != 262144 {
t.Fatalf("config context size should override request num_ctx: %+v", req.Options)
}
default:
t.Fatalf("unexpected num_ctx type %T in %+v", v, req.Options)
}
if req.KeepAlive != "10m" || req.Think != false || req.Format != "json" {
t.Fatalf("top-level fields not passed: keep_alive=%v think=%v format=%v", req.KeepAlive, req.Think, req.Format)

View file

@ -20,8 +20,11 @@ const (
// proto-socket go/base_client.go sendHeartBeat), but a larger wait window
// gives the peer's next heartbeat an extra chance to overwrite any stray
// timer on slow or jittery links before it fires.
heartbeatWaitSec = 45
registerTimeout = 10 * time.Second
heartbeatWaitSec = 45
registerTimeout = 10 * time.Second
registerInitialWait = 100 * time.Millisecond
registerRetryWait = 250 * time.Millisecond
registerAttempts = 3
)
// RegisterResult is returned by DialEdge after successful registration.
@ -49,11 +52,7 @@ func DialEdge(ctx context.Context, addr, token string, logger *zap.Logger) (*Reg
return nil, fmt.Errorf("transport: dial edge %s: %w", addr, err)
}
resp, err := toki.SendRequestTyped[*iop.RegisterRequest, *iop.RegisterResponse](
&client.Communicator,
&iop.RegisterRequest{Token: token},
registerTimeout,
)
resp, err := registerWithEdge(ctx, client, token, logger)
if err != nil {
_ = client.Close()
return nil, fmt.Errorf("transport: register: %w", err)
@ -75,3 +74,43 @@ func DialEdge(ctx context.Context, addr, token string, logger *zap.Logger) (*Reg
Config: resp.GetConfig(),
}, nil
}
func registerWithEdge(ctx context.Context, client *toki.TcpClient, token string, logger *zap.Logger) (*iop.RegisterResponse, error) {
timer := time.NewTimer(registerInitialWait)
select {
case <-ctx.Done():
timer.Stop()
return nil, ctx.Err()
case <-timer.C:
}
var lastErr error
for attempt := 1; attempt <= registerAttempts; attempt++ {
resp, err := toki.SendRequestTyped[*iop.RegisterRequest, *iop.RegisterResponse](
&client.Communicator,
&iop.RegisterRequest{Token: token},
registerTimeout,
)
if err == nil {
return resp, nil
}
lastErr = err
if attempt == registerAttempts || !client.IsAlive() {
break
}
if logger != nil {
logger.Warn("register request failed, retrying",
zap.Int("attempt", attempt),
zap.Error(err),
)
}
timer := time.NewTimer(registerRetryWait)
select {
case <-ctx.Done():
timer.Stop()
return nil, ctx.Err()
case <-timer.C:
}
}
return nil, lastErr
}

View file

@ -69,6 +69,8 @@ nodes:
# id is the stable node identity; omitting it falls back to an auto UUID (dev only).
# agent_kind selects the registration kind; omitting it defaults to "generic-node".
# Allowed values: "generic-node" (default).
#
# Single-adapter example (legacy style — still supported):
- id: "node-example-01"
alias: "example-node"
token: "<node-token>"
@ -101,3 +103,32 @@ nodes:
runtime:
concurrency: 1
workspace_root: ""
# Multi-adapter example: two Ollama instances + CLI profiles on one node.
# Each ollama_instances entry requires a unique "name" within the node.
# - id: "node-multi-01"
# alias: "multi-engine-node"
# token: "<node-token>"
# adapters:
# ollama_instances:
# - name: "ollama-local"
# enabled: true
# base_url: "http://127.0.0.1:11434"
# context_size: 131072
# - name: "ollama-dgx"
# enabled: true
# base_url: "http://192.168.0.91:11434"
# context_size: 262144
# vllm_instances:
# - name: "vllm-a100"
# enabled: true
# endpoint: "http://10.0.0.5:8000"
# cli:
# enabled: true
# profiles:
# claude:
# command: "claude"
# mode: "antigravity-print"
# runtime:
# concurrency: 4
# workspace_root: "/workspace"

View file

@ -166,9 +166,33 @@ type MetricsConf struct {
}
type AdaptersConf struct {
// Legacy single-instance fields. When non-empty they are normalised into
// OllamaInstances / VllmInstances during config load so that all downstream
// code only needs to inspect the slice fields.
Ollama OllamaConf `mapstructure:"ollama" yaml:"ollama"`
Vllm VllmConf `mapstructure:"vllm" yaml:"vllm"`
CLI CLIConf `mapstructure:"cli" yaml:"cli"`
// Multi-instance collections. Each entry carries a unique Name that acts as
// the stable adapter instance identity within the node. Names must be unique
// within each type collection; duplicate names are rejected at load time.
OllamaInstances []OllamaInstanceConf `mapstructure:"ollama_instances" yaml:"ollama_instances,omitempty"`
VllmInstances []VllmInstanceConf `mapstructure:"vllm_instances" yaml:"vllm_instances,omitempty"`
}
// OllamaInstanceConf is one named Ollama engine instance within a node.
type OllamaInstanceConf struct {
Name string `mapstructure:"name" yaml:"name"`
Enabled bool `mapstructure:"enabled" yaml:"enabled"`
BaseURL string `mapstructure:"base_url" yaml:"base_url"`
ContextSize int `mapstructure:"context_size" yaml:"context_size"`
}
// VllmInstanceConf is one named vLLM engine instance within a node.
type VllmInstanceConf struct {
Name string `mapstructure:"name" yaml:"name"`
Enabled bool `mapstructure:"enabled" yaml:"enabled"`
Endpoint string `mapstructure:"endpoint" yaml:"endpoint"`
}
type OllamaConf struct {
@ -248,10 +272,63 @@ func LoadEdge(cfgFile string) (*EdgeConfig, error) {
return nil, fmt.Errorf("nodes[%d] alias=%q: %w", i, cfg.Nodes[i].Alias, err)
}
cfg.Nodes[i].AgentKind = kind
if err := normalizeAdapters(&cfg.Nodes[i].Adapters); err != nil {
name := cfg.Nodes[i].ID
if name == "" {
name = cfg.Nodes[i].Alias
}
return nil, fmt.Errorf("nodes[%d] %q adapters: %w", i, name, err)
}
}
return &cfg, nil
}
// normalizeAdapters promotes legacy single-instance Ollama/Vllm fields into the
// typed instance slices and validates that all instance names are unique.
func normalizeAdapters(a *AdaptersConf) error {
if a.Ollama.Enabled {
legacy := OllamaInstanceConf{
Name: "ollama",
Enabled: a.Ollama.Enabled,
BaseURL: a.Ollama.BaseURL,
ContextSize: a.Ollama.ContextSize,
}
a.OllamaInstances = append([]OllamaInstanceConf{legacy}, a.OllamaInstances...)
}
if a.Vllm.Enabled {
legacy := VllmInstanceConf{
Name: "vllm",
Enabled: a.Vllm.Enabled,
Endpoint: a.Vllm.Endpoint,
}
a.VllmInstances = append([]VllmInstanceConf{legacy}, a.VllmInstances...)
}
if err := checkUniqueNames("ollama_instances", func(i int) string { return a.OllamaInstances[i].Name }, len(a.OllamaInstances)); err != nil {
return err
}
if err := checkUniqueNames("vllm_instances", func(i int) string { return a.VllmInstances[i].Name }, len(a.VllmInstances)); err != nil {
return err
}
return nil
}
func checkUniqueNames(field string, name func(int) string, n int) error {
seen := make(map[string]struct{}, n)
for i := 0; i < n; i++ {
k := name(i)
if k == "" {
return fmt.Errorf("%s[%d]: name must not be empty", field, i)
}
if _, dup := seen[k]; dup {
return fmt.Errorf("%s: duplicate name %q", field, k)
}
seen[k] = struct{}{}
}
return nil
}
func setDefaults(v *viper.Viper) {
v.SetDefault("transport.edge_addr", "localhost:9090")
v.SetDefault("logging.level", "info")

View file

@ -921,6 +921,145 @@ func TestLoadEdge_ControlPlaneDefaults(t *testing.T) {
}
}
func TestLoadEdge_MultiOllamaInstances(t *testing.T) {
dir := t.TempDir()
f := filepath.Join(dir, "edge.yaml")
yaml := `
server:
listen: "0.0.0.0:9090"
nodes:
- alias: "multi"
adapters:
ollama_instances:
- name: "local"
enabled: true
base_url: "http://127.0.0.1:11434"
context_size: 131072
- name: "dgx"
enabled: true
base_url: "http://192.168.0.91:11434"
context_size: 262144
`
if err := os.WriteFile(f, []byte(yaml), 0o600); err != nil {
t.Fatalf("write yaml: %v", err)
}
cfg, err := config.LoadEdge(f)
if err != nil {
t.Fatalf("load: %v", err)
}
if len(cfg.Nodes) == 0 {
t.Fatal("expected 1 node")
}
insts := cfg.Nodes[0].Adapters.OllamaInstances
if len(insts) != 2 {
t.Fatalf("expected 2 ollama instances, got %d", len(insts))
}
if insts[0].Name != "local" || insts[0].BaseURL != "http://127.0.0.1:11434" || insts[0].ContextSize != 131072 {
t.Errorf("unexpected instance[0]: %+v", insts[0])
}
if insts[1].Name != "dgx" || insts[1].BaseURL != "http://192.168.0.91:11434" || insts[1].ContextSize != 262144 {
t.Errorf("unexpected instance[1]: %+v", insts[1])
}
}
func TestLoadEdge_MultiVllmInstances(t *testing.T) {
dir := t.TempDir()
f := filepath.Join(dir, "edge.yaml")
yaml := `
server:
listen: "0.0.0.0:9090"
nodes:
- alias: "multi-vllm"
adapters:
vllm_instances:
- name: "a100"
enabled: true
endpoint: "http://10.0.0.5:8000"
- name: "h100"
enabled: true
endpoint: "http://10.0.0.6:8000"
`
if err := os.WriteFile(f, []byte(yaml), 0o600); err != nil {
t.Fatalf("write yaml: %v", err)
}
cfg, err := config.LoadEdge(f)
if err != nil {
t.Fatalf("load: %v", err)
}
if len(cfg.Nodes) == 0 {
t.Fatal("expected 1 node")
}
insts := cfg.Nodes[0].Adapters.VllmInstances
if len(insts) != 2 {
t.Fatalf("expected 2 vllm instances, got %d", len(insts))
}
if insts[0].Name != "a100" || insts[0].Endpoint != "http://10.0.0.5:8000" {
t.Errorf("unexpected instance[0]: %+v", insts[0])
}
if insts[1].Name != "h100" || insts[1].Endpoint != "http://10.0.0.6:8000" {
t.Errorf("unexpected instance[1]: %+v", insts[1])
}
}
func TestLoadEdge_LegacyOllamaPromotedToInstances(t *testing.T) {
dir := t.TempDir()
f := filepath.Join(dir, "edge.yaml")
yaml := `
server:
listen: "0.0.0.0:9090"
nodes:
- alias: "local"
adapters:
ollama:
enabled: true
base_url: "http://192.168.0.91:11434"
context_size: 262144
`
if err := os.WriteFile(f, []byte(yaml), 0o600); err != nil {
t.Fatalf("write yaml: %v", err)
}
cfg, err := config.LoadEdge(f)
if err != nil {
t.Fatalf("load: %v", err)
}
insts := cfg.Nodes[0].Adapters.OllamaInstances
if len(insts) != 1 {
t.Fatalf("expected 1 ollama instance from legacy field, got %d", len(insts))
}
if insts[0].Name != "ollama" {
t.Errorf("expected instance name %q, got %q", "ollama", insts[0].Name)
}
if insts[0].BaseURL != "http://192.168.0.91:11434" {
t.Errorf("expected base_url %q, got %q", "http://192.168.0.91:11434", insts[0].BaseURL)
}
}
func TestLoadEdge_DuplicateOllamaInstanceNameRejected(t *testing.T) {
dir := t.TempDir()
f := filepath.Join(dir, "edge.yaml")
yaml := `
server:
listen: "0.0.0.0:9090"
nodes:
- alias: "dup"
adapters:
ollama_instances:
- name: "same"
enabled: true
base_url: "http://127.0.0.1:11434"
- name: "same"
enabled: true
base_url: "http://127.0.0.2:11434"
`
if err := os.WriteFile(f, []byte(yaml), 0o600); err != nil {
t.Fatalf("write yaml: %v", err)
}
_, err := config.LoadEdge(f)
if err == nil {
t.Fatal("expected error for duplicate ollama instance name")
}
}
func TestLoadEdge_ControlPlaneOverride(t *testing.T) {
dir := t.TempDir()
f := filepath.Join(dir, "edge.yaml")

View file

@ -1220,6 +1220,9 @@ func (x *NodeConfigPayload) GetRuntime() *NodeRuntimeConfig {
}
// AdapterConfig describes one adapter to enable on the node.
// name is the stable instance identity within a node; for single-instance
// adapters it may be empty (equivalent to the type name). When a node carries
// multiple instances of the same adapter type each must have a unique name.
type AdapterConfig struct {
state protoimpl.MessageState `protogen:"open.v1"`
Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"` // "mock" | "ollama" | "vllm" | "cli"
@ -1232,6 +1235,8 @@ type AdapterConfig struct {
// *AdapterConfig_Vllm
// *AdapterConfig_Mock
Config isAdapterConfig_Config `protobuf_oneof:"config"`
Name string `protobuf:"bytes,8,opt,name=name,proto3" json:"name,omitempty"` // stable instance identity; empty = legacy single-instance
Target string `protobuf:"bytes,9,opt,name=target,proto3" json:"target,omitempty"` // optional default target/route hint for this instance
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@ -1330,6 +1335,20 @@ func (x *AdapterConfig) GetMock() *MockAdapterConfig {
return nil
}
func (x *AdapterConfig) GetName() string {
if x != nil {
return x.Name
}
return ""
}
func (x *AdapterConfig) GetTarget() string {
if x != nil {
return x.Target
}
return ""
}
type isAdapterConfig_Config interface {
isAdapterConfig_Config()
}
@ -1888,7 +1907,7 @@ const file_proto_iop_runtime_proto_rawDesc = "" +
"\x06config\x18\x05 \x01(\v2\x16.iop.NodeConfigPayloadR\x06config\"u\n" +
"\x11NodeConfigPayload\x12.\n" +
"\badapters\x18\x01 \x03(\v2\x12.iop.AdapterConfigR\badapters\x120\n" +
"\aruntime\x18\x02 \x01(\v2\x16.iop.NodeRuntimeConfigR\aruntime\"\xb7\x02\n" +
"\aruntime\x18\x02 \x01(\v2\x16.iop.NodeRuntimeConfigR\aruntime\"\xe3\x02\n" +
"\rAdapterConfig\x12\x12\n" +
"\x04type\x18\x01 \x01(\tR\x04type\x12\x18\n" +
"\aenabled\x18\x02 \x01(\bR\aenabled\x123\n" +
@ -1896,7 +1915,9 @@ const file_proto_iop_runtime_proto_rawDesc = "" +
"\x03cli\x18\x04 \x01(\v2\x15.iop.CLIAdapterConfigH\x00R\x03cli\x122\n" +
"\x06ollama\x18\x05 \x01(\v2\x18.iop.OllamaAdapterConfigH\x00R\x06ollama\x12,\n" +
"\x04vllm\x18\x06 \x01(\v2\x16.iop.VllmAdapterConfigH\x00R\x04vllm\x12,\n" +
"\x04mock\x18\a \x01(\v2\x16.iop.MockAdapterConfigH\x00R\x04mockB\b\n" +
"\x04mock\x18\a \x01(\v2\x16.iop.MockAdapterConfigH\x00R\x04mock\x12\x12\n" +
"\x04name\x18\b \x01(\tR\x04name\x12\x16\n" +
"\x06target\x18\t \x01(\tR\x06targetB\b\n" +
"\x06config\"\x13\n" +
"\x11MockAdapterConfig\"\xa7\x01\n" +
"\x10CLIAdapterConfig\x12?\n" +

View file

@ -151,6 +151,9 @@ message NodeConfigPayload {
}
// AdapterConfig describes one adapter to enable on the node.
// name is the stable instance identity within a node; for single-instance
// adapters it may be empty (equivalent to the type name). When a node carries
// multiple instances of the same adapter type each must have a unique name.
message AdapterConfig {
string type = 1; // "mock" | "ollama" | "vllm" | "cli"
bool enabled = 2;
@ -161,6 +164,8 @@ message AdapterConfig {
VllmAdapterConfig vllm = 6;
MockAdapterConfig mock = 7;
}
string name = 8; // stable instance identity; empty = legacy single-instance
string target = 9; // optional default target/route hint for this instance
}
message MockAdapterConfig {}