mirror of
https://github.com/mudler/LocalAI.git
synced 2026-10-28 00:59:28 -04:00
feat(mcp): install a backend from a library entry
The backend library route installs a backend from an add-on library entry, and the admin REST surface and the MCP catalog must stay in step. Add the install_library_backend tool with an in-process and an HTTP client, the route map entry, the safety-rule listing and a skill recipe. Listing library entries stays on the add-on routes, which the assistant does not expose, so no list tool is added. Assisted-by: Claude Code:claude-sonnet-5-5 Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
This commit is contained in:
16 files changed
+226
-106
No files matched your search
@@ -116,6 +116,12 @@ example Tables) follow a different, code-enforced rule, documented in
|
||||
`tools.Consumers` (`core/services/addons/tools/consumers.go`) as route or
|
||||
reject. The matrix test scans the sources and fails for an unclassified file.
|
||||
|
||||
One backend route sits outside the add-on surface: `POST /backends/library/install`
|
||||
installs a backend (not an add-on) from a library entry. It is exposed as the
|
||||
`install_library_backend` tool and is a plain admin mutation under safety
|
||||
rule 1. Listing library entries stays on `/api/addons` and has no assistant
|
||||
tool, for the reason above.
|
||||
|
||||
Do not rewrite the unrelated assistant tools to match this rule.
|
||||
|
||||
## Distributed mode
|
||||
|
||||
@@ -681,6 +681,9 @@ func (a *Application) start() (startErr error) {
|
||||
assistantClient.StatsRecorder = a.statsRecorder
|
||||
assistantClient.FallbackUser = a.fallbackUser
|
||||
assistantClient.VoiceProfiles = a.voiceProfileStore
|
||||
if a.backendLibrary != nil {
|
||||
assistantClient.BackendLibrary = a.backendLibrary
|
||||
}
|
||||
if a.distributed != nil && a.distributed.Unloader != nil {
|
||||
assistantClient.LoadStopper = a.distributed.Unloader
|
||||
}
|
||||
|
||||
@@ -62,6 +62,9 @@ type LocalAIClient interface {
|
||||
// ListKnownBackends returns the same shape as REST /backends/known.
|
||||
ListKnownBackends(ctx context.Context) ([]schema.KnownBackend, error)
|
||||
InstallBackend(ctx context.Context, req InstallBackendRequest) (jobID string, err error)
|
||||
// InstallLibraryBackend installs a backend from a library entry. It
|
||||
// returns when the install is done.
|
||||
InstallLibraryBackend(ctx context.Context, req InstallLibraryBackendRequest) (*InstallLibraryBackendResult, error)
|
||||
UpgradeBackend(ctx context.Context, name string) (jobID string, err error)
|
||||
|
||||
// ---- System ----
|
||||
|
||||
@@ -49,28 +49,29 @@ var toolToHTTPRoute = map[string]string{
|
||||
ToolListFailoverChains: "GET /api/failover",
|
||||
|
||||
// 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",
|
||||
ToolCancelModelLoad: "POST /api/models/:id/load-cancel",
|
||||
ToolLoadModel: "POST /backend/load",
|
||||
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)",
|
||||
ToolSetAlias: "PATCH /api/models/config-json/:name (swap) or POST /models/import (create)",
|
||||
ToolSeedRouterCorpus: "POST /api/router/:name/corpus",
|
||||
ToolClearRouterCorpus: "DELETE /api/router/:name/corpus",
|
||||
ToolCreateVoiceProfile: "POST /api/voice-profiles",
|
||||
ToolDeleteVoiceProfile: "DELETE /api/voice-profiles/:id",
|
||||
ToolSetNodeVRAMBudget: "PUT /api/nodes/:id/vram-budget",
|
||||
ToolSetScheduling: "POST /api/nodes/scheduling",
|
||||
ToolDeleteScheduling: "DELETE /api/nodes/scheduling/:model",
|
||||
ToolPinFailoverTarget: "POST /api/failover/:chain/pin",
|
||||
ToolUnpinFailoverTarget: "DELETE /api/failover/:chain/pin",
|
||||
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",
|
||||
ToolCancelModelLoad: "POST /api/models/:id/load-cancel",
|
||||
ToolLoadModel: "POST /backend/load",
|
||||
ToolInstallBackend: "POST /backends/apply",
|
||||
ToolInstallLibraryBackend: "POST /backends/library/install",
|
||||
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)",
|
||||
ToolSetAlias: "PATCH /api/models/config-json/:name (swap) or POST /models/import (create)",
|
||||
ToolSeedRouterCorpus: "POST /api/router/:name/corpus",
|
||||
ToolClearRouterCorpus: "DELETE /api/router/:name/corpus",
|
||||
ToolCreateVoiceProfile: "POST /api/voice-profiles",
|
||||
ToolDeleteVoiceProfile: "DELETE /api/voice-profiles/:id",
|
||||
ToolSetNodeVRAMBudget: "PUT /api/nodes/:id/vram-budget",
|
||||
ToolSetScheduling: "POST /api/nodes/scheduling",
|
||||
ToolDeleteScheduling: "DELETE /api/nodes/scheduling/:model",
|
||||
ToolPinFailoverTarget: "POST /api/failover/:chain/pin",
|
||||
ToolUnpinFailoverTarget: "DELETE /api/failover/:chain/pin",
|
||||
}
|
||||
|
||||
// allKnownTools is the union of expectedFullCatalog (defined in
|
||||
|
||||
@@ -76,6 +76,21 @@ type InstallBackendRequest struct {
|
||||
BackendName string `json:"backend_name" jsonschema:"Backend identifier (e.g. llama-cpp)."`
|
||||
}
|
||||
|
||||
// InstallLibraryBackendRequest is the input for install_library_backend.
|
||||
type InstallLibraryBackendRequest struct {
|
||||
SourceID string `json:"source_id" jsonschema:"The library the entry comes from, as shown in the Add-ons library list."`
|
||||
Name string `json:"name" jsonschema:"The backend name of the library entry."`
|
||||
Version string `json:"version" jsonschema:"The version or the channel of the library entry."`
|
||||
}
|
||||
|
||||
// InstallLibraryBackendResult is the output of install_library_backend. The
|
||||
// install runs to the end before the call returns, so there is no job id.
|
||||
type InstallLibraryBackendResult struct {
|
||||
Status string `json:"status"`
|
||||
Name string `json:"name"`
|
||||
SourceID string `json:"source_id"`
|
||||
}
|
||||
|
||||
// Backend is the LLM-facing summary returned by list_backends. We don't
|
||||
// expose gallery.SystemBackend directly because it carries filesystem
|
||||
// paths (RunFile, IsSystem, IsMeta, the full Metadata) the LLM doesn't
|
||||
|
||||
@@ -23,45 +23,46 @@ type fakeClient struct {
|
||||
calls []fakeCall
|
||||
|
||||
// Per-method overrides. Tests set these.
|
||||
gallerySearch func(GallerySearchQuery) ([]gallery.Metadata, error)
|
||||
listInstalledModels func(Capability) ([]InstalledModel, error)
|
||||
listGalleries func() ([]config.Gallery, error)
|
||||
getJobStatus func(string) (*JobStatus, error)
|
||||
getModelConfig func(string) (*ModelConfigView, error)
|
||||
installModel func(InstallModelRequest) (string, error)
|
||||
importModelURI func(ImportModelURIRequest) (*ImportModelURIResponse, error)
|
||||
deleteModel func(string) error
|
||||
editModelConfig func(string, map[string]any) error
|
||||
setAlias func(string, string) error
|
||||
listAliases func() ([]AliasInfo, error)
|
||||
reloadModels func() error
|
||||
loadModel func(string) ([]string, error)
|
||||
listBackends func() ([]Backend, error)
|
||||
listKnownBackends func() ([]schema.KnownBackend, error)
|
||||
installBackend func(InstallBackendRequest) (string, error)
|
||||
upgradeBackend func(string) (string, error)
|
||||
systemInfo func() (*SystemInfo, error)
|
||||
listNodes func() ([]Node, error)
|
||||
listScheduling func() ([]ModelSchedulingConfig, error)
|
||||
getScheduling func(string) (*ModelSchedulingConfig, error)
|
||||
setScheduling func(SetSchedulingRequest) (*ModelSchedulingConfig, error)
|
||||
deleteScheduling func(string) error
|
||||
setNodeVRAMBudget func(string, string) error
|
||||
vramEstimate func(VRAMEstimateRequest) (*vram.EstimateResult, error)
|
||||
toggleModelState func(string, modeladmin.Action) error
|
||||
toggleModelPinned func(string, modeladmin.Action) error
|
||||
getBranding func() (*Branding, error)
|
||||
setBranding func(SetBrandingRequest) (*Branding, error)
|
||||
listVoiceProfiles func() ([]VoiceProfile, error)
|
||||
createVoiceProfile func(CreateVoiceProfileRequest) (*VoiceProfile, error)
|
||||
deleteVoiceProfile func(string) error
|
||||
getUsageStats func(UsageStatsQuery) (*UsageStats, error)
|
||||
getPIIEvents func(PIIEventsQuery) ([]PIIEvent, error)
|
||||
getMiddlewareStatus func() (*MiddlewareStatus, error)
|
||||
getRouterDecisions func(RouterDecisionsQuery) ([]RouterDecision, error)
|
||||
listFailoverChains func() ([]FailoverChainInfo, error)
|
||||
pinFailoverTarget func(string, string) error
|
||||
unpinFailoverTarget func(string) error
|
||||
gallerySearch func(GallerySearchQuery) ([]gallery.Metadata, error)
|
||||
listInstalledModels func(Capability) ([]InstalledModel, error)
|
||||
listGalleries func() ([]config.Gallery, error)
|
||||
getJobStatus func(string) (*JobStatus, error)
|
||||
getModelConfig func(string) (*ModelConfigView, error)
|
||||
installModel func(InstallModelRequest) (string, error)
|
||||
importModelURI func(ImportModelURIRequest) (*ImportModelURIResponse, error)
|
||||
deleteModel func(string) error
|
||||
editModelConfig func(string, map[string]any) error
|
||||
setAlias func(string, string) error
|
||||
listAliases func() ([]AliasInfo, error)
|
||||
reloadModels func() error
|
||||
loadModel func(string) ([]string, error)
|
||||
listBackends func() ([]Backend, error)
|
||||
listKnownBackends func() ([]schema.KnownBackend, error)
|
||||
installBackend func(InstallBackendRequest) (string, error)
|
||||
installLibraryBackend func(InstallLibraryBackendRequest) (*InstallLibraryBackendResult, error)
|
||||
upgradeBackend func(string) (string, error)
|
||||
systemInfo func() (*SystemInfo, error)
|
||||
listNodes func() ([]Node, error)
|
||||
listScheduling func() ([]ModelSchedulingConfig, error)
|
||||
getScheduling func(string) (*ModelSchedulingConfig, error)
|
||||
setScheduling func(SetSchedulingRequest) (*ModelSchedulingConfig, error)
|
||||
deleteScheduling func(string) error
|
||||
setNodeVRAMBudget func(string, string) error
|
||||
vramEstimate func(VRAMEstimateRequest) (*vram.EstimateResult, error)
|
||||
toggleModelState func(string, modeladmin.Action) error
|
||||
toggleModelPinned func(string, modeladmin.Action) error
|
||||
getBranding func() (*Branding, error)
|
||||
setBranding func(SetBrandingRequest) (*Branding, error)
|
||||
listVoiceProfiles func() ([]VoiceProfile, error)
|
||||
createVoiceProfile func(CreateVoiceProfileRequest) (*VoiceProfile, error)
|
||||
deleteVoiceProfile func(string) error
|
||||
getUsageStats func(UsageStatsQuery) (*UsageStats, error)
|
||||
getPIIEvents func(PIIEventsQuery) ([]PIIEvent, error)
|
||||
getMiddlewareStatus func() (*MiddlewareStatus, error)
|
||||
getRouterDecisions func(RouterDecisionsQuery) ([]RouterDecision, error)
|
||||
listFailoverChains func() ([]FailoverChainInfo, error)
|
||||
pinFailoverTarget func(string, string) error
|
||||
unpinFailoverTarget func(string) error
|
||||
}
|
||||
|
||||
type fakeCall struct {
|
||||
@@ -218,6 +219,14 @@ func (f *fakeClient) InstallBackend(_ context.Context, req InstallBackendRequest
|
||||
return "", errNotConfigured
|
||||
}
|
||||
|
||||
func (f *fakeClient) InstallLibraryBackend(_ context.Context, req InstallLibraryBackendRequest) (*InstallLibraryBackendResult, error) {
|
||||
f.record("InstallLibraryBackend", req)
|
||||
if f.installLibraryBackend != nil {
|
||||
return f.installLibraryBackend(req)
|
||||
}
|
||||
return nil, errNotConfigured
|
||||
}
|
||||
|
||||
func (f *fakeClient) UpgradeBackend(_ context.Context, name string) (string, error) {
|
||||
f.record("UpgradeBackend", name)
|
||||
if f.upgradeBackend != nil {
|
||||
|
||||
@@ -428,6 +428,19 @@ func (c *Client) InstallBackend(ctx context.Context, req localaitools.InstallBac
|
||||
return resp.ID, nil
|
||||
}
|
||||
|
||||
func (c *Client) InstallLibraryBackend(ctx context.Context, req localaitools.InstallLibraryBackendRequest) (*localaitools.InstallLibraryBackendResult, error) {
|
||||
body := map[string]any{"sourceId": req.SourceID, "name": req.Name, "version": req.Version}
|
||||
var resp struct {
|
||||
Status string `json:"status"`
|
||||
Name string `json:"name"`
|
||||
SourceID string `json:"sourceId"`
|
||||
}
|
||||
if err := c.do(ctx, http.MethodPost, routeBackendsLibraryInstall, body, &resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &localaitools.InstallLibraryBackendResult{Status: resp.Status, Name: resp.Name, SourceID: resp.SourceID}, nil
|
||||
}
|
||||
|
||||
func (c *Client) UpgradeBackend(ctx context.Context, name string) (string, error) {
|
||||
var resp struct {
|
||||
ID string `json:"uuid"`
|
||||
|
||||
@@ -482,3 +482,22 @@ var _ = Describe("Bearer token", func() {
|
||||
Expect(sawAuth).To(Equal("Bearer secret-key"))
|
||||
})
|
||||
})
|
||||
|
||||
var _ = Describe("InstallLibraryBackend", func() {
|
||||
It("posts the library entry to the backend library route", func() {
|
||||
var path string
|
||||
var body map[string]any
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
path = r.Method + " " + r.URL.Path
|
||||
Expect(json.NewDecoder(r.Body).Decode(&body)).To(Succeed())
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"status": "installed", "name": "demo", "sourceId": "official"})
|
||||
}))
|
||||
DeferCleanup(srv.Close)
|
||||
|
||||
res, err := New(srv.URL, "").InstallLibraryBackend(context.Background(), localaitools.InstallLibraryBackendRequest{SourceID: "official", Name: "demo", Version: "1.0.0"})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(path).To(Equal("POST /backends/library/install"))
|
||||
Expect(body).To(Equal(map[string]any{"sourceId": "official", "name": "demo", "version": "1.0.0"}))
|
||||
Expect(res.Status).To(Equal("installed"))
|
||||
})
|
||||
})
|
||||
@@ -11,30 +11,31 @@ import (
|
||||
// registrations in core/http/routes/localai.go — the Tool↔REST drift detector
|
||||
// in coverage_test.go documents the mapping.
|
||||
const (
|
||||
routeWelcome = "/"
|
||||
routeModelsApply = "/models/apply"
|
||||
routeModelsAvail = "/models/available"
|
||||
routeModelsGall = "/models/galleries"
|
||||
routeModelsImport = "/models/import-uri"
|
||||
routeModelImport = "/models/import"
|
||||
routeAliases = "/api/aliases"
|
||||
routeModelsReload = "/models/reload"
|
||||
routeBackendLoad = "/backend/load"
|
||||
routeBackends = "/backends"
|
||||
routeBackendsKnown = "/backends/known"
|
||||
routeBackendsApply = "/backends/apply"
|
||||
routeNodes = "/api/nodes"
|
||||
routeScheduling = "/api/nodes/scheduling"
|
||||
routeVRAMEstimate = "/api/models/vram-estimate"
|
||||
routeBranding = "/api/branding"
|
||||
routeSettings = "/api/settings"
|
||||
routeUsage = "/api/usage"
|
||||
routeUsageAll = "/api/usage/all"
|
||||
routePIIEvents = "/api/pii/events"
|
||||
routeMiddleware = "/api/middleware/status"
|
||||
routeRouterDecisions = "/api/router/decisions"
|
||||
routeVoiceProfiles = "/api/voice-profiles"
|
||||
routeFailover = "/api/failover"
|
||||
routeWelcome = "/"
|
||||
routeModelsApply = "/models/apply"
|
||||
routeModelsAvail = "/models/available"
|
||||
routeModelsGall = "/models/galleries"
|
||||
routeModelsImport = "/models/import-uri"
|
||||
routeModelImport = "/models/import"
|
||||
routeAliases = "/api/aliases"
|
||||
routeModelsReload = "/models/reload"
|
||||
routeBackendLoad = "/backend/load"
|
||||
routeBackends = "/backends"
|
||||
routeBackendsKnown = "/backends/known"
|
||||
routeBackendsApply = "/backends/apply"
|
||||
routeBackendsLibraryInstall = "/backends/library/install"
|
||||
routeNodes = "/api/nodes"
|
||||
routeScheduling = "/api/nodes/scheduling"
|
||||
routeVRAMEstimate = "/api/models/vram-estimate"
|
||||
routeBranding = "/api/branding"
|
||||
routeSettings = "/api/settings"
|
||||
routeUsage = "/api/usage"
|
||||
routeUsageAll = "/api/usage/all"
|
||||
routePIIEvents = "/api/pii/events"
|
||||
routeMiddleware = "/api/middleware/status"
|
||||
routeRouterDecisions = "/api/router/decisions"
|
||||
routeVoiceProfiles = "/api/voice-profiles"
|
||||
routeFailover = "/api/failover"
|
||||
)
|
||||
|
||||
func routeJobStatus(jobID string) string {
|
||||
|
||||
@@ -56,6 +56,9 @@ type Client struct {
|
||||
// stopper the HTTP endpoint uses, so both paths stop work the same way.
|
||||
LoadStopper nodes.LoadAttemptStopper
|
||||
VoiceProfiles *voiceprofile.Store
|
||||
// BackendLibrary backs install_library_backend. nil when the add-on
|
||||
// library is off; the tool then reports it is not enabled.
|
||||
BackendLibrary LibraryBackendInstaller
|
||||
|
||||
// StatsRecorder and FallbackUser are optional — they back the
|
||||
// get_usage_stats tool. nil StatsRecorder makes the tool return an
|
||||
@@ -478,6 +481,25 @@ func (c *Client) InstallBackend(ctx context.Context, req localaitools.InstallBac
|
||||
return id.String(), nil
|
||||
}
|
||||
|
||||
// LibraryBackendInstaller installs a backend from a library entry. It is the
|
||||
// installer the REST route uses, so both paths share one code path.
|
||||
type LibraryBackendInstaller interface {
|
||||
Install(ctx context.Context, sourceID, backend, version string, status func(string, string, string, float64)) error
|
||||
}
|
||||
|
||||
func (c *Client) InstallLibraryBackend(ctx context.Context, req localaitools.InstallLibraryBackendRequest) (*localaitools.InstallLibraryBackendResult, error) {
|
||||
if req.SourceID == "" || req.Name == "" || req.Version == "" {
|
||||
return nil, errors.New("source_id, name and version are required")
|
||||
}
|
||||
if c.BackendLibrary == nil {
|
||||
return nil, errors.New("the backend library is not enabled on this instance")
|
||||
}
|
||||
if err := c.BackendLibrary.Install(ctx, req.SourceID, req.Name, req.Version, nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &localaitools.InstallLibraryBackendResult{Status: "installed", Name: req.Name, SourceID: req.SourceID}, nil
|
||||
}
|
||||
|
||||
func (c *Client) UpgradeBackend(ctx context.Context, name string) (string, error) {
|
||||
if name == "" {
|
||||
return "", errors.New("name is required")
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
These rules are non-negotiable. The user trusts you to operate their server without unintended changes.
|
||||
|
||||
1. **Confirm before mutating.** Before calling any of these tools — `install_model`, `import_model_uri`, `delete_model`, `install_backend`, `upgrade_backend`, `edit_model_config`, `reload_models`, `load_model`, `cancel_model_load`, `toggle_model_state`, `toggle_model_pinned`, `set_branding`, `set_alias`, `seed_router_corpus`, `clear_router_corpus`, `create_voice_profile`, `delete_voice_profile`, `set_node_vram_budget`, `set_scheduling`, `delete_scheduling`, `pin_failover_target`, `unpin_failover_target` — first state in plain language what you are about to do (which tool, which target, which arguments) and wait for the user's explicit confirmation in the next turn. "Yes", "do it", "go ahead", "proceed" all count as confirmation. Anything else does not.
|
||||
1. **Confirm before mutating.** Before calling any of these tools — `install_model`, `import_model_uri`, `delete_model`, `install_backend`, `install_library_backend`, `upgrade_backend`, `edit_model_config`, `reload_models`, `load_model`, `cancel_model_load`, `toggle_model_state`, `toggle_model_pinned`, `set_branding`, `set_alias`, `seed_router_corpus`, `clear_router_corpus`, `create_voice_profile`, `delete_voice_profile`, `set_node_vram_budget`, `set_scheduling`, `delete_scheduling`, `pin_failover_target`, `unpin_failover_target` — first state in plain language what you are about to do (which tool, which target, which arguments) and wait for the user's explicit confirmation in the next turn. "Yes", "do it", "go ahead", "proceed" all count as confirmation. Anything else does not.
|
||||
|
||||
2. **Disambiguate before mutating.** If the user's request is ambiguous (several gallery candidates match, the model name has multiple installed versions, the backend has variants), present the candidates as a numbered list and ask the user to pick before calling any mutating tool.
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ The MCP `tools/list` endpoint also exposes the full input schema for each of the
|
||||
- `import_model_uri` — Install a model from an arbitrary URI (HuggingFace, OCI, http(s), file://). May return `ambiguous_backend` when several backends apply; call again with `backend_preference` to disambiguate.
|
||||
- `delete_model` — Delete an installed model.
|
||||
- `install_backend` — Install a backend.
|
||||
- `install_library_backend` — Install a backend from an add-on library entry by `source_id`, `name` and `version`. The call returns when the install is done, with no job id. The backend gallery is not changed.
|
||||
- `upgrade_backend` — Upgrade an installed backend by name.
|
||||
- `edit_model_config` — Patch (deep-merge) JSON into an installed model's config.
|
||||
- `reload_models` — Reload all model configs from disk.
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
# Skill: Install a backend from the add-on library
|
||||
|
||||
1. The user names a library backend, or asks for one that the backend gallery does not have. You need the `source_id` (the library), the backend `name` and the `version` or channel, as the Add-ons library list shows them. Ask the user for any of the three that is missing. Do not guess them.
|
||||
2. Tell the user that the backend runs native code, that the backend gallery is not changed, and that the library checks the signature of the image.
|
||||
3. Wait for explicit confirmation, then call `install_library_backend` with `source_id`, `name` and `version`. The call returns when the install is done.
|
||||
4. On an error, report the reason as it is. A name installed from another origin, a failed integrity check and an image over the size limit are refusals of the host. Do not retry with other values.
|
||||
5. After a success, call `list_backends` to confirm the backend is installed.
|
||||
@@ -146,6 +146,7 @@ var _ = Describe("Tool dispatch", func() {
|
||||
{ToolImportModelURI, ImportModelURIRequest{URI: "Qwen/Qwen3-4B-GGUF"}, "ImportModelURI"},
|
||||
{ToolDeleteModel, map[string]any{"name": "foo"}, "DeleteModel"},
|
||||
{ToolInstallBackend, InstallBackendRequest{BackendName: "llama-cpp"}, "InstallBackend"},
|
||||
{ToolInstallLibraryBackend, InstallLibraryBackendRequest{SourceID: "official", Name: "demo", Version: "1.0.0"}, "InstallLibraryBackend"},
|
||||
{ToolUpgradeBackend, map[string]any{"name": "llama-cpp"}, "UpgradeBackend"},
|
||||
{ToolEditModelConfig, map[string]any{"name": "foo", "patch": map[string]any{"context_size": 4096}}, "EditModelConfig"},
|
||||
{ToolReloadModels, struct{}{}, "ReloadModels"},
|
||||
@@ -169,6 +170,9 @@ var _ = Describe("Tool dispatch", func() {
|
||||
installModel: func(InstallModelRequest) (string, error) { return "job-1", nil },
|
||||
installBackend: func(InstallBackendRequest) (string, error) { return "job-2", nil },
|
||||
upgradeBackend: func(string) (string, error) { return "job-3", nil },
|
||||
installLibraryBackend: func(InstallLibraryBackendRequest) (*InstallLibraryBackendResult, error) {
|
||||
return &InstallLibraryBackendResult{Status: "installed"}, nil
|
||||
},
|
||||
}
|
||||
ctx, sess, done := connectInMemory(fc, Options{})
|
||||
DeferCleanup(done)
|
||||
|
||||
@@ -30,26 +30,27 @@ const (
|
||||
|
||||
// Mutating tools — guarded by Options.DisableMutating and the
|
||||
// LLM-side safety prompt (see prompts/10_safety.md).
|
||||
ToolInstallModel = "install_model"
|
||||
ToolImportModelURI = "import_model_uri"
|
||||
ToolDeleteModel = "delete_model"
|
||||
ToolEditModelConfig = "edit_model_config"
|
||||
ToolReloadModels = "reload_models"
|
||||
ToolCancelModelLoad = "cancel_model_load"
|
||||
ToolLoadModel = "load_model"
|
||||
ToolInstallBackend = "install_backend"
|
||||
ToolUpgradeBackend = "upgrade_backend"
|
||||
ToolToggleModelState = "toggle_model_state"
|
||||
ToolToggleModelPinned = "toggle_model_pinned"
|
||||
ToolSetBranding = "set_branding"
|
||||
ToolSetAlias = "set_alias"
|
||||
ToolSeedRouterCorpus = "seed_router_corpus"
|
||||
ToolClearRouterCorpus = "clear_router_corpus"
|
||||
ToolCreateVoiceProfile = "create_voice_profile"
|
||||
ToolDeleteVoiceProfile = "delete_voice_profile"
|
||||
ToolSetNodeVRAMBudget = "set_node_vram_budget"
|
||||
ToolSetScheduling = "set_scheduling"
|
||||
ToolDeleteScheduling = "delete_scheduling"
|
||||
ToolInstallModel = "install_model"
|
||||
ToolImportModelURI = "import_model_uri"
|
||||
ToolDeleteModel = "delete_model"
|
||||
ToolEditModelConfig = "edit_model_config"
|
||||
ToolReloadModels = "reload_models"
|
||||
ToolCancelModelLoad = "cancel_model_load"
|
||||
ToolLoadModel = "load_model"
|
||||
ToolInstallBackend = "install_backend"
|
||||
ToolUpgradeBackend = "upgrade_backend"
|
||||
ToolInstallLibraryBackend = "install_library_backend"
|
||||
ToolToggleModelState = "toggle_model_state"
|
||||
ToolToggleModelPinned = "toggle_model_pinned"
|
||||
ToolSetBranding = "set_branding"
|
||||
ToolSetAlias = "set_alias"
|
||||
ToolSeedRouterCorpus = "seed_router_corpus"
|
||||
ToolClearRouterCorpus = "clear_router_corpus"
|
||||
ToolCreateVoiceProfile = "create_voice_profile"
|
||||
ToolDeleteVoiceProfile = "delete_voice_profile"
|
||||
ToolSetNodeVRAMBudget = "set_node_vram_budget"
|
||||
ToolSetScheduling = "set_scheduling"
|
||||
ToolDeleteScheduling = "delete_scheduling"
|
||||
// ToolPinFailoverTarget and ToolUnpinFailoverTarget live here (rather
|
||||
// than grouped with ToolListFailoverChains below) so mutatingToolNames
|
||||
// stays a contiguous scan of this block.
|
||||
@@ -83,6 +84,7 @@ var mutatingToolNames = []string{
|
||||
ToolLoadModel,
|
||||
ToolInstallBackend,
|
||||
ToolUpgradeBackend,
|
||||
ToolInstallLibraryBackend,
|
||||
ToolToggleModelState,
|
||||
ToolToggleModelPinned,
|
||||
ToolSetBranding,
|
||||
|
||||
@@ -47,6 +47,20 @@ func registerBackendTools(s *mcp.Server, client LocalAIClient, opts Options) {
|
||||
return jsonResult(map[string]any{"job_id": jobID}), nil, nil
|
||||
})
|
||||
|
||||
mcp.AddTool(s, &mcp.Tool{
|
||||
Name: ToolInstallLibraryBackend,
|
||||
Description: "Install a backend from an add-on library entry (a backend entry of a library source). The library resolves the image and checks its signature, so the call names only the source, the backend and the version. The backend gallery is not changed and a backend runs native code. The call returns when the install is done. Requires user confirmation per safety rule 1.",
|
||||
}, func(ctx context.Context, _ *mcp.CallToolRequest, args InstallLibraryBackendRequest) (*mcp.CallToolResult, any, error) {
|
||||
if args.SourceID == "" || args.Name == "" || args.Version == "" {
|
||||
return errorResultf("source_id, name and version are required"), nil, nil
|
||||
}
|
||||
res, err := client.InstallLibraryBackend(ctx, args)
|
||||
if err != nil {
|
||||
return errorResult(err), nil, nil
|
||||
}
|
||||
return jsonResult(res), nil, nil
|
||||
})
|
||||
|
||||
mcp.AddTool(s, &mcp.Tool{
|
||||
Name: ToolUpgradeBackend,
|
||||
Description: "Upgrade an installed backend by name. Requires user confirmation per safety rule 1.",
|
||||
|
||||
Reference in new issue
Block a user