mirror of
https://github.com/mudler/LocalAI.git
synced 2026-11-06 04:23:15 -05:00
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
102 lines
3.5 KiB
Go
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)
|
|
}
|