- Archive completed subtask plans/code reviews (04, 05+03,04, 07+03) - Add provider_pool_admission_test.go - Update edge config types, load, catalog validation - Update runtime, config refresh, service layers for admission - Update test docs and inventory - Update provider scheduling, resolution, tunnel, status modules
866 lines
28 KiB
Go
866 lines
28 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"net"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
toki "git.toki-labs.com/toki/proto-socket/go"
|
|
"google.golang.org/protobuf/proto"
|
|
|
|
edgeevents "iop/apps/edge/internal/events"
|
|
edgenode "iop/apps/edge/internal/node"
|
|
"iop/packages/go/config"
|
|
iop "iop/proto/gen/iop"
|
|
)
|
|
|
|
func TestSubmitRunProviderPoolRewritesAdapterAndTarget(t *testing.T) {
|
|
// Use net.Pipe to create a fake node connection that captures the RunRequest.
|
|
edgeConn, nodeConn := net.Pipe()
|
|
defer edgeConn.Close()
|
|
defer nodeConn.Close()
|
|
|
|
parserMap := toki.ParserMap{
|
|
toki.TypeNameOf(&iop.RunRequest{}): func(b []byte) (proto.Message, error) {
|
|
m := &iop.RunRequest{}
|
|
return m, proto.Unmarshal(b, m)
|
|
},
|
|
}
|
|
|
|
edgeClient := toki.NewTcpClient(edgeConn, 0, 0, parserMap)
|
|
nodeClient := toki.NewTcpClient(nodeConn, 0, 0, parserMap)
|
|
|
|
// Capture the RunRequest received by the fake node.
|
|
var capturedReq *iop.RunRequest
|
|
var capturedMu sync.Mutex
|
|
toki.AddListenerTyped[*iop.RunRequest](&nodeClient.Communicator, func(req *iop.RunRequest) {
|
|
capturedMu.Lock()
|
|
capturedReq = req
|
|
capturedMu.Unlock()
|
|
})
|
|
|
|
// Build the model catalog with provider references.
|
|
catalog := []config.ModelCatalogEntry{
|
|
{
|
|
ID: "qwen3.6:35b",
|
|
Providers: map[string]string{
|
|
"prov-vllm-01": "served-qwen",
|
|
},
|
|
},
|
|
}
|
|
|
|
// Build NodeStore with a provider-pool provider.
|
|
store := edgenode.NewNodeStore()
|
|
store.Add(&edgenode.NodeRecord{
|
|
ID: "node-pool",
|
|
Runtime: config.RuntimeConf{Concurrency: 4},
|
|
Adapters: config.AdaptersConf{
|
|
VllmInstances: []config.VllmInstanceConf{
|
|
{Name: "vllm-gpu", Enabled: true, Endpoint: "http://127.0.0.1:8000/v1"},
|
|
},
|
|
},
|
|
Providers: []config.NodeProviderConf{
|
|
{
|
|
ID: "prov-vllm-01",
|
|
Adapter: "vllm-gpu",
|
|
Models: []string{"served-qwen"},
|
|
Health: "available",
|
|
Capacity: 2,
|
|
},
|
|
},
|
|
})
|
|
|
|
// Build registry with the fake node.
|
|
reg := edgenode.NewRegistry()
|
|
reg.Register(&edgenode.NodeEntry{
|
|
NodeID: "node-pool",
|
|
LifecycleState: edgenode.LifecycleConnected,
|
|
Client: edgeClient,
|
|
})
|
|
|
|
// Create Service with queue and catalog.
|
|
// events bus must be non-nil to activate the queue path for provider-pool.
|
|
bus := edgeevents.NewBus()
|
|
svc := New(reg, bus)
|
|
svc.SetNodeStore(store)
|
|
svc.SetModelCatalog(catalog)
|
|
|
|
// SubmitRun with ProviderPool=true.
|
|
result, err := svc.SubmitRun(context.Background(), SubmitRunRequest{
|
|
RunID: "run-pool-test-001",
|
|
ModelGroupKey: "qwen3.6:35b",
|
|
ProviderPool: true,
|
|
Background: true,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("SubmitRun: %v", err)
|
|
}
|
|
if result == nil {
|
|
t.Fatal("expected non-nil RunResult")
|
|
}
|
|
|
|
// Wait for the fake node to receive the request.
|
|
time.Sleep(50 * time.Millisecond)
|
|
|
|
capturedMu.Lock()
|
|
defer capturedMu.Unlock()
|
|
|
|
if capturedReq == nil {
|
|
t.Fatal("no RunRequest captured from fake node; SubmitRun did not send")
|
|
}
|
|
|
|
// Verify that the adapter and target were rewritten from the provider-pool candidate.
|
|
if capturedReq.GetAdapter() != "vllm-gpu" {
|
|
t.Errorf("adapter: got %q, want %q", capturedReq.GetAdapter(), "vllm-gpu")
|
|
}
|
|
if capturedReq.GetTarget() != "served-qwen" {
|
|
t.Errorf("target: got %q, want %q", capturedReq.GetTarget(), "served-qwen")
|
|
}
|
|
if capturedReq.GetRunId() != "run-pool-test-001" {
|
|
t.Errorf("runID: got %q, want %q", capturedReq.GetRunId(), "run-pool-test-001")
|
|
}
|
|
}
|
|
|
|
// TestResolveProviderPoolCandidatesAdapterInstanceValidation verifies that the
|
|
// defensive isProviderAdapterInstanceValid check in resolveProviderPoolCandidates
|
|
// excludes providers whose adapter name resolves to a disabled exact instance,
|
|
// an ambiguous type route, a zero-instance type route, or an unknown adapter key,
|
|
// while valid named instances and single enabled type routes remain as candidates.
|
|
func TestResolveProviderPoolCandidatesAdapterInstanceValidation(t *testing.T) {
|
|
catalog := []config.ModelCatalogEntry{
|
|
{
|
|
ID: "ai-group",
|
|
Providers: map[string]string{
|
|
"prov-disabled": "ai-model",
|
|
"prov-ambiguous": "ai-model",
|
|
"prov-valid": "ai-model",
|
|
"prov-zero-type": "ai-model",
|
|
"prov-missing": "ai-model",
|
|
},
|
|
},
|
|
}
|
|
|
|
store := edgenode.NewNodeStore()
|
|
|
|
// prov-disabled: adapter="vllm-gpu" but VllmInstance "vllm-gpu" is disabled.
|
|
store.Add(&edgenode.NodeRecord{
|
|
ID: "node-disabled",
|
|
Adapters: config.AdaptersConf{
|
|
VllmInstances: []config.VllmInstanceConf{
|
|
{Name: "vllm-gpu", Enabled: false, Endpoint: "http://127.0.0.1:8000/v1"},
|
|
},
|
|
},
|
|
Providers: []config.NodeProviderConf{
|
|
{ID: "prov-disabled", Adapter: "vllm-gpu", Models: []string{"ai-model"}, Health: "available", Capacity: 2},
|
|
},
|
|
})
|
|
|
|
// prov-ambiguous: adapter="openai_compat" type route with 2 enabled instances → ambiguous.
|
|
store.Add(&edgenode.NodeRecord{
|
|
ID: "node-ambiguous",
|
|
Adapters: config.AdaptersConf{
|
|
OpenAICompatInstances: []config.OpenAICompatInstanceConf{
|
|
{Name: "compat-a", Enabled: true, Endpoint: "http://127.0.0.1:8000/v1"},
|
|
{Name: "compat-b", Enabled: true, Endpoint: "http://127.0.0.1:8001/v1"},
|
|
},
|
|
},
|
|
Providers: []config.NodeProviderConf{
|
|
{ID: "prov-ambiguous", Adapter: "openai_compat", Models: []string{"ai-model"}, Health: "available", Capacity: 2},
|
|
},
|
|
})
|
|
|
|
// prov-valid: adapter="vllm-gpu" and VllmInstance "vllm-gpu" is enabled.
|
|
store.Add(&edgenode.NodeRecord{
|
|
ID: "node-valid",
|
|
Adapters: config.AdaptersConf{
|
|
VllmInstances: []config.VllmInstanceConf{
|
|
{Name: "vllm-gpu", Enabled: true, Endpoint: "http://127.0.0.1:8000/v1"},
|
|
},
|
|
},
|
|
Providers: []config.NodeProviderConf{
|
|
{ID: "prov-valid", Adapter: "vllm-gpu", Models: []string{"ai-model"}, Health: "available", Capacity: 2},
|
|
},
|
|
})
|
|
|
|
// prov-zero-type: adapter="ollama" type route but no enabled ollama instances → zero-instance type route excluded.
|
|
store.Add(&edgenode.NodeRecord{
|
|
ID: "node-zero-type",
|
|
Adapters: config.AdaptersConf{
|
|
OllamaInstances: []config.OllamaInstanceConf{
|
|
{Name: "ollama-disabled", Enabled: false, BaseURL: "http://127.0.0.1:11434"},
|
|
},
|
|
},
|
|
Providers: []config.NodeProviderConf{
|
|
{ID: "prov-zero-type", Adapter: "ollama", Models: []string{"ai-model"}, Health: "available", Capacity: 2},
|
|
},
|
|
})
|
|
|
|
// prov-missing: adapter="unknown-missing" not in any instance list and not a known type key → unknown adapter excluded.
|
|
store.Add(&edgenode.NodeRecord{
|
|
ID: "node-missing",
|
|
Adapters: config.AdaptersConf{
|
|
VllmInstances: []config.VllmInstanceConf{
|
|
{Name: "vllm-gpu", Enabled: true, Endpoint: "http://127.0.0.1:9000/v1"},
|
|
},
|
|
},
|
|
Providers: []config.NodeProviderConf{
|
|
{ID: "prov-missing", Adapter: "unknown-missing", Models: []string{"ai-model"}, Health: "available", Capacity: 2},
|
|
},
|
|
})
|
|
|
|
reg := edgenode.NewRegistry()
|
|
allRecs := store.All()
|
|
for _, rec := range allRecs {
|
|
reg.Register(&edgenode.NodeEntry{NodeID: rec.ID, LifecycleState: edgenode.LifecycleConnected})
|
|
}
|
|
|
|
svc := New(reg, nil)
|
|
svc.SetNodeStore(store)
|
|
svc.SetModelCatalog(catalog)
|
|
|
|
req := SubmitRunRequest{ModelGroupKey: "ai-group", ProviderPool: true}
|
|
storeSnap, catalogSnap, _ := svc.runtimeConfigSnapshot()
|
|
candidates, _, err := svc.resolveProviderPoolCandidates(req, storeSnap, catalogSnap)
|
|
if err != nil {
|
|
t.Fatalf("resolveProviderPoolCandidates: %v", err)
|
|
}
|
|
|
|
// Only prov-valid should be a candidate.
|
|
got := make(map[string]bool)
|
|
for _, c := range candidates {
|
|
got[c.providerID] = true
|
|
}
|
|
if len(candidates) != 1 {
|
|
t.Fatalf("expected 1 candidate (prov-valid), got %d: %v", len(candidates), got)
|
|
}
|
|
if !got["prov-valid"] {
|
|
t.Error("prov-valid (enabled exact instance) must be a candidate")
|
|
}
|
|
if got["prov-disabled"] {
|
|
t.Error("prov-disabled (disabled exact instance) must be excluded")
|
|
}
|
|
if got["prov-ambiguous"] {
|
|
t.Error("prov-ambiguous (ambiguous type route) must be excluded")
|
|
}
|
|
if got["prov-zero-type"] {
|
|
t.Error("prov-zero-type (zero-instance type route) must be excluded")
|
|
}
|
|
if got["prov-missing"] {
|
|
t.Error("prov-missing (unknown adapter) must be excluded")
|
|
}
|
|
}
|
|
|
|
// TestGetSnapshotForNodeCatalogFirstNoDuplicates verifies that when a node has
|
|
// both adapter instances and a providers[] catalog, getSnapshotForNode returns
|
|
// only catalog snapshots (no adapter duplicates).
|
|
func TestGetSnapshotForNodeCatalogFirstNoDuplicates(t *testing.T) {
|
|
rec := &edgenode.NodeRecord{
|
|
ID: "node-catalog",
|
|
Adapters: config.AdaptersConf{
|
|
CLI: config.CLIConf{Enabled: true},
|
|
OpenAICompatInstances: []config.OpenAICompatInstanceConf{
|
|
{Name: "vllm-gpu", Enabled: true},
|
|
},
|
|
},
|
|
Providers: []config.NodeProviderConf{
|
|
{ID: "prov-a", Adapter: "vllm-gpu", Models: []string{"model-a"}, Health: "available", Capacity: 2},
|
|
{ID: "prov-b", Adapter: "vllm-gpu", Models: []string{"model-b"}, Health: "available", Capacity: 3},
|
|
},
|
|
Runtime: config.RuntimeConf{Concurrency: 1},
|
|
}
|
|
store := edgenode.NewNodeStore()
|
|
store.Add(rec)
|
|
|
|
m := newModelQueueManager(store)
|
|
|
|
snaps := m.getSnapshotForNode("node-catalog", rec)
|
|
|
|
// Must return exactly the catalog providers — no CLI or openai_compat adapter duplicates.
|
|
if len(snaps) != 2 {
|
|
t.Fatalf("expected 2 catalog snapshots, got %d: %+v", len(snaps), snaps)
|
|
}
|
|
ids := map[string]bool{}
|
|
for _, s := range snaps {
|
|
ids[s.Id] = true
|
|
}
|
|
if !ids["prov-a"] || !ids["prov-b"] {
|
|
t.Errorf("expected catalog provider IDs prov-a and prov-b, got: %v", ids)
|
|
}
|
|
// No adapter-only snapshots (no empty Id entries that correspond to adapter sections).
|
|
for _, s := range snaps {
|
|
if s.Id == "" {
|
|
t.Errorf("unexpected adapter-only snapshot (empty Id) in catalog-first result: %+v", s)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestResolveProviderPoolCandidatesSkipsDisabledProvider verifies that a
|
|
// provider with enabled=false is excluded from dispatch candidates.
|
|
func TestResolveProviderPoolCandidatesSkipsDisabledProvider(t *testing.T) {
|
|
disabled := false
|
|
catalog := []config.ModelCatalogEntry{
|
|
{
|
|
ID: "model-x",
|
|
Providers: map[string]string{
|
|
"prov-active": "served-x",
|
|
"prov-disabled": "served-x",
|
|
},
|
|
},
|
|
}
|
|
store := edgenode.NewNodeStore()
|
|
store.Add(&edgenode.NodeRecord{
|
|
ID: "node-d",
|
|
Providers: []config.NodeProviderConf{
|
|
{ID: "prov-active", Type: "vllm", Adapter: "vllm-gpu", Models: []string{"served-x"}, Health: "available", Capacity: 2},
|
|
{ID: "prov-disabled", Type: "vllm", Adapter: "vllm-gpu", Models: []string{"served-x"}, Health: "available", Capacity: 2, Enabled: &disabled},
|
|
},
|
|
Adapters: config.AdaptersConf{
|
|
VllmInstances: []config.VllmInstanceConf{{Name: "vllm-gpu", Enabled: true, Endpoint: "http://127.0.0.1:8000/v1"}},
|
|
},
|
|
})
|
|
reg := edgenode.NewRegistry()
|
|
reg.Register(&edgenode.NodeEntry{NodeID: "node-d", LifecycleState: edgenode.LifecycleConnected})
|
|
|
|
svc := New(reg, nil)
|
|
svc.SetNodeStore(store)
|
|
svc.SetModelCatalog(catalog)
|
|
|
|
storeSnap, catalogSnap, _ := svc.runtimeConfigSnapshot()
|
|
candidates, _, err := svc.resolveProviderPoolCandidates(SubmitRunRequest{ModelGroupKey: "model-x", ProviderPool: true}, storeSnap, catalogSnap)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if len(candidates) != 1 {
|
|
t.Fatalf("expected 1 candidate (disabled filtered), got %d", len(candidates))
|
|
}
|
|
if candidates[0].providerID != "prov-active" {
|
|
t.Errorf("expected prov-active, got %q", candidates[0].providerID)
|
|
}
|
|
}
|
|
|
|
// TestResolveProviderPoolCandidatesUsesProviderIDWhenAdapterOmitted verifies
|
|
// that a provider-first provider (empty Adapter field) uses its ID as the
|
|
// dispatch adapter key.
|
|
func TestResolveProviderPoolCandidatesUsesProviderIDWhenAdapterOmitted(t *testing.T) {
|
|
catalog := []config.ModelCatalogEntry{
|
|
{
|
|
ID: "model-y",
|
|
Providers: map[string]string{
|
|
"prov-first": "served-y",
|
|
},
|
|
},
|
|
}
|
|
store := edgenode.NewNodeStore()
|
|
store.Add(&edgenode.NodeRecord{
|
|
ID: "node-pf",
|
|
Providers: []config.NodeProviderConf{
|
|
{ID: "prov-first", Type: "vllm", Adapter: "", Models: []string{"served-y"}, Health: "available", Capacity: 3},
|
|
},
|
|
})
|
|
reg := edgenode.NewRegistry()
|
|
reg.Register(&edgenode.NodeEntry{NodeID: "node-pf", LifecycleState: edgenode.LifecycleConnected})
|
|
|
|
svc := New(reg, nil)
|
|
svc.SetNodeStore(store)
|
|
svc.SetModelCatalog(catalog)
|
|
|
|
storeSnap, catalogSnap, _ := svc.runtimeConfigSnapshot()
|
|
candidates, _, err := svc.resolveProviderPoolCandidates(SubmitRunRequest{ModelGroupKey: "model-y", ProviderPool: true}, storeSnap, catalogSnap)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if len(candidates) != 1 {
|
|
t.Fatalf("expected 1 candidate, got %d", len(candidates))
|
|
}
|
|
if candidates[0].adapter != "prov-first" {
|
|
t.Errorf("provider-first adapter key: got %q, want provider ID %q", candidates[0].adapter, "prov-first")
|
|
}
|
|
if candidates[0].providerID != "prov-first" {
|
|
t.Errorf("providerID: got %q", candidates[0].providerID)
|
|
}
|
|
}
|
|
|
|
// TestProviderSnapshotsReportDisabledProvider verifies that a disabled
|
|
// provider appears in snapshots with status=disabled and effective capacity 0.
|
|
func TestProviderSnapshotsReportDisabledProvider(t *testing.T) {
|
|
disabled := false
|
|
rec := &edgenode.NodeRecord{
|
|
ID: "node-snap",
|
|
Providers: []config.NodeProviderConf{
|
|
{ID: "prov-on", Type: "vllm", Health: "available", Capacity: 4},
|
|
{ID: "prov-off", Type: "vllm", Health: "available", Capacity: 4, Enabled: &disabled},
|
|
},
|
|
}
|
|
store := edgenode.NewNodeStore()
|
|
store.Add(rec)
|
|
m := newModelQueueManager(store)
|
|
|
|
snaps := m.getSnapshotForNode("node-snap", rec)
|
|
if len(snaps) != 2 {
|
|
t.Fatalf("expected 2 snapshots, got %d", len(snaps))
|
|
}
|
|
|
|
byID := map[string]*iop.ProviderSnapshot{}
|
|
for _, s := range snaps {
|
|
byID[s.Id] = s
|
|
}
|
|
|
|
on := byID["prov-on"]
|
|
if on == nil {
|
|
t.Fatal("prov-on snapshot missing")
|
|
}
|
|
if on.Status != "available" {
|
|
t.Errorf("prov-on status: got %q, want available", on.Status)
|
|
}
|
|
if on.Capacity != 4 {
|
|
t.Errorf("prov-on capacity: got %d, want 4", on.Capacity)
|
|
}
|
|
|
|
off := byID["prov-off"]
|
|
if off == nil {
|
|
t.Fatal("prov-off snapshot missing")
|
|
}
|
|
if off.Status != "disabled" {
|
|
t.Errorf("prov-off status: got %q, want disabled", off.Status)
|
|
}
|
|
if off.Health != "disabled" {
|
|
t.Errorf("prov-off health: got %q, want disabled", off.Health)
|
|
}
|
|
if off.Capacity != 0 {
|
|
t.Errorf("prov-off effective capacity: got %d, want 0", off.Capacity)
|
|
}
|
|
}
|
|
|
|
// TestStatusProviderProviderFirstNoAdapterDuplicates verifies that provider-first
|
|
// providers (empty Adapter) appear once in snapshots with no adapter duplicate.
|
|
func TestStatusProviderProviderFirstNoAdapterDuplicates(t *testing.T) {
|
|
rec := &edgenode.NodeRecord{
|
|
ID: "node-pf-snap",
|
|
Adapters: config.AdaptersConf{
|
|
VllmInstances: []config.VllmInstanceConf{{Name: "vllm-gpu", Enabled: true}},
|
|
},
|
|
Providers: []config.NodeProviderConf{
|
|
{ID: "prov-first-a", Type: "vllm", Adapter: "", Models: []string{"model-a"}, Health: "available", Capacity: 2},
|
|
{ID: "prov-first-b", Type: "vllm", Adapter: "", Models: []string{"model-b"}, Health: "available", Capacity: 2},
|
|
},
|
|
}
|
|
store := edgenode.NewNodeStore()
|
|
store.Add(rec)
|
|
m := newModelQueueManager(store)
|
|
|
|
snaps := m.getSnapshotForNode("node-pf-snap", rec)
|
|
// catalog-first: exactly 2 provider snapshots, no adapter duplicate.
|
|
if len(snaps) != 2 {
|
|
t.Fatalf("expected 2 provider-first snapshots, got %d: %+v", len(snaps), snaps)
|
|
}
|
|
for _, s := range snaps {
|
|
if s.Id == "" {
|
|
t.Errorf("unexpected adapter-only snapshot (empty Id): %+v", s)
|
|
}
|
|
if s.Adapter != "" {
|
|
t.Errorf("provider-first snapshot should have empty Adapter, got %q for id=%q", s.Adapter, s.Id)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestResolveProviderPoolCandidatesPropagatesPriority verifies that the actual
|
|
// resolveProviderPoolCandidates path propagates config provider Priority into
|
|
// candidateNode.priority.
|
|
func TestResolveProviderPoolCandidatesPropagatesPriority(t *testing.T) {
|
|
catalog := []config.ModelCatalogEntry{
|
|
{
|
|
ID: "model-prio",
|
|
Providers: map[string]string{
|
|
"prov-high-prio": "served-model-prio",
|
|
"prov-low-prio": "served-model-prio",
|
|
"prov-no-priority": "served-model-prio",
|
|
},
|
|
},
|
|
}
|
|
|
|
store := edgenode.NewNodeStore()
|
|
// High-priority provider (priority=3).
|
|
store.Add(&edgenode.NodeRecord{
|
|
ID: "node-prio",
|
|
Runtime: config.RuntimeConf{Concurrency: 4},
|
|
Adapters: config.AdaptersConf{
|
|
VllmInstances: []config.VllmInstanceConf{
|
|
{Name: "vllm-gpu", Enabled: true, Endpoint: "http://127.0.0.1:8000/v1"},
|
|
},
|
|
},
|
|
Providers: []config.NodeProviderConf{
|
|
{
|
|
ID: "prov-high-prio",
|
|
Adapter: "vllm-gpu",
|
|
Models: []string{"served-model-prio"},
|
|
Health: "available",
|
|
Capacity: 2,
|
|
Priority: 3,
|
|
},
|
|
{
|
|
ID: "prov-low-prio",
|
|
Adapter: "vllm-gpu",
|
|
Models: []string{"served-model-prio"},
|
|
Health: "available",
|
|
Capacity: 2,
|
|
Priority: 10,
|
|
},
|
|
{
|
|
ID: "prov-no-priority",
|
|
Adapter: "vllm-gpu",
|
|
Models: []string{"served-model-prio"},
|
|
Health: "available",
|
|
Capacity: 2,
|
|
// Priority omitted → defaults to 0.
|
|
},
|
|
},
|
|
})
|
|
|
|
reg := edgenode.NewRegistry()
|
|
for _, rec := range store.All() {
|
|
reg.Register(&edgenode.NodeEntry{NodeID: rec.ID, LifecycleState: edgenode.LifecycleConnected})
|
|
}
|
|
|
|
svc := New(reg, nil)
|
|
svc.SetNodeStore(store)
|
|
svc.SetModelCatalog(catalog)
|
|
|
|
storeSnap, catalogSnap, _ := svc.runtimeConfigSnapshot()
|
|
candidates, _, err := svc.resolveProviderPoolCandidates(
|
|
SubmitRunRequest{ModelGroupKey: "model-prio", ProviderPool: true},
|
|
storeSnap, catalogSnap,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("resolveProviderPoolCandidates: %v", err)
|
|
}
|
|
if len(candidates) != 3 {
|
|
t.Fatalf("expected 3 candidates, got %d", len(candidates))
|
|
}
|
|
|
|
byID := map[string]*candidateNode{}
|
|
for i := range candidates {
|
|
byID[candidates[i].providerID] = &candidates[i]
|
|
}
|
|
|
|
// prov-high-prio: fixture Priority=3 → candidate.priority must be 3.
|
|
hp := byID["prov-high-prio"]
|
|
if hp == nil {
|
|
t.Fatal("prov-high-prio not in candidates")
|
|
}
|
|
if hp.priority != 3 {
|
|
t.Errorf("prov-high-prio: got priority %d, want 3", hp.priority)
|
|
}
|
|
|
|
// prov-low-prio: fixture Priority=10 → candidate.priority must be 10.
|
|
lp := byID["prov-low-prio"]
|
|
if lp == nil {
|
|
t.Fatal("prov-low-prio not in candidates")
|
|
}
|
|
if lp.priority != 10 {
|
|
t.Errorf("prov-low-prio: got priority %d, want 10", lp.priority)
|
|
}
|
|
|
|
// prov-no-priority: Priority omitted → defaults to 0.
|
|
np := byID["prov-no-priority"]
|
|
if np == nil {
|
|
t.Fatal("prov-no-priority not in candidates")
|
|
}
|
|
if np.priority != 0 {
|
|
t.Errorf("prov-no-priority: got priority %d, want 0", np.priority)
|
|
}
|
|
}
|
|
|
|
// TestGetSnapshotForNodeLegacyAdapterFallback verifies that a node with no
|
|
// providers[] catalog returns adapter snapshots (CLI, OllamaInstances, etc.)
|
|
// via the legacy adapter fallback path.
|
|
func TestGetSnapshotForNodeLegacyAdapterFallback(t *testing.T) {
|
|
rec := &edgenode.NodeRecord{
|
|
ID: "node-legacy",
|
|
Adapters: config.AdaptersConf{
|
|
CLI: config.CLIConf{Enabled: true},
|
|
OllamaInstances: []config.OllamaInstanceConf{
|
|
{Name: "ollama-local", Enabled: true, Capacity: 3},
|
|
},
|
|
},
|
|
Runtime: config.RuntimeConf{Concurrency: 2},
|
|
}
|
|
store := edgenode.NewNodeStore()
|
|
store.Add(rec)
|
|
|
|
m := newModelQueueManager(store)
|
|
|
|
snaps := m.getSnapshotForNode("node-legacy", rec)
|
|
|
|
// Must return adapter snapshots: CLI + ollama-local = 2 entries.
|
|
if len(snaps) != 2 {
|
|
t.Fatalf("expected 2 adapter snapshots (cli + ollama-local), got %d: %+v", len(snaps), snaps)
|
|
}
|
|
adapters := map[string]bool{}
|
|
for _, s := range snaps {
|
|
adapters[s.Adapter] = true
|
|
}
|
|
if !adapters["cli"] {
|
|
t.Error("expected cli adapter snapshot in legacy fallback")
|
|
}
|
|
if !adapters["ollama-local"] {
|
|
t.Error("expected ollama-local adapter snapshot in legacy fallback")
|
|
}
|
|
}
|
|
|
|
func TestResolveProviderPoolCandidatesClassifiesExecutionPath(t *testing.T) {
|
|
catalog := []config.ModelCatalogEntry{
|
|
{
|
|
ID: "test-model",
|
|
Providers: map[string]string{
|
|
"prov-openai-compat": "served-model",
|
|
"prov-openai-api": "served-model",
|
|
"prov-vllm": "served-model",
|
|
"prov-vllm-mlx": "served-model",
|
|
"prov-lemonade": "served-model",
|
|
"prov-sglang": "served-model",
|
|
"prov-seulgivibe-claude": "served-model",
|
|
"prov-seulgivibe-openai": "served-model",
|
|
"prov-ollama": "served-model",
|
|
"prov-cli": "served-model",
|
|
"prov-unknown": "served-model",
|
|
},
|
|
},
|
|
}
|
|
|
|
store := edgenode.NewNodeStore()
|
|
|
|
// OpenAI-compatible providers (all should be tunnel path).
|
|
store.Add(&edgenode.NodeRecord{
|
|
ID: "node-openai",
|
|
Runtime: config.RuntimeConf{Concurrency: 4},
|
|
Providers: []config.NodeProviderConf{
|
|
{ID: "prov-openai-compat", Type: "openai_compat", Category: "api", Health: "available", Capacity: 2, Models: []string{"served-model"}},
|
|
{ID: "prov-openai-api", Type: "openai_api", Category: "api", Health: "available", Capacity: 2, Models: []string{"served-model"}},
|
|
{ID: "prov-vllm", Type: "vllm", Category: "local_inference", Health: "available", Capacity: 2, Models: []string{"served-model"}},
|
|
{ID: "prov-vllm-mlx", Type: "vllm-mlx", Category: "local_inference", Health: "available", Capacity: 2, Models: []string{"served-model"}},
|
|
{ID: "prov-lemonade", Type: "lemonade", Category: "local_inference", Health: "available", Capacity: 2, Models: []string{"served-model"}},
|
|
{ID: "prov-sglang", Type: "sglang", Category: "local_inference", Health: "available", Capacity: 2, Models: []string{"served-model"}},
|
|
{ID: "prov-seulgivibe-claude", Type: "seulgivibe_claude", Category: "api", Health: "available", Capacity: 2, Models: []string{"served-model"}},
|
|
{ID: "prov-seulgivibe-openai", Type: "seulgivibe_openai", Category: "api", Health: "available", Capacity: 2, Models: []string{"served-model"}},
|
|
},
|
|
})
|
|
|
|
// Ollama/CLI/unknown providers (all should be normalized path).
|
|
store.Add(&edgenode.NodeRecord{
|
|
ID: "node-normalized",
|
|
Runtime: config.RuntimeConf{Concurrency: 4},
|
|
Adapters: config.AdaptersConf{
|
|
OllamaInstances: []config.OllamaInstanceConf{{Name: "ollama", Enabled: true}},
|
|
CLI: config.CLIConf{Enabled: true, Profiles: map[string]config.CLIProfileConf{"cli-profile": {}}},
|
|
},
|
|
Providers: []config.NodeProviderConf{
|
|
{ID: "prov-ollama", Type: "ollama", Category: "local_inference", Adapter: "ollama", Health: "available", Capacity: 2, Models: []string{"served-model"}},
|
|
{ID: "prov-cli", Type: "cli", Category: "cli", Adapter: "cli", Health: "available", Capacity: 2, Models: []string{"served-model"}},
|
|
{ID: "prov-unknown", Type: "unknown_type", Category: "api", Health: "available", Capacity: 2, Models: []string{"served-model"}},
|
|
},
|
|
})
|
|
|
|
// Build a fake registry with all nodes.
|
|
reg := edgenode.NewRegistry()
|
|
allRecs := store.All()
|
|
for _, rec := range allRecs {
|
|
entry := &edgenode.NodeEntry{
|
|
NodeID: rec.ID,
|
|
LifecycleState: edgenode.LifecycleConnected,
|
|
}
|
|
reg.Register(entry)
|
|
}
|
|
|
|
// Create Service with catalog and node store.
|
|
svc := New(reg, nil)
|
|
svc.SetNodeStore(store)
|
|
svc.SetModelCatalog(catalog)
|
|
|
|
req := SubmitRunRequest{ModelGroupKey: "test-model", ProviderPool: true}
|
|
candidates, _, err := svc.resolveProviderPoolCandidates(req, store, catalog)
|
|
if err != nil {
|
|
t.Fatalf("resolveProviderPoolCandidates: %v", err)
|
|
}
|
|
|
|
expectedTunnel := map[string]bool{
|
|
"prov-openai-compat": true,
|
|
"prov-openai-api": true,
|
|
"prov-vllm": true,
|
|
"prov-vllm-mlx": true,
|
|
"prov-lemonade": true,
|
|
"prov-sglang": true,
|
|
"prov-seulgivibe-claude": true,
|
|
"prov-seulgivibe-openai": true,
|
|
}
|
|
expectedNormalized := map[string]bool{
|
|
"prov-ollama": true,
|
|
"prov-cli": true,
|
|
"prov-unknown": true,
|
|
}
|
|
|
|
// Convert returned candidates to a map.
|
|
got := map[string]providerExecutionPath{}
|
|
for _, c := range candidates {
|
|
got[c.providerID] = c.executionPath
|
|
}
|
|
|
|
// Verify tunnel-path providers are present and classified as tunnel.
|
|
for providerID := range expectedTunnel {
|
|
path, ok := got[providerID]
|
|
if !ok {
|
|
t.Errorf("expected tunnel provider %q missing from candidates (got %d total)", providerID, len(candidates))
|
|
continue
|
|
}
|
|
if path != providerExecutionPathTunnel {
|
|
t.Errorf("provider %q: expected tunnel path, got %q", providerID, path)
|
|
}
|
|
}
|
|
|
|
// Verify normalized-path providers are present and classified as normalized.
|
|
for providerID := range expectedNormalized {
|
|
path, ok := got[providerID]
|
|
if !ok {
|
|
t.Errorf("expected normalized provider %q missing from candidates (got %d total)", providerID, len(candidates))
|
|
continue
|
|
}
|
|
if path != providerExecutionPathNormalized {
|
|
t.Errorf("provider %q: expected normalized path, got %q", providerID, path)
|
|
}
|
|
}
|
|
|
|
// Extra sanity.
|
|
for _, c := range candidates {
|
|
if _, okTunnel := expectedTunnel[c.providerID]; !okTunnel {
|
|
if _, okNorm := expectedNormalized[c.providerID]; !okNorm {
|
|
t.Errorf("unexpected providerID %q", c.providerID)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestResolveProviderPoolCandidatesKeepsMixedProviderTypes verifies S05.
|
|
func TestResolveProviderPoolCandidatesKeepsMixedProviderTypes(t *testing.T) {
|
|
catalog := []config.ModelCatalogEntry{
|
|
{
|
|
ID: "mixed-model",
|
|
Providers: map[string]string{
|
|
"prov-vllm": "served-vllm",
|
|
"prov-vllm-mlx": "served-vllm-mlx",
|
|
"prov-lemonade": "served-lemonade",
|
|
"prov-ollama": "served-ollama",
|
|
},
|
|
},
|
|
}
|
|
|
|
store := edgenode.NewNodeStore()
|
|
store.Add(&edgenode.NodeRecord{
|
|
ID: "node-mixed",
|
|
Runtime: config.RuntimeConf{Concurrency: 4},
|
|
Adapters: config.AdaptersConf{
|
|
VllmInstances: []config.VllmInstanceConf{
|
|
{Name: "vllm-gpu", Enabled: true, Endpoint: "http://127.0.0.1:8000/v1"},
|
|
{Name: "vllm-gpu-mlx", Enabled: true, Endpoint: "http://127.0.0.1:8001/v1"},
|
|
},
|
|
OllamaInstances: []config.OllamaInstanceConf{
|
|
{Name: "ollama", Enabled: true, BaseURL: "http://127.0.0.1:11434"},
|
|
},
|
|
OpenAICompatInstances: []config.OpenAICompatInstanceConf{
|
|
{Name: "oaicompat", Enabled: true, Endpoint: "http://127.0.0.1:9000/v1"},
|
|
},
|
|
},
|
|
Providers: []config.NodeProviderConf{
|
|
{
|
|
ID: "prov-vllm",
|
|
Type: "vllm",
|
|
Adapter: "vllm-gpu",
|
|
Models: []string{"served-vllm"},
|
|
Health: "available",
|
|
Capacity: 2,
|
|
},
|
|
{
|
|
ID: "prov-vllm-mlx",
|
|
Type: "vllm-mlx",
|
|
Adapter: "vllm-gpu-mlx",
|
|
Models: []string{"served-vllm-mlx"},
|
|
Health: "available",
|
|
Capacity: 2,
|
|
},
|
|
{
|
|
ID: "prov-lemonade",
|
|
Type: "lemonade",
|
|
Adapter: "oaicompat",
|
|
Models: []string{"served-lemonade"},
|
|
Health: "available",
|
|
Capacity: 2,
|
|
},
|
|
{
|
|
ID: "prov-ollama",
|
|
Type: "ollama",
|
|
Adapter: "ollama",
|
|
Models: []string{"served-ollama"},
|
|
Health: "available",
|
|
Capacity: 2,
|
|
},
|
|
},
|
|
})
|
|
|
|
reg := edgenode.NewRegistry()
|
|
for _, rec := range store.All() {
|
|
reg.Register(&edgenode.NodeEntry{NodeID: rec.ID, LifecycleState: edgenode.LifecycleConnected})
|
|
}
|
|
|
|
svc := New(reg, nil)
|
|
svc.SetNodeStore(store)
|
|
svc.SetModelCatalog(catalog)
|
|
|
|
req := SubmitRunRequest{ModelGroupKey: "mixed-model", ProviderPool: true}
|
|
candidates, _, err := svc.resolveProviderPoolCandidates(req, store, catalog)
|
|
if err != nil {
|
|
t.Fatalf("resolveProviderPoolCandidates: %v", err)
|
|
}
|
|
|
|
// All 4 providers must be candidates.
|
|
if len(candidates) != 4 {
|
|
ids := make([]string, len(candidates))
|
|
for i, c := range candidates {
|
|
ids[i] = c.providerID
|
|
}
|
|
t.Fatalf("expected 4 candidates (prov-vllm, prov-vllm-mlx, prov-lemonade, prov-ollama), got %d: %v", len(candidates), ids)
|
|
}
|
|
|
|
byID := map[string]*candidateNode{}
|
|
for i := range candidates {
|
|
byID[candidates[i].providerID] = &candidates[i]
|
|
}
|
|
|
|
// OpenAI-compatible providers (vLLM, vLLM-MLX, lemonade) must be tunnel path.
|
|
for _, provID := range []string{"prov-vllm", "prov-vllm-mlx", "prov-lemonade"} {
|
|
c, ok := byID[provID]
|
|
if !ok {
|
|
t.Errorf("%q not in candidates", provID)
|
|
continue
|
|
}
|
|
if c.executionPath != providerExecutionPathTunnel {
|
|
t.Errorf("%q: expected tunnel path, got %q", provID, c.executionPath)
|
|
}
|
|
// Served target must match the catalog entry.
|
|
expectedServed := catalog[0].Providers[provID]
|
|
if c.servedTarget != expectedServed {
|
|
t.Errorf("%q servedTarget: got %q, want %q", provID, c.servedTarget, expectedServed)
|
|
}
|
|
}
|
|
|
|
// Ollama provider must be normalized path.
|
|
ollama, ok := byID["prov-ollama"]
|
|
if !ok {
|
|
t.Fatal("prov-ollama not in candidates")
|
|
}
|
|
if ollama.executionPath != providerExecutionPathNormalized {
|
|
t.Errorf("prov-ollama: expected normalized path, got %q", ollama.executionPath)
|
|
}
|
|
if ollama.servedTarget != "served-ollama" {
|
|
t.Errorf("prov-ollama servedTarget: got %q, want %q", ollama.servedTarget, "served-ollama")
|
|
}
|
|
}
|