fix: clarify provider request accounting
This commit is contained in:
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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user