mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-31 04:12:10 +03:00
feat(settings): add request body limit setting (#1968)
Integrated into release/v3.7.9
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "Политики",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "מדיניות",
|
||||
|
||||
@@ -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": "नीतियाँ",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "ポリシー",
|
||||
|
||||
@@ -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": "정책",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "Политика",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "політики",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "策略",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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 = {}) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
39
src/shared/constants/bodySize.ts
Normal file
39
src/shared/constants/bodySize.ts
Normal 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));
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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 ...
|
||||
|
||||
@@ -15,6 +15,7 @@ export interface Settings {
|
||||
stickyRoundRobinLimit: number;
|
||||
requestRetry: number;
|
||||
maxRetryIntervalSec: number;
|
||||
maxBodySizeMb?: number;
|
||||
jwtSecret?: string;
|
||||
mcpEnabled?: boolean;
|
||||
mcpTransport?: "stdio" | "sse" | "streamable-http";
|
||||
|
||||
42
tests/unit/body-size-guard.test.ts
Normal file
42
tests/unit/body-size-guard.test.ts
Normal 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/);
|
||||
});
|
||||
@@ -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`
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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 });
|
||||
|
||||
Reference in New Issue
Block a user