fix: clarify provider request accounting

This commit is contained in:
ajnart committed 2026-08-12 12:58:17 +02:00
1 parent c16e00f94c
commit 019bd11017
3 files changed
+34 -15

No files matched your search

@@ -28,6 +28,8 @@ changing the user-facing provider. Each user signs in with GitHub and receives a
- Each user starts with **50 request units per UTC day**.
- Each model request uses one unit. Agent tool loops can make multiple model requests.
- Once Workshop forwards a request, it uses one unit even if the upstream later fails. This prevents failed requests
from bypassing the shared cost and abuse limits.
- The allowance resets at 00:00 UTC.
The battery beside the model selector shows the remaining allowance and reset time. The Workshop server enforces the
+19 -13
View File
@@ -47,6 +47,8 @@ var (
errProviderResponseTooLarge = errors.New("provider response is too large")
errInvalidTools = errors.New("invalid request tools")
errUnsupportedTool = errors.New("unsupported request tool")
errTooManyImages = errors.New("too many images or image too large")
errInputTooLarge = errors.New("request exceeds model input limit")
)
var clientControlledRoutingFields = []string{
@@ -224,7 +226,11 @@ func (provider *homarrProvider) chat(event *core.RequestEvent) error {
return event.JSON(http.StatusBadRequest, openAIError("The request body is not valid OpenAI chat JSON."))
}
if err := validateProviderInput(payload); err != nil {
return event.JSON(http.StatusRequestEntityTooLarge, openAIError(err.Error()))
message := "The request exceeds the Homarr model input limit."
if errors.Is(err, errTooManyImages) {
message = "The request contains too many images or an image that is too large."
}
return event.JSON(http.StatusRequestEntityTooLarge, openAIError(message))
}
if err := sanitizeProviderPayload(payload, provider.modelID); err != nil {
message := "The request contains an unsupported tool."
@@ -238,6 +244,15 @@ func (provider *homarrProvider) chat(event *core.RequestEvent) error {
return event.JSON(http.StatusBadRequest, openAIError("The request body could not be prepared."))
}
upstreamRequest, err := http.NewRequestWithContext(
event.Request.Context(),
http.MethodPost,
provider.baseURL+"/chat/completions",
bytes.NewReader(upstreamBody),
)
if err != nil {
return event.JSON(http.StatusInternalServerError, openAIError("The Homarr provider request could not be prepared."))
}
snapshot, err := provider.startRequest(event.App, event.Auth)
if err != nil {
var quotaErr *quotaExceededError
@@ -249,15 +264,6 @@ func (provider *homarrProvider) chat(event *core.RequestEvent) error {
}
return event.JSON(http.StatusInternalServerError, openAIError("The Homarr provider request could not be started."))
}
upstreamRequest, err := http.NewRequestWithContext(
event.Request.Context(),
http.MethodPost,
provider.baseURL+"/chat/completions",
bytes.NewReader(upstreamBody),
)
if err != nil {
return event.JSON(http.StatusInternalServerError, openAIError("The Homarr provider request could not be prepared."))
}
upstreamRequest.Header.Set("Authorization", "Bearer "+provider.apiKey)
upstreamRequest.Header.Set("Content-Type", "application/json")
upstreamRequest.Header.Set("Accept", "text/event-stream, application/json")
@@ -376,13 +382,13 @@ func validateProviderInput(payload map[string]any) error {
if strings.HasPrefix(typed, "data:image/") {
imageCount++
if imageCount > maxChatImages || len(typed) > maxChatImageDataBytes {
return errors.New("The request contains too many images or an image that is too large.")
return errTooManyImages
}
return nil
}
textBytes += len(typed)
if textBytes > maxChatTextBytes {
return errors.New("The request exceeds the Homarr model input limit.")
return errInputTooLarge
}
case []any:
for _, item := range typed {
@@ -394,7 +400,7 @@ func validateProviderInput(payload map[string]any) error {
for key, item := range typed {
textBytes += len(key)
if textBytes > maxChatTextBytes {
return errors.New("The request exceeds the Homarr model input limit.")
return errInputTooLarge
}
if err := walk(item); err != nil {
return err
+13 -2
View File
@@ -2,6 +2,7 @@ package main
import (
"encoding/json"
"errors"
"strings"
"testing"
)
@@ -125,8 +126,18 @@ func TestValidateProviderInput(t *testing.T) {
t.Fatalf("expected a supported Homarr image request, got %v", err)
}
payload["messages"] = []any{map[string]any{"role": "user", "content": strings.Repeat("x", maxChatTextBytes+1)}}
if err := validateProviderInput(payload); err == nil {
t.Fatal("expected oversized text input to be rejected")
if err := validateProviderInput(payload); !errors.Is(err, errInputTooLarge) {
t.Fatalf("expected oversized text input to be rejected, got %v", err)
}
payload["messages"] = []any{map[string]any{
"role": "user",
"content": []any{map[string]any{
"type": "image_url",
"image_url": map[string]any{"url": "data:image/png;base64," + strings.Repeat("a", maxChatImageDataBytes)},
}},
}}
if err := validateProviderInput(payload); !errors.Is(err, errTooManyImages) {
t.Fatalf("expected oversized image input to be rejected, got %v", err)
}
}