Files
LocalAI/core/application/addon_policy_reader.go
T
Ettore Di Giacinto 4ba1b7b2bd feat(addons): move connector file bytes by handle
A connector add-on that declares files.transfer no longer carries file
bytes in the connector envelope. The host puts a random handle in the
request of the op media (write) or send_file (read), and the add-on
streams the file with two new host operations, connector.file.put and
connector.file.get, in chunks of at most 256 KiB.

The process that runs the add-on registers the handle when the call
arrives and ends it when the call returns. A handle is bound to the
add-on, so another add-on gets the same answer as for an unknown
handle. An unfinished inbound file is dropped with its partial bytes,
and a handle with no call for two minutes expires. Inbound bytes go
to the blob store of the workspace, outbound bytes come from it, so a
cluster needs the shared object store and no shared disk. The file is
never held in memory. The host overwrites the size and hash of an
inbound file with its own record.

The change is additive: the field files.transfer needs manifest API 2,
a kind without it keeps the inline path and its 1 MiB cap, and the host
never sends it a handle. A call that carries a handle may run up to 15
minutes. The ceiling of a file also stays under the largest file of a
workspace.

Assisted-by: Claude:claude-sonnet-5-5 go test
2026-11-03 15:54:45 +00:00

102 lines
3.5 KiB
Go

// SPDX-License-Identifier: MIT
package application
import (
"context"
"fmt"
"maps"
"github.com/mudler/LocalAI/core/config"
"github.com/mudler/LocalAI/core/services/addons/gateway"
"github.com/mudler/LocalAI/core/services/studio/egress"
"github.com/mudler/LocalAI/pkg/addons/contract"
)
// studioEgressSettings reads a fresh bounded owner policy on every request.
// The no-addon server retains its existing built-in settings; an enabled
// addon deployment must never substitute frontend configuration on failure.
func (a *Application) studioEgressSettings(ctx context.Context) (egress.Settings, error) {
if err := ctx.Err(); err != nil {
return egress.Settings{}, err
}
if !a.applicationConfig.Addons.Enabled {
return a.localStudioEgressSettings(), nil
}
snapshot, err := a.readAddonHostPolicy(ctx)
if err != nil {
return egress.Settings{}, err
}
p := snapshot.Policy
ports, err := config.ParseEgressPorts(p.Ports)
if err != nil {
return egress.Settings{}, fmt.Errorf("owner egress ports: %w", err)
}
extra, err := config.ParseEgressHosts(p.ExtraRegistries)
if err != nil {
return egress.Settings{}, fmt.Errorf("owner egress registries: %w", err)
}
hosts, err := config.ParseEgressAllowedHosts(p.AllowedHosts, true)
if err != nil {
return egress.Settings{}, fmt.Errorf("owner egress hosts: %w", err)
}
out := egress.Settings{Ceiling: egress.Preset(p.Ceiling), Default: egress.Preset(p.Default), AllowPrivate: p.AllowPrivate, Ports: ports, ExtraRegistries: extra}
if p.Ceiling != "" && !out.Ceiling.Valid() || p.Default != "" && !out.Default.Valid() {
return egress.Settings{}, gateway.ErrUnavailable
}
for _, h := range hosts {
out.AllowedHosts = append(out.AllowedHosts, egress.AllowedHost{Host: h.Host, Port: h.Port})
}
return out, nil
}
// studioToolPins never substitutes frontend pins when the owner is unavailable.
func (a *Application) studioToolPins(ctx context.Context) (map[string]string, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
if !a.applicationConfig.Addons.Enabled {
return maps.Clone(a.applicationConfig.ChatsToolProviders), nil
}
snapshot, err := a.readAddonHostPolicy(ctx)
if err != nil {
return nil, err
}
return maps.Clone(snapshot.Policy.ToolProviders), nil
}
// studioConnectorFilesCeiling is the host ceiling of a connector file, from
// the owner policy. Without add-ons there is no owner policy and no connector
// to move a file, so the default stands.
func (a *Application) studioConnectorFilesCeiling(ctx context.Context) (int64, error) {
n, err := a.policyFilesCeiling(ctx)
if err != nil {
return 0, err
}
// A file that a sender shares becomes a file of the chat, so the ceiling
// never passes what one file of a workspace may be.
return min(n, a.studioUploadLimit()), nil
}
func (a *Application) policyFilesCeiling(ctx context.Context) (int64, error) {
if err := ctx.Err(); err != nil {
return 0, err
}
if !a.applicationConfig.Addons.Enabled {
return contract.DefaultFileCeilingBytes, nil
}
snapshot, err := a.readAddonHostPolicy(ctx)
if err != nil {
return 0, err
}
return snapshot.Policy.FilesCeiling(), nil
}
// readAddonHostPolicy is shared by host consumers; the DTO keeps revision
// and policy together and excludes owner-only migration provenance.
func (a *Application) readAddonHostPolicy(ctx context.Context) (gateway.HostSettingsPolicy, error) {
if a.addonGateway == nil {
return gateway.HostSettingsPolicy{}, gateway.ErrUnavailable
}
return a.addonGateway.ReadHostSettingsInternal(ctx)
}