Harden Homarr provider limits
This commit is contained in:
1 parent
47c349bcd4
commit
0774b02878
14 files changed
+463
-459
No files matched your search
@@ -23,18 +23,27 @@ import (
|
||||
const (
|
||||
homarrProviderModelID = "homarr/deepseek-v4-flash-latest"
|
||||
openRouterModelID = "~deepseek/deepseek-v4-flash-latest"
|
||||
maxChatInputTokens = 256 * 1024
|
||||
maxChatOutputTokens = 32 * 1024
|
||||
defaultDailyLimit = 50
|
||||
maxProviderRequestUnits = 1000
|
||||
maxChatRequestBytes = 8 << 20
|
||||
maxChatResponseBytes = 8 << 20
|
||||
maxUpstreamErrorBytes = 1 << 20
|
||||
openRouterDefaultURL = "https://openrouter.ai/api/v1"
|
||||
providerRequestTimeout = 5 * time.Minute
|
||||
// Do not trust a client-side token estimate. Applying the same numeric ceiling to
|
||||
// the complete serialized payload is deliberately stricter than a tokenizer-based
|
||||
// limit and also bounds attachments and tool schemas.
|
||||
maxChatRequestBytes = maxChatInputTokens
|
||||
maxChatResponseBytes = 8 << 20
|
||||
maxUpstreamErrorBytes = 1 << 20
|
||||
maxWebSearchResults = 5
|
||||
maxWebSearchUses = 3
|
||||
openRouterDefaultURL = "https://openrouter.ai/api/v1"
|
||||
providerRequestTimeout = 5 * time.Minute
|
||||
)
|
||||
|
||||
var errProviderResponseTooLarge = errors.New("provider response is too large")
|
||||
|
||||
var clientControlledRoutingFields = []string{"models", "provider", "route", "plugins", "transforms"}
|
||||
var clientControlledRoutingFields = []string{
|
||||
"models", "provider", "route", "plugins", "transforms", "extra_body", "extra_headers",
|
||||
}
|
||||
|
||||
type homarrProvider struct {
|
||||
apiKey string
|
||||
@@ -97,8 +106,9 @@ func newHomarrProviderFromEnv() (*homarrProvider, error) {
|
||||
baseURL = openRouterDefaultURL
|
||||
}
|
||||
parsedURL, err := url.Parse(baseURL)
|
||||
if err != nil || parsedURL.Host == "" || (parsedURL.Scheme != "https" && parsedURL.Scheme != "http") {
|
||||
return nil, errors.New("HOMARR_AI_OPENROUTER_BASE_URL must be an absolute HTTP(S) URL")
|
||||
if err != nil || parsedURL.Host == "" || (parsedURL.Scheme != "https" && parsedURL.Scheme != "http") ||
|
||||
parsedURL.User != nil || parsedURL.RawQuery != "" || parsedURL.Fragment != "" {
|
||||
return nil, errors.New("HOMARR_AI_OPENROUTER_BASE_URL must be an absolute HTTP(S) URL without credentials, a query, or a fragment")
|
||||
}
|
||||
|
||||
dailyLimit := defaultDailyLimit
|
||||
@@ -136,7 +146,7 @@ func (provider *homarrProvider) models(event *core.RequestEvent) error {
|
||||
"name": "DeepSeek V4 Flash Latest",
|
||||
"description": "Fast agentic model provided by the community-funded Homarr provider.",
|
||||
"owned_by": "homarr",
|
||||
"context_length": 1_048_576,
|
||||
"context_length": maxChatInputTokens,
|
||||
"supported_parameters": []string{
|
||||
"tools", "tool_choice", "reasoning", "include_reasoning", "structured_outputs",
|
||||
},
|
||||
@@ -183,7 +193,9 @@ func (provider *homarrProvider) chat(event *core.RequestEvent) error {
|
||||
if err := json.Unmarshal(body, &payload); err != nil {
|
||||
return event.JSON(http.StatusBadRequest, openAIError("The request body is not valid OpenAI chat JSON."))
|
||||
}
|
||||
sanitizeProviderPayload(payload)
|
||||
if err := sanitizeProviderPayload(payload); err != nil {
|
||||
return event.JSON(http.StatusBadRequest, openAIError(err.Error()))
|
||||
}
|
||||
upstreamBody, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return event.JSON(http.StatusBadRequest, openAIError("The request body could not be prepared."))
|
||||
@@ -208,7 +220,7 @@ func (provider *homarrProvider) chat(event *core.RequestEvent) error {
|
||||
bytes.NewReader(upstreamBody),
|
||||
)
|
||||
if err != nil {
|
||||
provider.finishActivityWithRefund(event.App, privateRequest, publicActivity, "failed", requestUsage{}, startedAt, requestUnits)
|
||||
provider.finishActivity(event.App, privateRequest, publicActivity, "failed", requestUsage{}, startedAt)
|
||||
return event.JSON(http.StatusInternalServerError, openAIError("The Homarr provider request could not be prepared."))
|
||||
}
|
||||
upstreamRequest.Header.Set("Authorization", "Bearer "+provider.apiKey)
|
||||
@@ -220,7 +232,7 @@ func (provider *homarrProvider) chat(event *core.RequestEvent) error {
|
||||
|
||||
upstreamResponse, err := provider.httpClient.Do(upstreamRequest)
|
||||
if err != nil {
|
||||
provider.finishActivityWithRefund(event.App, privateRequest, publicActivity, "failed", requestUsage{}, startedAt, requestUnits)
|
||||
provider.finishActivity(event.App, privateRequest, publicActivity, "failed", requestUsage{}, startedAt)
|
||||
return event.JSON(http.StatusBadGateway, openAIError("The model endpoint could not be reached."))
|
||||
}
|
||||
defer func() { _ = upstreamResponse.Body.Close() }()
|
||||
@@ -239,17 +251,15 @@ func (provider *homarrProvider) chat(event *core.RequestEvent) error {
|
||||
contentType = "application/json"
|
||||
}
|
||||
if upstreamResponse.StatusCode < 200 || upstreamResponse.StatusCode >= 300 {
|
||||
errorBody, _ := io.ReadAll(io.LimitReader(upstreamResponse.Body, maxUpstreamErrorBytes))
|
||||
provider.finishActivityWithRefund(event.App, privateRequest, publicActivity, "failed", requestUsage{}, startedAt, requestUnits)
|
||||
event.Response.Header().Set("X-Homarr-Quota-Remaining", strconv.Itoa(min(snapshot.Limit, snapshot.Remaining+requestUnits)))
|
||||
return event.Blob(upstreamResponse.StatusCode, contentType, errorBody)
|
||||
provider.finishActivity(event.App, privateRequest, publicActivity, "failed", requestUsage{}, startedAt)
|
||||
return event.JSON(upstreamResponse.StatusCode, openAIError("The model endpoint rejected the request."))
|
||||
}
|
||||
|
||||
if requestBody.Stream {
|
||||
capture := newSSEUsageCapture()
|
||||
event.Response.Header().Set("Content-Type", contentType)
|
||||
event.Response.WriteHeader(upstreamResponse.StatusCode)
|
||||
_, copyErr := io.Copy(flushingWriter{event.Response}, io.TeeReader(upstreamResponse.Body, capture))
|
||||
_, copyErr := copyBounded(flushingWriter{event.Response}, io.TeeReader(upstreamResponse.Body, capture), maxChatResponseBytes)
|
||||
provider.finishActivity(event.App, privateRequest, publicActivity, statusForError(copyErr), capture.Usage(), startedAt)
|
||||
return nil
|
||||
}
|
||||
@@ -264,12 +274,72 @@ func (provider *homarrProvider) chat(event *core.RequestEvent) error {
|
||||
return event.Blob(upstreamResponse.StatusCode, contentType, responseBody)
|
||||
}
|
||||
|
||||
func sanitizeProviderPayload(payload map[string]any) {
|
||||
func sanitizeProviderPayload(payload map[string]any) error {
|
||||
for _, field := range clientControlledRoutingFields {
|
||||
delete(payload, field)
|
||||
}
|
||||
delete(payload, "max_completion_tokens")
|
||||
delete(payload, "metadata")
|
||||
delete(payload, "user")
|
||||
if reasoning, ok := payload["reasoning"].(map[string]any); ok {
|
||||
delete(reasoning, "max_tokens")
|
||||
}
|
||||
|
||||
if rawTools, exists := payload["tools"]; exists {
|
||||
tools, ok := rawTools.([]any)
|
||||
if !ok {
|
||||
return errors.New("The request tools are not valid.")
|
||||
}
|
||||
sanitizedTools := make([]any, 0, len(tools))
|
||||
webSearchAdded := false
|
||||
for _, rawTool := range tools {
|
||||
tool, ok := rawTool.(map[string]any)
|
||||
if !ok {
|
||||
return errors.New("The request contains an unsupported tool.")
|
||||
}
|
||||
switch tool["type"] {
|
||||
case "function":
|
||||
sanitizedTools = append(sanitizedTools, tool)
|
||||
case "openrouter:web_search":
|
||||
if !webSearchAdded {
|
||||
sanitizedTools = append(sanitizedTools, map[string]any{
|
||||
"type": "openrouter:web_search",
|
||||
"parameters": map[string]any{
|
||||
"max_results": maxWebSearchResults,
|
||||
"max_uses": maxWebSearchUses,
|
||||
},
|
||||
})
|
||||
webSearchAdded = true
|
||||
}
|
||||
default:
|
||||
return errors.New("The request contains an unsupported tool.")
|
||||
}
|
||||
}
|
||||
payload["tools"] = sanitizedTools
|
||||
}
|
||||
|
||||
payload["model"] = openRouterModelID
|
||||
payload["max_tokens"] = maxChatOutputTokens
|
||||
payload["n"] = 1
|
||||
payload["parallel_tool_calls"] = false
|
||||
payload["usage"] = map[string]any{"include": true}
|
||||
return nil
|
||||
}
|
||||
|
||||
func copyBounded(writer io.Writer, reader io.Reader, limit int64) (int64, error) {
|
||||
written, err := io.Copy(writer, io.LimitReader(reader, limit))
|
||||
if err != nil {
|
||||
return written, err
|
||||
}
|
||||
var extra [1]byte
|
||||
read, err := reader.Read(extra[:])
|
||||
if read > 0 {
|
||||
return written, errProviderResponseTooLarge
|
||||
}
|
||||
if err != nil && !errors.Is(err, io.EOF) {
|
||||
return written, err
|
||||
}
|
||||
return written, nil
|
||||
}
|
||||
|
||||
func readBoundedBody(reader io.Reader, limit int64) ([]byte, error) {
|
||||
@@ -393,18 +463,6 @@ func (provider *homarrProvider) finishActivity(
|
||||
status string,
|
||||
usage requestUsage,
|
||||
startedAt time.Time,
|
||||
) {
|
||||
provider.finishActivityWithRefund(app, privateRequest, publicActivity, status, usage, startedAt, 0)
|
||||
}
|
||||
|
||||
func (provider *homarrProvider) finishActivityWithRefund(
|
||||
app core.App,
|
||||
privateRequest *core.Record,
|
||||
publicActivity *core.Record,
|
||||
status string,
|
||||
usage requestUsage,
|
||||
startedAt time.Time,
|
||||
refundUnits int,
|
||||
) {
|
||||
durationMs := max(0, int(provider.now().UTC().Sub(startedAt).Milliseconds()))
|
||||
err := app.RunInTransaction(func(txApp core.App) error {
|
||||
@@ -430,9 +488,6 @@ func (provider *homarrProvider) finishActivityWithRefund(
|
||||
if !requestBelongsToQuotaDay(quota.GetString("day"), startedAt) {
|
||||
return nil
|
||||
}
|
||||
if refundUnits > 0 {
|
||||
quota.Set("used", max(0, quota.GetInt("used")-refundUnits))
|
||||
}
|
||||
quota.Set("inputTokens", quota.GetInt("inputTokens")+usage.InputTokens)
|
||||
quota.Set("outputTokens", quota.GetInt("outputTokens")+usage.OutputTokens)
|
||||
quota.Set("totalTokens", quota.GetInt("totalTokens")+usage.TotalTokens)
|
||||
|
||||
@@ -10,15 +10,34 @@ import (
|
||||
|
||||
func TestSanitizeProviderPayload(t *testing.T) {
|
||||
payload := map[string]any{
|
||||
"model": homarrProviderModelID,
|
||||
"models": []string{"attacker/model"},
|
||||
"provider": map[string]any{"order": []string{"attacker"}},
|
||||
"route": "fallback",
|
||||
"plugins": []string{"web"},
|
||||
"transforms": []string{"middle-out"},
|
||||
"messages": []any{map[string]any{"role": "user", "content": "hello"}},
|
||||
"model": homarrProviderModelID,
|
||||
"models": []string{"attacker/model"},
|
||||
"provider": map[string]any{"order": []string{"attacker"}},
|
||||
"route": "fallback",
|
||||
"plugins": []string{"web"},
|
||||
"transforms": []string{"middle-out"},
|
||||
"max_tokens": 1_000_000,
|
||||
"max_completion_tokens": 1_000_000,
|
||||
"n": 100,
|
||||
"parallel_tool_calls": true,
|
||||
"extra_body": map[string]any{"provider": map[string]any{"order": []string{"attacker"}}},
|
||||
"extra_headers": map[string]any{"Authorization": "Bearer attacker"},
|
||||
"metadata": map[string]any{"user": "private-user-id"},
|
||||
"reasoning": map[string]any{"effort": "high", "max_tokens": 1_000_000},
|
||||
"user": "private-user-id",
|
||||
"tools": []any{
|
||||
map[string]any{"type": "function", "function": map[string]any{"name": "board_list"}},
|
||||
map[string]any{
|
||||
"type": "openrouter:web_search",
|
||||
"parameters": map[string]any{"max_results": 1000, "max_uses": 1000},
|
||||
},
|
||||
map[string]any{"type": "openrouter:web_search"},
|
||||
},
|
||||
"messages": []any{map[string]any{"role": "user", "content": "hello"}},
|
||||
}
|
||||
if err := sanitizeProviderPayload(payload); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sanitizeProviderPayload(payload)
|
||||
for _, field := range clientControlledRoutingFields {
|
||||
if _, exists := payload[field]; exists {
|
||||
t.Fatalf("client-controlled routing field %q was forwarded", field)
|
||||
@@ -27,12 +46,44 @@ func TestSanitizeProviderPayload(t *testing.T) {
|
||||
if payload["model"] != openRouterModelID {
|
||||
t.Fatalf("unexpected upstream model: %v", payload["model"])
|
||||
}
|
||||
if payload["max_tokens"] != maxChatOutputTokens || payload["n"] != 1 || payload["parallel_tool_calls"] != false {
|
||||
t.Fatalf("unsafe generation controls were forwarded: %#v", payload)
|
||||
}
|
||||
if _, exists := payload["max_completion_tokens"]; exists {
|
||||
t.Fatal("client max_completion_tokens was forwarded")
|
||||
}
|
||||
if _, exists := payload["user"]; exists {
|
||||
t.Fatal("client user identifier was forwarded")
|
||||
}
|
||||
if _, exists := payload["metadata"]; exists {
|
||||
t.Fatal("client metadata was forwarded")
|
||||
}
|
||||
reasoning := payload["reasoning"].(map[string]any)
|
||||
if _, exists := reasoning["max_tokens"]; exists || reasoning["effort"] != "high" {
|
||||
t.Fatalf("reasoning budget was not bounded safely: %#v", reasoning)
|
||||
}
|
||||
tools := payload["tools"].([]any)
|
||||
if len(tools) != 2 {
|
||||
t.Fatalf("unexpected sanitized tool count: %d", len(tools))
|
||||
}
|
||||
webSearch := tools[1].(map[string]any)
|
||||
parameters := webSearch["parameters"].(map[string]any)
|
||||
if parameters["max_results"] != maxWebSearchResults || parameters["max_uses"] != maxWebSearchUses {
|
||||
t.Fatalf("web search limits were not enforced: %#v", parameters)
|
||||
}
|
||||
usage, err := json.Marshal(payload["usage"])
|
||||
if err != nil || string(usage) != `{"include":true}` {
|
||||
t.Fatalf("unexpected usage options: %s, %v", usage, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeProviderPayloadRejectsUnsupportedServerTools(t *testing.T) {
|
||||
payload := map[string]any{"tools": []any{map[string]any{"type": "openrouter:computer"}}}
|
||||
if err := sanitizeProviderPayload(payload); err == nil {
|
||||
t.Fatal("expected unsupported server tool to be rejected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCountRequestUnits(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -111,6 +162,18 @@ func TestProviderEnvironment(t *testing.T) {
|
||||
if _, err := newHomarrProviderFromEnv(); err == nil {
|
||||
t.Fatal("expected invalid daily limit to fail")
|
||||
}
|
||||
|
||||
t.Setenv("HOMARR_AI_DAILY_REQUEST_LIMIT", "50")
|
||||
for _, unsafeURL := range []string{
|
||||
"https://user:password@router.example/v1",
|
||||
"https://router.example/v1?key=secret",
|
||||
"https://router.example/v1#fragment",
|
||||
} {
|
||||
t.Setenv("HOMARR_AI_OPENROUTER_BASE_URL", unsafeURL)
|
||||
if _, err := newHomarrProviderFromEnv(); err == nil {
|
||||
t.Fatalf("expected unsafe upstream URL %q to fail", unsafeURL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadBoundedBody(t *testing.T) {
|
||||
@@ -123,6 +186,20 @@ func TestReadBoundedBody(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCopyBounded(t *testing.T) {
|
||||
var target strings.Builder
|
||||
written, err := copyBounded(&target, strings.NewReader("1234"), 4)
|
||||
if err != nil || written != 4 || target.String() != "1234" {
|
||||
t.Fatalf("unexpected bounded copy: %q, %d, %v", target.String(), written, err)
|
||||
}
|
||||
|
||||
target.Reset()
|
||||
written, err = copyBounded(&target, strings.NewReader("12345"), 4)
|
||||
if err != errProviderResponseTooLarge || written != 4 || target.String() != "1234" {
|
||||
t.Fatalf("expected bounded copy failure, got %q, %d, %v", target.String(), written, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequestBelongsToQuotaDay(t *testing.T) {
|
||||
startedAt := time.Date(2026, 8, 12, 1, 30, 0, 0, time.FixedZone("CEST", 2*60*60))
|
||||
if !requestBelongsToQuotaDay("2026-08-11", startedAt) {
|
||||
|
||||
@@ -28,11 +28,34 @@ const server = createServer(async (request, response) => {
|
||||
json(response, 400, { error: { message: "Unexpected upstream model" } });
|
||||
return;
|
||||
}
|
||||
if (body.max_tokens !== 32_768 || body.n !== 1 || body.parallel_tool_calls !== false) {
|
||||
json(response, 400, { error: { message: "Unsafe generation controls reached the upstream" } });
|
||||
return;
|
||||
}
|
||||
if (
|
||||
"max_completion_tokens" in body ||
|
||||
"user" in body ||
|
||||
"metadata" in body ||
|
||||
"extra_body" in body ||
|
||||
"extra_headers" in body ||
|
||||
"max_tokens" in (body.reasoning ?? {})
|
||||
) {
|
||||
json(response, 400, { error: { message: "Private or unbounded request fields reached the upstream" } });
|
||||
return;
|
||||
}
|
||||
const forbiddenRoutingField = ["models", "provider", "route", "plugins", "transforms"].find((field) => field in body);
|
||||
if (forbiddenRoutingField) {
|
||||
json(response, 400, { error: { message: `Client routing field was forwarded: ${forbiddenRoutingField}` } });
|
||||
return;
|
||||
}
|
||||
const webSearchTools = body.tools?.filter((tool) => tool.type === "openrouter:web_search") ?? [];
|
||||
if (
|
||||
webSearchTools.length > 1 ||
|
||||
webSearchTools.some((tool) => tool.parameters?.max_results !== 5 || tool.parameters?.max_uses !== 3)
|
||||
) {
|
||||
json(response, 400, { error: { message: "Unbounded web search controls reached the upstream" } });
|
||||
return;
|
||||
}
|
||||
if (body.messages?.some((message) => message.content === "upstream failure")) {
|
||||
json(response, 503, { error: { message: "Simulated upstream failure" } });
|
||||
return;
|
||||
|
||||
@@ -136,11 +136,23 @@ const modelList = await request("/api/ai/v1/models");
|
||||
if (
|
||||
modelList.object !== "list" ||
|
||||
modelList.data.length !== 1 ||
|
||||
modelList.data[0].id !== "homarr/deepseek-v4-flash-latest"
|
||||
modelList.data[0].id !== "homarr/deepseek-v4-flash-latest" ||
|
||||
modelList.data[0].context_length !== 256 * 1024
|
||||
) {
|
||||
throw new Error("The Homarr provider must advertise exactly one model");
|
||||
}
|
||||
await expectStatus("/api/ai/usage", {}, 401);
|
||||
await expectStatus(
|
||||
"/api/ai/v1/chat/completions",
|
||||
{
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
model: "homarr/deepseek-v4-flash-latest",
|
||||
messages: [{ role: "user", content: "unauthenticated" }],
|
||||
}),
|
||||
},
|
||||
401,
|
||||
);
|
||||
|
||||
const initialUsage = await request("/api/ai/usage", { headers: authorSession.headers });
|
||||
const initialReset = new Date(initialUsage.resetsAt);
|
||||
@@ -173,6 +185,19 @@ await expectStatus(
|
||||
},
|
||||
400,
|
||||
);
|
||||
|
||||
await expectStatus(
|
||||
"/api/ai/v1/chat/completions",
|
||||
{
|
||||
method: "POST",
|
||||
headers: authorSession.headers,
|
||||
body: JSON.stringify({
|
||||
model: "homarr/deepseek-v4-flash-latest",
|
||||
messages: [{ role: "user", content: "x".repeat(256 * 1024) }],
|
||||
}),
|
||||
},
|
||||
413,
|
||||
);
|
||||
const usageAfterRejectedRequest = await request("/api/ai/usage", { headers: authorSession.headers });
|
||||
if (usageAfterRejectedRequest.used !== 0) {
|
||||
throw new Error("A rejected oversized request-unit batch consumed allowance");
|
||||
@@ -266,6 +291,20 @@ const toolResponse = await fetch(`${baseUrl}/api/ai/v1/chat/completions`, {
|
||||
route: "fallback",
|
||||
plugins: ["web"],
|
||||
transforms: ["middle-out"],
|
||||
max_tokens: 1_000_000,
|
||||
max_completion_tokens: 1_000_000,
|
||||
n: 100,
|
||||
parallel_tool_calls: true,
|
||||
extra_body: { provider: { order: ["attacker"] } },
|
||||
extra_headers: { Authorization: "Bearer attacker" },
|
||||
metadata: { user: "must-not-reach-openrouter" },
|
||||
reasoning: { effort: "high", max_tokens: 1_000_000 },
|
||||
user: "must-not-reach-openrouter",
|
||||
tools: [
|
||||
{ type: "function", function: { name: "board_list" } },
|
||||
{ type: "openrouter:web_search", parameters: { max_results: 1000, max_uses: 1000 } },
|
||||
{ type: "openrouter:web_search" },
|
||||
],
|
||||
}),
|
||||
});
|
||||
if (!toolResponse.ok || toolResponse.headers.get("x-homarr-quota-remaining") !== "46") {
|
||||
@@ -286,12 +325,20 @@ const upstreamFailure = await fetch(`${baseUrl}/api/ai/v1/chat/completions`, {
|
||||
stream: false,
|
||||
}),
|
||||
});
|
||||
if (upstreamFailure.status !== 503 || upstreamFailure.headers.get("x-homarr-quota-remaining") !== "46") {
|
||||
throw new Error("Failed upstream requests must refund their Homarr provider allowance");
|
||||
if (upstreamFailure.status !== 503 || upstreamFailure.headers.get("x-homarr-quota-remaining") !== "45") {
|
||||
throw new Error("Every request forwarded upstream must consume Homarr provider allowance");
|
||||
}
|
||||
const upstreamFailureBody = await upstreamFailure.text();
|
||||
if (upstreamFailureBody.includes("Simulated upstream failure")) {
|
||||
throw new Error("Upstream error details must not be exposed to clients");
|
||||
}
|
||||
const afterFailureUsage = await request("/api/ai/usage", { headers: authorSession.headers });
|
||||
if (afterFailureUsage.used !== 4 || afterFailureUsage.remaining !== 46) {
|
||||
throw new Error(`Failed upstream request was charged: ${JSON.stringify(afterFailureUsage)}`);
|
||||
if (afterFailureUsage.used !== 5 || afterFailureUsage.remaining !== 45) {
|
||||
throw new Error(`Forwarded upstream failure escaped quota accounting: ${JSON.stringify(afterFailureUsage)}`);
|
||||
}
|
||||
const visitorUsage = await request("/api/ai/usage", { headers: visitorSession.headers });
|
||||
if (visitorUsage.used !== 0 || visitorUsage.remaining !== 50) {
|
||||
throw new Error(`Provider allowance leaked across users: ${JSON.stringify(visitorUsage)}`);
|
||||
}
|
||||
|
||||
const publicActivities = await request("/api/collections/assistant_activity/records?sort=-created&perPage=10");
|
||||
@@ -343,7 +390,7 @@ if (retainedRequest.id !== requestToRetain.id || retainedRequest.publicActivity)
|
||||
}
|
||||
const quotas = await request("/api/collections/assistant_quotas/records?perPage=20", { headers: rootHeaders });
|
||||
const authorQuota = quotas.items.find((item) => item.user === author.id);
|
||||
if (!authorQuota || authorQuota.dailyLimit !== 50 || authorQuota.used !== 4 || authorQuota.totalTokens !== 38) {
|
||||
if (!authorQuota || authorQuota.dailyLimit !== 50 || authorQuota.used !== 5 || authorQuota.totalTokens !== 38) {
|
||||
throw new Error(`Private provider quota is incorrect: ${JSON.stringify(authorQuota)}`);
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user