Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 58 additions & 16 deletions go/internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,48 @@ import (

// Config is the root of forge's infra config, built by LoadFromStore.
type Config struct {
Server Server
Paths Paths
Slots map[string]Slot
Ports map[string]int // auxiliary services: embedding, stt, ...
Modes map[string]Mode
Scheduler SchedulerDefault
Monitor Monitor
Tailscale Tailscale
Cost Cost
Server Server
Paths Paths
Slots map[string]Slot
Ports map[string]int // auxiliary services: embedding, stt, ...
ServiceIcons map[string]string
Modes map[string]Mode
Scheduler SchedulerDefault
Monitor Monitor
Tailscale Tailscale
Cost Cost
}

// defaultServiceIcons are the fallback vendor-icon manifest slugs (see
// web/src/assets/icons/manifest.ts) for the four fixed infra services
// (STT/Embedding/Aligner/TTS — bare [ports] entries with no catalog-backed
// model metadata, unlike service_mode rows like ComfyUI, which carry their
// own services.icon). Previously a Go literal map in
// httpapi/services_handlers.go (serviceVendorLogoBySlug) — changing which
// icon a service showed meant editing source and shipping a new binary.
// Moved into infra.service_icons (operator feedback 2026-09-06: "bad design
// to need a restart to change an icon") so it's store-backed and
// SIGHUP-reloadable like every other infra.* value, with these as the
// zero-value fallback so nothing changes for anyone who hasn't overridden
// it. The icon names the MODEL, not the company (2026-08-14 precedent) — all
// four fixed services currently run Qwen models.
var defaultServiceIcons = map[string]string{
"STT": "qwen",
"Embedding": "qwen",
"Aligner": "qwen",
"TTS": "qwen",
}

// DefaultServiceIcons returns a copy of the fallback vendor-icon map (see
// defaultServiceIcons above) for callers outside this package — httpapi's
// GET /api/v1/service-icons overlays these under any operator override
// without exposing the shared package-level map itself for mutation.
func DefaultServiceIcons() map[string]string {
out := make(map[string]string, len(defaultServiceIcons))
for k, v := range defaultServiceIcons {
out[k] = v
}
return out
}

// Server holds listen addresses and the state-db location. Canonical V4
Expand Down Expand Up @@ -336,13 +369,14 @@ func LoadFromStore(ctx context.Context, st store.Store) (*Config, error) {
settings := st.Settings()

for key, dst := range map[string]any{
"infra.server": &cfg.Server,
"infra.paths": &cfg.Paths,
"infra.ports": &cfg.Ports,
"infra.scheduler": &cfg.Scheduler,
"infra.monitor": &cfg.Monitor,
"infra.tailscale": &cfg.Tailscale,
"infra.cost": &cfg.Cost,
"infra.server": &cfg.Server,
"infra.paths": &cfg.Paths,
"infra.ports": &cfg.Ports,
"infra.service_icons": &cfg.ServiceIcons,
"infra.scheduler": &cfg.Scheduler,
"infra.monitor": &cfg.Monitor,
"infra.tailscale": &cfg.Tailscale,
"infra.cost": &cfg.Cost,
} {
if err := getSetting(ctx, settings, key, dst); err != nil {
return nil, err
Expand Down Expand Up @@ -446,6 +480,14 @@ func (c *Config) applyDefaults() {
if c.Cost.MaxPowerW <= 0 {
c.Cost.MaxPowerW = DefaultMaxPowerW
}
if c.ServiceIcons == nil {
c.ServiceIcons = map[string]string{}
}
for name, icon := range defaultServiceIcons {
if _, ok := c.ServiceIcons[name]; !ok {
c.ServiceIcons[name] = icon
}
}
for name, svc := range c.allServices() {
if svc.Backend == "" {
svc.Backend = "vulkan"
Expand Down
47 changes: 46 additions & 1 deletion go/internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ func sampleConfig() Config {
Paths: Paths{ModelsDir: "/opt/forge/models"},
Slots: map[string]Slot{
"a1": {Unit: "forge-a1", Port: 8080, Label: "A1", Order: 1},
"a3": {Unit: "forge-a3", Port: 8087, Label: "A3", Order: 3},
"a3": {Unit: "forge-a3", Port: 8087, Label: "A3", Order: 3},
},
Ports: map[string]int{"embedding": 8083, "stt": 8084},
Modes: map[string]Mode{
Expand Down Expand Up @@ -242,6 +242,51 @@ func TestLoadFromStoreEmpty(t *testing.T) {
}
}

// TestServiceIconsDefaultsAndOverride covers the fix for "changing a fixed
// infra service's icon needs a rebuild+restart" (operator feedback
// 2026-09-06): defaultServiceIcons fills in for any key the operator hasn't
// overridden, and applyDefaults must not clobber a real override — the same
// per-key-fill contract as infra.ports would need if it ever gained a
// similar default map.
func TestServiceIconsDefaultsAndOverride(t *testing.T) {
db, err := store.Open(":memory:")
if err != nil {
t.Fatalf("store.Open: %v", err)
}
defer db.Close()
ctx := context.Background()

cfg, err := LoadFromStore(ctx, db)
if err != nil {
t.Fatalf("LoadFromStore: %v", err)
}
for name, want := range defaultServiceIcons {
if got := cfg.ServiceIcons[name]; got != want {
t.Errorf("ServiceIcons[%q] = %q, want default %q", name, got, want)
}
}

raw, err := json.Marshal(map[string]string{"STT": "nvidia"})
if err != nil {
t.Fatalf("marshal override: %v", err)
}
if err := db.Settings().Set(ctx, "infra.service_icons", raw); err != nil {
t.Fatalf("Settings.Set: %v", err)
}

cfg2, err := LoadFromStore(ctx, db)
if err != nil {
t.Fatalf("LoadFromStore (with override): %v", err)
}
if cfg2.ServiceIcons["STT"] != "nvidia" {
t.Errorf("ServiceIcons[STT] = %q, want operator override %q", cfg2.ServiceIcons["STT"], "nvidia")
}
if cfg2.ServiceIcons["Embedding"] != "qwen" {
t.Errorf("ServiceIcons[Embedding] = %q, want default %q (override of one key must not drop the others)",
cfg2.ServiceIcons["Embedding"], "qwen")
}
}

// TestLoadFromStorePopulated exercises the real path: infra.* settings keys
// set and slots rows present, mirroring what the Phase 2 cutover migration
// will write.
Expand Down
Loading
Loading