From e30fb2494eaa31bb42fdd66cb4c611b42bdb0f1f Mon Sep 17 00:00:00 2001 From: toki Date: Mon, 29 Jun 2026 14:39:27 +0900 Subject: [PATCH] feat(edge): node resource model unification - update phase/SDD docs and edge service - Move node-resource-model-unification milestone and SDD to archive - Update PHASE.md for operational-observability-provider-management - Refactor edge node registration and model queue logic - Add status provider tests for model queue validation - Update edge config and Go config structs --- .../node-resource-model-unification.md | 10 ++-- .../node-resource-model-unification/SDD.md | 0 .../PHASE.md | 6 +- apps/edge/internal/edgecmd/node_register.go | 2 +- apps/edge/internal/service/model_queue.go | 12 ++-- apps/edge/internal/service/run_dispatch.go | 5 +- apps/edge/internal/service/status_provider.go | 4 -- .../internal/service/status_provider_test.go | 60 +++++++++++++++++++ configs/edge.yaml | 2 +- packages/go/config/config.go | 4 +- 10 files changed, 80 insertions(+), 25 deletions(-) rename agent-roadmap/{ => archive}/phase/operational-observability-provider-management/milestones/node-resource-model-unification.md (94%) rename agent-roadmap/{ => archive}/sdd/operational-observability-provider-management/node-resource-model-unification/SDD.md (100%) diff --git a/agent-roadmap/phase/operational-observability-provider-management/milestones/node-resource-model-unification.md b/agent-roadmap/archive/phase/operational-observability-provider-management/milestones/node-resource-model-unification.md similarity index 94% rename from agent-roadmap/phase/operational-observability-provider-management/milestones/node-resource-model-unification.md rename to agent-roadmap/archive/phase/operational-observability-provider-management/milestones/node-resource-model-unification.md index 9bf6b17..a4c85c3 100644 --- a/agent-roadmap/phase/operational-observability-provider-management/milestones/node-resource-model-unification.md +++ b/agent-roadmap/archive/phase/operational-observability-provider-management/milestones/node-resource-model-unification.md @@ -13,7 +13,7 @@ Mac dev-runtime에서는 Codex CLI와 MLX vLLM provider를 하나의 `mac-codex- ## 상태 -[검토중] +[완료] ## 구현 잠금 @@ -51,11 +51,11 @@ Node를 Edge 연결 owner로 두고, 실행 제어는 Node 하위 resource/provi ## 완료 리뷰 -- 상태: 검토중 +- 상태: 통과 - 요청일: 2026-06-29 -- 완료 근거: `contract-schema`, `node-admission`, `edge-routing-status`, `dev-runtime-profile` 모두 PASS complete log와 `Roadmap Completion`을 남겼고, SDD Acceptance Scenario S01-S07과 Evidence Map을 충족한다. -- 검토 항목: archive 전환 전 최종 완료 확인 -- 리뷰 코멘트: 남은 차단 항목 없음. 완료 archive 전환은 별도 승인 후 진행한다. +- 완료 근거: `contract-schema`, `node-admission`, `edge-routing-status`, `dev-runtime-profile` 모두 PASS complete log와 `Roadmap Completion`을 남겼고, SDD Acceptance Scenario S01-S07과 Evidence Map을 충족한다. 종료 전 코드레벨 검토에서 provider catalog zero-capacity status fallback과 stale CLI help 문구를 보정했고 `go test -count=1 ./...`가 통과했다. +- 검토 항목: 완료 archive 전환 +- 리뷰 코멘트: 남은 차단 항목 없음. 2026-06-29 완료 archive로 이동한다. ## 범위 제외 diff --git a/agent-roadmap/sdd/operational-observability-provider-management/node-resource-model-unification/SDD.md b/agent-roadmap/archive/sdd/operational-observability-provider-management/node-resource-model-unification/SDD.md similarity index 100% rename from agent-roadmap/sdd/operational-observability-provider-management/node-resource-model-unification/SDD.md rename to agent-roadmap/archive/sdd/operational-observability-provider-management/node-resource-model-unification/SDD.md diff --git a/agent-roadmap/phase/operational-observability-provider-management/PHASE.md b/agent-roadmap/phase/operational-observability-provider-management/PHASE.md index 2c42fee..32aa87a 100644 --- a/agent-roadmap/phase/operational-observability-provider-management/PHASE.md +++ b/agent-roadmap/phase/operational-observability-provider-management/PHASE.md @@ -20,9 +20,9 @@ provider 확장 Phase에서 검증한 Ollama, vLLM, SGLang, Lemonade 같은 추 - 경로: `agent-roadmap/archive/phase/operational-observability-provider-management/milestones/provider-catalog-device-status.md` - 요약: `models[]`를 외부 model key이자 운영 catalog model로 두고, Node 하위 provider와 served model list를 기준으로 Edge가 provider 선택, load-ratio routing, target rewrite, read-only catalog 상태를 소유하는 MVP 계약을 완료했다. -- [검토중] Node Resource Model Unification - - 경로: `agent-roadmap/phase/operational-observability-provider-management/milestones/node-resource-model-unification.md` - - 요약: Node를 Edge 연결 identity로 두고 CLI, OpenAI-compatible provider, 기타 resource를 같은 Node 아래 나열하며 provider/resource capacity만 concurrency를 소유하도록 runtime 계약과 dev-runtime 구성을 정렬했고, 완료 근거와 archive 전환 검토를 남긴다. +- [완료] Node Resource Model Unification + - 경로: `agent-roadmap/archive/phase/operational-observability-provider-management/milestones/node-resource-model-unification.md` + - 요약: Node를 Edge 연결 identity로 두고 CLI, OpenAI-compatible provider, 기타 resource를 같은 Node 아래 나열하며 provider/resource capacity만 concurrency를 소유하도록 runtime 계약과 dev-runtime 구성을 정렬했다. - [스케치] 사용량, 토큰, 로그 운영 추적 MVP - 경로: `agent-roadmap/phase/operational-observability-provider-management/milestones/usage-token-log-ops-mvp.md` diff --git a/apps/edge/internal/edgecmd/node_register.go b/apps/edge/internal/edgecmd/node_register.go index dbe7038..28fa58e 100644 --- a/apps/edge/internal/edgecmd/node_register.go +++ b/apps/edge/internal/edgecmd/node_register.go @@ -264,7 +264,7 @@ directly in the default flow.`, c.Flags().StringVar(®Adapter, "adapter", "cli", "node adapter: ollama or cli") c.Flags().StringVar(®OllamaBaseURL, "ollama-base-url", "", "ollama base URL") c.Flags().IntVar(®OllamaContextSize, "ollama-context-size", 2048, "ollama context size") - c.Flags().IntVar(®RuntimeConcurrency, "runtime-concurrency", 1, "runtime concurrency limit") + c.Flags().IntVar(®RuntimeConcurrency, "runtime-concurrency", 1, "legacy runtime concurrency metadata") c.Flags().StringVar(®Target, "target", "", "bootstrap target platform (e.g. darwin-arm64, linux-amd64)") c.Flags().StringVar(®ArtifactBaseURL, "artifact-base-url", "", "artifact base URL for bootstrap download") diff --git a/apps/edge/internal/service/model_queue.go b/apps/edge/internal/service/model_queue.go index 2d19823..8dd840a 100644 --- a/apps/edge/internal/service/model_queue.go +++ b/apps/edge/internal/service/model_queue.go @@ -532,11 +532,6 @@ func (m *modelQueueManager) getSnapshotForNode(nodeID string, rec *edgenode.Node var snaps []*iop.ProviderSnapshot - concurrencyFallback := 1 - if rec.Runtime.Concurrency > 0 { - concurrencyFallback = rec.Runtime.Concurrency - } - // Catalog-first: nodes with a providers[] catalog emit only catalog snapshots. // Adapter snapshots (sections 1-4) are omitted to prevent duplicate metrics // when a node has both adapter instances and provider-pool catalog entries. @@ -546,9 +541,6 @@ func (m *modelQueueManager) getSnapshotForNode(nodeID string, rec *edgenode.Node continue } capVal := prov.Capacity - if capVal <= 0 { - capVal = concurrencyFallback - } inflight, queued := m.getStatsForProviderLocked(nodeID, prov.ID) servedModels := make([]string, len(prov.Models)) @@ -581,6 +573,10 @@ func (m *modelQueueManager) getSnapshotForNode(nodeID string, rec *edgenode.Node } // Legacy adapter snapshots for nodes with no providers catalog. + concurrencyFallback := 1 + if rec.Runtime.Concurrency > 0 { + concurrencyFallback = rec.Runtime.Concurrency + } // 1. CLI if rec.Adapters.CLI.Enabled { diff --git a/apps/edge/internal/service/run_dispatch.go b/apps/edge/internal/service/run_dispatch.go index f419399..aded58d 100644 --- a/apps/edge/internal/service/run_dispatch.go +++ b/apps/edge/internal/service/run_dispatch.go @@ -471,8 +471,9 @@ func providerCanServe(prov config.NodeProviderConf, servedModel string) bool { return false } -// providerDispatchable checks whether a provider has a non-empty adapter and -// valid capacity so it can be used for dispatch. +// providerDispatchable checks whether a provider has a non-empty adapter. +// Capacity is checked separately so zero-capacity providers remain visible in +// status snapshots while unavailable for provider-pool dispatch. func providerDispatchable(prov config.NodeProviderConf) bool { return strings.TrimSpace(prov.Adapter) != "" } diff --git a/apps/edge/internal/service/status_provider.go b/apps/edge/internal/service/status_provider.go index c32392a..d584516 100644 --- a/apps/edge/internal/service/status_provider.go +++ b/apps/edge/internal/service/status_provider.go @@ -72,16 +72,12 @@ func NodeEntrySnapshot(entry *edgenode.NodeEntry) NodeSnapshot { // zero inflight/queued for nodes whose config has a providers[] catalog but // whose service has no queue (e.g. queue disabled at startup). func staticProviderCatalogSnapshots(rec *edgenode.NodeRecord) []*iop.ProviderSnapshot { - concurrencyFallback := positiveOr(rec.Runtime.Concurrency, defaultNodeCapacity) snaps := make([]*iop.ProviderSnapshot, 0, len(rec.Providers)) for _, prov := range rec.Providers { if prov.ID == "" { continue } capVal := prov.Capacity - if capVal <= 0 { - capVal = concurrencyFallback - } servedModels := make([]string, len(prov.Models)) copy(servedModels, prov.Models) lifecycleCaps := make([]string, len(prov.LifecycleCapabilities)) diff --git a/apps/edge/internal/service/status_provider_test.go b/apps/edge/internal/service/status_provider_test.go index 2da2648..f6e3638 100644 --- a/apps/edge/internal/service/status_provider_test.go +++ b/apps/edge/internal/service/status_provider_test.go @@ -276,6 +276,66 @@ func TestListNodeSnapshotsProviderCatalogSupersedesAdapterSnapshots(t *testing.T } } +func TestListNodeSnapshotsCatalogProviderZeroCapacityDoesNotUseRuntimeFallback(t *testing.T) { + reg := edgenode.NewRegistry() + reg.Register(&edgenode.NodeEntry{NodeID: "node-zero-cap", Alias: "node-zero-cap"}) + + store := edgenode.NewNodeStore() + rec := &edgenode.NodeRecord{ + ID: "node-zero-cap", + Adapters: config.AdaptersConf{ + OpenAICompatInstances: []config.OpenAICompatInstanceConf{ + {Name: "vllm-gpu", Enabled: true, Capacity: 7}, + }, + }, + Providers: []config.NodeProviderConf{ + {ID: "prov-zero", Adapter: "vllm-gpu", Models: []string{"model-zero"}, Health: "available", Capacity: 0}, + }, + Runtime: config.RuntimeConf{Concurrency: 7}, + } + store.Add(rec) + + bus := edgeevents.NewBus() + svc := New(reg, bus) + svc.SetNodeStore(store) + + snaps := svc.ListNodeSnapshots() + if len(snaps) != 1 { + t.Fatalf("expected 1 node snapshot, got %d", len(snaps)) + } + if len(snaps[0].ProviderSnapshots) != 1 { + t.Fatalf("expected 1 provider snapshot, got %d", len(snaps[0].ProviderSnapshots)) + } + p := snaps[0].ProviderSnapshots[0] + if p.Id != "prov-zero" { + t.Fatalf("expected provider id prov-zero, got %q", p.Id) + } + if p.Capacity != 0 { + t.Fatalf("provider catalog capacity must not fall back to runtime concurrency: got %d", p.Capacity) + } + if p.LoadRatio != 0 { + t.Fatalf("expected zero load ratio for zero capacity provider, got %.2f", p.LoadRatio) + } +} + +func TestStaticProviderCatalogSnapshotsZeroCapacityDoesNotUseRuntimeFallback(t *testing.T) { + rec := &edgenode.NodeRecord{ + ID: "node-static-zero-cap", + Providers: []config.NodeProviderConf{ + {ID: "prov-zero", Adapter: "vllm-gpu", Models: []string{"model-zero"}, Health: "available", Capacity: 0}, + }, + Runtime: config.RuntimeConf{Concurrency: 7}, + } + + snaps := staticProviderCatalogSnapshots(rec) + if len(snaps) != 1 { + t.Fatalf("expected 1 provider snapshot, got %d", len(snaps)) + } + if snaps[0].Capacity != 0 { + t.Fatalf("static provider catalog capacity must not fall back to runtime concurrency: got %d", snaps[0].Capacity) + } +} + // TestListNodeSnapshotsLegacyAdapterFallbackWhenNoProviders verifies that when // a node has no providers[] catalog, ListNodeSnapshots returns the legacy adapter // snapshots (CLI, Ollama, etc.) rather than empty results. diff --git a/configs/edge.yaml b/configs/edge.yaml index 8423725..c2a5b5f 100644 --- a/configs/edge.yaml +++ b/configs/edge.yaml @@ -128,7 +128,7 @@ openai: # nodes[].providers[].adapter — concrete adapter instance key used by edge to dispatch. # nodes[].providers[].models — served model names this provider can serve. # nodes[].providers[].health — observed health state string. - # nodes[].providers[].capacity — max concurrent execution slots. + # nodes[].providers[].capacity — provider-pool max concurrent execution slots; 0 is not dispatchable. # nodes[].providers[].max_queue — max queue depth (per-provider). # nodes[].providers[].queue_timeout_ms — queue timeout in milliseconds. # nodes[].providers[].lifecycle_capabilities — coarse lifecycle capabilities list. diff --git a/packages/go/config/config.go b/packages/go/config/config.go index 3e7a4b4..9b92d62 100644 --- a/packages/go/config/config.go +++ b/packages/go/config/config.go @@ -128,7 +128,9 @@ type NodeProviderConf struct { Models []string `mapstructure:"models" yaml:"models,omitempty"` // Health is the observed provider health state. Health string `mapstructure:"health" yaml:"health,omitempty"` - // Capacity is the maximum concurrent execution slots. + // Capacity is the provider/resource maximum concurrent execution slots. + // Provider-pool dispatch treats 0 as not dispatchable rather than falling + // back to node runtime concurrency metadata. Capacity int `mapstructure:"capacity" yaml:"capacity,omitempty"` // MaxQueue is the maximum queue depth. MaxQueue int `mapstructure:"max_queue" yaml:"max_queue,omitempty"`