mirror of
https://github.com/mudler/LocalAI.git
synced 2026-11-06 04:23:15 -05:00
feat(chats): show and set the file limits of a connector binding
The connector list shows, for each binding and each kind, the limit in effect in both directions, the layer that sets it, the ability the kind declares and the most an owner may set. A binding accepts file_receive_max_bytes and file_send_max_bytes; a value above the limit of the kind is a 400. Assisted-by: Claude Code:claude-sonnet-5-5 Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
This commit is contained in:
5 files changed
+106
-3
No files matched your search
@@ -24,7 +24,15 @@ func connectorOut(i studiosvc.ConnectorInfo) schema.ChatsConnector {
|
||||
}
|
||||
return schema.ChatsConnector{ID: i.ID, Name: i.Name, Kind: i.Kind, Addon: i.AddonID, Active: i.Active, Reason: i.Reason, AgentID: i.AgentID, Enabled: i.Enabled, AcceptAny: i.AcceptAny,
|
||||
Allow: allow, NotifyChat: i.NotifyChat, HasToken: i.HasToken, Created: i.Created,
|
||||
ReplyMode: i.ReplyMode(), Conversation: i.ConversationMode(), WindowMinutes: i.WindowMinutes, SendFiles: i.SendsFiles(), ThreadContext: i.ReadsThread(), Options: i.Options}
|
||||
ReplyMode: i.ReplyMode(), Conversation: i.ConversationMode(), WindowMinutes: i.WindowMinutes, SendFiles: i.SendsFiles(), ThreadContext: i.ReadsThread(), Options: i.Options,
|
||||
Files: filesOut(i.Files), FileReceiveMaxBytes: i.FileReceiveMax, FileSendMaxBytes: i.FileSendMax}
|
||||
}
|
||||
|
||||
func filesOut(l studiosvc.FileLimits) schema.ChatsConnectorFiles {
|
||||
dir := func(e studiosvc.FileLimit, declared, allowed int64) schema.ChatsConnectorFileLimit {
|
||||
return schema.ChatsConnectorFileLimit{EffectiveBytes: e.Bytes, Layer: string(e.Layer), DeclaredBytes: declared, AllowedBytes: allowed}
|
||||
}
|
||||
return schema.ChatsConnectorFiles{Receive: dir(l.Receive, l.ReceiveDeclared, l.ReceiveMax), Send: dir(l.Send, l.SendDeclared, l.SendMax), InlineBytes: contract.MaxConnectorFileBytes}
|
||||
}
|
||||
|
||||
// acceptAnyConfirmation is what a request must say to turn on accept_any.
|
||||
@@ -38,7 +46,8 @@ func failAcceptAny(c echo.Context) error {
|
||||
func connectorIn(req schema.ChatsConnectorWrite) studiosvc.ConnectorInput {
|
||||
return studiosvc.ConnectorInput{Kind: req.Kind, Name: req.Name, Token: req.Token, AgentID: req.AgentID, Enabled: req.Enabled,
|
||||
AcceptAny: req.AcceptAny, Allow: req.Allow, NotifyChat: req.NotifyChat,
|
||||
Reply: req.ReplyMode, Conversation: req.Conversation, WindowMinutes: req.WindowMinutes, SendFiles: req.SendFiles, ThreadContext: req.ThreadContext, Options: req.Options}
|
||||
Reply: req.ReplyMode, Conversation: req.Conversation, WindowMinutes: req.WindowMinutes, SendFiles: req.SendFiles, ThreadContext: req.ThreadContext, Options: req.Options,
|
||||
FileReceiveMax: req.FileReceiveMaxBytes, FileSendMax: req.FileSendMaxBytes}
|
||||
}
|
||||
|
||||
var connectorRules = []errRule{
|
||||
@@ -99,7 +108,8 @@ func (h *Handlers) ConnectorKinds(c echo.Context) error {
|
||||
}
|
||||
out := schema.ChatsConnectorKindList{Kinds: make([]schema.ChatsConnectorKind, 0, len(kinds))}
|
||||
for _, k := range kinds {
|
||||
out.Kinds = append(out.Kinds, schema.ChatsConnectorKind{Addon: k.AddonID, Kind: k.ID, Title: k.Title, Description: k.Description, SenderHint: k.SenderHint, Page: k.Page, ThreadHistory: k.ServesThreadHistory(), Options: optionsOut(k.Options)})
|
||||
out.Kinds = append(out.Kinds, schema.ChatsConnectorKind{Addon: k.AddonID, Kind: k.ID, Title: k.Title, Description: k.Description, SenderHint: k.SenderHint, Page: k.Page, ThreadHistory: k.ServesThreadHistory(), Options: optionsOut(k.Options),
|
||||
Files: filesOut(host.KindFileLimits(c.Request().Context(), k))})
|
||||
}
|
||||
return c.JSON(http.StatusOK, out)
|
||||
}
|
||||
|
||||
@@ -113,6 +113,38 @@ var _ = Describe("Chats connectors endpoints", func() {
|
||||
Expect(create("alice", map[string]any{"kind": "carrier-pigeon", "name": "x", "token": specToken, "agent_id": ag}).Code).To(Equal(http.StatusBadRequest))
|
||||
})
|
||||
|
||||
It("shows the file limits of a kind and of a binding, and lets the owner only lower them", func() {
|
||||
kinds := decodeJSON(s.do("alice", http.MethodGet, "/api/chats/connectors/kinds", nil))["kinds"].([]any)
|
||||
files := kinds[0].(map[string]any)["files"].(map[string]any)
|
||||
// The Telegram add-on declares no ability: the inline cap is the limit.
|
||||
Expect(files).To(HaveKeyWithValue("inline_bytes", BeNumerically("==", 1<<20)))
|
||||
send := files["send"].(map[string]any)
|
||||
Expect(send).To(And(HaveKeyWithValue("effective_bytes", BeNumerically("==", 1<<20)), HaveKeyWithValue("layer", "inline"),
|
||||
HaveKeyWithValue("allowed_bytes", BeNumerically("==", 1<<20)), HaveKeyWithValue("declared_bytes", BeNumerically("==", 0))))
|
||||
|
||||
ag := agent("alice", "Support", "test-model")
|
||||
made := create("alice", map[string]any{"name": "B", "token": specToken, "agent_id": ag, "file_send_max_bytes": 2 << 20})
|
||||
Expect(made.Code).To(Equal(http.StatusBadRequest), "a limit above the one of the kind is refused: %s", made.Body.String())
|
||||
Expect(made.Body.String()).To(ContainSubstring("can only lower"))
|
||||
|
||||
made = create("alice", map[string]any{"name": "B", "token": specToken, "agent_id": ag, "file_send_max_bytes": 100 << 10})
|
||||
Expect(made.Code).To(Equal(http.StatusCreated), made.Body.String())
|
||||
b := decodeJSON(made)
|
||||
Expect(b).To(HaveKeyWithValue("file_send_max_bytes", BeNumerically("==", 100<<10)))
|
||||
Expect(b["files"].(map[string]any)["send"]).To(And(HaveKeyWithValue("effective_bytes", BeNumerically("==", 100<<10)), HaveKeyWithValue("layer", "owner")))
|
||||
Expect(b["files"].(map[string]any)["receive"]).To(HaveKeyWithValue("layer", "inline"))
|
||||
id := b["id"].(string)
|
||||
|
||||
// An update that says nothing about the limits keeps them; zero clears one.
|
||||
keep := s.do("alice", http.MethodPut, "/api/chats/connectors/"+id, map[string]any{"name": "B2", "agent_id": ag, "enabled": true})
|
||||
Expect(keep.Code).To(Equal(http.StatusOK), keep.Body.String())
|
||||
Expect(decodeJSON(keep)).To(HaveKeyWithValue("file_send_max_bytes", BeNumerically("==", 100<<10)))
|
||||
clear := s.do("alice", http.MethodPut, "/api/chats/connectors/"+id, map[string]any{"name": "B2", "agent_id": ag, "enabled": true, "file_send_max_bytes": 0})
|
||||
Expect(decodeJSON(clear)).To(HaveKeyWithValue("file_send_max_bytes", BeNumerically("==", 0)))
|
||||
Expect(decodeJSON(clear)["files"].(map[string]any)["send"]).To(HaveKeyWithValue("layer", "inline"))
|
||||
Expect(s.do("alice", http.MethodPut, "/api/chats/connectors/"+id, map[string]any{"name": "B2", "agent_id": ag, "enabled": true, "file_receive_max_bytes": -5}).Code).To(Equal(http.StatusBadRequest))
|
||||
})
|
||||
|
||||
It("keeps bindings private to their owner", func() {
|
||||
ag := agent("alice", "Support", "test-model")
|
||||
id := decodeJSON(create("alice", map[string]any{"name": "B", "token": specToken, "agent_id": ag}))["id"].(string)
|
||||
|
||||
@@ -1189,6 +1189,40 @@ type ChatsConnector struct {
|
||||
Active bool `json:"active"`
|
||||
// Reason says in plain words why the binding is not active.
|
||||
Reason string `json:"reason,omitempty"`
|
||||
// Files are the limits of the files the bot reads and sends, with the
|
||||
// limit that you set for this bot.
|
||||
Files ChatsConnectorFiles `json:"files"`
|
||||
// FileReceiveMaxBytes and FileSendMaxBytes are your limits for this bot;
|
||||
// zero means you set none and the limit of the kind applies.
|
||||
FileReceiveMaxBytes int64 `json:"file_receive_max_bytes"`
|
||||
FileSendMaxBytes int64 `json:"file_send_max_bytes"`
|
||||
}
|
||||
|
||||
// ChatsConnectorFiles are the file limits of a binding or of a kind, for both
|
||||
// directions.
|
||||
type ChatsConnectorFiles struct {
|
||||
// Receive is the limit of the files that people send to the bot.
|
||||
Receive ChatsConnectorFileLimit `json:"receive"`
|
||||
// Send is the limit of the files that the bot sends.
|
||||
Send ChatsConnectorFileLimit `json:"send"`
|
||||
// InlineBytes is the cap of the inline path. Until the add-on can stream
|
||||
// a file, nothing larger moves, whatever the other limits are.
|
||||
InlineBytes int64 `json:"inline_bytes"`
|
||||
}
|
||||
|
||||
// ChatsConnectorFileLimit is the limit of one direction.
|
||||
type ChatsConnectorFileLimit struct {
|
||||
// EffectiveBytes is the largest file that moves now: the smallest of the
|
||||
// host ceiling, the declared ability, your limit and the inline cap.
|
||||
EffectiveBytes int64 `json:"effective_bytes"`
|
||||
// Layer names the one that sets EffectiveBytes: owner (your limit for
|
||||
// this bot), addon (the ability the kind declares), host (the ceiling of
|
||||
// the server) or inline (the cap of the inline path).
|
||||
Layer string `json:"layer"`
|
||||
// DeclaredBytes is the ability the kind declares, or zero.
|
||||
DeclaredBytes int64 `json:"declared_bytes"`
|
||||
// AllowedBytes is the most you may set for a bot.
|
||||
AllowedBytes int64 `json:"allowed_bytes"`
|
||||
}
|
||||
|
||||
// ChatsConnectorKind is a kind of binding that an installed connector add-on serves.
|
||||
@@ -1205,6 +1239,9 @@ type ChatsConnectorKind struct {
|
||||
ThreadHistory bool `json:"thread_history,omitempty"`
|
||||
// Options are the choices the add-on lets each binding of this kind make.
|
||||
Options []ChatsConnectorOption `json:"options,omitempty"`
|
||||
// Files are the limits of a binding of this kind that has no limit of
|
||||
// your own.
|
||||
Files ChatsConnectorFiles `json:"files"`
|
||||
}
|
||||
|
||||
// ChatsConnectorOption is one choice of a connector kind.
|
||||
@@ -1260,6 +1297,12 @@ type ChatsConnectorWrite struct {
|
||||
// Options are the choices for the options of the kind, by option id. Absent
|
||||
// means the defaults for a new binding and unchanged for an update.
|
||||
Options map[string]string `json:"options,omitempty"`
|
||||
// FileReceiveMaxBytes and FileSendMaxBytes limit the files the bot reads
|
||||
// and sends, in bytes. A limit can only lower the limit of the kind, which
|
||||
// the kind list shows as allowed_bytes. Zero removes your limit. Absent
|
||||
// means none for a new binding and unchanged for an update.
|
||||
FileReceiveMaxBytes *int64 `json:"file_receive_max_bytes,omitempty"`
|
||||
FileSendMaxBytes *int64 `json:"file_send_max_bytes,omitempty"`
|
||||
}
|
||||
|
||||
// ChatsConnectorList is the response of the connector list endpoint.
|
||||
|
||||
@@ -66,6 +66,9 @@ type FileLimits struct {
|
||||
// ReceiveMax and SendMax are the most an owner may set: the ability of
|
||||
// the kind (the inline cap, if it declares none) under the host ceiling.
|
||||
ReceiveMax, SendMax int64
|
||||
// ReceiveDeclared and SendDeclared are the abilities the kind declares;
|
||||
// zero when it declares none.
|
||||
ReceiveDeclared, SendDeclared int64
|
||||
// KindTitle is the name of the platform, for the sentences.
|
||||
KindTitle string
|
||||
}
|
||||
@@ -99,6 +102,12 @@ func (c *Connectors) FileLimits(ctx context.Context, b connectors.Binding) FileL
|
||||
return resolveFileLimits(c.fileCeiling(ctx), files, title, b)
|
||||
}
|
||||
|
||||
// KindFileLimits resolves the limits of a kind for a binding that has no
|
||||
// limit of its own, such as one that is not made yet.
|
||||
func (c *Connectors) KindFileLimits(ctx context.Context, k ConnectorKind) FileLimits {
|
||||
return resolveFileLimits(c.fileCeiling(ctx), k.Files, k.Title, connectors.Binding{})
|
||||
}
|
||||
|
||||
func resolveFileLimits(ceiling int64, files *contract.ConnectorFiles, title string, b connectors.Binding) FileLimits {
|
||||
if files == nil {
|
||||
files = &contract.ConnectorFiles{}
|
||||
@@ -108,6 +117,12 @@ func resolveFileLimits(ceiling int64, files *contract.ConnectorFiles, title stri
|
||||
l.Send = EffectiveFileLimit(ceiling, files.Send, b.FileSendMax)
|
||||
l.ReceiveMax = ownerMax(ceiling, files.Receive)
|
||||
l.SendMax = ownerMax(ceiling, files.Send)
|
||||
if files.Receive != nil {
|
||||
l.ReceiveDeclared = files.Receive.MaxBytes
|
||||
}
|
||||
if files.Send != nil {
|
||||
l.SendDeclared = files.Send.MaxBytes
|
||||
}
|
||||
return l
|
||||
}
|
||||
|
||||
|
||||
@@ -127,6 +127,8 @@ type ConnectorInfo struct {
|
||||
// installed; Reason then says so in plain words.
|
||||
Active bool
|
||||
Reason string
|
||||
// Files are the file limits in effect for the binding.
|
||||
Files FileLimits
|
||||
}
|
||||
|
||||
func info(b connectors.Binding) ConnectorInfo {
|
||||
@@ -137,6 +139,7 @@ func info(b connectors.Binding) ConnectorInfo {
|
||||
|
||||
// withState marks a binding whose add-on does not run.
|
||||
func (c *Connectors) withState(ctx context.Context, i ConnectorInfo) ConnectorInfo {
|
||||
i.Files = c.FileLimits(ctx, i.Binding)
|
||||
kinds, err := c.o.Transport.Kinds(ctx)
|
||||
if err != nil {
|
||||
return i
|
||||
|
||||
Reference in new issue
Block a user