From 34643e383a8893754a0ca09e2daa9a47d0e8dddb Mon Sep 17 00:00:00 2001 From: Codex Date: Tue, 9 Jun 2026 15:09:07 +0800 Subject: [PATCH 1/2] fix(external): don't list AIMA's own deployment backends as external services MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The external-service scanner probes a fixed port list that includes 8080 — llama.cpp's default port — which AIMA's own native deployments also bind. So a model AIMA deployed itself (e.g. Qwen3.6-27B-Q4_K_M on 127.0.0.1:8080) was also surfaced under "external services" as importable, and importing it would register a self-referential backend. Exclude any scanned/persisted service whose host:port matches an AIMA-owned (non-external) proxy backend: - normalizeHostPort: reduce a base URL/address to a comparable host:port, folding localhost/::1/0.0.0.0 to 127.0.0.1. - Reconciler.ownDeploymentAddrs: host:port set of non-external proxy backends. - Scan skips own-deployment addresses (no upsert); List filters them out so a previously-recorded self entry disappears too. Tests: normalizeHostPort table; List excludes the own :8080 deployment while a genuine external service (ollama :11434) is kept. Co-Authored-By: Claude Opus 4.8 (1M context) --- internal/external/reconciler.go | 72 +++++++++++++++++++++++++++- internal/external/reconciler_test.go | 68 ++++++++++++++++++++++++++ 2 files changed, 139 insertions(+), 1 deletion(-) diff --git a/internal/external/reconciler.go b/internal/external/reconciler.go index e5194ae..3a8ad50 100644 --- a/internal/external/reconciler.go +++ b/internal/external/reconciler.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "log/slog" + "net" "net/url" "strings" "time" @@ -65,8 +66,14 @@ func (r *Reconciler) Scan(ctx context.Context) ([]Overview, error) { if err != nil { return nil, err } + own := r.ownDeploymentAddrs() out := make([]Overview, 0, len(services)) for _, svc := range services { + // Skip AIMA's own deployment backends (e.g. native llama.cpp on :8080); + // they are managed deployments, not importable external services. + if _, ok := own[normalizeHostPort(svc.BaseURL)]; ok { + continue + } overview := OverviewFromScan(svc) if err := r.store.UpsertExternalService(ctx, RecordFromOverview(overview)); err != nil { slog.Warn("external service scan: failed to persist service", "base_url", svc.BaseURL, "error", err) @@ -93,7 +100,9 @@ func (r *Reconciler) List(ctx context.Context) ([]Overview, error) { for _, row := range rows { out = append(out, OverviewFromRecord(row)) } - return out, nil + // Hide AIMA's own deployment backends that a prior scan may have recorded + // (e.g. a native llama.cpp deployment on llama.cpp's default port 8080). + return filterOutOwnDeployments(out, r.ownDeploymentAddrs()), nil } func (r *Reconciler) Import(ctx context.Context, idOrBaseURL string, models []string) (ImportResult, error) { @@ -404,3 +413,64 @@ func sameStringSet(a, b []string) bool { } return true } + +// normalizeHostPort reduces a base URL or address to a comparable "host:port", +// dropping scheme/path and folding loopback/any-interface hosts to 127.0.0.1 so +// a scanned endpoint can be matched against an AIMA deployment address. +func normalizeHostPort(addr string) string { + addr = strings.TrimSpace(strings.ToLower(addr)) + if addr == "" { + return "" + } + if i := strings.Index(addr, "://"); i >= 0 { + addr = addr[i+3:] + } + if i := strings.IndexByte(addr, '/'); i >= 0 { + addr = addr[:i] + } + host, port, err := net.SplitHostPort(addr) + if err != nil { + return addr + } + switch host { + case "localhost", "::1", "[::1]", "0.0.0.0", "": + host = "127.0.0.1" + } + return host + ":" + port +} + +// ownDeploymentAddrs returns the set of host:port addresses backing AIMA's own +// (non-external) proxy deployments. The external scanner probes llama.cpp's +// default port 8080, which an AIMA native deployment also binds, so these must +// be excluded to avoid surfacing AIMA's own backend as an importable service. +func (r *Reconciler) ownDeploymentAddrs() map[string]struct{} { + own := make(map[string]struct{}) + if r.proxy == nil { + return own + } + for _, b := range r.proxy.ListBackends() { + if b == nil || b.External { + continue + } + if a := normalizeHostPort(b.Address); a != "" { + own[a] = struct{}{} + } + } + return own +} + +// filterOutOwnDeployments drops services whose address matches an AIMA-owned +// deployment backend. +func filterOutOwnDeployments(services []Overview, own map[string]struct{}) []Overview { + if len(own) == 0 { + return services + } + out := make([]Overview, 0, len(services)) + for _, svc := range services { + if _, ok := own[normalizeHostPort(svc.BaseURL)]; ok { + continue + } + out = append(out, svc) + } + return out +} diff --git a/internal/external/reconciler_test.go b/internal/external/reconciler_test.go index 6b2e843..eb303f4 100644 --- a/internal/external/reconciler_test.go +++ b/internal/external/reconciler_test.go @@ -370,3 +370,71 @@ func TestRestoreKeepsImportedSubsetOnRestore(t *testing.T) { t.Fatal("new-model backend should be restored") } } + +func TestNormalizeHostPort(t *testing.T) { + cases := []struct { + in string + want string + }{ + {"127.0.0.1:8080", "127.0.0.1:8080"}, + {"http://127.0.0.1:8080", "127.0.0.1:8080"}, + {"http://127.0.0.1:8080/v1", "127.0.0.1:8080"}, + {"localhost:8080", "127.0.0.1:8080"}, + {"http://localhost:8080", "127.0.0.1:8080"}, + {"0.0.0.0:8080", "127.0.0.1:8080"}, + {"10.42.0.8:8000", "10.42.0.8:8000"}, + } + for _, c := range cases { + if got := normalizeHostPort(c.in); got != c.want { + t.Errorf("normalizeHostPort(%q) = %q, want %q", c.in, got, c.want) + } + } +} + +// AIMA's own native deployment binds llama.cpp's default port 8080, which the +// external scanner also probes. List must not surface AIMA's own deployment +// backend as an importable external service; genuine external services stay. +func TestListExcludesOwnDeploymentServices(t *testing.T) { + ctx := context.Background() + db, err := state.Open(ctx, ":memory:") + if err != nil { + t.Fatalf("Open(:memory:): %v", err) + } + t.Cleanup(func() { db.Close() }) + + for _, svc := range []*state.ExternalService{ + {ID: "own-8080", BaseURL: "http://127.0.0.1:8080", Kind: "openai", Status: "reachable", Source: "scan", ModelsJSON: `["Qwen3.6-27B-Q4_K_M.gguf"]`}, + {ID: "real-ollama", BaseURL: "http://127.0.0.1:11434", Kind: "ollama", Status: "reachable", Source: "scan", ModelsJSON: `["llama3"]`}, + } { + if err := db.UpsertExternalService(ctx, svc); err != nil { + t.Fatalf("UpsertExternalService: %v", err) + } + } + + proxyServer := proxy.NewServer() + // AIMA's own native deployment backend (External defaults to false). + proxyServer.RegisterBackend("Qwen3.6-27B-Q4_K_M", &proxy.Backend{ + ModelName: "Qwen3.6-27B-Q4_K_M", + Address: "127.0.0.1:8080", + Ready: true, + }) + + services, err := NewReconciler(db, proxyServer).List(ctx) + if err != nil { + t.Fatalf("List: %v", err) + } + for _, svc := range services { + if svc.BaseURL == "http://127.0.0.1:8080" { + t.Errorf("List returned AIMA's own deployment as external service: %s", svc.BaseURL) + } + } + var sawOllama bool + for _, svc := range services { + if svc.BaseURL == "http://127.0.0.1:11434" { + sawOllama = true + } + } + if !sawOllama { + t.Errorf("genuine external service (ollama) was wrongly excluded; got %d services", len(services)) + } +} From e66375919eb9a7189642a8e765ee267472725c48 Mon Sep 17 00:00:00 2001 From: Codex Date: Tue, 9 Jun 2026 17:25:45 +0800 Subject: [PATCH 2/2] fix(external): drop vanished scanned services from the list A scanned (auto-discovered) external service was upserted when found but never reconciled away when it disappeared. So after a model was undeployed and its backend (e.g. native llama.cpp on :8080) stopped, the dead service lingered as a stale "reachable" row and kept showing under external services. On each scan, mark previously-discovered scanned (non-imported, non-own) services that are no longer reachable as unreachable; List now hides scanned, non-imported, unreachable services. Imported services are untouched (they still show, with their status, by user intent). Tests: staleScannedAddrs (skips imported / already-unreachable / own deployments); List hides a vanished scanned :8080 while keeping a reachable :11434 and an offline imported :9000. Co-Authored-By: Claude Opus 4.8 (1M context) --- internal/external/reconciler.go | 39 ++++++++++++++++- internal/external/reconciler_test.go | 64 ++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+), 1 deletion(-) diff --git a/internal/external/reconciler.go b/internal/external/reconciler.go index 3a8ad50..7b69803 100644 --- a/internal/external/reconciler.go +++ b/internal/external/reconciler.go @@ -85,6 +85,13 @@ func (r *Reconciler) Scan(ctx context.Context) ([]Overview, error) { for _, svc := range out { reachable[svc.BaseURL] = struct{}{} } + // Mark previously-discovered services that have disappeared (e.g. an + // undeployed model's backend) unreachable so they stop being listed. + for _, url := range staleScannedAddrs(existing, reachable, own) { + if err := r.store.SetExternalServiceStatus(ctx, url, "unreachable", "not found in last scan"); err != nil { + slog.Warn("external service scan: failed to mark stale service unreachable", "base_url", url, "error", err) + } + } if err := r.Restore(ctx, reachable); err != nil { slog.Warn("external service scan: failed to restore imported services", "error", err) } @@ -98,7 +105,13 @@ func (r *Reconciler) List(ctx context.Context) ([]Overview, error) { } out := make([]Overview, 0, len(rows)) for _, row := range rows { - out = append(out, OverviewFromRecord(row)) + ov := OverviewFromRecord(row) + // Hide auto-discovered services that have vanished (dead scanned, not + // imported) — e.g. the backend of a model the user just undeployed. + if ov.Source == "scan" && !ov.Imported && strings.EqualFold(ov.Status, "unreachable") { + continue + } + out = append(out, ov) } // Hide AIMA's own deployment backends that a prior scan may have recorded // (e.g. a native llama.cpp deployment on llama.cpp's default port 8080). @@ -474,3 +487,27 @@ func filterOutOwnDeployments(services []Overview, own map[string]struct{}) []Ove } return out } + +// staleScannedAddrs returns base URLs of auto-discovered (scanned, non-imported) +// services that were not seen in the latest scan and are not AIMA's own +// deployments — i.e. services that have disappeared (e.g. a model that was +// undeployed) and should be marked unreachable so they drop out of the list. +func staleScannedAddrs(existing []*state.ExternalService, reachable, own map[string]struct{}) []string { + var stale []string + for _, ex := range existing { + if ex == nil || ex.Imported || ex.Source != "scan" { + continue + } + if strings.EqualFold(ex.Status, "unreachable") { + continue + } + if _, ok := reachable[ex.BaseURL]; ok { + continue + } + if _, ok := own[normalizeHostPort(ex.BaseURL)]; ok { + continue + } + stale = append(stale, ex.BaseURL) + } + return stale +} diff --git a/internal/external/reconciler_test.go b/internal/external/reconciler_test.go index eb303f4..ac81d98 100644 --- a/internal/external/reconciler_test.go +++ b/internal/external/reconciler_test.go @@ -438,3 +438,67 @@ func TestListExcludesOwnDeploymentServices(t *testing.T) { t.Errorf("genuine external service (ollama) was wrongly excluded; got %d services", len(services)) } } + +func TestStaleScannedAddrs(t *testing.T) { + existing := []*state.ExternalService{ + {BaseURL: "http://127.0.0.1:8080", Source: "scan", Status: "reachable"}, // vanished -> stale + {BaseURL: "http://127.0.0.1:11434", Source: "scan", Status: "reachable"}, // still reachable + {BaseURL: "http://127.0.0.1:9000", Source: "scan", Status: "reachable", Imported: true}, // imported -> keep + {BaseURL: "http://127.0.0.1:5000", Source: "scan", Status: "unreachable"}, // already marked -> skip + } + reachable := map[string]struct{}{"http://127.0.0.1:11434": {}} + got := staleScannedAddrs(existing, reachable, map[string]struct{}{}) + if len(got) != 1 || got[0] != "http://127.0.0.1:8080" { + t.Fatalf("got %v, want [http://127.0.0.1:8080]", got) + } +} + +func TestStaleScannedAddrsSkipsOwnDeployment(t *testing.T) { + existing := []*state.ExternalService{{BaseURL: "http://127.0.0.1:8080", Source: "scan", Status: "reachable"}} + own := map[string]struct{}{"127.0.0.1:8080": {}} + if got := staleScannedAddrs(existing, map[string]struct{}{}, own); len(got) != 0 { + t.Errorf("own deployment must not be marked stale, got %v", got) + } +} + +// A scanned service that has died (status unreachable) must drop out of List, +// while a reachable scanned service and an imported (even if offline) one stay. +func TestListHidesVanishedScannedService(t *testing.T) { + ctx := context.Background() + db, err := state.Open(ctx, ":memory:") + if err != nil { + t.Fatalf("Open: %v", err) + } + t.Cleanup(func() { db.Close() }) + + for _, svc := range []*state.ExternalService{ + {ID: "dead", BaseURL: "http://127.0.0.1:8080", Kind: "openai", Status: "unreachable", Source: "scan", ModelsJSON: `["x"]`}, + {ID: "live", BaseURL: "http://127.0.0.1:11434", Kind: "ollama", Status: "reachable", Source: "scan", ModelsJSON: `["y"]`}, + {ID: "imp", BaseURL: "http://127.0.0.1:9000", Kind: "openai", Status: "unreachable", Source: "scan", ModelsJSON: `["z"]`}, + } { + if err := db.UpsertExternalService(ctx, svc); err != nil { + t.Fatalf("Upsert: %v", err) + } + } + if err := db.SetExternalServiceImportedModels(ctx, "http://127.0.0.1:9000", true, []string{"z"}); err != nil { + t.Fatalf("import: %v", err) + } + + services, err := NewReconciler(db, proxy.NewServer()).List(ctx) + if err != nil { + t.Fatalf("List: %v", err) + } + seen := map[string]bool{} + for _, s := range services { + seen[s.BaseURL] = true + } + if seen["http://127.0.0.1:8080"] { + t.Error("vanished scanned service :8080 should be hidden") + } + if !seen["http://127.0.0.1:11434"] { + t.Error("reachable scanned service :11434 should be shown") + } + if !seen["http://127.0.0.1:9000"] { + t.Error("imported service :9000 should be shown even when unreachable") + } +}