mirror of
https://github.com/mudler/LocalAI.git
synced 2026-05-17 13:10:23 -04:00
Adds a whitelabeling feature so an operator can replace the LocalAI
instance name, tagline, square logo, horizontal logo, and favicon from
the admin Settings page. Defaults fall back to the bundled assets so
existing installs are unaffected.
The public GET /api/branding endpoint is reachable pre-auth so the
login screen can render the configured branding before sign-in.
Mutating routes (POST/DELETE /api/branding/asset/:kind) remain
admin-only. Text fields (instance_name, instance_tagline) ride the
existing /api/settings flow; binary assets get a dedicated multipart
upload route that persists files under DynamicConfigsDir/branding/.
To prevent the Settings page's stale local state from clobbering an
upload on save, UpdateSettingsEndpoint preserves whatever the on-disk
asset filename fields are regardless of the body — /api/branding/asset/*
are the sole writers for those fields.
The MCP catalog gains get_branding and set_branding tools (text fields
only; file upload stays UI-only) plus a configure_branding skill prompt.
While wiring this up, the same restart-loss class of bug surfaced for
several existing fields whose RuntimeSettings entries were never read
by the startup loader. Fix loadRuntimeSettingsFromFile() to load:
- branding (instance_name, instance_tagline, *_file basenames)
- auto_upgrade_backends, prefer_development_backends
- localai_assistant_enabled
- open_responses_store_ttl
- the 7 existing AgentPool fields (enabled, default/embedding model,
chunking sizes, enable_logs, collection_db_path)
Also exposes 3 new AgentPool runtime settings (vector_engine,
database_url, agent_hub_url) via /api/settings + the Settings UI, with
the same load-on-startup wiring. The file watcher's manual-edit path
is intentionally not changed — the in-process API endpoints already
update appConfig directly, so the watcher is redundant for supported
flows and a separate refactor for everything else.
15 TDD specs cover the loader behaviour (1 branding + 11 adjacent + 3
new agent-pool); 2 specs cover the persistence helpers and the
clobber-prevention contract.
Assisted-by: claude-code:claude-opus-4-7
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
92 lines
3.9 KiB
Go
92 lines
3.9 KiB
Go
package localaitools
|
|
|
|
import (
|
|
. "github.com/onsi/ginkgo/v2"
|
|
. "github.com/onsi/gomega"
|
|
)
|
|
|
|
// toolToHTTPRoute is the canonical mapping between MCP tools and the
|
|
// LocalAI admin REST endpoints they wrap. The httpapi.Client MUST hit the
|
|
// listed route for the tool; the inproc.Client may bypass HTTP and call
|
|
// services directly, but the on-the-wire shape is documented here so the
|
|
// two sides stay aligned.
|
|
//
|
|
// Updating the map is REQUIRED when:
|
|
// - You add a Tool* constant (tools.go).
|
|
// - You change which REST endpoint the httpapi.Client calls.
|
|
//
|
|
// The TestToolHTTPRouteMappingComplete spec below FAILS until every Tool*
|
|
// is in the map. That is the drift detector — see
|
|
// .agents/localai-assistant-mcp.md for the contributor contract.
|
|
//
|
|
// "(none)" is a deliberate sentinel for tools whose data is not exposed
|
|
// over a single REST endpoint (e.g. system_info aggregates data the
|
|
// inproc client picks up directly from services). The httpapi.Client may
|
|
// approximate via the welcome JSON; the test still requires an entry so
|
|
// the contributor explicitly acknowledges the asymmetry.
|
|
var toolToHTTPRoute = map[string]string{
|
|
// Read-only tools.
|
|
ToolGallerySearch: "GET /models/available",
|
|
ToolListInstalledModels: "GET / (welcome JSON, ModelsConfig field)",
|
|
ToolListGalleries: "GET /models/galleries",
|
|
ToolGetJobStatus: "GET /models/jobs/:uuid",
|
|
ToolGetModelConfig: "(none) — no JSON-only REST yet; httpapi.Client returns a documented stub",
|
|
ToolListBackends: "GET /backends",
|
|
ToolListKnownBackends: "GET /backends/known",
|
|
ToolSystemInfo: "GET / (welcome JSON)",
|
|
ToolListNodes: "GET /api/nodes",
|
|
ToolVRAMEstimate: "POST /api/models/vram-estimate",
|
|
ToolGetBranding: "GET /api/branding",
|
|
|
|
// Mutating tools.
|
|
ToolInstallModel: "POST /models/apply",
|
|
ToolImportModelURI: "POST /models/import-uri",
|
|
ToolDeleteModel: "POST /models/delete/:name",
|
|
ToolEditModelConfig: "PATCH /api/models/config-json/:name",
|
|
ToolReloadModels: "POST /models/reload",
|
|
ToolInstallBackend: "POST /backends/apply",
|
|
ToolUpgradeBackend: "POST /backends/upgrade/:name",
|
|
ToolToggleModelState: "PUT /models/toggle-state/:name/:action",
|
|
ToolToggleModelPinned: "PUT /models/toggle-pinned/:name/:action",
|
|
ToolSetBranding: "POST /api/settings (instance_name, instance_tagline)",
|
|
}
|
|
|
|
// allKnownTools is the union of expectedFullCatalog (defined in
|
|
// server_test.go). Keeping a single source of truth — the slice from
|
|
// server_test — and asserting the route map covers every entry catches
|
|
// the case "you added a Tool* but forgot to register it as MCP" indirectly
|
|
// (it'd be missing from expectedFullCatalog, which has its own assertion
|
|
// in TestServerRegistersExpectedToolCatalog).
|
|
var _ = Describe("Tool ↔ HTTP route coverage map", func() {
|
|
It("has an entry for every Tool* in the published catalog", func() {
|
|
for _, name := range expectedFullCatalog {
|
|
_, ok := toolToHTTPRoute[name]
|
|
Expect(ok).To(BeTrue(),
|
|
"Tool %q is in expectedFullCatalog but not in toolToHTTPRoute. "+
|
|
"When adding an MCP tool, update toolToHTTPRoute in coverage_test.go "+
|
|
"with the REST endpoint the httpapi.Client calls (or '(none)' with a reason).",
|
|
name)
|
|
}
|
|
})
|
|
|
|
It("does not document tools that no longer exist in the catalog", func() {
|
|
catalog := map[string]struct{}{}
|
|
for _, name := range expectedFullCatalog {
|
|
catalog[name] = struct{}{}
|
|
}
|
|
for name := range toolToHTTPRoute {
|
|
_, ok := catalog[name]
|
|
Expect(ok).To(BeTrue(),
|
|
"toolToHTTPRoute documents %q but the tool is not registered. "+
|
|
"Remove the stale entry.",
|
|
name)
|
|
}
|
|
})
|
|
|
|
// Deliberate non-test: we don't enumerate admin REST routes here. That
|
|
// would require booting Application or parsing core/http/routes/localai.go,
|
|
// both of which are brittle. The contract for "new admin REST endpoint
|
|
// → MCP tool" is enforced by the PR checklist in
|
|
// .agents/api-endpoints-and-auth.md, not by this test.
|
|
})
|