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
This commit is contained in:
toki 2026-06-29 14:39:27 +09:00
parent 1204a06960
commit e30fb2494e
10 changed files with 80 additions and 25 deletions

View file

@ -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로 이동한다.
## 범위 제외

View file

@ -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`

View file

@ -264,7 +264,7 @@ directly in the default flow.`,
c.Flags().StringVar(&regAdapter, "adapter", "cli", "node adapter: ollama or cli")
c.Flags().StringVar(&regOllamaBaseURL, "ollama-base-url", "", "ollama base URL")
c.Flags().IntVar(&regOllamaContextSize, "ollama-context-size", 2048, "ollama context size")
c.Flags().IntVar(&regRuntimeConcurrency, "runtime-concurrency", 1, "runtime concurrency limit")
c.Flags().IntVar(&regRuntimeConcurrency, "runtime-concurrency", 1, "legacy runtime concurrency metadata")
c.Flags().StringVar(&regTarget, "target", "", "bootstrap target platform (e.g. darwin-arm64, linux-amd64)")
c.Flags().StringVar(&regArtifactBaseURL, "artifact-base-url", "", "artifact base URL for bootstrap download")

View file

@ -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 {

View file

@ -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) != ""
}

View file

@ -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))

View file

@ -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.

View file

@ -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.

View file

@ -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"`