feat(settings): add request body limit setting (#1968)

Integrated into release/v3.7.9
This commit is contained in:
Sergey Morozov
2026-05-05 15:12:35 +03:00
committed by GitHub
parent 2082ffbad5
commit 8077e9b62b
59 changed files with 1352 additions and 38 deletions

View File

@@ -5,6 +5,7 @@ import { Card, Button, ProxyConfigModal, Toggle } from "@/shared/components";
import { useTranslations } from "next-intl";
import ProxyRegistryManager from "./ProxyRegistryManager";
import OneproxyTab from "./OneproxyTab";
import RequestLimitsTab from "./RequestLimitsTab";
export default function ProxyTab() {
const [proxyModalOpen, setProxyModalOpen] = useState(false);
@@ -184,6 +185,7 @@ export default function ProxyTab() {
</div>
</div>
</Card>
<RequestLimitsTab />
</div>
<ProxyConfigModal

View File

@@ -0,0 +1,167 @@
"use client";
import { useCallback, useEffect, useMemo, useState } from "react";
import { Button, Card } from "@/shared/components";
import {
DEFAULT_REQUEST_BODY_LIMIT_MB,
MAX_REQUEST_BODY_LIMIT_MB,
MIN_REQUEST_BODY_LIMIT_MB,
} from "@/shared/constants/bodySize";
import { useTranslations } from "next-intl";
type Message = { type: "success" | "error"; text: string };
interface SettingsResponse {
maxBodySizeMb?: number;
[key: string]: unknown;
}
function normalizeInputValue(value: unknown): string {
return typeof value === "number" && Number.isFinite(value)
? String(value)
: String(DEFAULT_REQUEST_BODY_LIMIT_MB);
}
export default function RequestLimitsTab() {
const t = useTranslations("settings");
const [value, setValue] = useState(String(DEFAULT_REQUEST_BODY_LIMIT_MB));
const [savedValue, setSavedValue] = useState(String(DEFAULT_REQUEST_BODY_LIMIT_MB));
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [message, setMessage] = useState<Message | null>(null);
useEffect(() => {
let active = true;
fetch("/api/settings")
.then((response) => {
if (!response.ok) throw new Error(`Settings API returned ${response.status}`);
return response.json() as Promise<SettingsResponse>;
})
.then((settings) => {
if (!active) return;
const nextValue = normalizeInputValue(settings.maxBodySizeMb);
setValue(nextValue);
setSavedValue(nextValue);
})
.catch((error) => {
console.error("Failed to load request limit settings:", error);
if (active) {
setMessage({ type: "error", text: t("requestBodyLimitLoadFailed") });
}
})
.finally(() => {
if (active) setLoading(false);
});
return () => {
active = false;
};
}, [t]);
const validationError = useMemo(() => {
const trimmed = value.trim();
if (!trimmed) return t("requestBodyLimitEmptyError");
const parsed = Number(trimmed);
if (!Number.isInteger(parsed)) return t("requestBodyLimitWholeNumberError");
if (parsed < MIN_REQUEST_BODY_LIMIT_MB) {
return t("requestBodyLimitMinimumError", { min: MIN_REQUEST_BODY_LIMIT_MB });
}
if (parsed > MAX_REQUEST_BODY_LIMIT_MB) {
return t("requestBodyLimitMaximumError", { max: MAX_REQUEST_BODY_LIMIT_MB });
}
return null;
}, [t, value]);
const dirty = value.trim() !== savedValue;
const saveLimit = useCallback(async () => {
if (validationError || !dirty) return;
const nextValue = Number(value.trim());
setSaving(true);
setMessage(null);
try {
const response = await fetch("/api/settings", {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ maxBodySizeMb: nextValue }),
});
if (!response.ok) throw new Error(`Settings API returned ${response.status}`);
const settings = (await response.json()) as SettingsResponse;
const saved = normalizeInputValue(settings.maxBodySizeMb ?? nextValue);
setValue(saved);
setSavedValue(saved);
setMessage({ type: "success", text: t("requestBodyLimitSaveSuccess") });
} catch (error) {
console.error("Failed to save request body limit:", error);
setMessage({ type: "error", text: t("requestBodyLimitSaveFailed") });
} finally {
setSaving(false);
}
}, [dirty, t, validationError, value]);
return (
<Card className="p-6 mt-4">
<div className="flex flex-col gap-3">
<div>
<p className="font-medium">{t("requestBodyLimitTitle")}</p>
<p className="text-sm text-text-muted mt-1">{t("requestBodyLimitDescription")}</p>
</div>
<div className="flex items-center gap-3">
<label htmlFor="request-body-limit-mb" className="sr-only">
{t("requestBodyLimitInputLabel")}
</label>
<input
id="request-body-limit-mb"
type="number"
min={MIN_REQUEST_BODY_LIMIT_MB}
max={MAX_REQUEST_BODY_LIMIT_MB}
step={1}
value={value}
onChange={(event) => {
setValue(event.target.value);
setMessage(null);
}}
onKeyDown={(event) => {
if (event.key === "Enter" && dirty) void saveLimit();
}}
className="w-32 px-3 py-1.5 rounded bg-surface-2 border border-border text-sm text-text-primary"
disabled={loading || saving}
/>
<span className="text-xs text-text-muted">MB</span>
<Button
size="sm"
variant="primary"
disabled={loading || Boolean(validationError) || !dirty}
onClick={saveLimit}
>
{saving ? t("requestBodyLimitSaving") : t("requestBodyLimitSave")}
</Button>
{dirty && (
<span className="text-xs text-text-muted">
{t("requestBodyLimitCurrent", { value: savedValue })}
</span>
)}
</div>
{validationError && <p className="text-xs text-red-500">{validationError}</p>}
{message && (
<p
className={`text-xs ${
message.type === "success"
? "text-green-600 dark:text-green-400"
: "text-red-600 dark:text-red-400"
}`}
>
{message.text}
</p>
)}
</div>
</Card>
);
}

View File

@@ -4,6 +4,12 @@ import { useState, useEffect, useRef } from "react";
import { Card, Button, Badge } from "@/shared/components";
import { useLocale, useTranslations } from "next-intl";
const rowCountFormatter = new Intl.NumberFormat("en-US");
function formatRows(rows: number | null | undefined) {
return typeof rows === "number" ? rowCountFormatter.format(rows) : "100K";
}
export default function SystemStorageTab() {
const [backups, setBackups] = useState([]);
const [backupsLoading, setBackupsLoading] = useState(false);
@@ -588,7 +594,7 @@ export default function SystemStorageTab() {
App {storageHealth.retentionDays.app}d
</Badge>
<Badge variant="default" size="sm">
{storageHealth.tableMaxRows?.callLogs?.toLocaleString() || "100K"} rows
{formatRows(storageHealth.tableMaxRows?.callLogs)} rows
</Badge>
</div>
</div>

View File

@@ -136,6 +136,20 @@ function resolveModelPricing(
}
}
// Last resort fallback for historical usage (e.g. "gpt-4" missing, matches "gpt-4.1" or first available)
if (!pricing && providerPricing && typeof providerPricing === "object") {
for (const [key, val] of Object.entries(providerPricing as Record<string, unknown>)) {
if (key.includes(lowerModel) || lowerModel.includes(key)) {
pricing = val;
break;
}
}
if (!pricing) {
const keys = Object.keys(providerPricing as Record<string, unknown>);
if (keys.length > 0) pricing = (providerPricing as Record<string, unknown>)[keys[0]];
}
}
return pricing as Record<string, unknown> | null;
}

View File

@@ -3294,7 +3294,23 @@
"compressionModeRtk": "RTK",
"compressionModeRtkDesc": "Command-aware tool output filtering",
"compressionModeStacked": "Stacked",
"compressionModeStackedDesc": "RTK tool-output filtering followed by Caveman message compression"
"compressionModeStackedDesc": "RTK tool-output filtering followed by Caveman message compression",
"httpProxy": "HTTP Proxy",
"1proxy": "1proxy",
"proxySubTabsAria": "Proxy sections",
"requestBodyLimitTitle": "Request Body Limit",
"requestBodyLimitDescription": "Maximum API payload size accepted before a request body is parsed. Dedicated upload routes keep at least their built-in 100 MB limit.",
"requestBodyLimitInputLabel": "Request body limit in MB",
"requestBodyLimitEmptyError": "Enter a limit in MB",
"requestBodyLimitWholeNumberError": "Use a whole number",
"requestBodyLimitMinimumError": "Minimum is {min} MB",
"requestBodyLimitMaximumError": "Maximum is {max} MB",
"requestBodyLimitLoadFailed": "Failed to load request limit settings",
"requestBodyLimitSaveSuccess": "Request body limit saved",
"requestBodyLimitSaveFailed": "Failed to save request body limit",
"requestBodyLimitSaving": "Saving...",
"requestBodyLimitSave": "Save",
"requestBodyLimitCurrent": "Current: {value}"
},
"contextRtk": {
"title": "RTK Engine",

View File

@@ -2720,6 +2720,22 @@
"systemPrompt": "Системен ред",
"thinkingBudget": "Мислен бюджет",
"proxy": "Прокси",
"httpProxy": "HTTP Proxy",
"1proxy": "1proxy",
"proxySubTabsAria": "Proxy sections",
"requestBodyLimitTitle": "Request Body Limit",
"requestBodyLimitDescription": "Maximum API payload size accepted before a request body is parsed. Dedicated upload routes keep at least their built-in 100 MB limit.",
"requestBodyLimitInputLabel": "Request body limit in MB",
"requestBodyLimitEmptyError": "Enter a limit in MB",
"requestBodyLimitWholeNumberError": "Use a whole number",
"requestBodyLimitMinimumError": "Minimum is {min} MB",
"requestBodyLimitMaximumError": "Maximum is {max} MB",
"requestBodyLimitLoadFailed": "Failed to load request limit settings",
"requestBodyLimitSaveSuccess": "Request body limit saved",
"requestBodyLimitSaveFailed": "Failed to save request body limit",
"requestBodyLimitSaving": "Saving...",
"requestBodyLimitSave": "Save",
"requestBodyLimitCurrent": "Current: {value}",
"pricing": "Ценообразуване",
"storage": "Съхранение",
"policies": "Политики",

View File

@@ -3451,7 +3451,23 @@
"compressionModeRtk": "RTK",
"compressionModeRtkDesc": "Command-aware tool output filtering",
"compressionModeStacked": "Stacked",
"compressionModeStackedDesc": "RTK tool-output filtering followed by Caveman message compression"
"compressionModeStackedDesc": "RTK tool-output filtering followed by Caveman message compression",
"httpProxy": "HTTP Proxy",
"1proxy": "1proxy",
"proxySubTabsAria": "Proxy sections",
"requestBodyLimitTitle": "Request Body Limit",
"requestBodyLimitDescription": "Maximum API payload size accepted before a request body is parsed. Dedicated upload routes keep at least their built-in 100 MB limit.",
"requestBodyLimitInputLabel": "Request body limit in MB",
"requestBodyLimitEmptyError": "Enter a limit in MB",
"requestBodyLimitWholeNumberError": "Use a whole number",
"requestBodyLimitMinimumError": "Minimum is {min} MB",
"requestBodyLimitMaximumError": "Maximum is {max} MB",
"requestBodyLimitLoadFailed": "Failed to load request limit settings",
"requestBodyLimitSaveSuccess": "Request body limit saved",
"requestBodyLimitSaveFailed": "Failed to save request body limit",
"requestBodyLimitSaving": "Saving...",
"requestBodyLimitSave": "Save",
"requestBodyLimitCurrent": "Current: {value}"
},
"contextRtk": {
"title": "RTK Engine",

View File

@@ -2720,6 +2720,22 @@
"systemPrompt": "Systémový Prompt",
"thinkingBudget": "Promýšlení rozpočtu",
"proxy": "Proxy",
"httpProxy": "HTTP Proxy",
"1proxy": "1proxy",
"proxySubTabsAria": "Proxy sections",
"requestBodyLimitTitle": "Request Body Limit",
"requestBodyLimitDescription": "Maximum API payload size accepted before a request body is parsed. Dedicated upload routes keep at least their built-in 100 MB limit.",
"requestBodyLimitInputLabel": "Request body limit in MB",
"requestBodyLimitEmptyError": "Enter a limit in MB",
"requestBodyLimitWholeNumberError": "Use a whole number",
"requestBodyLimitMinimumError": "Minimum is {min} MB",
"requestBodyLimitMaximumError": "Maximum is {max} MB",
"requestBodyLimitLoadFailed": "Failed to load request limit settings",
"requestBodyLimitSaveSuccess": "Request body limit saved",
"requestBodyLimitSaveFailed": "Failed to save request body limit",
"requestBodyLimitSaving": "Saving...",
"requestBodyLimitSave": "Save",
"requestBodyLimitCurrent": "Current: {value}",
"pricing": "Ceny",
"storage": "Skladování",
"policies": "Zásady",

View File

@@ -3294,7 +3294,23 @@
"compressionModeRtk": "RTK",
"compressionModeRtkDesc": "Command-aware tool output filtering",
"compressionModeStacked": "Stacked",
"compressionModeStackedDesc": "RTK tool-output filtering followed by Caveman message compression"
"compressionModeStackedDesc": "RTK tool-output filtering followed by Caveman message compression",
"httpProxy": "HTTP Proxy",
"1proxy": "1proxy",
"proxySubTabsAria": "Proxy sections",
"requestBodyLimitTitle": "Request Body Limit",
"requestBodyLimitDescription": "Maximum API payload size accepted before a request body is parsed. Dedicated upload routes keep at least their built-in 100 MB limit.",
"requestBodyLimitInputLabel": "Request body limit in MB",
"requestBodyLimitEmptyError": "Enter a limit in MB",
"requestBodyLimitWholeNumberError": "Use a whole number",
"requestBodyLimitMinimumError": "Minimum is {min} MB",
"requestBodyLimitMaximumError": "Maximum is {max} MB",
"requestBodyLimitLoadFailed": "Failed to load request limit settings",
"requestBodyLimitSaveSuccess": "Request body limit saved",
"requestBodyLimitSaveFailed": "Failed to save request body limit",
"requestBodyLimitSaving": "Saving...",
"requestBodyLimitSave": "Save",
"requestBodyLimitCurrent": "Current: {value}"
},
"contextRtk": {
"title": "RTK Engine",

View File

@@ -3294,7 +3294,23 @@
"compressionModeRtk": "RTK",
"compressionModeRtkDesc": "Command-aware tool output filtering",
"compressionModeStacked": "Stacked",
"compressionModeStackedDesc": "RTK tool-output filtering followed by Caveman message compression"
"compressionModeStackedDesc": "RTK tool-output filtering followed by Caveman message compression",
"httpProxy": "HTTP Proxy",
"1proxy": "1proxy",
"proxySubTabsAria": "Proxy sections",
"requestBodyLimitTitle": "Request Body Limit",
"requestBodyLimitDescription": "Maximum API payload size accepted before a request body is parsed. Dedicated upload routes keep at least their built-in 100 MB limit.",
"requestBodyLimitInputLabel": "Request body limit in MB",
"requestBodyLimitEmptyError": "Enter a limit in MB",
"requestBodyLimitWholeNumberError": "Use a whole number",
"requestBodyLimitMinimumError": "Minimum is {min} MB",
"requestBodyLimitMaximumError": "Maximum is {max} MB",
"requestBodyLimitLoadFailed": "Failed to load request limit settings",
"requestBodyLimitSaveSuccess": "Request body limit saved",
"requestBodyLimitSaveFailed": "Failed to save request body limit",
"requestBodyLimitSaving": "Saving...",
"requestBodyLimitSave": "Save",
"requestBodyLimitCurrent": "Current: {value}"
},
"contextRtk": {
"title": "RTK Engine",

View File

@@ -3102,6 +3102,22 @@
"systemPrompt": "System Prompt",
"thinkingBudget": "Thinking Budget",
"proxy": "Proxy",
"httpProxy": "HTTP Proxy",
"1proxy": "1proxy",
"proxySubTabsAria": "Proxy sections",
"requestBodyLimitTitle": "Request Body Limit",
"requestBodyLimitDescription": "Maximum API payload size accepted before a request body is parsed. Dedicated upload routes keep at least their built-in 100 MB limit.",
"requestBodyLimitInputLabel": "Request body limit in MB",
"requestBodyLimitEmptyError": "Enter a limit in MB",
"requestBodyLimitWholeNumberError": "Use a whole number",
"requestBodyLimitMinimumError": "Minimum is {min} MB",
"requestBodyLimitMaximumError": "Maximum is {max} MB",
"requestBodyLimitLoadFailed": "Failed to load request limit settings",
"requestBodyLimitSaveSuccess": "Request body limit saved",
"requestBodyLimitSaveFailed": "Failed to save request body limit",
"requestBodyLimitSaving": "Saving...",
"requestBodyLimitSave": "Save",
"requestBodyLimitCurrent": "Current: {value}",
"mitmProxy": "MITM Proxy",
"pricing": "Pricing",
"storage": "Storage",

View File

@@ -3294,7 +3294,23 @@
"compressionModeRtk": "RTK",
"compressionModeRtkDesc": "Command-aware tool output filtering",
"compressionModeStacked": "Stacked",
"compressionModeStackedDesc": "RTK tool-output filtering followed by Caveman message compression"
"compressionModeStackedDesc": "RTK tool-output filtering followed by Caveman message compression",
"httpProxy": "HTTP Proxy",
"1proxy": "1proxy",
"proxySubTabsAria": "Proxy sections",
"requestBodyLimitTitle": "Request Body Limit",
"requestBodyLimitDescription": "Maximum API payload size accepted before a request body is parsed. Dedicated upload routes keep at least their built-in 100 MB limit.",
"requestBodyLimitInputLabel": "Request body limit in MB",
"requestBodyLimitEmptyError": "Enter a limit in MB",
"requestBodyLimitWholeNumberError": "Use a whole number",
"requestBodyLimitMinimumError": "Minimum is {min} MB",
"requestBodyLimitMaximumError": "Maximum is {max} MB",
"requestBodyLimitLoadFailed": "Failed to load request limit settings",
"requestBodyLimitSaveSuccess": "Request body limit saved",
"requestBodyLimitSaveFailed": "Failed to save request body limit",
"requestBodyLimitSaving": "Saving...",
"requestBodyLimitSave": "Save",
"requestBodyLimitCurrent": "Current: {value}"
},
"contextRtk": {
"title": "RTK Engine",

View File

@@ -3451,7 +3451,23 @@
"compressionModeRtk": "RTK",
"compressionModeRtkDesc": "Command-aware tool output filtering",
"compressionModeStacked": "Stacked",
"compressionModeStackedDesc": "RTK tool-output filtering followed by Caveman message compression"
"compressionModeStackedDesc": "RTK tool-output filtering followed by Caveman message compression",
"httpProxy": "HTTP Proxy",
"1proxy": "1proxy",
"proxySubTabsAria": "Proxy sections",
"requestBodyLimitTitle": "Request Body Limit",
"requestBodyLimitDescription": "Maximum API payload size accepted before a request body is parsed. Dedicated upload routes keep at least their built-in 100 MB limit.",
"requestBodyLimitInputLabel": "Request body limit in MB",
"requestBodyLimitEmptyError": "Enter a limit in MB",
"requestBodyLimitWholeNumberError": "Use a whole number",
"requestBodyLimitMinimumError": "Minimum is {min} MB",
"requestBodyLimitMaximumError": "Maximum is {max} MB",
"requestBodyLimitLoadFailed": "Failed to load request limit settings",
"requestBodyLimitSaveSuccess": "Request body limit saved",
"requestBodyLimitSaveFailed": "Failed to save request body limit",
"requestBodyLimitSaving": "Saving...",
"requestBodyLimitSave": "Save",
"requestBodyLimitCurrent": "Current: {value}"
},
"contextRtk": {
"title": "RTK Engine",

View File

@@ -2720,6 +2720,22 @@
"systemPrompt": "Järjestelmäkehote",
"thinkingBudget": "Miettivä budjetti",
"proxy": "Välityspalvelin",
"httpProxy": "HTTP Proxy",
"1proxy": "1proxy",
"proxySubTabsAria": "Proxy sections",
"requestBodyLimitTitle": "Request Body Limit",
"requestBodyLimitDescription": "Maximum API payload size accepted before a request body is parsed. Dedicated upload routes keep at least their built-in 100 MB limit.",
"requestBodyLimitInputLabel": "Request body limit in MB",
"requestBodyLimitEmptyError": "Enter a limit in MB",
"requestBodyLimitWholeNumberError": "Use a whole number",
"requestBodyLimitMinimumError": "Minimum is {min} MB",
"requestBodyLimitMaximumError": "Maximum is {max} MB",
"requestBodyLimitLoadFailed": "Failed to load request limit settings",
"requestBodyLimitSaveSuccess": "Request body limit saved",
"requestBodyLimitSaveFailed": "Failed to save request body limit",
"requestBodyLimitSaving": "Saving...",
"requestBodyLimitSave": "Save",
"requestBodyLimitCurrent": "Current: {value}",
"pricing": "Hinnoittelu",
"storage": "Varastointi",
"policies": "Käytännöt",

View File

@@ -3294,7 +3294,23 @@
"compressionModeRtk": "RTK",
"compressionModeRtkDesc": "Command-aware tool output filtering",
"compressionModeStacked": "Stacked",
"compressionModeStackedDesc": "RTK tool-output filtering followed by Caveman message compression"
"compressionModeStackedDesc": "RTK tool-output filtering followed by Caveman message compression",
"httpProxy": "HTTP Proxy",
"1proxy": "1proxy",
"proxySubTabsAria": "Proxy sections",
"requestBodyLimitTitle": "Request Body Limit",
"requestBodyLimitDescription": "Maximum API payload size accepted before a request body is parsed. Dedicated upload routes keep at least their built-in 100 MB limit.",
"requestBodyLimitInputLabel": "Request body limit in MB",
"requestBodyLimitEmptyError": "Enter a limit in MB",
"requestBodyLimitWholeNumberError": "Use a whole number",
"requestBodyLimitMinimumError": "Minimum is {min} MB",
"requestBodyLimitMaximumError": "Maximum is {max} MB",
"requestBodyLimitLoadFailed": "Failed to load request limit settings",
"requestBodyLimitSaveSuccess": "Request body limit saved",
"requestBodyLimitSaveFailed": "Failed to save request body limit",
"requestBodyLimitSaving": "Saving...",
"requestBodyLimitSave": "Save",
"requestBodyLimitCurrent": "Current: {value}"
},
"contextRtk": {
"title": "RTK Engine",

View File

@@ -3451,7 +3451,23 @@
"compressionModeRtk": "RTK",
"compressionModeRtkDesc": "Command-aware tool output filtering",
"compressionModeStacked": "Stacked",
"compressionModeStackedDesc": "RTK tool-output filtering followed by Caveman message compression"
"compressionModeStackedDesc": "RTK tool-output filtering followed by Caveman message compression",
"httpProxy": "HTTP Proxy",
"1proxy": "1proxy",
"proxySubTabsAria": "Proxy sections",
"requestBodyLimitTitle": "Request Body Limit",
"requestBodyLimitDescription": "Maximum API payload size accepted before a request body is parsed. Dedicated upload routes keep at least their built-in 100 MB limit.",
"requestBodyLimitInputLabel": "Request body limit in MB",
"requestBodyLimitEmptyError": "Enter a limit in MB",
"requestBodyLimitWholeNumberError": "Use a whole number",
"requestBodyLimitMinimumError": "Minimum is {min} MB",
"requestBodyLimitMaximumError": "Maximum is {max} MB",
"requestBodyLimitLoadFailed": "Failed to load request limit settings",
"requestBodyLimitSaveSuccess": "Request body limit saved",
"requestBodyLimitSaveFailed": "Failed to save request body limit",
"requestBodyLimitSaving": "Saving...",
"requestBodyLimitSave": "Save",
"requestBodyLimitCurrent": "Current: {value}"
},
"contextRtk": {
"title": "RTK Engine",

View File

@@ -2720,6 +2720,22 @@
"systemPrompt": "הנחית מערכת",
"thinkingBudget": "חשיבה תקציב",
"proxy": "פרוקסי",
"httpProxy": "HTTP Proxy",
"1proxy": "1proxy",
"proxySubTabsAria": "Proxy sections",
"requestBodyLimitTitle": "Request Body Limit",
"requestBodyLimitDescription": "Maximum API payload size accepted before a request body is parsed. Dedicated upload routes keep at least their built-in 100 MB limit.",
"requestBodyLimitInputLabel": "Request body limit in MB",
"requestBodyLimitEmptyError": "Enter a limit in MB",
"requestBodyLimitWholeNumberError": "Use a whole number",
"requestBodyLimitMinimumError": "Minimum is {min} MB",
"requestBodyLimitMaximumError": "Maximum is {max} MB",
"requestBodyLimitLoadFailed": "Failed to load request limit settings",
"requestBodyLimitSaveSuccess": "Request body limit saved",
"requestBodyLimitSaveFailed": "Failed to save request body limit",
"requestBodyLimitSaving": "Saving...",
"requestBodyLimitSave": "Save",
"requestBodyLimitCurrent": "Current: {value}",
"pricing": "תמחור",
"storage": "אחסון",
"policies": "מדיניות",

View File

@@ -2720,6 +2720,22 @@
"systemPrompt": "सिस्टम प्रॉम्प्ट",
"thinkingBudget": "सोच बजट",
"proxy": "प्रॉक्सी",
"httpProxy": "HTTP Proxy",
"1proxy": "1proxy",
"proxySubTabsAria": "Proxy sections",
"requestBodyLimitTitle": "Request Body Limit",
"requestBodyLimitDescription": "Maximum API payload size accepted before a request body is parsed. Dedicated upload routes keep at least their built-in 100 MB limit.",
"requestBodyLimitInputLabel": "Request body limit in MB",
"requestBodyLimitEmptyError": "Enter a limit in MB",
"requestBodyLimitWholeNumberError": "Use a whole number",
"requestBodyLimitMinimumError": "Minimum is {min} MB",
"requestBodyLimitMaximumError": "Maximum is {max} MB",
"requestBodyLimitLoadFailed": "Failed to load request limit settings",
"requestBodyLimitSaveSuccess": "Request body limit saved",
"requestBodyLimitSaveFailed": "Failed to save request body limit",
"requestBodyLimitSaving": "Saving...",
"requestBodyLimitSave": "Save",
"requestBodyLimitCurrent": "Current: {value}",
"pricing": "मूल्य निर्धारण",
"storage": "भंडारण",
"policies": "नीतियाँ",

View File

@@ -2720,6 +2720,22 @@
"systemPrompt": "Rendszer Prompt",
"thinkingBudget": "Gondolkodó költségvetés",
"proxy": "Proxy",
"httpProxy": "HTTP Proxy",
"1proxy": "1proxy",
"proxySubTabsAria": "Proxy sections",
"requestBodyLimitTitle": "Request Body Limit",
"requestBodyLimitDescription": "Maximum API payload size accepted before a request body is parsed. Dedicated upload routes keep at least their built-in 100 MB limit.",
"requestBodyLimitInputLabel": "Request body limit in MB",
"requestBodyLimitEmptyError": "Enter a limit in MB",
"requestBodyLimitWholeNumberError": "Use a whole number",
"requestBodyLimitMinimumError": "Minimum is {min} MB",
"requestBodyLimitMaximumError": "Maximum is {max} MB",
"requestBodyLimitLoadFailed": "Failed to load request limit settings",
"requestBodyLimitSaveSuccess": "Request body limit saved",
"requestBodyLimitSaveFailed": "Failed to save request body limit",
"requestBodyLimitSaving": "Saving...",
"requestBodyLimitSave": "Save",
"requestBodyLimitCurrent": "Current: {value}",
"pricing": "Árképzés",
"storage": "Tárolás",
"policies": "Irányelvek",

View File

@@ -2720,6 +2720,22 @@
"systemPrompt": "Perintah Sistem",
"thinkingBudget": "Memikirkan Anggaran",
"proxy": "Proksi",
"httpProxy": "HTTP Proxy",
"1proxy": "1proxy",
"proxySubTabsAria": "Proxy sections",
"requestBodyLimitTitle": "Request Body Limit",
"requestBodyLimitDescription": "Maximum API payload size accepted before a request body is parsed. Dedicated upload routes keep at least their built-in 100 MB limit.",
"requestBodyLimitInputLabel": "Request body limit in MB",
"requestBodyLimitEmptyError": "Enter a limit in MB",
"requestBodyLimitWholeNumberError": "Use a whole number",
"requestBodyLimitMinimumError": "Minimum is {min} MB",
"requestBodyLimitMaximumError": "Maximum is {max} MB",
"requestBodyLimitLoadFailed": "Failed to load request limit settings",
"requestBodyLimitSaveSuccess": "Request body limit saved",
"requestBodyLimitSaveFailed": "Failed to save request body limit",
"requestBodyLimitSaving": "Saving...",
"requestBodyLimitSave": "Save",
"requestBodyLimitCurrent": "Current: {value}",
"pricing": "Harga",
"storage": "Penyimpanan",
"policies": "Kebijakan",

View File

@@ -3451,7 +3451,23 @@
"compressionModeRtk": "RTK",
"compressionModeRtkDesc": "Command-aware tool output filtering",
"compressionModeStacked": "Stacked",
"compressionModeStackedDesc": "RTK tool-output filtering followed by Caveman message compression"
"compressionModeStackedDesc": "RTK tool-output filtering followed by Caveman message compression",
"httpProxy": "HTTP Proxy",
"1proxy": "1proxy",
"proxySubTabsAria": "Proxy sections",
"requestBodyLimitTitle": "Request Body Limit",
"requestBodyLimitDescription": "Maximum API payload size accepted before a request body is parsed. Dedicated upload routes keep at least their built-in 100 MB limit.",
"requestBodyLimitInputLabel": "Request body limit in MB",
"requestBodyLimitEmptyError": "Enter a limit in MB",
"requestBodyLimitWholeNumberError": "Use a whole number",
"requestBodyLimitMinimumError": "Minimum is {min} MB",
"requestBodyLimitMaximumError": "Maximum is {max} MB",
"requestBodyLimitLoadFailed": "Failed to load request limit settings",
"requestBodyLimitSaveSuccess": "Request body limit saved",
"requestBodyLimitSaveFailed": "Failed to save request body limit",
"requestBodyLimitSaving": "Saving...",
"requestBodyLimitSave": "Save",
"requestBodyLimitCurrent": "Current: {value}"
},
"contextRtk": {
"title": "RTK Engine",

View File

@@ -3294,7 +3294,23 @@
"compressionModeRtk": "RTK",
"compressionModeRtkDesc": "Command-aware tool output filtering",
"compressionModeStacked": "Stacked",
"compressionModeStackedDesc": "RTK tool-output filtering followed by Caveman message compression"
"compressionModeStackedDesc": "RTK tool-output filtering followed by Caveman message compression",
"httpProxy": "HTTP Proxy",
"1proxy": "1proxy",
"proxySubTabsAria": "Proxy sections",
"requestBodyLimitTitle": "Request Body Limit",
"requestBodyLimitDescription": "Maximum API payload size accepted before a request body is parsed. Dedicated upload routes keep at least their built-in 100 MB limit.",
"requestBodyLimitInputLabel": "Request body limit in MB",
"requestBodyLimitEmptyError": "Enter a limit in MB",
"requestBodyLimitWholeNumberError": "Use a whole number",
"requestBodyLimitMinimumError": "Minimum is {min} MB",
"requestBodyLimitMaximumError": "Maximum is {max} MB",
"requestBodyLimitLoadFailed": "Failed to load request limit settings",
"requestBodyLimitSaveSuccess": "Request body limit saved",
"requestBodyLimitSaveFailed": "Failed to save request body limit",
"requestBodyLimitSaving": "Saving...",
"requestBodyLimitSave": "Save",
"requestBodyLimitCurrent": "Current: {value}"
},
"contextRtk": {
"title": "RTK Engine",

View File

@@ -2720,6 +2720,22 @@
"systemPrompt": "システムプロンプト",
"thinkingBudget": "予算を考える",
"proxy": "プロキシ",
"httpProxy": "HTTP Proxy",
"1proxy": "1proxy",
"proxySubTabsAria": "Proxy sections",
"requestBodyLimitTitle": "Request Body Limit",
"requestBodyLimitDescription": "Maximum API payload size accepted before a request body is parsed. Dedicated upload routes keep at least their built-in 100 MB limit.",
"requestBodyLimitInputLabel": "Request body limit in MB",
"requestBodyLimitEmptyError": "Enter a limit in MB",
"requestBodyLimitWholeNumberError": "Use a whole number",
"requestBodyLimitMinimumError": "Minimum is {min} MB",
"requestBodyLimitMaximumError": "Maximum is {max} MB",
"requestBodyLimitLoadFailed": "Failed to load request limit settings",
"requestBodyLimitSaveSuccess": "Request body limit saved",
"requestBodyLimitSaveFailed": "Failed to save request body limit",
"requestBodyLimitSaving": "Saving...",
"requestBodyLimitSave": "Save",
"requestBodyLimitCurrent": "Current: {value}",
"pricing": "価格設定",
"storage": "ストレージ",
"policies": "ポリシー",

View File

@@ -2720,6 +2720,22 @@
"systemPrompt": "시스템 프롬프트",
"thinkingBudget": "생각하는 예산",
"proxy": "프록시",
"httpProxy": "HTTP Proxy",
"1proxy": "1proxy",
"proxySubTabsAria": "Proxy sections",
"requestBodyLimitTitle": "Request Body Limit",
"requestBodyLimitDescription": "Maximum API payload size accepted before a request body is parsed. Dedicated upload routes keep at least their built-in 100 MB limit.",
"requestBodyLimitInputLabel": "Request body limit in MB",
"requestBodyLimitEmptyError": "Enter a limit in MB",
"requestBodyLimitWholeNumberError": "Use a whole number",
"requestBodyLimitMinimumError": "Minimum is {min} MB",
"requestBodyLimitMaximumError": "Maximum is {max} MB",
"requestBodyLimitLoadFailed": "Failed to load request limit settings",
"requestBodyLimitSaveSuccess": "Request body limit saved",
"requestBodyLimitSaveFailed": "Failed to save request body limit",
"requestBodyLimitSaving": "Saving...",
"requestBodyLimitSave": "Save",
"requestBodyLimitCurrent": "Current: {value}",
"pricing": "가격",
"storage": "저장",
"policies": "정책",

View File

@@ -3451,7 +3451,23 @@
"compressionModeRtk": "RTK",
"compressionModeRtkDesc": "Command-aware tool output filtering",
"compressionModeStacked": "Stacked",
"compressionModeStackedDesc": "RTK tool-output filtering followed by Caveman message compression"
"compressionModeStackedDesc": "RTK tool-output filtering followed by Caveman message compression",
"httpProxy": "HTTP Proxy",
"1proxy": "1proxy",
"proxySubTabsAria": "Proxy sections",
"requestBodyLimitTitle": "Request Body Limit",
"requestBodyLimitDescription": "Maximum API payload size accepted before a request body is parsed. Dedicated upload routes keep at least their built-in 100 MB limit.",
"requestBodyLimitInputLabel": "Request body limit in MB",
"requestBodyLimitEmptyError": "Enter a limit in MB",
"requestBodyLimitWholeNumberError": "Use a whole number",
"requestBodyLimitMinimumError": "Minimum is {min} MB",
"requestBodyLimitMaximumError": "Maximum is {max} MB",
"requestBodyLimitLoadFailed": "Failed to load request limit settings",
"requestBodyLimitSaveSuccess": "Request body limit saved",
"requestBodyLimitSaveFailed": "Failed to save request body limit",
"requestBodyLimitSaving": "Saving...",
"requestBodyLimitSave": "Save",
"requestBodyLimitCurrent": "Current: {value}"
},
"contextRtk": {
"title": "RTK Engine",

View File

@@ -3294,7 +3294,23 @@
"compressionModeRtk": "RTK",
"compressionModeRtkDesc": "Command-aware tool output filtering",
"compressionModeStacked": "Stacked",
"compressionModeStackedDesc": "RTK tool-output filtering followed by Caveman message compression"
"compressionModeStackedDesc": "RTK tool-output filtering followed by Caveman message compression",
"httpProxy": "HTTP Proxy",
"1proxy": "1proxy",
"proxySubTabsAria": "Proxy sections",
"requestBodyLimitTitle": "Request Body Limit",
"requestBodyLimitDescription": "Maximum API payload size accepted before a request body is parsed. Dedicated upload routes keep at least their built-in 100 MB limit.",
"requestBodyLimitInputLabel": "Request body limit in MB",
"requestBodyLimitEmptyError": "Enter a limit in MB",
"requestBodyLimitWholeNumberError": "Use a whole number",
"requestBodyLimitMinimumError": "Minimum is {min} MB",
"requestBodyLimitMaximumError": "Maximum is {max} MB",
"requestBodyLimitLoadFailed": "Failed to load request limit settings",
"requestBodyLimitSaveSuccess": "Request body limit saved",
"requestBodyLimitSaveFailed": "Failed to save request body limit",
"requestBodyLimitSaving": "Saving...",
"requestBodyLimitSave": "Save",
"requestBodyLimitCurrent": "Current: {value}"
},
"contextRtk": {
"title": "RTK Engine",

View File

@@ -2720,6 +2720,22 @@
"systemPrompt": "Systeemprompt",
"thinkingBudget": "Denkbudget",
"proxy": "Proxy",
"httpProxy": "HTTP Proxy",
"1proxy": "1proxy",
"proxySubTabsAria": "Proxy sections",
"requestBodyLimitTitle": "Request Body Limit",
"requestBodyLimitDescription": "Maximum API payload size accepted before a request body is parsed. Dedicated upload routes keep at least their built-in 100 MB limit.",
"requestBodyLimitInputLabel": "Request body limit in MB",
"requestBodyLimitEmptyError": "Enter a limit in MB",
"requestBodyLimitWholeNumberError": "Use a whole number",
"requestBodyLimitMinimumError": "Minimum is {min} MB",
"requestBodyLimitMaximumError": "Maximum is {max} MB",
"requestBodyLimitLoadFailed": "Failed to load request limit settings",
"requestBodyLimitSaveSuccess": "Request body limit saved",
"requestBodyLimitSaveFailed": "Failed to save request body limit",
"requestBodyLimitSaving": "Saving...",
"requestBodyLimitSave": "Save",
"requestBodyLimitCurrent": "Current: {value}",
"pricing": "Prijzen",
"storage": "Opslag",
"policies": "Beleid",

View File

@@ -3294,7 +3294,23 @@
"compressionModeRtk": "RTK",
"compressionModeRtkDesc": "Command-aware tool output filtering",
"compressionModeStacked": "Stacked",
"compressionModeStackedDesc": "RTK tool-output filtering followed by Caveman message compression"
"compressionModeStackedDesc": "RTK tool-output filtering followed by Caveman message compression",
"httpProxy": "HTTP Proxy",
"1proxy": "1proxy",
"proxySubTabsAria": "Proxy sections",
"requestBodyLimitTitle": "Request Body Limit",
"requestBodyLimitDescription": "Maximum API payload size accepted before a request body is parsed. Dedicated upload routes keep at least their built-in 100 MB limit.",
"requestBodyLimitInputLabel": "Request body limit in MB",
"requestBodyLimitEmptyError": "Enter a limit in MB",
"requestBodyLimitWholeNumberError": "Use a whole number",
"requestBodyLimitMinimumError": "Minimum is {min} MB",
"requestBodyLimitMaximumError": "Maximum is {max} MB",
"requestBodyLimitLoadFailed": "Failed to load request limit settings",
"requestBodyLimitSaveSuccess": "Request body limit saved",
"requestBodyLimitSaveFailed": "Failed to save request body limit",
"requestBodyLimitSaving": "Saving...",
"requestBodyLimitSave": "Save",
"requestBodyLimitCurrent": "Current: {value}"
},
"contextRtk": {
"title": "RTK Engine",

View File

@@ -2720,6 +2720,22 @@
"systemPrompt": "System Prompt",
"thinkingBudget": "Pag-iisip na Badyet",
"proxy": "Proxy",
"httpProxy": "HTTP Proxy",
"1proxy": "1proxy",
"proxySubTabsAria": "Proxy sections",
"requestBodyLimitTitle": "Request Body Limit",
"requestBodyLimitDescription": "Maximum API payload size accepted before a request body is parsed. Dedicated upload routes keep at least their built-in 100 MB limit.",
"requestBodyLimitInputLabel": "Request body limit in MB",
"requestBodyLimitEmptyError": "Enter a limit in MB",
"requestBodyLimitWholeNumberError": "Use a whole number",
"requestBodyLimitMinimumError": "Minimum is {min} MB",
"requestBodyLimitMaximumError": "Maximum is {max} MB",
"requestBodyLimitLoadFailed": "Failed to load request limit settings",
"requestBodyLimitSaveSuccess": "Request body limit saved",
"requestBodyLimitSaveFailed": "Failed to save request body limit",
"requestBodyLimitSaving": "Saving...",
"requestBodyLimitSave": "Save",
"requestBodyLimitCurrent": "Current: {value}",
"pricing": "Pagpepresyo",
"storage": "Imbakan",
"policies": "Mga patakaran",

View File

@@ -3294,7 +3294,23 @@
"compressionModeRtk": "RTK",
"compressionModeRtkDesc": "Command-aware tool output filtering",
"compressionModeStacked": "Stacked",
"compressionModeStackedDesc": "RTK tool-output filtering followed by Caveman message compression"
"compressionModeStackedDesc": "RTK tool-output filtering followed by Caveman message compression",
"httpProxy": "HTTP Proxy",
"1proxy": "1proxy",
"proxySubTabsAria": "Proxy sections",
"requestBodyLimitTitle": "Request Body Limit",
"requestBodyLimitDescription": "Maximum API payload size accepted before a request body is parsed. Dedicated upload routes keep at least their built-in 100 MB limit.",
"requestBodyLimitInputLabel": "Request body limit in MB",
"requestBodyLimitEmptyError": "Enter a limit in MB",
"requestBodyLimitWholeNumberError": "Use a whole number",
"requestBodyLimitMinimumError": "Minimum is {min} MB",
"requestBodyLimitMaximumError": "Maximum is {max} MB",
"requestBodyLimitLoadFailed": "Failed to load request limit settings",
"requestBodyLimitSaveSuccess": "Request body limit saved",
"requestBodyLimitSaveFailed": "Failed to save request body limit",
"requestBodyLimitSaving": "Saving...",
"requestBodyLimitSave": "Save",
"requestBodyLimitCurrent": "Current: {value}"
},
"contextRtk": {
"title": "RTK Engine",

View File

@@ -2822,6 +2822,22 @@
"systemPrompt": "Prompt do Sistema",
"thinkingBudget": "Orçamento de Raciocínio",
"proxy": "Proxy",
"httpProxy": "HTTP Proxy",
"1proxy": "1proxy",
"proxySubTabsAria": "Proxy sections",
"requestBodyLimitTitle": "Request Body Limit",
"requestBodyLimitDescription": "Maximum API payload size accepted before a request body is parsed. Dedicated upload routes keep at least their built-in 100 MB limit.",
"requestBodyLimitInputLabel": "Request body limit in MB",
"requestBodyLimitEmptyError": "Enter a limit in MB",
"requestBodyLimitWholeNumberError": "Use a whole number",
"requestBodyLimitMinimumError": "Minimum is {min} MB",
"requestBodyLimitMaximumError": "Maximum is {max} MB",
"requestBodyLimitLoadFailed": "Failed to load request limit settings",
"requestBodyLimitSaveSuccess": "Request body limit saved",
"requestBodyLimitSaveFailed": "Failed to save request body limit",
"requestBodyLimitSaving": "Saving...",
"requestBodyLimitSave": "Save",
"requestBodyLimitCurrent": "Current: {value}",
"pricing": "Preços",
"storage": "Armazenamento",
"policies": "Políticas",

View File

@@ -3381,7 +3381,23 @@
"compressionModeRtk": "RTK",
"compressionModeRtkDesc": "Command-aware tool output filtering",
"compressionModeStacked": "Stacked",
"compressionModeStackedDesc": "RTK tool-output filtering followed by Caveman message compression"
"compressionModeStackedDesc": "RTK tool-output filtering followed by Caveman message compression",
"httpProxy": "HTTP Proxy",
"1proxy": "1proxy",
"proxySubTabsAria": "Proxy sections",
"requestBodyLimitTitle": "Request Body Limit",
"requestBodyLimitDescription": "Maximum API payload size accepted before a request body is parsed. Dedicated upload routes keep at least their built-in 100 MB limit.",
"requestBodyLimitInputLabel": "Request body limit in MB",
"requestBodyLimitEmptyError": "Enter a limit in MB",
"requestBodyLimitWholeNumberError": "Use a whole number",
"requestBodyLimitMinimumError": "Minimum is {min} MB",
"requestBodyLimitMaximumError": "Maximum is {max} MB",
"requestBodyLimitLoadFailed": "Failed to load request limit settings",
"requestBodyLimitSaveSuccess": "Request body limit saved",
"requestBodyLimitSaveFailed": "Failed to save request body limit",
"requestBodyLimitSaving": "Saving...",
"requestBodyLimitSave": "Save",
"requestBodyLimitCurrent": "Current: {value}"
},
"contextRtk": {
"title": "RTK Engine",

View File

@@ -2720,6 +2720,22 @@
"systemPrompt": "Prompt de sistem",
"thinkingBudget": "Gândirea bugetului",
"proxy": "Proxy",
"httpProxy": "HTTP Proxy",
"1proxy": "1proxy",
"proxySubTabsAria": "Proxy sections",
"requestBodyLimitTitle": "Request Body Limit",
"requestBodyLimitDescription": "Maximum API payload size accepted before a request body is parsed. Dedicated upload routes keep at least their built-in 100 MB limit.",
"requestBodyLimitInputLabel": "Request body limit in MB",
"requestBodyLimitEmptyError": "Enter a limit in MB",
"requestBodyLimitWholeNumberError": "Use a whole number",
"requestBodyLimitMinimumError": "Minimum is {min} MB",
"requestBodyLimitMaximumError": "Maximum is {max} MB",
"requestBodyLimitLoadFailed": "Failed to load request limit settings",
"requestBodyLimitSaveSuccess": "Request body limit saved",
"requestBodyLimitSaveFailed": "Failed to save request body limit",
"requestBodyLimitSaving": "Saving...",
"requestBodyLimitSave": "Save",
"requestBodyLimitCurrent": "Current: {value}",
"pricing": "Prețuri",
"storage": "Depozitare",
"policies": "Politici",

View File

@@ -2744,6 +2744,22 @@
"systemPrompt": "Системная подсказка",
"thinkingBudget": "Думая о бюджете",
"proxy": "Прокси",
"httpProxy": "HTTP Proxy",
"1proxy": "1proxy",
"proxySubTabsAria": "Proxy sections",
"requestBodyLimitTitle": "Request Body Limit",
"requestBodyLimitDescription": "Maximum API payload size accepted before a request body is parsed. Dedicated upload routes keep at least their built-in 100 MB limit.",
"requestBodyLimitInputLabel": "Request body limit in MB",
"requestBodyLimitEmptyError": "Enter a limit in MB",
"requestBodyLimitWholeNumberError": "Use a whole number",
"requestBodyLimitMinimumError": "Minimum is {min} MB",
"requestBodyLimitMaximumError": "Maximum is {max} MB",
"requestBodyLimitLoadFailed": "Failed to load request limit settings",
"requestBodyLimitSaveSuccess": "Request body limit saved",
"requestBodyLimitSaveFailed": "Failed to save request body limit",
"requestBodyLimitSaving": "Saving...",
"requestBodyLimitSave": "Save",
"requestBodyLimitCurrent": "Current: {value}",
"pricing": "Цены",
"storage": "Хранение",
"policies": "Политика",

View File

@@ -2720,6 +2720,22 @@
"systemPrompt": "Systémová výzva",
"thinkingBudget": "Myslenie na rozpočet",
"proxy": "Proxy",
"httpProxy": "HTTP Proxy",
"1proxy": "1proxy",
"proxySubTabsAria": "Proxy sections",
"requestBodyLimitTitle": "Request Body Limit",
"requestBodyLimitDescription": "Maximum API payload size accepted before a request body is parsed. Dedicated upload routes keep at least their built-in 100 MB limit.",
"requestBodyLimitInputLabel": "Request body limit in MB",
"requestBodyLimitEmptyError": "Enter a limit in MB",
"requestBodyLimitWholeNumberError": "Use a whole number",
"requestBodyLimitMinimumError": "Minimum is {min} MB",
"requestBodyLimitMaximumError": "Maximum is {max} MB",
"requestBodyLimitLoadFailed": "Failed to load request limit settings",
"requestBodyLimitSaveSuccess": "Request body limit saved",
"requestBodyLimitSaveFailed": "Failed to save request body limit",
"requestBodyLimitSaving": "Saving...",
"requestBodyLimitSave": "Save",
"requestBodyLimitCurrent": "Current: {value}",
"pricing": "Stanovenie cien",
"storage": "Skladovanie",
"policies": "zásady",

View File

@@ -2720,6 +2720,22 @@
"systemPrompt": "Systemprompt",
"thinkingBudget": "Tänkande budget",
"proxy": "Proxy",
"httpProxy": "HTTP Proxy",
"1proxy": "1proxy",
"proxySubTabsAria": "Proxy sections",
"requestBodyLimitTitle": "Request Body Limit",
"requestBodyLimitDescription": "Maximum API payload size accepted before a request body is parsed. Dedicated upload routes keep at least their built-in 100 MB limit.",
"requestBodyLimitInputLabel": "Request body limit in MB",
"requestBodyLimitEmptyError": "Enter a limit in MB",
"requestBodyLimitWholeNumberError": "Use a whole number",
"requestBodyLimitMinimumError": "Minimum is {min} MB",
"requestBodyLimitMaximumError": "Maximum is {max} MB",
"requestBodyLimitLoadFailed": "Failed to load request limit settings",
"requestBodyLimitSaveSuccess": "Request body limit saved",
"requestBodyLimitSaveFailed": "Failed to save request body limit",
"requestBodyLimitSaving": "Saving...",
"requestBodyLimitSave": "Save",
"requestBodyLimitCurrent": "Current: {value}",
"pricing": "Prissättning",
"storage": "Förvaring",
"policies": "Policyer",

View File

@@ -3451,7 +3451,23 @@
"compressionModeRtk": "RTK",
"compressionModeRtkDesc": "Command-aware tool output filtering",
"compressionModeStacked": "Stacked",
"compressionModeStackedDesc": "RTK tool-output filtering followed by Caveman message compression"
"compressionModeStackedDesc": "RTK tool-output filtering followed by Caveman message compression",
"httpProxy": "HTTP Proxy",
"1proxy": "1proxy",
"proxySubTabsAria": "Proxy sections",
"requestBodyLimitTitle": "Request Body Limit",
"requestBodyLimitDescription": "Maximum API payload size accepted before a request body is parsed. Dedicated upload routes keep at least their built-in 100 MB limit.",
"requestBodyLimitInputLabel": "Request body limit in MB",
"requestBodyLimitEmptyError": "Enter a limit in MB",
"requestBodyLimitWholeNumberError": "Use a whole number",
"requestBodyLimitMinimumError": "Minimum is {min} MB",
"requestBodyLimitMaximumError": "Maximum is {max} MB",
"requestBodyLimitLoadFailed": "Failed to load request limit settings",
"requestBodyLimitSaveSuccess": "Request body limit saved",
"requestBodyLimitSaveFailed": "Failed to save request body limit",
"requestBodyLimitSaving": "Saving...",
"requestBodyLimitSave": "Save",
"requestBodyLimitCurrent": "Current: {value}"
},
"contextRtk": {
"title": "RTK Engine",

View File

@@ -3451,7 +3451,23 @@
"compressionModeRtk": "RTK",
"compressionModeRtkDesc": "Command-aware tool output filtering",
"compressionModeStacked": "Stacked",
"compressionModeStackedDesc": "RTK tool-output filtering followed by Caveman message compression"
"compressionModeStackedDesc": "RTK tool-output filtering followed by Caveman message compression",
"httpProxy": "HTTP Proxy",
"1proxy": "1proxy",
"proxySubTabsAria": "Proxy sections",
"requestBodyLimitTitle": "Request Body Limit",
"requestBodyLimitDescription": "Maximum API payload size accepted before a request body is parsed. Dedicated upload routes keep at least their built-in 100 MB limit.",
"requestBodyLimitInputLabel": "Request body limit in MB",
"requestBodyLimitEmptyError": "Enter a limit in MB",
"requestBodyLimitWholeNumberError": "Use a whole number",
"requestBodyLimitMinimumError": "Minimum is {min} MB",
"requestBodyLimitMaximumError": "Maximum is {max} MB",
"requestBodyLimitLoadFailed": "Failed to load request limit settings",
"requestBodyLimitSaveSuccess": "Request body limit saved",
"requestBodyLimitSaveFailed": "Failed to save request body limit",
"requestBodyLimitSaving": "Saving...",
"requestBodyLimitSave": "Save",
"requestBodyLimitCurrent": "Current: {value}"
},
"contextRtk": {
"title": "RTK Engine",

View File

@@ -3451,7 +3451,23 @@
"compressionModeRtk": "RTK",
"compressionModeRtkDesc": "Command-aware tool output filtering",
"compressionModeStacked": "Stacked",
"compressionModeStackedDesc": "RTK tool-output filtering followed by Caveman message compression"
"compressionModeStackedDesc": "RTK tool-output filtering followed by Caveman message compression",
"httpProxy": "HTTP Proxy",
"1proxy": "1proxy",
"proxySubTabsAria": "Proxy sections",
"requestBodyLimitTitle": "Request Body Limit",
"requestBodyLimitDescription": "Maximum API payload size accepted before a request body is parsed. Dedicated upload routes keep at least their built-in 100 MB limit.",
"requestBodyLimitInputLabel": "Request body limit in MB",
"requestBodyLimitEmptyError": "Enter a limit in MB",
"requestBodyLimitWholeNumberError": "Use a whole number",
"requestBodyLimitMinimumError": "Minimum is {min} MB",
"requestBodyLimitMaximumError": "Maximum is {max} MB",
"requestBodyLimitLoadFailed": "Failed to load request limit settings",
"requestBodyLimitSaveSuccess": "Request body limit saved",
"requestBodyLimitSaveFailed": "Failed to save request body limit",
"requestBodyLimitSaving": "Saving...",
"requestBodyLimitSave": "Save",
"requestBodyLimitCurrent": "Current: {value}"
},
"contextRtk": {
"title": "RTK Engine",

View File

@@ -3294,7 +3294,23 @@
"compressionModeRtk": "RTK",
"compressionModeRtkDesc": "Command-aware tool output filtering",
"compressionModeStacked": "Stacked",
"compressionModeStackedDesc": "RTK tool-output filtering followed by Caveman message compression"
"compressionModeStackedDesc": "RTK tool-output filtering followed by Caveman message compression",
"httpProxy": "HTTP Proxy",
"1proxy": "1proxy",
"proxySubTabsAria": "Proxy sections",
"requestBodyLimitTitle": "Request Body Limit",
"requestBodyLimitDescription": "Maximum API payload size accepted before a request body is parsed. Dedicated upload routes keep at least their built-in 100 MB limit.",
"requestBodyLimitInputLabel": "Request body limit in MB",
"requestBodyLimitEmptyError": "Enter a limit in MB",
"requestBodyLimitWholeNumberError": "Use a whole number",
"requestBodyLimitMinimumError": "Minimum is {min} MB",
"requestBodyLimitMaximumError": "Maximum is {max} MB",
"requestBodyLimitLoadFailed": "Failed to load request limit settings",
"requestBodyLimitSaveSuccess": "Request body limit saved",
"requestBodyLimitSaveFailed": "Failed to save request body limit",
"requestBodyLimitSaving": "Saving...",
"requestBodyLimitSave": "Save",
"requestBodyLimitCurrent": "Current: {value}"
},
"contextRtk": {
"title": "RTK Engine",

View File

@@ -2720,6 +2720,22 @@
"systemPrompt": "Sistem İstemi",
"thinkingBudget": "Düşünme Bütçesi",
"proxy": "Proxy",
"httpProxy": "HTTP Proxy",
"1proxy": "1proxy",
"proxySubTabsAria": "Proxy sections",
"requestBodyLimitTitle": "Request Body Limit",
"requestBodyLimitDescription": "Maximum API payload size accepted before a request body is parsed. Dedicated upload routes keep at least their built-in 100 MB limit.",
"requestBodyLimitInputLabel": "Request body limit in MB",
"requestBodyLimitEmptyError": "Enter a limit in MB",
"requestBodyLimitWholeNumberError": "Use a whole number",
"requestBodyLimitMinimumError": "Minimum is {min} MB",
"requestBodyLimitMaximumError": "Maximum is {max} MB",
"requestBodyLimitLoadFailed": "Failed to load request limit settings",
"requestBodyLimitSaveSuccess": "Request body limit saved",
"requestBodyLimitSaveFailed": "Failed to save request body limit",
"requestBodyLimitSaving": "Saving...",
"requestBodyLimitSave": "Save",
"requestBodyLimitCurrent": "Current: {value}",
"pricing": "Fiyatlandırma",
"storage": "Depolama",
"policies": "Politikalar",

View File

@@ -2720,6 +2720,22 @@
"systemPrompt": "Системна підказка",
"thinkingBudget": "Мислення про бюджет",
"proxy": "Проксі",
"httpProxy": "HTTP Proxy",
"1proxy": "1proxy",
"proxySubTabsAria": "Proxy sections",
"requestBodyLimitTitle": "Request Body Limit",
"requestBodyLimitDescription": "Maximum API payload size accepted before a request body is parsed. Dedicated upload routes keep at least their built-in 100 MB limit.",
"requestBodyLimitInputLabel": "Request body limit in MB",
"requestBodyLimitEmptyError": "Enter a limit in MB",
"requestBodyLimitWholeNumberError": "Use a whole number",
"requestBodyLimitMinimumError": "Minimum is {min} MB",
"requestBodyLimitMaximumError": "Maximum is {max} MB",
"requestBodyLimitLoadFailed": "Failed to load request limit settings",
"requestBodyLimitSaveSuccess": "Request body limit saved",
"requestBodyLimitSaveFailed": "Failed to save request body limit",
"requestBodyLimitSaving": "Saving...",
"requestBodyLimitSave": "Save",
"requestBodyLimitCurrent": "Current: {value}",
"pricing": "Ціноутворення",
"storage": "Зберігання",
"policies": "політики",

View File

@@ -3451,7 +3451,23 @@
"compressionModeRtk": "RTK",
"compressionModeRtkDesc": "Command-aware tool output filtering",
"compressionModeStacked": "Stacked",
"compressionModeStackedDesc": "RTK tool-output filtering followed by Caveman message compression"
"compressionModeStackedDesc": "RTK tool-output filtering followed by Caveman message compression",
"httpProxy": "HTTP Proxy",
"1proxy": "1proxy",
"proxySubTabsAria": "Proxy sections",
"requestBodyLimitTitle": "Request Body Limit",
"requestBodyLimitDescription": "Maximum API payload size accepted before a request body is parsed. Dedicated upload routes keep at least their built-in 100 MB limit.",
"requestBodyLimitInputLabel": "Request body limit in MB",
"requestBodyLimitEmptyError": "Enter a limit in MB",
"requestBodyLimitWholeNumberError": "Use a whole number",
"requestBodyLimitMinimumError": "Minimum is {min} MB",
"requestBodyLimitMaximumError": "Maximum is {max} MB",
"requestBodyLimitLoadFailed": "Failed to load request limit settings",
"requestBodyLimitSaveSuccess": "Request body limit saved",
"requestBodyLimitSaveFailed": "Failed to save request body limit",
"requestBodyLimitSaving": "Saving...",
"requestBodyLimitSave": "Save",
"requestBodyLimitCurrent": "Current: {value}"
},
"contextRtk": {
"title": "RTK Engine",

View File

@@ -3294,7 +3294,23 @@
"compressionModeRtk": "RTK",
"compressionModeRtkDesc": "Command-aware tool output filtering",
"compressionModeStacked": "Stacked",
"compressionModeStackedDesc": "RTK tool-output filtering followed by Caveman message compression"
"compressionModeStackedDesc": "RTK tool-output filtering followed by Caveman message compression",
"httpProxy": "HTTP Proxy",
"1proxy": "1proxy",
"proxySubTabsAria": "Proxy sections",
"requestBodyLimitTitle": "Request Body Limit",
"requestBodyLimitDescription": "Maximum API payload size accepted before a request body is parsed. Dedicated upload routes keep at least their built-in 100 MB limit.",
"requestBodyLimitInputLabel": "Request body limit in MB",
"requestBodyLimitEmptyError": "Enter a limit in MB",
"requestBodyLimitWholeNumberError": "Use a whole number",
"requestBodyLimitMinimumError": "Minimum is {min} MB",
"requestBodyLimitMaximumError": "Maximum is {max} MB",
"requestBodyLimitLoadFailed": "Failed to load request limit settings",
"requestBodyLimitSaveSuccess": "Request body limit saved",
"requestBodyLimitSaveFailed": "Failed to save request body limit",
"requestBodyLimitSaving": "Saving...",
"requestBodyLimitSave": "Save",
"requestBodyLimitCurrent": "Current: {value}"
},
"contextRtk": {
"title": "RTK Engine",

View File

@@ -2821,6 +2821,22 @@
"systemPrompt": "系统提示",
"thinkingBudget": "思考预算",
"proxy": "代理",
"httpProxy": "HTTP Proxy",
"1proxy": "1proxy",
"proxySubTabsAria": "Proxy sections",
"requestBodyLimitTitle": "Request Body Limit",
"requestBodyLimitDescription": "Maximum API payload size accepted before a request body is parsed. Dedicated upload routes keep at least their built-in 100 MB limit.",
"requestBodyLimitInputLabel": "Request body limit in MB",
"requestBodyLimitEmptyError": "Enter a limit in MB",
"requestBodyLimitWholeNumberError": "Use a whole number",
"requestBodyLimitMinimumError": "Minimum is {min} MB",
"requestBodyLimitMaximumError": "Maximum is {max} MB",
"requestBodyLimitLoadFailed": "Failed to load request limit settings",
"requestBodyLimitSaveSuccess": "Request body limit saved",
"requestBodyLimitSaveFailed": "Failed to save request body limit",
"requestBodyLimitSaving": "Saving...",
"requestBodyLimitSave": "Save",
"requestBodyLimitCurrent": "Current: {value}",
"pricing": "定价",
"storage": "存储",
"policies": "策略",

View File

@@ -16,6 +16,7 @@ import {
writeCallArtifact,
type CallLogArtifact,
} from "../usage/callLogArtifacts";
import { autoMigrateLegacyEncryptedConnections } from "./providers";
type SqliteDatabase = import("better-sqlite3").Database;
type JsonRecord = Record<string, unknown>;
@@ -1248,6 +1249,15 @@ export function getDbInstance(): SqliteDatabase {
}
setDb(db);
// Re-encrypt any tokens using the legacy dynamic salt to canonical static salt
try {
autoMigrateLegacyEncryptedConnections();
} catch (err: unknown) {
const message = err instanceof Error ? err.message : String(err);
console.error(`[DB] Legacy encryption migration failed: ${message}`);
}
startDbHealthCheckScheduler(db);
console.log(`[DB] SQLite database ready: ${sqliteFile}`);
return db;

View File

@@ -34,7 +34,7 @@ const PREFIX = "enc:v1:";
const STATIC_SALT = "omniroute-field-encryption-v1";
let _staticKey: Buffer | null = null;
let _legacyDynamicKey: Buffer | null = null;
/** Connection object with potentially encrypted credential fields. */
export interface ConnectionFields {
apiKey?: string | null;
@@ -68,6 +68,28 @@ function getStaticKey(): Buffer | null {
return _staticKey;
}
/**
* Derive the LEGACY key using the old dynamic salt method.
* Used exclusively for fallback decryption of tokens encrypted by older versions.
*
* The old dynamic salt was: createHash("sha256").update(secret).digest().slice(0, 16)
* This produced a different derived key than the static salt, causing incompatibility.
*/
function getLegacyDynamicKey(): Buffer | null {
if (_legacyDynamicKey !== null) return _legacyDynamicKey;
const secret = process.env.STORAGE_ENCRYPTION_KEY;
if (!secret || typeof secret !== "string" || secret.trim().length === 0) return null;
const dynamicSalt = createHash("sha256").update(secret).digest().slice(0, 16);
try {
_legacyDynamicKey = scryptSync(secret, dynamicSalt, KEY_LENGTH);
} catch {
return null;
}
return _legacyDynamicKey;
}
/** Check if encryption is enabled. */
export function isEncryptionEnabled(): boolean {
return !!process.env.STORAGE_ENCRYPTION_KEY;
@@ -243,3 +265,60 @@ export function validateEncryptionConfig(): { valid: boolean; error?: string } {
};
}
}
/**
* Specifically tests a ciphertext against the legacy key. If it succeeds, it
* re-encrypts the decrypted value with the canonical static key.
* Used exclusively by the startup migration script.
*/
export function migrateLegacyEncryptedString(ciphertext: string | null | undefined): {
updated: boolean;
value: string | null | undefined;
} {
if (!isEncryptionEnabled()) return { updated: false, value: ciphertext };
if (!ciphertext || ciphertext.trim().length === 0) return { updated: false, value: ciphertext };
if (!ciphertext.startsWith(PREFIX)) return { updated: false, value: ciphertext };
const staticKey = getStaticKey();
const legacyKey = getLegacyDynamicKey();
if (!staticKey) return { updated: false, value: null };
const rawPayload = ciphertext.slice(PREFIX.length);
const parts = rawPayload.split(":");
if (parts.length !== 3) return { updated: false, value: ciphertext };
const [ivHex, authTagHex, encryptedHex] = parts;
const iv = Buffer.from(ivHex, "hex");
const authTag = Buffer.from(authTagHex, "hex");
const encrypted = Buffer.from(encryptedHex, "hex");
const tryDecryptWithKey = (key: Buffer): string | null => {
try {
const decipher = createDecipheriv(ALGORITHM, key, iv);
decipher.setAuthTag(authTag);
let decrypted = decipher.update(encrypted, undefined, "utf8");
decrypted += decipher.final("utf8");
return decrypted;
} catch {
return null;
}
};
// 1. If it already decrypts with the static key, no migration needed.
if (tryDecryptWithKey(staticKey) !== null) {
return { updated: false, value: ciphertext };
}
// 2. If it decrypts with the legacy key, it needs migration!
if (legacyKey) {
const legacyDecrypted = tryDecryptWithKey(legacyKey);
if (legacyDecrypted !== null) {
// Re-encrypt using the canonical static key and return updated
return { updated: true, value: encrypt(legacyDecrypted) };
}
}
// 3. Un-decryptable or corrupted, leave it alone
return { updated: false, value: ciphertext };
}

View File

@@ -5,7 +5,11 @@
import { v4 as uuidv4 } from "uuid";
import { getDbInstance, rowToCamel, cleanNulls } from "./core";
import { backupDbFile } from "./backup";
import { encryptConnectionFields, decryptConnectionFields } from "./encryption";
import {
encryptConnectionFields,
decryptConnectionFields,
migrateLegacyEncryptedString,
} from "./encryption";
import { invalidateDbCache } from "./readCache";
import { normalizeProviderSpecificData } from "@/lib/providers/requestDefaults";
@@ -487,6 +491,67 @@ export async function getDistinctGroups(): Promise<string[]> {
return rows.map((r) => String(r.group ?? "")).filter(Boolean);
}
// ──────────────── Auto Migration ────────────────
/**
* Scans all connections and re-encrypts any fields using the old dynamic salt
* so they use the new canonical static salt.
*/
export function autoMigrateLegacyEncryptedConnections(): number {
const db = getDbInstance() as unknown as DbLike;
const rows = db.prepare("SELECT * FROM provider_connections").all();
let migratedCount = 0;
for (const row of rows) {
const camelRow = rowToCamel(row);
if (!camelRow) continue;
let updatedRow = false;
const encryptedFields = ["apiKey", "idToken", "accessToken", "refreshToken"];
for (const field of encryptedFields) {
if (typeof camelRow[field] === "string") {
const { updated, value } = migrateLegacyEncryptedString(camelRow[field] as string);
if (updated) {
camelRow[field] = value;
updatedRow = true;
}
}
}
if (updatedRow) {
// camelRow[field] is already re-encrypted!
// But _updateConnectionRow does not re-encrypt automatically, so we pass it safely.
// Wait, _updateConnectionRow runs the full data through `encryptConnectionFields`,
// but `encryptConnectionFields` will re-encrypt plain text.
// BUT `migrateLegacyEncryptedString` returns ALREADY ENCRYPTED ciphertext!
// Wait... if we pass ALREADY ENCRYPTED text to `_updateConnectionRow`,
// `encryptConnectionFields` in `_updateConnectionRow` will encrypt it AGAIN!
// Let's modify the DB directly so we don't double encrypt.
db.prepare(
"UPDATE provider_connections SET api_key = @apiKey, id_token = @idToken, access_token = @accessToken, refresh_token = @refreshToken, updated_at = @updatedAt WHERE id = @id"
).run({
id: camelRow.id,
apiKey: camelRow.apiKey ?? null,
idToken: camelRow.idToken ?? null,
accessToken: camelRow.accessToken ?? null,
refreshToken: camelRow.refreshToken ?? null,
updatedAt: new Date().toISOString(),
});
migratedCount++;
}
}
if (migratedCount > 0) {
backupDbFile("pre-write");
invalidateDbCache("connections");
console.log(`[DB] Auto-migrated ${migratedCount} connection(s) to new static-salt encryption.`);
}
return migratedCount;
}
// ──────────────── Provider Nodes ────────────────
export async function getProviderNodes(filter: JsonRecord = {}) {

View File

@@ -8,6 +8,7 @@ import { PROVIDER_ID_TO_ALIAS } from "@omniroute/open-sse/config/providerModels.
import { invalidateDbCache } from "./readCache";
import { resolveProxyForConnectionFromRegistry } from "./proxies";
import { getComboModelProvider as getComboEntryProvider } from "@/lib/combos/steps";
import { requestBodyLimitMbFromEnv } from "@/shared/constants/bodySize";
type JsonRecord = Record<string, unknown>;
type PricingModels = Record<string, JsonRecord>;
@@ -63,6 +64,7 @@ export async function getSettings() {
alwaysPreserveClientCache: "auto",
idempotencyWindowMs: 5000,
wsAuth: false,
maxBodySizeMb: requestBodyLimitMbFromEnv(process.env.MAX_BODY_SIZE_BYTES),
};
for (const row of rows) {
const record = toRecord(row);

View File

@@ -281,8 +281,10 @@ export function migrateUsageJsonToSqlite() {
connectionId: entry.connectionId || null,
apiKeyId: entry.apiKeyId || null,
apiKeyName: entry.apiKeyName || null,
tokensInput: entry.tokens?.input ?? entry.tokens?.prompt_tokens ?? 0,
tokensOutput: entry.tokens?.output ?? entry.tokens?.completion_tokens ?? 0,
tokensInput:
entry.tokens?.input ?? entry.tokens?.prompt_tokens ?? entry.tokens?.in ?? 0,
tokensOutput:
entry.tokens?.output ?? entry.tokens?.completion_tokens ?? entry.tokens?.out ?? 0,
tokensCacheRead: entry.tokens?.cacheRead ?? entry.tokens?.cached_tokens ?? 0,
tokensCacheCreation:
entry.tokens?.cacheCreation ?? entry.tokens?.cache_creation_input_tokens ?? 0,

View File

@@ -1,5 +1,6 @@
import { jwtVerify, SignJWT } from "jose";
import { NextResponse, type NextRequest } from "next/server";
import { getCachedSettings } from "../../lib/db/readCache";
import { isDraining } from "../../lib/gracefulShutdown";
import { checkBodySize, getBodySizeLimit } from "../../shared/middleware/bodySizeGuard";
import { generateRequestId } from "../../shared/utils/requestId";
@@ -168,6 +169,18 @@ function stampRouteResponse(
return response;
}
async function getBodySizeSettings(): Promise<Record<string, unknown> | undefined> {
try {
return await getCachedSettings();
} catch (error) {
console.warn(
"[Authz] Failed to load request body limit settings:",
error instanceof Error ? error.message : error
);
return undefined;
}
}
export async function runAuthzPipeline(
request: NextRequest,
options: AuthzPipelineOptions = {}
@@ -194,7 +207,11 @@ export async function runAuthzPipeline(
}
if (guardedPathname.startsWith("/api/") && method !== "GET" && method !== "OPTIONS") {
const bodySizeRejection = checkBodySize(request, getBodySizeLimit(guardedPathname));
const bodySizeSettings = await getBodySizeSettings();
const bodySizeRejection = checkBodySize(
request,
getBodySizeLimit(guardedPathname, bodySizeSettings)
);
if (bodySizeRejection) {
stampRouteResponse(bodySizeRejection, requestId, classification.routeClass);
applyCorsHeaders(bodySizeRejection, request);

View File

@@ -0,0 +1,39 @@
export const REQUEST_BODY_BYTES_PER_MB = 1024 * 1024;
export const DEFAULT_REQUEST_BODY_LIMIT_MB = 10;
export const MIN_REQUEST_BODY_LIMIT_MB = 1;
export const MAX_REQUEST_BODY_LIMIT_MB = 500;
export const DEFAULT_REQUEST_BODY_LIMIT_BYTES =
DEFAULT_REQUEST_BODY_LIMIT_MB * REQUEST_BODY_BYTES_PER_MB;
export function normalizeRequestBodyLimitMb(value: unknown): number | null {
const parsed =
typeof value === "number" ? value : typeof value === "string" ? Number(value) : NaN;
if (!Number.isFinite(parsed)) return null;
const normalized = Math.floor(parsed);
if (normalized < MIN_REQUEST_BODY_LIMIT_MB || normalized > MAX_REQUEST_BODY_LIMIT_MB) {
return null;
}
return normalized;
}
export function requestBodyLimitMbToBytes(value: number): number {
return value * REQUEST_BODY_BYTES_PER_MB;
}
export function parseRequestBodyLimitBytes(value: string | undefined): number {
if (!value) return DEFAULT_REQUEST_BODY_LIMIT_BYTES;
const parsed = parseInt(value, 10);
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_REQUEST_BODY_LIMIT_BYTES;
}
export function requestBodyLimitBytesToMb(value: number): number {
const configuredMb = Math.round(value / REQUEST_BODY_BYTES_PER_MB);
return Math.min(MAX_REQUEST_BODY_LIMIT_MB, Math.max(MIN_REQUEST_BODY_LIMIT_MB, configuredMb));
}
export function requestBodyLimitMbFromEnv(value: string | undefined): number {
return requestBodyLimitBytesToMb(parseRequestBodyLimitBytes(value));
}

View File

@@ -13,8 +13,12 @@
* @module shared/middleware/bodySizeGuard
*/
/** Default maximum body size: 10 MB */
const DEFAULT_MAX_BODY_BYTES = 10 * 1024 * 1024;
import {
normalizeRequestBodyLimitMb,
parseRequestBodyLimitBytes,
requestBodyLimitBytesToMb,
requestBodyLimitMbToBytes,
} from "../constants/bodySize";
/** Larger limit for backup/import routes: 100 MB */
export const MAX_BODY_BYTES_IMPORT = 100 * 1024 * 1024;
@@ -23,10 +27,7 @@ export const MAX_BODY_BYTES_IMPORT = 100 * 1024 * 1024;
export const MAX_BODY_BYTES_AUDIO = 100 * 1024 * 1024;
/** Configured limit — reads from env or falls back to 10 MB */
export const MAX_BODY_BYTES = parseInt(
process.env.MAX_BODY_SIZE_BYTES || String(DEFAULT_MAX_BODY_BYTES),
10
);
export const MAX_BODY_BYTES = parseRequestBodyLimitBytes(process.env.MAX_BODY_SIZE_BYTES);
type BodySizeRule = { prefix: string; limit: number };
@@ -35,12 +36,22 @@ const ROUTE_LIMITS: BodySizeRule[] = [
{ prefix: "/api/v1/audio/transcriptions", limit: MAX_BODY_BYTES_AUDIO },
];
export function getDefaultRequestBodyLimitMb(): number {
return requestBodyLimitBytesToMb(MAX_BODY_BYTES);
}
export function getConfiguredBodySizeLimitBytes(settings?: Record<string, unknown>): number {
const configuredMb = normalizeRequestBodyLimitMb(settings?.maxBodySizeMb);
return configuredMb === null ? MAX_BODY_BYTES : requestBodyLimitMbToBytes(configuredMb);
}
/**
* Resolve the body size limit for a request path.
*/
export function getBodySizeLimit(pathname: string): number {
export function getBodySizeLimit(pathname: string, settings?: Record<string, unknown>): number {
const configuredLimit = getConfiguredBodySizeLimitBytes(settings);
const customRule = ROUTE_LIMITS.find((rule) => pathname.startsWith(rule.prefix));
return customRule?.limit ?? MAX_BODY_BYTES;
return customRule ? Math.max(customRule.limit, configuredLimit) : configuredLimit;
}
/**

View File

@@ -4,6 +4,7 @@ import {
ROUTING_STRATEGY_VALUES,
} from "@/shared/constants/routingStrategies";
import { SUPPORTED_BATCH_ENDPOINTS } from "@/shared/constants/batchEndpoints";
import { MAX_REQUEST_BODY_LIMIT_MB, MIN_REQUEST_BODY_LIMIT_MB } from "@/shared/constants/bodySize";
import { COMBO_CONFIG_MODES } from "@/shared/constants/comboConfigMode";
import { isLocalProvider } from "@/shared/constants/providers";
import { HIDEABLE_SIDEBAR_ITEM_IDS } from "@/shared/constants/sidebarVisibility";
@@ -444,6 +445,12 @@ export const updateSettingsSchema = z.object({
stickyRoundRobinLimit: z.number().int().min(0).max(1000).optional(),
requestRetry: z.number().int().min(0).max(10).optional(),
maxRetryIntervalSec: z.number().int().min(0).max(300).optional(),
maxBodySizeMb: z
.number()
.int()
.min(MIN_REQUEST_BODY_LIMIT_MB)
.max(MAX_REQUEST_BODY_LIMIT_MB)
.optional(),
// Auto intent classifier settings (multilingual routing)
intentDetectionEnabled: z.boolean().optional(),
intentSimpleMaxWords: z.number().int().min(1).max(500).optional(),

View File

@@ -1,4 +1,109 @@
// ... existing imports ...
/**
* Settings-specific Zod schemas.
*
* Extracted from schemas.ts to work around the webpack barrel-file
* optimization bug that makes large schema barrel exports `undefined`
* at runtime (see: https://github.com/vercel/next.js/issues/12557).
*/
import { z } from "zod";
import { COMBO_CONFIG_MODES } from "@/shared/constants/comboConfigMode";
import { MAX_REQUEST_BODY_LIMIT_MB, MIN_REQUEST_BODY_LIMIT_MB } from "@/shared/constants/bodySize";
import { HIDEABLE_SIDEBAR_ITEM_IDS } from "@/shared/constants/sidebarVisibility";
import { ACCOUNT_FALLBACK_STRATEGY_VALUES } from "@/shared/constants/routingStrategies";
const signatureCacheModeValues = ["enabled", "bypass", "bypass-strict"] as const;
export const updateSettingsSchema = z.object({
newPassword: z.string().min(1).max(200).optional(),
currentPassword: z.string().max(200).optional(),
theme: z.string().max(50).optional(),
language: z.string().max(10).optional(),
requireLogin: z.boolean().optional(),
enableSocks5Proxy: z.boolean().optional(),
instanceName: z.string().max(100).optional(),
customLogoUrl: z.string().max(2000).optional(),
customLogoBase64: z.string().max(100000).optional(),
customFaviconUrl: z.string().max(2000).optional(),
customFaviconBase64: z.string().max(50000).optional(),
corsOrigins: z.string().max(500).optional(),
cloudUrl: z.string().max(500).optional(),
baseUrl: z.string().max(500).optional(),
setupComplete: z.boolean().optional(),
blockedProviders: z.array(z.string().max(100)).optional(),
hideHealthCheckLogs: z.boolean().optional(),
hideEndpointCloudflaredTunnel: z.boolean().optional(),
hideEndpointTailscaleFunnel: z.boolean().optional(),
hideEndpointNgrokTunnel: z.boolean().optional(),
debugMode: z.boolean().optional(),
hiddenSidebarItems: z.array(z.enum(HIDEABLE_SIDEBAR_ITEM_IDS)).optional(),
comboConfigMode: z.enum(COMBO_CONFIG_MODES).optional(),
// Routing settings (#134)
fallbackStrategy: z.enum(ACCOUNT_FALLBACK_STRATEGY_VALUES).optional(),
wildcardAliases: z.array(z.object({ pattern: z.string(), target: z.string() })).optional(),
stickyRoundRobinLimit: z.number().int().min(0).max(1000).optional(),
requestRetry: z.number().int().min(0).max(10).optional(),
maxRetryIntervalSec: z.number().int().min(0).max(300).optional(),
maxBodySizeMb: z
.number()
.int()
.min(MIN_REQUEST_BODY_LIMIT_MB)
.max(MAX_REQUEST_BODY_LIMIT_MB)
.optional(),
// Auto intent classifier settings (multilingual routing)
intentDetectionEnabled: z.boolean().optional(),
intentSimpleMaxWords: z.number().int().min(1).max(500).optional(),
intentExtraCodeKeywords: z.array(z.string().max(100)).optional(),
intentExtraReasoningKeywords: z.array(z.string().max(100)).optional(),
intentExtraSimpleKeywords: z.array(z.string().max(100)).optional(),
// Protocol toggles (default: disabled)
mcpEnabled: z.boolean().optional(),
mcpTransport: z.enum(["stdio", "sse", "streamable-http"]).optional(),
a2aEnabled: z.boolean().optional(),
wsAuth: z.boolean().optional(),
// CLI Fingerprint compatibility (per-provider)
cliCompatProviders: z.array(z.string().max(100)).optional(),
// Strip provider/model prefix at proxy layer (e.g. "openai/gpt-4" → "gpt-4")
stripModelPrefix: z.boolean().optional(),
// Cache control preservation mode
alwaysPreserveClientCache: z.enum(["auto", "always", "never"]).optional(),
antigravitySignatureCacheMode: z.enum(signatureCacheModeValues).optional(),
// Adaptive Volume Routing
adaptiveVolumeRouting: z.boolean().optional(),
// Usage token buffer — safety margin added to reported prompt/input token counts.
// Prevents CLI tools from overrunning context windows. Set to 0 to disable.
usageTokenBuffer: z.number().int().min(0).max(50000).optional(),
// Custom CLI agent definitions for ACP
customAgents: z
.array(
z.object({
id: z.string().max(50),
name: z.string().max(100),
binary: z.string().max(200),
versionCommand: z.string().max(300),
providerAlias: z.string().max(50),
spawnArgs: z.array(z.string().max(200)),
protocol: z.enum(["stdio", "http"]),
})
)
.optional(),
// SkillsMP marketplace API key
skillsmpApiKey: z.string().max(200).optional(),
// Active skills provider (single source of truth for skills page)
skillsProvider: z.enum(["skillsmp", "skillssh"]).optional(),
// models.dev sync settings
modelsDevSyncEnabled: z.boolean().optional(),
modelsDevSyncInterval: z.number().int().min(3600000).max(604800000).optional(),
// Vision Bridge settings
visionBridgeEnabled: z.boolean().optional(),
visionBridgeModel: z.string().max(200).optional(),
visionBridgePrompt: z.string().max(5000).optional(),
visionBridgeTimeout: z.number().int().min(1000).max(300000).optional(),
visionBridgeMaxImages: z.number().int().min(1).max(20).optional(),
// Missing settings
lkgpEnabled: z.boolean().optional(),
backgroundDegradation: z.unknown().optional(),
bruteForceProtection: z.boolean().optional(),
});
export const databaseSettingsSchema = z.object(
{
@@ -69,5 +174,3 @@ export const databaseSettingsSchema = z.object(
);
export type DatabaseSettingsSchema = z.infer<typeof databaseSettingsSchema>;
// ... rest of the file ...

View File

@@ -15,6 +15,7 @@ export interface Settings {
stickyRoundRobinLimit: number;
requestRetry: number;
maxRetryIntervalSec: number;
maxBodySizeMb?: number;
jwtSecret?: string;
mcpEnabled?: boolean;
mcpTransport?: "stdio" | "sse" | "streamable-http";

View File

@@ -0,0 +1,42 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
MAX_BODY_BYTES_AUDIO,
getBodySizeLimit,
checkBodySize,
} from "../../src/shared/middleware/bodySizeGuard.ts";
import { requestBodyLimitMbToBytes } from "../../src/shared/constants/bodySize.ts";
test("body size guard uses maxBodySizeMb from settings for regular API routes", () => {
assert.equal(
getBodySizeLimit("/api/v1/responses", { maxBodySizeMb: 100 }),
requestBodyLimitMbToBytes(100)
);
});
test("body size guard keeps dedicated upload limits as lower bounds", () => {
assert.equal(
getBodySizeLimit("/api/v1/audio/transcriptions", { maxBodySizeMb: 1 }),
MAX_BODY_BYTES_AUDIO
);
assert.equal(
getBodySizeLimit("/api/v1/audio/transcriptions", { maxBodySizeMb: 200 }),
requestBodyLimitMbToBytes(200)
);
});
test("checkBodySize reports the configured request limit in 413 responses", async () => {
const limit = requestBodyLimitMbToBytes(100);
const request = new Request("http://localhost/api/v1/responses", {
method: "POST",
headers: { "content-length": String(limit + 1) },
});
const response = checkBodySize(request, limit);
assert.ok(response);
assert.equal(response.status, 413);
const body = await response.json();
assert.equal(body.error.code, "PAYLOAD_TOO_LARGE");
assert.match(body.error.message, /100 MB/);
});

View File

@@ -1,10 +1,13 @@
import test from "node:test";
import assert from "node:assert/strict";
import { createRequire } from "node:module";
import fs from "node:fs";
import path from "node:path";
const require = createRequire(import.meta.url);
const en = require("../../src/i18n/messages/en.json");
const zhCn = require("../../src/i18n/messages/zh-CN.json");
const { SIDEBAR_SECTIONS } = await import("../../src/shared/constants/sidebarVisibility.ts");
const requiredSettingsKeys = [
"adaptiveVolumeRouting",
@@ -21,9 +24,85 @@ const requiredSettingsKeys = [
"purgeLogsFailed",
];
const requestBodyLimitSettingsKeys = [
"requestBodyLimitTitle",
"requestBodyLimitDescription",
"requestBodyLimitInputLabel",
"requestBodyLimitEmptyError",
"requestBodyLimitWholeNumberError",
"requestBodyLimitMinimumError",
"requestBodyLimitMaximumError",
"requestBodyLimitLoadFailed",
"requestBodyLimitSaveSuccess",
"requestBodyLimitSaveFailed",
"requestBodyLimitSaving",
"requestBodyLimitSave",
"requestBodyLimitCurrent",
];
const proxyPageSettingsKeys = ["httpProxy", "1proxy", "proxySubTabsAria"];
test("settings translations include LKGP and maintenance keys in English and Simplified Chinese", () => {
for (const key of requiredSettingsKeys) {
assert.equal(typeof en.settings?.[key], "string", `en.settings.${key} should exist`);
assert.equal(typeof zhCn.settings?.[key], "string", `zh-CN.settings.${key} should exist`);
}
});
test("English sidebar translations include every configured sidebar item", () => {
const sidebarKeys = new Set(
SIDEBAR_SECTIONS.flatMap((section) => [
section.titleKey,
...section.items.map((item) => item.i18nKey),
])
);
for (const key of sidebarKeys) {
assert.equal(typeof en.sidebar?.[key], "string", `en.sidebar.${key} should exist`);
}
});
test("all locales include the proxy sidebar label", () => {
const messagesDir = path.resolve(process.cwd(), "src/i18n/messages");
const messageFiles = fs.readdirSync(messagesDir).filter((file) => file.endsWith(".json"));
for (const file of messageFiles) {
const messages = require(path.join(messagesDir, file));
assert.equal(typeof messages.sidebar?.proxy, "string", `${file}: sidebar.proxy should exist`);
}
});
test("all locales include request body limit settings labels", () => {
const messagesDir = path.resolve(process.cwd(), "src/i18n/messages");
const messageFiles = fs.readdirSync(messagesDir).filter((file) => file.endsWith(".json"));
for (const file of messageFiles) {
const messages = require(path.join(messagesDir, file));
for (const key of requestBodyLimitSettingsKeys) {
assert.equal(
typeof messages.settings?.[key],
"string",
`${file}: settings.${key} should exist`
);
}
}
});
test("all locales include proxy page tab labels", () => {
const messagesDir = path.resolve(process.cwd(), "src/i18n/messages");
const messageFiles = fs.readdirSync(messagesDir).filter((file) => file.endsWith(".json"));
for (const file of messageFiles) {
const messages = require(path.join(messagesDir, file));
for (const key of proxyPageSettingsKeys) {
assert.equal(
typeof messages.settings?.[key],
"string",
`${file}: settings.${key} should exist`
);
}
}
});

View File

@@ -38,6 +38,18 @@ test("settings schemas accept cooldown-aware retry knobs", () => {
assert.equal(sharedParsed.maxRetryIntervalSec, 30);
});
test("settings schemas accept request body limit", () => {
const routeParsed = settingsRouteSchema.parse({ maxBodySizeMb: 100 });
const sharedParsed = sharedSettingsSchema.parse({ maxBodySizeMb: 100 });
assert.equal(routeParsed.maxBodySizeMb, 100);
assert.equal(sharedParsed.maxBodySizeMb, 100);
assert.equal(settingsRouteSchema.safeParse({ maxBodySizeMb: 0 }).success, false);
assert.equal(settingsRouteSchema.safeParse({ maxBodySizeMb: 501 }).success, false);
assert.equal(sharedSettingsSchema.safeParse({ maxBodySizeMb: 0 }).success, false);
assert.equal(sharedSettingsSchema.safeParse({ maxBodySizeMb: 501 }).success, false);
});
test("settings schemas accept wsAuth toggle", () => {
const routeParsed = settingsRouteSchema.parse({ wsAuth: true });
const sharedParsed = sharedSettingsSchema.parse({ wsAuth: false });