Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6637f294df | |||
| ad8a444105 | |||
| 877cfa0071 | |||
| e6f0a780b7 | |||
| dd9de2efa9 | |||
| f6b0811f78 | |||
| eba9d854a9 |
@@ -4,6 +4,25 @@
|
||||
|
||||
---
|
||||
|
||||
## [3.2.0] — 2026-03-28
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
- **Cache Management UI** — Added a dedicated semantic caching dashboard at \`/dashboard/cache\` with targeted API invalidation and 31-language i18n support (PR #701 by @oyi77)
|
||||
- **GLM Quota Tracking** — Added real-time usage and session quota tracking for the GLM Coding (Z.AI) provider (PR #698 by @christopher-s)
|
||||
- **Detailed Log Payloads** — Wired full four-stage pipeline payload capturing (original, translated, provider-response, streamed-deltas) directly into the UI (PR #705 by @rdself)
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- **Fix #708** — Prevented token bleeding for Claude Code users routing through OmniRoute by correctly preserving native \`cache_control\` headers during Claude-to-Claude passthrough (PR #708 by @tombii)
|
||||
- **Fix #719** — Setup internal auth boundaries for \`ModelSyncScheduler\` to prevent unauthenticated daemon failures on startup (PR #719 by @rdself)
|
||||
- **Fix #718** — Rebuilt badge rendering in Provider Limits UI preventing bad quota boundaries overlap (PR #718 by @rdself)
|
||||
- **Fix #704** — Fixed Combo Fallbacks breaking on HTTP 400 content-policy errors preventing model-rotation dead-routing (PR #704 by @rdself)
|
||||
|
||||
### 🔒 Security & Dependencies
|
||||
|
||||
- Bumped \`path-to-regexp\` to \`8.4.0\` resolving dependabot vulnerabilities (PR #715)
|
||||
|
||||
## [3.1.10] — 2026-03-28
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
@@ -274,8 +274,9 @@ Domain State DB (SQLite):
|
||||
|
||||
## 5) Cloud Sync
|
||||
|
||||
- Scheduler init: `src/lib/initCloudSync.ts`, `src/shared/services/initializeCloudSync.ts`
|
||||
- Scheduler init: `src/lib/initCloudSync.ts`, `src/shared/services/initializeCloudSync.ts`, `src/shared/services/modelSyncScheduler.ts`
|
||||
- Periodic task: `src/shared/services/cloudSyncScheduler.ts`
|
||||
- Periodic task: `src/shared/services/modelSyncScheduler.ts`
|
||||
- Control route: `src/app/api/sync/cloud/route.ts`
|
||||
|
||||
## Request Lifecycle (`/v1/chat/completions`)
|
||||
@@ -355,7 +356,7 @@ flowchart TD
|
||||
Q -- No --> R[Return all unavailable]
|
||||
```
|
||||
|
||||
Fallback decisions are driven by `open-sse/services/accountFallback.ts` using status codes and error-message heuristics.
|
||||
Fallback decisions are driven by `open-sse/services/accountFallback.ts` using status codes and error-message heuristics. Combo routing adds one extra guard: provider-scoped 400s such as upstream content-block and role-validation failures are treated as model-local failures so later combo targets can still run.
|
||||
|
||||
## OAuth Onboarding and Token Refresh Lifecycle
|
||||
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
openapi: 3.1.0
|
||||
info:
|
||||
title: OmniRoute API
|
||||
version: 3.1.10
|
||||
version: 3.2.0
|
||||
description: |
|
||||
OmniRoute is a local-first AI API proxy router. It provides an OpenAI-compatible
|
||||
endpoint that routes requests to multiple AI providers with load balancing,
|
||||
|
||||
@@ -20,6 +20,15 @@ import { supportsToolCalling } from "./modelCapabilities.ts";
|
||||
|
||||
// Status codes that should mark semaphore + record circuit breaker failures
|
||||
const TRANSIENT_FOR_BREAKER = [429, 502, 503, 504];
|
||||
const COMBO_BAD_REQUEST_FALLBACK_PATTERNS = [
|
||||
/\bprohibited_content\b/i,
|
||||
/request blocked by .*api/i,
|
||||
/provided message roles? is not valid/i,
|
||||
/unsupported .*message role/i,
|
||||
/no such tool available/i,
|
||||
/unsupported content part type/i,
|
||||
/tool(?:_call|_use)? .* not (?:available|found)/i,
|
||||
];
|
||||
|
||||
const MAX_COMBO_DEPTH = 3;
|
||||
|
||||
@@ -258,6 +267,12 @@ function extractPromptForIntent(body) {
|
||||
return "";
|
||||
}
|
||||
|
||||
export function shouldFallbackComboBadRequest(status, errorText) {
|
||||
if (status !== 400 || !errorText) return false;
|
||||
const message = String(errorText);
|
||||
return COMBO_BAD_REQUEST_FALLBACK_PATTERNS.some((pattern) => pattern.test(message));
|
||||
}
|
||||
|
||||
function mapIntentToTaskType(intent) {
|
||||
switch (intent) {
|
||||
case "code":
|
||||
@@ -890,17 +905,25 @@ export async function handleComboChat({
|
||||
provider,
|
||||
result.headers
|
||||
);
|
||||
const comboBadRequestFallback = shouldFallbackComboBadRequest(result.status, errorText);
|
||||
|
||||
// Record failure in circuit breaker for transient errors
|
||||
if (TRANSIENT_FOR_BREAKER.includes(result.status)) {
|
||||
breaker._onFailure();
|
||||
}
|
||||
|
||||
if (!shouldFallback) {
|
||||
if (!shouldFallback && !comboBadRequestFallback) {
|
||||
log.warn("COMBO", `Model ${modelStr} failed (no fallback)`, { status: result.status });
|
||||
return result;
|
||||
}
|
||||
|
||||
if (comboBadRequestFallback) {
|
||||
log.info(
|
||||
"COMBO",
|
||||
`Treating provider-scoped 400 from ${modelStr} as model-local failure; trying next combo target`
|
||||
);
|
||||
}
|
||||
|
||||
// Check if this is a transient error worth retrying on same model
|
||||
const isTransient = [408, 429, 500, 502, 503, 504].includes(result.status);
|
||||
if (retry < maxRetries && isTransient) {
|
||||
@@ -1146,6 +1169,7 @@ async function handleRoundRobinCombo({
|
||||
provider,
|
||||
result.headers
|
||||
);
|
||||
const comboBadRequestFallback = shouldFallbackComboBadRequest(result.status, errorText);
|
||||
|
||||
// Transient errors → mark in semaphore AND record circuit breaker failure
|
||||
if (TRANSIENT_FOR_BREAKER.includes(result.status) && cooldownMs > 0) {
|
||||
@@ -1157,11 +1181,18 @@ async function handleRoundRobinCombo({
|
||||
);
|
||||
}
|
||||
|
||||
if (!shouldFallback) {
|
||||
if (!shouldFallback && !comboBadRequestFallback) {
|
||||
log.warn("COMBO-RR", `${modelStr} failed (no fallback)`, { status: result.status });
|
||||
return result;
|
||||
}
|
||||
|
||||
if (comboBadRequestFallback) {
|
||||
log.info(
|
||||
"COMBO-RR",
|
||||
`Treating provider-scoped 400 from ${modelStr} as model-local failure; trying next model`
|
||||
);
|
||||
}
|
||||
|
||||
// Transient error → retry same model
|
||||
const isTransient = [408, 429, 500, 502, 503, 504].includes(result.status);
|
||||
if (retry < maxRetries && isTransient) {
|
||||
|
||||
@@ -100,13 +100,66 @@ function shouldDisplayGitHubQuota(quota: UsageQuota | null): quota is UsageQuota
|
||||
return quota.total > 0 || quota.remainingPercentage !== undefined;
|
||||
}
|
||||
|
||||
// GLM (Z.AI) quota API config
|
||||
const GLM_QUOTA_URLS: Record<string, string> = {
|
||||
international: "https://api.z.ai/api/monitor/usage/quota/limit",
|
||||
china: "https://open.bigmodel.cn/api/monitor/usage/quota/limit",
|
||||
};
|
||||
|
||||
async function getGlmUsage(apiKey: string, providerSpecificData?: Record<string, unknown>) {
|
||||
const region = providerSpecificData?.apiRegion || "international";
|
||||
const quotaUrl = GLM_QUOTA_URLS[region] || GLM_QUOTA_URLS.international;
|
||||
|
||||
const res = await fetch(quotaUrl, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
Accept: "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
if (res.status === 401) throw new Error("Invalid API key");
|
||||
throw new Error(`GLM quota API error (${res.status})`);
|
||||
}
|
||||
|
||||
const json = await res.json();
|
||||
const data = toRecord(json.data);
|
||||
const limits: unknown[] = Array.isArray(data.limits) ? data.limits : [];
|
||||
const quotas: Record<string, UsageQuota> = {};
|
||||
|
||||
for (const limit of limits) {
|
||||
const src = toRecord(limit);
|
||||
if (src.type !== "TOKENS_LIMIT") continue;
|
||||
|
||||
const usedPercent = toNumber(src.percentage, 0);
|
||||
const resetMs = toNumber(src.nextResetTime, 0);
|
||||
const remaining = Math.max(0, 100 - usedPercent);
|
||||
|
||||
quotas["session"] = {
|
||||
used: usedPercent,
|
||||
total: 100,
|
||||
remaining,
|
||||
remainingPercentage: remaining,
|
||||
resetAt: resetMs > 0 ? new Date(resetMs).toISOString() : null,
|
||||
unlimited: false,
|
||||
};
|
||||
}
|
||||
|
||||
const levelRaw = typeof data.level === "string" ? data.level : "";
|
||||
const plan = levelRaw
|
||||
? levelRaw.charAt(0).toUpperCase() + levelRaw.slice(1).toLowerCase()
|
||||
: "Unknown";
|
||||
|
||||
return { plan, quotas };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get usage data for a provider connection
|
||||
* @param {Object} connection - Provider connection with accessToken
|
||||
* @returns {Promise<unknown>} Usage data with quotas
|
||||
*/
|
||||
export async function getUsageForProvider(connection) {
|
||||
const { provider, accessToken, providerSpecificData } = connection;
|
||||
const { provider, accessToken, apiKey, providerSpecificData } = connection;
|
||||
|
||||
switch (provider) {
|
||||
case "github":
|
||||
@@ -127,6 +180,8 @@ export async function getUsageForProvider(connection) {
|
||||
return await getQwenUsage(accessToken, providerSpecificData);
|
||||
case "iflow":
|
||||
return await getIflowUsage(accessToken);
|
||||
case "glm":
|
||||
return await getGlmUsage(apiKey, providerSpecificData);
|
||||
default:
|
||||
return { message: `Usage API not implemented for ${provider}` };
|
||||
}
|
||||
|
||||
Generated
+11
-11
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "omniroute",
|
||||
"version": "3.1.10",
|
||||
"version": "3.2.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "omniroute",
|
||||
"version": "3.1.10",
|
||||
"version": "3.2.0",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
@@ -6346,9 +6346,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
|
||||
"version": "5.0.4",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz",
|
||||
"integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==",
|
||||
"version": "5.0.5",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
|
||||
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -7574,9 +7574,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||
"version": "1.1.13",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
|
||||
"integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -15527,9 +15527,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/path-to-regexp": {
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz",
|
||||
"integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==",
|
||||
"version": "8.4.0",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.0.tgz",
|
||||
"integrity": "sha512-PuseHIvAnz3bjrM2rGJtSgo1zjgxapTLZ7x2pjhzWwlp4SJQgK3f3iZIQwkpEnBaKz6seKBADpM4B4ySkuYypg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
|
||||
+3
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "omniroute",
|
||||
"version": "3.1.10",
|
||||
"version": "3.2.0",
|
||||
"description": "Smart AI Router with auto fallback — route to FREE & cheap models, zero downtime. Works with Cursor, Cline, Claude Desktop, Codex, and any OpenAI-compatible tool.",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
@@ -161,6 +161,7 @@
|
||||
]
|
||||
},
|
||||
"overrides": {
|
||||
"dompurify": "^3.3.2"
|
||||
"dompurify": "^3.3.2",
|
||||
"path-to-regexp": "^8.4.0"
|
||||
}
|
||||
}
|
||||
|
||||
+340
@@ -0,0 +1,340 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { Card, Button, EmptyState } from "@/shared/components";
|
||||
import { useNotificationStore } from "@/store/notificationStore";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
// ─── Types ───────────────────────────────────────────────────────────────────
|
||||
|
||||
interface SemanticCacheStats {
|
||||
memoryEntries: number;
|
||||
dbEntries: number;
|
||||
hits: number;
|
||||
misses: number;
|
||||
hitRate: string;
|
||||
tokensSaved: number;
|
||||
}
|
||||
|
||||
interface IdempotencyStats {
|
||||
activeKeys: number;
|
||||
windowMs: number;
|
||||
}
|
||||
|
||||
interface CacheStats {
|
||||
semanticCache: SemanticCacheStats;
|
||||
idempotency: IdempotencyStats;
|
||||
}
|
||||
|
||||
// ─── Sub-components ──────────────────────────────────────────────────────────
|
||||
|
||||
function StatCard({
|
||||
icon,
|
||||
label,
|
||||
value,
|
||||
sub,
|
||||
valueClass = "text-text",
|
||||
}: {
|
||||
icon: string;
|
||||
label: string;
|
||||
value: string | number;
|
||||
sub?: string;
|
||||
valueClass?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex flex-col gap-1 p-4 rounded-xl bg-surface-raised border border-border/40">
|
||||
<div className="flex items-center gap-1.5 text-text-muted text-xs">
|
||||
<span className="material-symbols-outlined text-base leading-none" aria-hidden="true">
|
||||
{icon}
|
||||
</span>
|
||||
{label}
|
||||
</div>
|
||||
<div className={`text-2xl font-semibold tabular-nums ${valueClass}`}>{value}</div>
|
||||
{sub && <div className="text-xs text-text-muted">{sub}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function HitRateBar({ hitRate, label }: { hitRate: number; label: string }) {
|
||||
const colorClass = hitRate >= 70 ? "bg-green-500" : hitRate >= 40 ? "bg-amber-400" : "bg-red-500";
|
||||
const textClass =
|
||||
hitRate >= 70 ? "text-green-500" : hitRate >= 40 ? "text-amber-400" : "text-red-500";
|
||||
|
||||
return (
|
||||
<div
|
||||
className="w-full"
|
||||
role="progressbar"
|
||||
aria-label={label}
|
||||
aria-valuenow={hitRate}
|
||||
aria-valuemin={0}
|
||||
aria-valuemax={100}
|
||||
>
|
||||
<div className="flex justify-between text-xs mb-1.5">
|
||||
<span className="text-text-muted">{label}</span>
|
||||
<span className={`font-semibold tabular-nums ${textClass}`}>{hitRate.toFixed(1)}%</span>
|
||||
</div>
|
||||
<div className="w-full h-2 rounded-full bg-surface/50 overflow-hidden">
|
||||
<div
|
||||
className={`h-full rounded-full transition-all duration-500 ${colorClass}`}
|
||||
style={{ width: `${Math.min(hitRate, 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function InfoRow({ icon, children }: { icon: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="flex gap-2 text-sm text-text-muted">
|
||||
<span
|
||||
className="material-symbols-outlined text-base leading-5 text-blue-400 shrink-0"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{icon}
|
||||
</span>
|
||||
<span>{children}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Page ────────────────────────────────────────────────────────────────────
|
||||
|
||||
const REFRESH_INTERVAL_MS = 10_000;
|
||||
const REFRESH_INTERVAL_SECONDS = REFRESH_INTERVAL_MS / 1000;
|
||||
|
||||
export default function CachePage() {
|
||||
const t = useTranslations("cache");
|
||||
const [stats, setStats] = useState<CacheStats | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [clearing, setClearing] = useState(false);
|
||||
const notify = useNotificationStore();
|
||||
|
||||
const fetchStats = useCallback(async () => {
|
||||
try {
|
||||
const res = await fetch("/api/cache");
|
||||
if (res.ok) {
|
||||
const data: CacheStats = await res.json();
|
||||
setStats(data);
|
||||
}
|
||||
} catch (error) {
|
||||
// Network error — keep stale stats rather than clearing the UI
|
||||
console.error("[CachePage] Failed to fetch cache stats:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
void fetchStats();
|
||||
const id = setInterval(() => void fetchStats(), REFRESH_INTERVAL_MS);
|
||||
return () => clearInterval(id);
|
||||
}, [fetchStats]);
|
||||
|
||||
const handleClearAll = async () => {
|
||||
setClearing(true);
|
||||
try {
|
||||
const res = await fetch("/api/cache", { method: "DELETE" });
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
notify.add({
|
||||
type: "success",
|
||||
message: t("clearSuccess", { count: data.expiredRemoved ?? 0 }),
|
||||
});
|
||||
await fetchStats();
|
||||
} else {
|
||||
notify.add({ type: "error", message: t("clearError") });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[CachePage] Failed to clear cache:", error);
|
||||
notify.add({ type: "error", message: t("clearError") });
|
||||
} finally {
|
||||
setClearing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const sc = stats?.semanticCache;
|
||||
const idp = stats?.idempotency;
|
||||
const hitRate = sc ? parseFloat(sc.hitRate) : 0;
|
||||
const totalRequests = sc ? sc.hits + sc.misses : 0;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-xl font-semibold">{t("title")}</h1>
|
||||
<p className="text-sm text-text-muted mt-0.5">{t("description")}</p>
|
||||
</div>
|
||||
<div className="flex gap-2 shrink-0">
|
||||
<Button
|
||||
variant="secondary"
|
||||
icon="refresh"
|
||||
size="sm"
|
||||
onClick={() => void fetchStats()}
|
||||
disabled={loading}
|
||||
aria-label={t("refresh")}
|
||||
>
|
||||
{t("refresh")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="danger"
|
||||
icon="delete_sweep"
|
||||
size="sm"
|
||||
onClick={() => void handleClearAll()}
|
||||
disabled={clearing || loading}
|
||||
loading={clearing}
|
||||
aria-label={t("clearAll")}
|
||||
>
|
||||
{t("clearAll")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Loading skeleton */}
|
||||
{loading && (
|
||||
<div
|
||||
className="grid grid-cols-2 md:grid-cols-4 gap-4"
|
||||
aria-busy="true"
|
||||
aria-label="Loading cache statistics"
|
||||
>
|
||||
{Array.from({ length: 4 }).map((_, i) => (
|
||||
<div key={i} className="h-24 rounded-xl bg-surface-raised animate-pulse" />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error / empty state */}
|
||||
{!loading && !stats && (
|
||||
<EmptyState
|
||||
icon="cached"
|
||||
title={t("unavailable")}
|
||||
description={t("unavailableDesc")}
|
||||
actionLabel={t("refresh")}
|
||||
onAction={() => void fetchStats()}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Main content */}
|
||||
{!loading && stats && (
|
||||
<>
|
||||
{/* Stats grid */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<StatCard
|
||||
icon="memory"
|
||||
label={t("memoryEntries")}
|
||||
value={sc?.memoryEntries ?? 0}
|
||||
sub={t("memoryEntriesSub")}
|
||||
/>
|
||||
<StatCard
|
||||
icon="storage"
|
||||
label={t("dbEntries")}
|
||||
value={sc?.dbEntries ?? 0}
|
||||
sub={t("dbEntriesSub")}
|
||||
/>
|
||||
<StatCard
|
||||
icon="trending_up"
|
||||
label={t("cacheHits")}
|
||||
value={sc?.hits ?? 0}
|
||||
sub={t("cacheHitsSub", { total: totalRequests })}
|
||||
valueClass="text-green-500"
|
||||
/>
|
||||
<StatCard
|
||||
icon="token"
|
||||
label={t("tokensSaved")}
|
||||
value={(sc?.tokensSaved ?? 0).toLocaleString()}
|
||||
sub={t("tokensSavedSub")}
|
||||
valueClass="text-blue-400"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Hit rate + breakdown */}
|
||||
<Card>
|
||||
<div className="p-5 flex flex-col gap-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="font-medium text-sm">{t("performance")}</h2>
|
||||
<span className="text-xs text-text-muted">
|
||||
{t("autoRefresh", { seconds: REFRESH_INTERVAL_SECONDS })}
|
||||
</span>
|
||||
</div>
|
||||
<HitRateBar hitRate={hitRate} label={t("hitRate")} />
|
||||
<div className="grid grid-cols-3 gap-4 pt-3 border-t border-border/30 text-center">
|
||||
<div>
|
||||
<div className="text-lg font-semibold tabular-nums text-green-500">
|
||||
{sc?.hits ?? 0}
|
||||
</div>
|
||||
<div className="text-xs text-text-muted mt-0.5">{t("hits")}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-lg font-semibold tabular-nums text-red-400">
|
||||
{sc?.misses ?? 0}
|
||||
</div>
|
||||
<div className="text-xs text-text-muted mt-0.5">{t("misses")}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-lg font-semibold tabular-nums">{totalRequests}</div>
|
||||
<div className="text-xs text-text-muted mt-0.5">{t("total")}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Cache behavior */}
|
||||
<Card>
|
||||
<div className="p-5 flex flex-col gap-3">
|
||||
<h2 className="font-medium text-sm">{t("behavior")}</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<InfoRow icon="info">{t("behaviorDeterministic")}</InfoRow>
|
||||
<InfoRow icon="info">
|
||||
{t.rich("behaviorBypass", {
|
||||
header: () => (
|
||||
<code className="bg-surface px-1 py-0.5 rounded text-xs font-mono">
|
||||
X-OmniRoute-No-Cache: true
|
||||
</code>
|
||||
),
|
||||
})}
|
||||
</InfoRow>
|
||||
<InfoRow icon="info">{t("behaviorTwoTier")}</InfoRow>
|
||||
<InfoRow icon="info">
|
||||
{t.rich("behaviorTtl", {
|
||||
envVar: () => (
|
||||
<code className="bg-surface px-1 py-0.5 rounded text-xs font-mono">
|
||||
SEMANTIC_CACHE_TTL_MS
|
||||
</code>
|
||||
),
|
||||
})}
|
||||
</InfoRow>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Idempotency */}
|
||||
<Card>
|
||||
<div className="p-5 flex flex-col gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className="material-symbols-outlined text-base text-text-muted"
|
||||
aria-hidden="true"
|
||||
>
|
||||
fingerprint
|
||||
</span>
|
||||
<h2 className="font-medium text-sm">{t("idempotency")}</h2>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="p-3 rounded-lg bg-surface/50">
|
||||
<div className="text-lg font-semibold tabular-nums">{idp?.activeKeys ?? 0}</div>
|
||||
<div className="text-xs text-text-muted mt-0.5">{t("activeDedupKeys")}</div>
|
||||
</div>
|
||||
<div className="p-3 rounded-lg bg-surface/50">
|
||||
<div className="text-lg font-semibold tabular-nums">
|
||||
{idp ? `${(idp.windowMs / 1000).toFixed(0)}s` : "—"}
|
||||
</div>
|
||||
<div className="text-xs text-text-muted mt-0.5">{t("dedupWindow")}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1122,18 +1122,27 @@ function TestResultsView({ results }) {
|
||||
{results.results?.map((r, i) => (
|
||||
<div
|
||||
key={i}
|
||||
title={r.error || undefined}
|
||||
className="flex items-center gap-2 text-xs px-2 py-1.5 rounded bg-black/[0.02] dark:bg-white/[0.02]"
|
||||
>
|
||||
<span
|
||||
className={`material-symbols-outlined text-[14px] ${
|
||||
r.status === "ok"
|
||||
? "text-emerald-500"
|
||||
: r.status === "skipped"
|
||||
? "text-text-muted"
|
||||
: "text-red-500"
|
||||
: r.status === "reachable"
|
||||
? "text-amber-500"
|
||||
: r.status === "skipped"
|
||||
? "text-text-muted"
|
||||
: "text-red-500"
|
||||
}`}
|
||||
>
|
||||
{r.status === "ok" ? "check_circle" : r.status === "skipped" ? "skip_next" : "error"}
|
||||
{r.status === "ok"
|
||||
? "check_circle"
|
||||
: r.status === "reachable"
|
||||
? "network_check"
|
||||
: r.status === "skipped"
|
||||
? "skip_next"
|
||||
: "error"}
|
||||
</span>
|
||||
<code className="font-mono flex-1">{r.model}</code>
|
||||
{r.latencyMs !== undefined && <span className="text-text-muted">{r.latencyMs}ms</span>}
|
||||
@@ -1141,9 +1150,11 @@ function TestResultsView({ results }) {
|
||||
className={`text-[10px] uppercase font-medium ${
|
||||
r.status === "ok"
|
||||
? "text-emerald-500"
|
||||
: r.status === "skipped"
|
||||
? "text-text-muted"
|
||||
: "text-red-500"
|
||||
: r.status === "reachable"
|
||||
? "text-amber-500"
|
||||
: r.status === "skipped"
|
||||
? "text-text-muted"
|
||||
: "text-red-500"
|
||||
}`}
|
||||
>
|
||||
{r.status}
|
||||
|
||||
@@ -4289,6 +4289,7 @@ function AddApiKeyModal({
|
||||
const defaultBailianUrl = "https://coding-intl.dashscope.aliyuncs.com/apps/anthropic/v1";
|
||||
const isVertex = provider === "vertex";
|
||||
const defaultRegion = "us-central1";
|
||||
const isGlm = provider === "glm";
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
name: "",
|
||||
@@ -4296,6 +4297,7 @@ function AddApiKeyModal({
|
||||
priority: 1,
|
||||
baseUrl: isBailian ? defaultBailianUrl : "",
|
||||
region: isVertex ? defaultRegion : "",
|
||||
apiRegion: "international",
|
||||
validationModelId: "",
|
||||
});
|
||||
const [validating, setValidating] = useState(false);
|
||||
@@ -4385,6 +4387,10 @@ function AddApiKeyModal({
|
||||
payload.providerSpecificData = {
|
||||
region: formData.region,
|
||||
};
|
||||
} else if (isGlm) {
|
||||
payload.providerSpecificData = {
|
||||
apiRegion: formData.apiRegion,
|
||||
};
|
||||
}
|
||||
|
||||
const error = await onSave(payload);
|
||||
@@ -4484,6 +4490,22 @@ function AddApiKeyModal({
|
||||
hint="ex: us-central1 ou europe-west4. Partner models usam a região global automaticamente."
|
||||
/>
|
||||
)}
|
||||
{isGlm && (
|
||||
<div>
|
||||
<label className="text-sm font-medium text-text-main mb-1 block">API Region</label>
|
||||
<select
|
||||
value={formData.apiRegion}
|
||||
onChange={(e) => setFormData({ ...formData, apiRegion: e.target.value })}
|
||||
className="w-full px-3 py-2 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary"
|
||||
>
|
||||
<option value="international">International (api.z.ai)</option>
|
||||
<option value="china">China Mainland (open.bigmodel.cn)</option>
|
||||
</select>
|
||||
<p className="text-xs text-text-muted mt-1">
|
||||
Select the endpoint region for API access and quota tracking.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
@@ -4533,6 +4555,7 @@ function EditConnectionModal({ isOpen, connection, onSave, onClose }: EditConnec
|
||||
healthCheckInterval: 60,
|
||||
baseUrl: "",
|
||||
region: "",
|
||||
apiRegion: "international",
|
||||
validationModelId: "",
|
||||
tag: "",
|
||||
});
|
||||
@@ -4548,6 +4571,7 @@ function EditConnectionModal({ isOpen, connection, onSave, onClose }: EditConnec
|
||||
const isBailian = connection?.provider === "bailian-coding-plan";
|
||||
const defaultBailianUrl = "https://coding-intl.dashscope.aliyuncs.com/apps/anthropic/v1";
|
||||
const isVertex = connection?.provider === "vertex";
|
||||
const isGlm = connection?.provider === "glm";
|
||||
const defaultRegion = "us-central1";
|
||||
|
||||
useEffect(() => {
|
||||
@@ -4563,6 +4587,7 @@ function EditConnectionModal({ isOpen, connection, onSave, onClose }: EditConnec
|
||||
healthCheckInterval: connection.healthCheckInterval ?? 60,
|
||||
baseUrl: existingBaseUrl || (isBailian ? defaultBailianUrl : ""),
|
||||
region: existingRegion || (isVertex ? defaultRegion : ""),
|
||||
apiRegion: (connection.providerSpecificData?.apiRegion as string) || "international",
|
||||
validationModelId: (connection.providerSpecificData?.validationModelId as string) || "",
|
||||
tag: (connection.providerSpecificData?.tag as string) || "",
|
||||
});
|
||||
@@ -4698,6 +4723,8 @@ function EditConnectionModal({ isOpen, connection, onSave, onClose }: EditConnec
|
||||
updates.providerSpecificData.baseUrl = validatedBailianBaseUrl;
|
||||
} else if (isVertex) {
|
||||
updates.providerSpecificData.region = formData.region;
|
||||
} else if (isGlm) {
|
||||
updates.providerSpecificData.apiRegion = formData.apiRegion;
|
||||
}
|
||||
} else {
|
||||
// Also persist tag for OAuth accounts
|
||||
@@ -4832,6 +4859,23 @@ function EditConnectionModal({ isOpen, connection, onSave, onClose }: EditConnec
|
||||
/>
|
||||
)}
|
||||
|
||||
{isGlm && (
|
||||
<div>
|
||||
<label className="text-sm font-medium text-text-main mb-1 block">API Region</label>
|
||||
<select
|
||||
value={formData.apiRegion}
|
||||
onChange={(e) => setFormData({ ...formData, apiRegion: e.target.value })}
|
||||
className="w-full px-3 py-2 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary"
|
||||
>
|
||||
<option value="international">International (api.z.ai)</option>
|
||||
<option value="china">China Mainland (open.bigmodel.cn)</option>
|
||||
</select>
|
||||
<p className="text-xs text-text-muted mt-1">
|
||||
Select the endpoint region for API access and quota tracking.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* T07: Extra API Keys for round-robin rotation */}
|
||||
{!isOAuth && (
|
||||
<div className="flex flex-col gap-2">
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useTranslations } from "next-intl";
|
||||
|
||||
import { useState, useEffect, useCallback, useMemo, useRef } from "react";
|
||||
import Image from "next/image";
|
||||
import { parseQuotaData, calculatePercentage, normalizePlanTier } from "./utils";
|
||||
import { parseQuotaData, calculatePercentage, normalizePlanTier, resolvePlanValue } from "./utils";
|
||||
import Card from "@/shared/components/Card";
|
||||
import Badge from "@/shared/components/Badge";
|
||||
import { CardSkeleton } from "@/shared/components/Loading";
|
||||
@@ -16,6 +16,8 @@ const LS_EXPANDED_GROUPS = "omniroute:limits:expandedGroups";
|
||||
|
||||
const REFRESH_INTERVAL_MS = 120000;
|
||||
const MIN_FETCH_INTERVAL_MS = 30000; // Debounce per-connection fetches
|
||||
const QUOTA_BAR_GREEN_THRESHOLD = 50;
|
||||
const QUOTA_BAR_YELLOW_THRESHOLD = 20;
|
||||
|
||||
// Provider display config
|
||||
const PROVIDER_CONFIG = {
|
||||
@@ -24,7 +26,7 @@ const PROVIDER_CONFIG = {
|
||||
kiro: { label: "Kiro AI", color: "#FF6B35" },
|
||||
codex: { label: "OpenAI Codex", color: "#10A37F" },
|
||||
claude: { label: "Claude Code", color: "#D97757" },
|
||||
glm: { label: "GLM (Z.AI)", color: "#4A90D9" },
|
||||
glm: { label: "GLM Coding", color: "#4A90D9" },
|
||||
"kimi-coding": { label: "Kimi Coding", color: "#1E3A8A" },
|
||||
};
|
||||
|
||||
@@ -64,9 +66,13 @@ function getShortModelName(name) {
|
||||
}
|
||||
|
||||
// Get bar color based on remaining percentage
|
||||
function getBarColor(remaining) {
|
||||
if (remaining > 70) return { bar: "#22c55e", text: "#22c55e", bg: "rgba(34,197,94,0.12)" };
|
||||
if (remaining >= 30) return { bar: "#eab308", text: "#eab308", bg: "rgba(234,179,8,0.12)" };
|
||||
function getBarColor(remainingPercentage) {
|
||||
if (remainingPercentage > QUOTA_BAR_GREEN_THRESHOLD) {
|
||||
return { bar: "#22c55e", text: "#22c55e", bg: "rgba(34,197,94,0.12)" };
|
||||
}
|
||||
if (remainingPercentage > QUOTA_BAR_YELLOW_THRESHOLD) {
|
||||
return { bar: "#eab308", text: "#eab308", bg: "rgba(234,179,8,0.12)" };
|
||||
}
|
||||
return { bar: "#ef4444", text: "#ef4444", bg: "rgba(239,68,68,0.12)" };
|
||||
}
|
||||
|
||||
@@ -297,14 +303,22 @@ export default function ProviderLimits() {
|
||||
);
|
||||
}, [filteredConnections]);
|
||||
|
||||
const tierByConnection = useMemo(() => {
|
||||
const resolvedPlanByConnection = useMemo(() => {
|
||||
const out = {};
|
||||
for (const conn of sortedConnections) {
|
||||
out[conn.id] = normalizePlanTier(quotaData[conn.id]?.plan);
|
||||
out[conn.id] = resolvePlanValue(quotaData[conn.id]?.plan, conn.providerSpecificData);
|
||||
}
|
||||
return out;
|
||||
}, [sortedConnections, quotaData]);
|
||||
|
||||
const tierByConnection = useMemo(() => {
|
||||
const out = {};
|
||||
for (const conn of sortedConnections) {
|
||||
out[conn.id] = normalizePlanTier(resolvedPlanByConnection[conn.id]);
|
||||
}
|
||||
return out;
|
||||
}, [sortedConnections, resolvedPlanByConnection]);
|
||||
|
||||
const tierCounts = useMemo(() => {
|
||||
const counts = {
|
||||
all: sortedConnections.length,
|
||||
@@ -313,6 +327,7 @@ export default function ProviderLimits() {
|
||||
business: 0,
|
||||
ultra: 0,
|
||||
pro: 0,
|
||||
plus: 0,
|
||||
free: 0,
|
||||
unknown: 0,
|
||||
};
|
||||
@@ -533,6 +548,7 @@ export default function ProviderLimits() {
|
||||
color: "#666",
|
||||
};
|
||||
const tierMeta = tierByConnection[conn.id] || normalizePlanTier(null);
|
||||
const resolvedPlan = resolvedPlanByConnection[conn.id];
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -558,21 +574,29 @@ export default function ProviderLimits() {
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<div className="text-[13px] font-semibold text-text-main truncate">
|
||||
{conn.name || config.label}
|
||||
{conn.name || conn.displayName || conn.email || config.label}
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 mt-0.5">
|
||||
<div className="flex items-center gap-1.5 mt-1 min-h-5">
|
||||
<span
|
||||
title={
|
||||
quota?.plan
|
||||
? t("rawPlanWithValue", { plan: quota.plan })
|
||||
resolvedPlan
|
||||
? t("rawPlanWithValue", { plan: resolvedPlan })
|
||||
: t("noPlanFromProvider")
|
||||
}
|
||||
className="inline-flex items-center shrink-0"
|
||||
>
|
||||
<Badge variant={tierMeta.variant} size="sm" dot>
|
||||
<Badge
|
||||
variant={tierMeta.variant}
|
||||
size="sm"
|
||||
dot
|
||||
className="h-5 leading-none"
|
||||
>
|
||||
{tierMeta.label}
|
||||
</Badge>
|
||||
</span>
|
||||
<span className="text-[11px] text-text-muted">{config.label}</span>
|
||||
<span className="text-[11px] leading-none text-text-muted">
|
||||
{config.label}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -597,17 +621,19 @@ export default function ProviderLimits() {
|
||||
<div className="text-xs text-text-muted italic">{quota.message}</div>
|
||||
) : quota?.quotas?.length > 0 ? (
|
||||
quota.quotas.map((q, i) => {
|
||||
const remaining =
|
||||
q.remainingPercentage !== undefined
|
||||
? Math.round(q.remainingPercentage)
|
||||
: calculatePercentage(q.used, q.total);
|
||||
const colors = getBarColor(remaining);
|
||||
const remainingPercentage = calculatePercentage(q.used, q.total);
|
||||
const colors = getBarColor(remainingPercentage);
|
||||
const cd = formatCountdown(q.resetAt);
|
||||
const shortName = getShortModelName(q.name);
|
||||
const staleAfterReset = q.staleAfterReset === true;
|
||||
|
||||
return (
|
||||
<div key={i} className="flex items-center gap-1.5 min-w-[200px] shrink-0">
|
||||
<div
|
||||
key={i}
|
||||
className={`flex items-center gap-1.5 min-w-[200px] shrink-0 ${
|
||||
i > 0 ? "border-l border-border/80 pl-3 ml-1" : ""
|
||||
}`}
|
||||
>
|
||||
{/* Model label */}
|
||||
<span
|
||||
className="text-[11px] font-semibold py-0.5 px-2 rounded whitespace-nowrap min-w-[60px] text-center"
|
||||
@@ -632,7 +658,7 @@ export default function ProviderLimits() {
|
||||
<div
|
||||
className="h-full rounded-sm transition-[width] duration-300 ease-out"
|
||||
style={{
|
||||
width: `${Math.min(remaining, 100)}%`,
|
||||
width: `${Math.min(remainingPercentage, 100)}%`,
|
||||
background: colors.bar,
|
||||
}}
|
||||
/>
|
||||
@@ -643,7 +669,7 @@ export default function ProviderLimits() {
|
||||
className="text-[11px] font-semibold min-w-[32px] text-right"
|
||||
style={{ color: colors.text }}
|
||||
>
|
||||
{remaining}%
|
||||
{remainingPercentage}%
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,30 @@
|
||||
import { getModelsByProviderId } from "@omniroute/open-sse/config/providerModels.ts";
|
||||
import { safePercentage } from "@/shared/utils/formatting";
|
||||
|
||||
const PROVIDER_PLAN_FALLBACKS = new Set([
|
||||
"claude code",
|
||||
"kimi coding",
|
||||
"kiro",
|
||||
"openai codex",
|
||||
"codex",
|
||||
"github copilot",
|
||||
]);
|
||||
|
||||
function toRecord(value: unknown): Record<string, unknown> {
|
||||
return value && typeof value === "object" && !Array.isArray(value)
|
||||
? (value as Record<string, unknown>)
|
||||
: {};
|
||||
}
|
||||
|
||||
function normalizePlanCandidate(value: unknown) {
|
||||
if (typeof value !== "string") return null;
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) return null;
|
||||
if (trimmed.toLowerCase() === "unknown") return null;
|
||||
if (PROVIDER_PLAN_FALLBACKS.has(trimmed.toLowerCase())) return null;
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format ISO date string to countdown format (inspired by vscode-antigravity-cockpit)
|
||||
* @param {string|Date} date - ISO date string or Date object
|
||||
@@ -180,7 +204,6 @@ export function parseQuotaData(provider, data) {
|
||||
break;
|
||||
|
||||
default:
|
||||
// Generic fallback for unknown providers
|
||||
if (data.quotas) {
|
||||
Object.entries(data.quotas).forEach(([name, quota]: [string, any]) => {
|
||||
normalizedQuotas.push(normalizeQuotaEntry(name, quota));
|
||||
@@ -210,9 +233,32 @@ export function parseQuotaData(provider, data) {
|
||||
return normalizedQuotas;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the best available plan label using live usage first, then persisted
|
||||
* provider-specific connection metadata.
|
||||
*/
|
||||
export function resolvePlanValue(plan, providerSpecificData) {
|
||||
const psd = toRecord(providerSpecificData);
|
||||
const candidates = [
|
||||
plan,
|
||||
psd.workspacePlanType,
|
||||
psd.plan,
|
||||
psd.subscription,
|
||||
psd.tier,
|
||||
psd.accountTier,
|
||||
];
|
||||
|
||||
for (const candidate of candidates) {
|
||||
const normalized = normalizePlanCandidate(candidate);
|
||||
if (normalized) return normalized;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize provider-specific plan labels into a shared tier taxonomy.
|
||||
* Supported tiers: enterprise, business, team, ultra, pro, free, unknown.
|
||||
* Supported tiers: enterprise, business, team, ultra, pro, plus, free, unknown.
|
||||
*/
|
||||
export function normalizePlanTier(plan) {
|
||||
const raw = typeof plan === "string" ? plan.trim() : "";
|
||||
@@ -223,12 +269,12 @@ export function normalizePlanTier(plan) {
|
||||
const upper = raw.toUpperCase();
|
||||
|
||||
// Provider names that are not real plan tiers — treat as unknown
|
||||
if (upper === "CLAUDE CODE" || upper === "KIMI CODING" || upper === "KIRO") {
|
||||
return { key: "unknown", label: raw, variant: "default", rank: 0, raw };
|
||||
if (PROVIDER_PLAN_FALLBACKS.has(raw.toLowerCase())) {
|
||||
return { key: "unknown", label: "Unknown", variant: "default", rank: 0, raw };
|
||||
}
|
||||
|
||||
if (upper.includes("PRO+") || upper.includes("PRO PLUS") || upper.includes("PROPLUS")) {
|
||||
return { key: "plus", label: "Pro+", variant: "secondary", rank: 4, raw };
|
||||
return { key: "plus", label: "Pro+", variant: "success", rank: 4, raw };
|
||||
}
|
||||
|
||||
if (upper.includes("ENTERPRISE") || upper.includes("CORP") || upper.includes("ORG")) {
|
||||
@@ -245,7 +291,7 @@ export function normalizePlanTier(plan) {
|
||||
}
|
||||
|
||||
if (upper.includes("STUDENT")) {
|
||||
return { key: "pro", label: "Student", variant: "primary", rank: 3, raw };
|
||||
return { key: "pro", label: "Student", variant: "success", rank: 3, raw };
|
||||
}
|
||||
|
||||
if (upper.includes("ULTRA")) {
|
||||
@@ -253,11 +299,11 @@ export function normalizePlanTier(plan) {
|
||||
}
|
||||
|
||||
if (upper.includes("PRO") || upper.includes("PREMIUM")) {
|
||||
return { key: "pro", label: "Pro", variant: "primary", rank: 3, raw };
|
||||
return { key: "pro", label: "Pro", variant: "success", rank: 3, raw };
|
||||
}
|
||||
|
||||
if (upper.includes("PLUS") || upper.includes("PAID")) {
|
||||
return { key: "plus", label: "Plus", variant: "secondary", rank: 2, raw };
|
||||
return { key: "plus", label: "Plus", variant: "success", rank: 2, raw };
|
||||
}
|
||||
|
||||
if (
|
||||
|
||||
Vendored
+67
-8
@@ -1,7 +1,18 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getCacheStats, clearCache, cleanExpiredEntries } from "@/lib/semanticCache";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import {
|
||||
getCacheStats,
|
||||
clearCache,
|
||||
cleanExpiredEntries,
|
||||
invalidateByModel,
|
||||
invalidateBySignature,
|
||||
invalidateStale,
|
||||
} from "@/lib/semanticCache";
|
||||
import { getIdempotencyStats } from "@/lib/idempotencyLayer";
|
||||
|
||||
function errorMessage(error: unknown): string {
|
||||
return error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/cache — Cache statistics
|
||||
*/
|
||||
@@ -15,19 +26,67 @@ export async function GET() {
|
||||
idempotency: idempotencyStats,
|
||||
});
|
||||
} catch (error) {
|
||||
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
return NextResponse.json({ error: errorMessage(error) }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE /api/cache — Clear all caches
|
||||
* DELETE /api/cache — Clear all caches or targeted invalidation.
|
||||
*
|
||||
* Exactly one optional query parameter may be provided:
|
||||
* ?model=<name> — invalidate all entries for a specific model
|
||||
* ?signature=<hex> — invalidate a single entry by its SHA-256 signature
|
||||
* ?staleMs=<number> — invalidate entries older than N milliseconds
|
||||
* (no params) — clear all cache entries
|
||||
*
|
||||
* Providing more than one parameter returns 400 Bad Request.
|
||||
*/
|
||||
export async function DELETE() {
|
||||
export async function DELETE(req: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(req.url);
|
||||
const model = searchParams.get("model");
|
||||
const signature = searchParams.get("signature");
|
||||
const staleMsParam = searchParams.get("staleMs");
|
||||
|
||||
// Enforce mutual exclusivity — only one invalidation mode per request
|
||||
const paramCount = [model, signature, staleMsParam].filter(Boolean).length;
|
||||
if (paramCount > 1) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error:
|
||||
"Only one invalidation parameter (model, signature, or staleMs) may be provided per request.",
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
if (model) {
|
||||
const removed = invalidateByModel(model);
|
||||
return NextResponse.json({ ok: true, invalidated: removed, scope: "model", model });
|
||||
}
|
||||
|
||||
if (signature) {
|
||||
const removed = invalidateBySignature(signature);
|
||||
return NextResponse.json({ ok: true, invalidated: removed ? 1 : 0, scope: "signature" });
|
||||
}
|
||||
|
||||
if (staleMsParam) {
|
||||
const maxAgeMs = parseInt(staleMsParam, 10);
|
||||
if (Number.isNaN(maxAgeMs) || maxAgeMs <= 0) {
|
||||
return NextResponse.json(
|
||||
{ error: "staleMs must be a positive integer (milliseconds)." },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
const removed = invalidateStale(maxAgeMs);
|
||||
return NextResponse.json({ ok: true, invalidated: removed, scope: "stale", maxAgeMs });
|
||||
}
|
||||
|
||||
// Full clear
|
||||
clearCache();
|
||||
const cleaned = cleanExpiredEntries();
|
||||
return NextResponse.json({ ok: true, expiredRemoved: cleaned });
|
||||
const expiredRemoved = cleanExpiredEntries();
|
||||
return NextResponse.json({ ok: true, expiredRemoved, scope: "all" });
|
||||
} catch (error) {
|
||||
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
return NextResponse.json({ error: errorMessage(error) }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import {
|
||||
buildComboTestRequestBody,
|
||||
probeComboModelReachability,
|
||||
shouldProbeComboTestReachability,
|
||||
} from "@/lib/combos/testHealth";
|
||||
import { getComboByName } from "@/lib/localDb";
|
||||
import { testComboSchema } from "@/shared/validation/schemas";
|
||||
import { isValidationFailure, validateBody } from "@/shared/validation/helpers";
|
||||
@@ -49,13 +54,9 @@ export async function POST(request) {
|
||||
const startTime = Date.now();
|
||||
try {
|
||||
// Send a minimal chat request to the internal SSE handler
|
||||
// Use OpenAI-compatible format — universally accepted by all providers via the translator
|
||||
const testBody = {
|
||||
model: modelStr,
|
||||
messages: [{ role: "user", content: "Hi" }],
|
||||
max_tokens: 5,
|
||||
stream: false,
|
||||
};
|
||||
// Use a tiny but realistic request body so gateway-routed models do not
|
||||
// get flagged as dead just because the probe payload is too synthetic.
|
||||
const testBody = buildComboTestRequestBody(modelStr);
|
||||
|
||||
const internalUrl = `${getBaseUrl(request)}/v1/chat/completions`;
|
||||
const controller = new AbortController();
|
||||
@@ -88,6 +89,29 @@ export async function POST(request) {
|
||||
} catch {
|
||||
errorMsg = res.statusText;
|
||||
}
|
||||
|
||||
let reachability = null;
|
||||
if (shouldProbeComboTestReachability(res.status)) {
|
||||
try {
|
||||
reachability = await probeComboModelReachability(modelStr);
|
||||
} catch {
|
||||
reachability = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (reachability?.reachable) {
|
||||
results.push({
|
||||
model: modelStr,
|
||||
status: "reachable",
|
||||
statusCode: res.status,
|
||||
error: errorMsg,
|
||||
latencyMs,
|
||||
provider: reachability.provider,
|
||||
probeMethod: reachability.method,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
results.push({
|
||||
model: modelStr,
|
||||
status: "error",
|
||||
|
||||
@@ -3,6 +3,10 @@ import { getProviderConnectionById } from "@/models";
|
||||
import { replaceCustomModels } from "@/lib/db/models";
|
||||
import { saveCallLog } from "@/lib/usage/callLogs";
|
||||
import { isAuthenticated } from "@/shared/utils/apiAuth";
|
||||
import {
|
||||
buildModelSyncInternalHeaders,
|
||||
isModelSyncInternalRequest,
|
||||
} from "@/shared/services/modelSyncScheduler";
|
||||
|
||||
/**
|
||||
* POST /api/providers/[id]/sync-models
|
||||
@@ -19,7 +23,7 @@ export async function POST(request: Request, { params }: { params: Promise<{ id:
|
||||
const { id } = await params;
|
||||
|
||||
try {
|
||||
if (!(await isAuthenticated(request))) {
|
||||
if (!(await isAuthenticated(request)) && !isModelSyncInternalRequest(request)) {
|
||||
return NextResponse.json(
|
||||
{ error: { message: "Authentication required", type: "invalid_api_key" } },
|
||||
{ status: 401 }
|
||||
@@ -41,7 +45,7 @@ export async function POST(request: Request, { params }: { params: Promise<{ id:
|
||||
method: "GET",
|
||||
headers: {
|
||||
cookie: request.headers.get("cookie") || "",
|
||||
"x-internal": "model-sync",
|
||||
...buildModelSyncInternalHeaders(),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -126,11 +126,23 @@ export async function GET(
|
||||
return Response.json({ error: "Connection not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
// Only OAuth connections have usage APIs
|
||||
if (connection.authType !== "oauth") {
|
||||
// Only OAuth connections and specific API key providers have usage APIs
|
||||
const apikeyUsageProviders = ["glm"];
|
||||
if (connection.authType !== "oauth" && !apikeyUsageProviders.includes(connection.provider)) {
|
||||
return Response.json({ message: "Usage not available for API key connections" });
|
||||
}
|
||||
|
||||
// API key providers skip OAuth refresh — call usage fetcher directly
|
||||
if (connection.authType !== "oauth") {
|
||||
try {
|
||||
const usageData = await getUsageForProvider(connection);
|
||||
return Response.json(usageData);
|
||||
} catch (error) {
|
||||
console.error("[Usage API] Error fetching usage:", error);
|
||||
return Response.json({ error: (error as any).message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve proxy for this connection FIRST (key → combo → provider → global → direct)
|
||||
// so that both credential refresh AND usage fetch go through the proxy.
|
||||
const proxyInfo = await resolveProxyForConnection(connectionId);
|
||||
|
||||
@@ -185,7 +185,9 @@
|
||||
"agents": "وكلاء",
|
||||
"cliToolsShort": "أدوات",
|
||||
"autoCombo": "Auto Combo",
|
||||
"searchTools": "Search Tools"
|
||||
"searchTools": "Search Tools",
|
||||
"cache": "Cache",
|
||||
"cacheShort": "Cache"
|
||||
},
|
||||
"themesPage": {
|
||||
"title": "المواضيع",
|
||||
@@ -2847,5 +2849,37 @@
|
||||
"userInitial": "انا بحاجة الى مساعدة مع",
|
||||
"userFollowUp": "هل يمكنك توضيح ذلك؟"
|
||||
}
|
||||
},
|
||||
"cache": {
|
||||
"title": "Cache Management",
|
||||
"description": "Monitor and manage semantic response cache, hit rates, and token savings.",
|
||||
"refresh": "Refresh",
|
||||
"clearAll": "Clear All",
|
||||
"memoryEntries": "Memory Entries",
|
||||
"dbEntries": "DB Entries",
|
||||
"cacheHits": "Cache Hits",
|
||||
"tokensSaved": "Tokens Saved",
|
||||
"hitRate": "Hit Rate",
|
||||
"performance": "Cache Performance",
|
||||
"behavior": "Cache Behavior",
|
||||
"idempotency": "Idempotency Layer",
|
||||
"clearSuccess": "Cache cleared. {count} expired entries removed.",
|
||||
"clearError": "Failed to clear cache.",
|
||||
"unavailable": "Cache unavailable",
|
||||
"unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.",
|
||||
"memoryEntriesSub": "In-memory LRU",
|
||||
"dbEntriesSub": "Persisted (SQLite)",
|
||||
"cacheHitsSub": "of {total} total",
|
||||
"tokensSavedSub": "Estimated from hits",
|
||||
"autoRefresh": "Auto-refreshes every {seconds}s",
|
||||
"hits": "Hits",
|
||||
"misses": "Misses",
|
||||
"total": "Total",
|
||||
"behaviorDeterministic": "Only non-streaming requests with temperature=0 are cached.",
|
||||
"behaviorBypass": "Bypass with header {header}.",
|
||||
"behaviorTwoTier": "Two-tier storage: in-memory LRU (fast) + SQLite (persistent across restarts).",
|
||||
"behaviorTtl": "Default TTL: 30 minutes. Configure via {envVar}.",
|
||||
"activeDedupKeys": "Active Dedup Keys",
|
||||
"dedupWindow": "Dedup Window"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,7 +185,9 @@
|
||||
"agents": "Агенти",
|
||||
"cliToolsShort": "Инструменти",
|
||||
"autoCombo": "Auto Combo",
|
||||
"searchTools": "Search Tools"
|
||||
"searchTools": "Search Tools",
|
||||
"cache": "Cache",
|
||||
"cacheShort": "Cache"
|
||||
},
|
||||
"themesPage": {
|
||||
"title": "Теми",
|
||||
@@ -2847,5 +2849,37 @@
|
||||
"userInitial": "Имам нужда от помощ за",
|
||||
"userFollowUp": "Можете ли да разкажете по-подробно за това?"
|
||||
}
|
||||
},
|
||||
"cache": {
|
||||
"title": "Cache Management",
|
||||
"description": "Monitor and manage semantic response cache, hit rates, and token savings.",
|
||||
"refresh": "Refresh",
|
||||
"clearAll": "Clear All",
|
||||
"memoryEntries": "Memory Entries",
|
||||
"dbEntries": "DB Entries",
|
||||
"cacheHits": "Cache Hits",
|
||||
"tokensSaved": "Tokens Saved",
|
||||
"hitRate": "Hit Rate",
|
||||
"performance": "Cache Performance",
|
||||
"behavior": "Cache Behavior",
|
||||
"idempotency": "Idempotency Layer",
|
||||
"clearSuccess": "Cache cleared. {count} expired entries removed.",
|
||||
"clearError": "Failed to clear cache.",
|
||||
"unavailable": "Cache unavailable",
|
||||
"unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.",
|
||||
"memoryEntriesSub": "In-memory LRU",
|
||||
"dbEntriesSub": "Persisted (SQLite)",
|
||||
"cacheHitsSub": "of {total} total",
|
||||
"tokensSavedSub": "Estimated from hits",
|
||||
"autoRefresh": "Auto-refreshes every {seconds}s",
|
||||
"hits": "Hits",
|
||||
"misses": "Misses",
|
||||
"total": "Total",
|
||||
"behaviorDeterministic": "Only non-streaming requests with temperature=0 are cached.",
|
||||
"behaviorBypass": "Bypass with header {header}.",
|
||||
"behaviorTwoTier": "Two-tier storage: in-memory LRU (fast) + SQLite (persistent across restarts).",
|
||||
"behaviorTtl": "Default TTL: 30 minutes. Configure via {envVar}.",
|
||||
"activeDedupKeys": "Active Dedup Keys",
|
||||
"dedupWindow": "Dedup Window"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,7 +185,9 @@
|
||||
"themeViolet": "Fialová",
|
||||
"themeOrange": "Oranžová",
|
||||
"themeCyan": "Azurová",
|
||||
"cliToolsShort": "Nástroje"
|
||||
"cliToolsShort": "Nástroje",
|
||||
"cache": "Cache",
|
||||
"cacheShort": "Cache"
|
||||
},
|
||||
"themesPage": {
|
||||
"title": "Motivy",
|
||||
@@ -2847,5 +2849,37 @@
|
||||
"userInitial": "Potřebuji pomoct",
|
||||
"userFollowUp": "Můžete to upřesnit?"
|
||||
}
|
||||
},
|
||||
"cache": {
|
||||
"title": "Cache Management",
|
||||
"description": "Monitor and manage semantic response cache, hit rates, and token savings.",
|
||||
"refresh": "Refresh",
|
||||
"clearAll": "Clear All",
|
||||
"memoryEntries": "Memory Entries",
|
||||
"dbEntries": "DB Entries",
|
||||
"cacheHits": "Cache Hits",
|
||||
"tokensSaved": "Tokens Saved",
|
||||
"hitRate": "Hit Rate",
|
||||
"performance": "Cache Performance",
|
||||
"behavior": "Cache Behavior",
|
||||
"idempotency": "Idempotency Layer",
|
||||
"clearSuccess": "Cache cleared. {count} expired entries removed.",
|
||||
"clearError": "Failed to clear cache.",
|
||||
"unavailable": "Cache unavailable",
|
||||
"unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.",
|
||||
"memoryEntriesSub": "In-memory LRU",
|
||||
"dbEntriesSub": "Persisted (SQLite)",
|
||||
"cacheHitsSub": "of {total} total",
|
||||
"tokensSavedSub": "Estimated from hits",
|
||||
"autoRefresh": "Auto-refreshes every {seconds}s",
|
||||
"hits": "Hits",
|
||||
"misses": "Misses",
|
||||
"total": "Total",
|
||||
"behaviorDeterministic": "Only non-streaming requests with temperature=0 are cached.",
|
||||
"behaviorBypass": "Bypass with header {header}.",
|
||||
"behaviorTwoTier": "Two-tier storage: in-memory LRU (fast) + SQLite (persistent across restarts).",
|
||||
"behaviorTtl": "Default TTL: 30 minutes. Configure via {envVar}.",
|
||||
"activeDedupKeys": "Active Dedup Keys",
|
||||
"dedupWindow": "Dedup Window"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,7 +185,9 @@
|
||||
"agents": "Agenter",
|
||||
"cliToolsShort": "Værktøjer",
|
||||
"autoCombo": "Auto Combo",
|
||||
"searchTools": "Search Tools"
|
||||
"searchTools": "Search Tools",
|
||||
"cache": "Cache",
|
||||
"cacheShort": "Cache"
|
||||
},
|
||||
"themesPage": {
|
||||
"title": "Temaer",
|
||||
@@ -2847,5 +2849,37 @@
|
||||
"userInitial": "Jeg har brug for hjælp til",
|
||||
"userFollowUp": "Kan du uddybe det?"
|
||||
}
|
||||
},
|
||||
"cache": {
|
||||
"title": "Cache Management",
|
||||
"description": "Monitor and manage semantic response cache, hit rates, and token savings.",
|
||||
"refresh": "Refresh",
|
||||
"clearAll": "Clear All",
|
||||
"memoryEntries": "Memory Entries",
|
||||
"dbEntries": "DB Entries",
|
||||
"cacheHits": "Cache Hits",
|
||||
"tokensSaved": "Tokens Saved",
|
||||
"hitRate": "Hit Rate",
|
||||
"performance": "Cache Performance",
|
||||
"behavior": "Cache Behavior",
|
||||
"idempotency": "Idempotency Layer",
|
||||
"clearSuccess": "Cache cleared. {count} expired entries removed.",
|
||||
"clearError": "Failed to clear cache.",
|
||||
"unavailable": "Cache unavailable",
|
||||
"unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.",
|
||||
"memoryEntriesSub": "In-memory LRU",
|
||||
"dbEntriesSub": "Persisted (SQLite)",
|
||||
"cacheHitsSub": "of {total} total",
|
||||
"tokensSavedSub": "Estimated from hits",
|
||||
"autoRefresh": "Auto-refreshes every {seconds}s",
|
||||
"hits": "Hits",
|
||||
"misses": "Misses",
|
||||
"total": "Total",
|
||||
"behaviorDeterministic": "Only non-streaming requests with temperature=0 are cached.",
|
||||
"behaviorBypass": "Bypass with header {header}.",
|
||||
"behaviorTwoTier": "Two-tier storage: in-memory LRU (fast) + SQLite (persistent across restarts).",
|
||||
"behaviorTtl": "Default TTL: 30 minutes. Configure via {envVar}.",
|
||||
"activeDedupKeys": "Active Dedup Keys",
|
||||
"dedupWindow": "Dedup Window"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,7 +185,9 @@
|
||||
"agents": "Agenten",
|
||||
"cliToolsShort": "Werkzeuge",
|
||||
"autoCombo": "Auto Combo",
|
||||
"searchTools": "Search Tools"
|
||||
"searchTools": "Search Tools",
|
||||
"cache": "Cache",
|
||||
"cacheShort": "Cache"
|
||||
},
|
||||
"themesPage": {
|
||||
"title": "Themen",
|
||||
@@ -2847,5 +2849,37 @@
|
||||
"userInitial": "Ich brauche Hilfe dabei",
|
||||
"userFollowUp": "Können Sie das näher erläutern?"
|
||||
}
|
||||
},
|
||||
"cache": {
|
||||
"title": "Cache Management",
|
||||
"description": "Monitor and manage semantic response cache, hit rates, and token savings.",
|
||||
"refresh": "Refresh",
|
||||
"clearAll": "Clear All",
|
||||
"memoryEntries": "Memory Entries",
|
||||
"dbEntries": "DB Entries",
|
||||
"cacheHits": "Cache Hits",
|
||||
"tokensSaved": "Tokens Saved",
|
||||
"hitRate": "Hit Rate",
|
||||
"performance": "Cache Performance",
|
||||
"behavior": "Cache Behavior",
|
||||
"idempotency": "Idempotency Layer",
|
||||
"clearSuccess": "Cache cleared. {count} expired entries removed.",
|
||||
"clearError": "Failed to clear cache.",
|
||||
"unavailable": "Cache unavailable",
|
||||
"unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.",
|
||||
"memoryEntriesSub": "In-memory LRU",
|
||||
"dbEntriesSub": "Persisted (SQLite)",
|
||||
"cacheHitsSub": "of {total} total",
|
||||
"tokensSavedSub": "Estimated from hits",
|
||||
"autoRefresh": "Auto-refreshes every {seconds}s",
|
||||
"hits": "Hits",
|
||||
"misses": "Misses",
|
||||
"total": "Total",
|
||||
"behaviorDeterministic": "Only non-streaming requests with temperature=0 are cached.",
|
||||
"behaviorBypass": "Bypass with header {header}.",
|
||||
"behaviorTwoTier": "Two-tier storage: in-memory LRU (fast) + SQLite (persistent across restarts).",
|
||||
"behaviorTtl": "Default TTL: 30 minutes. Configure via {envVar}.",
|
||||
"activeDedupKeys": "Active Dedup Keys",
|
||||
"dedupWindow": "Dedup Window"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,7 +185,9 @@
|
||||
"themeViolet": "Violet",
|
||||
"themeOrange": "Orange",
|
||||
"themeCyan": "Cyan",
|
||||
"cliToolsShort": "Tools"
|
||||
"cliToolsShort": "Tools",
|
||||
"cache": "Cache",
|
||||
"cacheShort": "Cache"
|
||||
},
|
||||
"themesPage": {
|
||||
"title": "Themes",
|
||||
@@ -2849,5 +2851,37 @@
|
||||
"userInitial": "I need help with",
|
||||
"userFollowUp": "Can you elaborate on that?"
|
||||
}
|
||||
},
|
||||
"cache": {
|
||||
"title": "Cache Management",
|
||||
"description": "Monitor and manage semantic response cache, hit rates, and token savings.",
|
||||
"refresh": "Refresh",
|
||||
"clearAll": "Clear All",
|
||||
"memoryEntries": "Memory Entries",
|
||||
"memoryEntriesSub": "In-memory LRU",
|
||||
"dbEntries": "DB Entries",
|
||||
"dbEntriesSub": "Persisted (SQLite)",
|
||||
"cacheHits": "Cache Hits",
|
||||
"cacheHitsSub": "of {total} total",
|
||||
"tokensSaved": "Tokens Saved",
|
||||
"tokensSavedSub": "Estimated from hits",
|
||||
"hitRate": "Hit Rate",
|
||||
"performance": "Cache Performance",
|
||||
"autoRefresh": "Auto-refreshes every {seconds}s",
|
||||
"hits": "Hits",
|
||||
"misses": "Misses",
|
||||
"total": "Total",
|
||||
"behavior": "Cache Behavior",
|
||||
"behaviorDeterministic": "Only non-streaming requests with temperature=0 are cached.",
|
||||
"behaviorBypass": "Bypass with header {header}.",
|
||||
"behaviorTwoTier": "Two-tier storage: in-memory LRU (fast) + SQLite (persistent across restarts).",
|
||||
"behaviorTtl": "Default TTL: 30 minutes. Configure via {envVar}.",
|
||||
"idempotency": "Idempotency Layer",
|
||||
"activeDedupKeys": "Active Dedup Keys",
|
||||
"dedupWindow": "Dedup Window",
|
||||
"clearSuccess": "Cache cleared. {count} expired entries removed.",
|
||||
"clearError": "Failed to clear cache.",
|
||||
"unavailable": "Cache unavailable",
|
||||
"unavailableDesc": "Could not fetch cache statistics. Make sure the server is running."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,7 +185,9 @@
|
||||
"agents": "Agentes",
|
||||
"cliToolsShort": "Herramientas",
|
||||
"autoCombo": "Auto Combo",
|
||||
"searchTools": "Search Tools"
|
||||
"searchTools": "Search Tools",
|
||||
"cache": "Cache",
|
||||
"cacheShort": "Cache"
|
||||
},
|
||||
"themesPage": {
|
||||
"title": "Temas",
|
||||
@@ -2847,5 +2849,37 @@
|
||||
"userInitial": "necesito ayuda con",
|
||||
"userFollowUp": "¿Puedes dar más detalles sobre eso?"
|
||||
}
|
||||
},
|
||||
"cache": {
|
||||
"title": "Cache Management",
|
||||
"description": "Monitor and manage semantic response cache, hit rates, and token savings.",
|
||||
"refresh": "Refresh",
|
||||
"clearAll": "Clear All",
|
||||
"memoryEntries": "Memory Entries",
|
||||
"dbEntries": "DB Entries",
|
||||
"cacheHits": "Cache Hits",
|
||||
"tokensSaved": "Tokens Saved",
|
||||
"hitRate": "Hit Rate",
|
||||
"performance": "Cache Performance",
|
||||
"behavior": "Cache Behavior",
|
||||
"idempotency": "Idempotency Layer",
|
||||
"clearSuccess": "Cache cleared. {count} expired entries removed.",
|
||||
"clearError": "Failed to clear cache.",
|
||||
"unavailable": "Cache unavailable",
|
||||
"unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.",
|
||||
"memoryEntriesSub": "In-memory LRU",
|
||||
"dbEntriesSub": "Persisted (SQLite)",
|
||||
"cacheHitsSub": "of {total} total",
|
||||
"tokensSavedSub": "Estimated from hits",
|
||||
"autoRefresh": "Auto-refreshes every {seconds}s",
|
||||
"hits": "Hits",
|
||||
"misses": "Misses",
|
||||
"total": "Total",
|
||||
"behaviorDeterministic": "Only non-streaming requests with temperature=0 are cached.",
|
||||
"behaviorBypass": "Bypass with header {header}.",
|
||||
"behaviorTwoTier": "Two-tier storage: in-memory LRU (fast) + SQLite (persistent across restarts).",
|
||||
"behaviorTtl": "Default TTL: 30 minutes. Configure via {envVar}.",
|
||||
"activeDedupKeys": "Active Dedup Keys",
|
||||
"dedupWindow": "Dedup Window"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,7 +185,9 @@
|
||||
"agents": "Agentit",
|
||||
"cliToolsShort": "Työkalut",
|
||||
"autoCombo": "Auto Combo",
|
||||
"searchTools": "Search Tools"
|
||||
"searchTools": "Search Tools",
|
||||
"cache": "Cache",
|
||||
"cacheShort": "Cache"
|
||||
},
|
||||
"themesPage": {
|
||||
"title": "Teemat",
|
||||
@@ -2847,5 +2849,37 @@
|
||||
"userInitial": "Tarvitsen apua",
|
||||
"userFollowUp": "Voitko tarkentaa sitä?"
|
||||
}
|
||||
},
|
||||
"cache": {
|
||||
"title": "Cache Management",
|
||||
"description": "Monitor and manage semantic response cache, hit rates, and token savings.",
|
||||
"refresh": "Refresh",
|
||||
"clearAll": "Clear All",
|
||||
"memoryEntries": "Memory Entries",
|
||||
"dbEntries": "DB Entries",
|
||||
"cacheHits": "Cache Hits",
|
||||
"tokensSaved": "Tokens Saved",
|
||||
"hitRate": "Hit Rate",
|
||||
"performance": "Cache Performance",
|
||||
"behavior": "Cache Behavior",
|
||||
"idempotency": "Idempotency Layer",
|
||||
"clearSuccess": "Cache cleared. {count} expired entries removed.",
|
||||
"clearError": "Failed to clear cache.",
|
||||
"unavailable": "Cache unavailable",
|
||||
"unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.",
|
||||
"memoryEntriesSub": "In-memory LRU",
|
||||
"dbEntriesSub": "Persisted (SQLite)",
|
||||
"cacheHitsSub": "of {total} total",
|
||||
"tokensSavedSub": "Estimated from hits",
|
||||
"autoRefresh": "Auto-refreshes every {seconds}s",
|
||||
"hits": "Hits",
|
||||
"misses": "Misses",
|
||||
"total": "Total",
|
||||
"behaviorDeterministic": "Only non-streaming requests with temperature=0 are cached.",
|
||||
"behaviorBypass": "Bypass with header {header}.",
|
||||
"behaviorTwoTier": "Two-tier storage: in-memory LRU (fast) + SQLite (persistent across restarts).",
|
||||
"behaviorTtl": "Default TTL: 30 minutes. Configure via {envVar}.",
|
||||
"activeDedupKeys": "Active Dedup Keys",
|
||||
"dedupWindow": "Dedup Window"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,7 +185,9 @@
|
||||
"agents": "Agents",
|
||||
"cliToolsShort": "Outils",
|
||||
"autoCombo": "Auto Combo",
|
||||
"searchTools": "Search Tools"
|
||||
"searchTools": "Search Tools",
|
||||
"cache": "Cache",
|
||||
"cacheShort": "Cache"
|
||||
},
|
||||
"themesPage": {
|
||||
"title": "Thèmes",
|
||||
@@ -2847,5 +2849,37 @@
|
||||
"userInitial": "J'ai besoin d'aide pour",
|
||||
"userFollowUp": "Pouvez-vous développer cela ?"
|
||||
}
|
||||
},
|
||||
"cache": {
|
||||
"title": "Cache Management",
|
||||
"description": "Monitor and manage semantic response cache, hit rates, and token savings.",
|
||||
"refresh": "Refresh",
|
||||
"clearAll": "Clear All",
|
||||
"memoryEntries": "Memory Entries",
|
||||
"dbEntries": "DB Entries",
|
||||
"cacheHits": "Cache Hits",
|
||||
"tokensSaved": "Tokens Saved",
|
||||
"hitRate": "Hit Rate",
|
||||
"performance": "Cache Performance",
|
||||
"behavior": "Cache Behavior",
|
||||
"idempotency": "Idempotency Layer",
|
||||
"clearSuccess": "Cache cleared. {count} expired entries removed.",
|
||||
"clearError": "Failed to clear cache.",
|
||||
"unavailable": "Cache unavailable",
|
||||
"unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.",
|
||||
"memoryEntriesSub": "In-memory LRU",
|
||||
"dbEntriesSub": "Persisted (SQLite)",
|
||||
"cacheHitsSub": "of {total} total",
|
||||
"tokensSavedSub": "Estimated from hits",
|
||||
"autoRefresh": "Auto-refreshes every {seconds}s",
|
||||
"hits": "Hits",
|
||||
"misses": "Misses",
|
||||
"total": "Total",
|
||||
"behaviorDeterministic": "Only non-streaming requests with temperature=0 are cached.",
|
||||
"behaviorBypass": "Bypass with header {header}.",
|
||||
"behaviorTwoTier": "Two-tier storage: in-memory LRU (fast) + SQLite (persistent across restarts).",
|
||||
"behaviorTtl": "Default TTL: 30 minutes. Configure via {envVar}.",
|
||||
"activeDedupKeys": "Active Dedup Keys",
|
||||
"dedupWindow": "Dedup Window"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,7 +185,9 @@
|
||||
"agents": "סוכנים",
|
||||
"cliToolsShort": "כלים",
|
||||
"autoCombo": "Auto Combo",
|
||||
"searchTools": "Search Tools"
|
||||
"searchTools": "Search Tools",
|
||||
"cache": "Cache",
|
||||
"cacheShort": "Cache"
|
||||
},
|
||||
"themesPage": {
|
||||
"title": "Themes",
|
||||
@@ -2847,5 +2849,37 @@
|
||||
"userInitial": "אני צריך עזרה עם",
|
||||
"userFollowUp": "אתה יכול לפרט על זה?"
|
||||
}
|
||||
},
|
||||
"cache": {
|
||||
"title": "Cache Management",
|
||||
"description": "Monitor and manage semantic response cache, hit rates, and token savings.",
|
||||
"refresh": "Refresh",
|
||||
"clearAll": "Clear All",
|
||||
"memoryEntries": "Memory Entries",
|
||||
"dbEntries": "DB Entries",
|
||||
"cacheHits": "Cache Hits",
|
||||
"tokensSaved": "Tokens Saved",
|
||||
"hitRate": "Hit Rate",
|
||||
"performance": "Cache Performance",
|
||||
"behavior": "Cache Behavior",
|
||||
"idempotency": "Idempotency Layer",
|
||||
"clearSuccess": "Cache cleared. {count} expired entries removed.",
|
||||
"clearError": "Failed to clear cache.",
|
||||
"unavailable": "Cache unavailable",
|
||||
"unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.",
|
||||
"memoryEntriesSub": "In-memory LRU",
|
||||
"dbEntriesSub": "Persisted (SQLite)",
|
||||
"cacheHitsSub": "of {total} total",
|
||||
"tokensSavedSub": "Estimated from hits",
|
||||
"autoRefresh": "Auto-refreshes every {seconds}s",
|
||||
"hits": "Hits",
|
||||
"misses": "Misses",
|
||||
"total": "Total",
|
||||
"behaviorDeterministic": "Only non-streaming requests with temperature=0 are cached.",
|
||||
"behaviorBypass": "Bypass with header {header}.",
|
||||
"behaviorTwoTier": "Two-tier storage: in-memory LRU (fast) + SQLite (persistent across restarts).",
|
||||
"behaviorTtl": "Default TTL: 30 minutes. Configure via {envVar}.",
|
||||
"activeDedupKeys": "Active Dedup Keys",
|
||||
"dedupWindow": "Dedup Window"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,7 +107,9 @@
|
||||
"agents": "एजेंट",
|
||||
"cliToolsShort": "उपकरण",
|
||||
"autoCombo": "Auto Combo",
|
||||
"searchTools": "Search Tools"
|
||||
"searchTools": "Search Tools",
|
||||
"cache": "Cache",
|
||||
"cacheShort": "Cache"
|
||||
},
|
||||
"themesPage": {
|
||||
"title": "Themes",
|
||||
@@ -2689,5 +2691,37 @@
|
||||
"domainPlaceholder": "example.com",
|
||||
"requestTimedOut": "Request timed out ({seconds}s)",
|
||||
"networkError": "Network error"
|
||||
},
|
||||
"cache": {
|
||||
"title": "Cache Management",
|
||||
"description": "Monitor and manage semantic response cache, hit rates, and token savings.",
|
||||
"refresh": "Refresh",
|
||||
"clearAll": "Clear All",
|
||||
"memoryEntries": "Memory Entries",
|
||||
"dbEntries": "DB Entries",
|
||||
"cacheHits": "Cache Hits",
|
||||
"tokensSaved": "Tokens Saved",
|
||||
"hitRate": "Hit Rate",
|
||||
"performance": "Cache Performance",
|
||||
"behavior": "Cache Behavior",
|
||||
"idempotency": "Idempotency Layer",
|
||||
"clearSuccess": "Cache cleared. {count} expired entries removed.",
|
||||
"clearError": "Failed to clear cache.",
|
||||
"unavailable": "Cache unavailable",
|
||||
"unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.",
|
||||
"memoryEntriesSub": "In-memory LRU",
|
||||
"dbEntriesSub": "Persisted (SQLite)",
|
||||
"cacheHitsSub": "of {total} total",
|
||||
"tokensSavedSub": "Estimated from hits",
|
||||
"autoRefresh": "Auto-refreshes every {seconds}s",
|
||||
"hits": "Hits",
|
||||
"misses": "Misses",
|
||||
"total": "Total",
|
||||
"behaviorDeterministic": "Only non-streaming requests with temperature=0 are cached.",
|
||||
"behaviorBypass": "Bypass with header {header}.",
|
||||
"behaviorTwoTier": "Two-tier storage: in-memory LRU (fast) + SQLite (persistent across restarts).",
|
||||
"behaviorTtl": "Default TTL: 30 minutes. Configure via {envVar}.",
|
||||
"activeDedupKeys": "Active Dedup Keys",
|
||||
"dedupWindow": "Dedup Window"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,7 +185,9 @@
|
||||
"agents": "Ügynökök",
|
||||
"cliToolsShort": "Eszközök",
|
||||
"autoCombo": "Auto Combo",
|
||||
"searchTools": "Search Tools"
|
||||
"searchTools": "Search Tools",
|
||||
"cache": "Cache",
|
||||
"cacheShort": "Cache"
|
||||
},
|
||||
"themesPage": {
|
||||
"title": "Témák",
|
||||
@@ -2847,5 +2849,37 @@
|
||||
"userInitial": "Segítségre van szükségem",
|
||||
"userFollowUp": "Kifejtenéd ezt bővebben?"
|
||||
}
|
||||
},
|
||||
"cache": {
|
||||
"title": "Cache Management",
|
||||
"description": "Monitor and manage semantic response cache, hit rates, and token savings.",
|
||||
"refresh": "Refresh",
|
||||
"clearAll": "Clear All",
|
||||
"memoryEntries": "Memory Entries",
|
||||
"dbEntries": "DB Entries",
|
||||
"cacheHits": "Cache Hits",
|
||||
"tokensSaved": "Tokens Saved",
|
||||
"hitRate": "Hit Rate",
|
||||
"performance": "Cache Performance",
|
||||
"behavior": "Cache Behavior",
|
||||
"idempotency": "Idempotency Layer",
|
||||
"clearSuccess": "Cache cleared. {count} expired entries removed.",
|
||||
"clearError": "Failed to clear cache.",
|
||||
"unavailable": "Cache unavailable",
|
||||
"unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.",
|
||||
"memoryEntriesSub": "In-memory LRU",
|
||||
"dbEntriesSub": "Persisted (SQLite)",
|
||||
"cacheHitsSub": "of {total} total",
|
||||
"tokensSavedSub": "Estimated from hits",
|
||||
"autoRefresh": "Auto-refreshes every {seconds}s",
|
||||
"hits": "Hits",
|
||||
"misses": "Misses",
|
||||
"total": "Total",
|
||||
"behaviorDeterministic": "Only non-streaming requests with temperature=0 are cached.",
|
||||
"behaviorBypass": "Bypass with header {header}.",
|
||||
"behaviorTwoTier": "Two-tier storage: in-memory LRU (fast) + SQLite (persistent across restarts).",
|
||||
"behaviorTtl": "Default TTL: 30 minutes. Configure via {envVar}.",
|
||||
"activeDedupKeys": "Active Dedup Keys",
|
||||
"dedupWindow": "Dedup Window"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,7 +185,9 @@
|
||||
"agents": "Agen",
|
||||
"cliToolsShort": "Alat",
|
||||
"autoCombo": "Auto Combo",
|
||||
"searchTools": "Search Tools"
|
||||
"searchTools": "Search Tools",
|
||||
"cache": "Cache",
|
||||
"cacheShort": "Cache"
|
||||
},
|
||||
"themesPage": {
|
||||
"title": "Themes",
|
||||
@@ -2847,5 +2849,37 @@
|
||||
"userInitial": "Saya butuh bantuan",
|
||||
"userFollowUp": "Bisakah Anda menjelaskannya lebih lanjut?"
|
||||
}
|
||||
},
|
||||
"cache": {
|
||||
"title": "Cache Management",
|
||||
"description": "Monitor and manage semantic response cache, hit rates, and token savings.",
|
||||
"refresh": "Refresh",
|
||||
"clearAll": "Clear All",
|
||||
"memoryEntries": "Memory Entries",
|
||||
"dbEntries": "DB Entries",
|
||||
"cacheHits": "Cache Hits",
|
||||
"tokensSaved": "Tokens Saved",
|
||||
"hitRate": "Hit Rate",
|
||||
"performance": "Cache Performance",
|
||||
"behavior": "Cache Behavior",
|
||||
"idempotency": "Idempotency Layer",
|
||||
"clearSuccess": "Cache cleared. {count} expired entries removed.",
|
||||
"clearError": "Failed to clear cache.",
|
||||
"unavailable": "Cache unavailable",
|
||||
"unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.",
|
||||
"memoryEntriesSub": "In-memory LRU",
|
||||
"dbEntriesSub": "Persisted (SQLite)",
|
||||
"cacheHitsSub": "of {total} total",
|
||||
"tokensSavedSub": "Estimated from hits",
|
||||
"autoRefresh": "Auto-refreshes every {seconds}s",
|
||||
"hits": "Hits",
|
||||
"misses": "Misses",
|
||||
"total": "Total",
|
||||
"behaviorDeterministic": "Only non-streaming requests with temperature=0 are cached.",
|
||||
"behaviorBypass": "Bypass with header {header}.",
|
||||
"behaviorTwoTier": "Two-tier storage: in-memory LRU (fast) + SQLite (persistent across restarts).",
|
||||
"behaviorTtl": "Default TTL: 30 minutes. Configure via {envVar}.",
|
||||
"activeDedupKeys": "Active Dedup Keys",
|
||||
"dedupWindow": "Dedup Window"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,7 +185,9 @@
|
||||
"themeViolet": "बैंगनी",
|
||||
"themeOrange": "नारंगी",
|
||||
"themeCyan": "सियान",
|
||||
"cliToolsShort": "उपकरण"
|
||||
"cliToolsShort": "उपकरण",
|
||||
"cache": "Cache",
|
||||
"cacheShort": "Cache"
|
||||
},
|
||||
"themesPage": {
|
||||
"title": "थीम्स",
|
||||
@@ -2843,5 +2845,37 @@
|
||||
"userInitial": "मुझे मदद चाहिए",
|
||||
"userFollowUp": "क्या आप इसके बारे में विस्तार से बता सकते हैं?"
|
||||
}
|
||||
},
|
||||
"cache": {
|
||||
"title": "Cache Management",
|
||||
"description": "Monitor and manage semantic response cache, hit rates, and token savings.",
|
||||
"refresh": "Refresh",
|
||||
"clearAll": "Clear All",
|
||||
"memoryEntries": "Memory Entries",
|
||||
"dbEntries": "DB Entries",
|
||||
"cacheHits": "Cache Hits",
|
||||
"tokensSaved": "Tokens Saved",
|
||||
"hitRate": "Hit Rate",
|
||||
"performance": "Cache Performance",
|
||||
"behavior": "Cache Behavior",
|
||||
"idempotency": "Idempotency Layer",
|
||||
"clearSuccess": "Cache cleared. {count} expired entries removed.",
|
||||
"clearError": "Failed to clear cache.",
|
||||
"unavailable": "Cache unavailable",
|
||||
"unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.",
|
||||
"memoryEntriesSub": "In-memory LRU",
|
||||
"dbEntriesSub": "Persisted (SQLite)",
|
||||
"cacheHitsSub": "of {total} total",
|
||||
"tokensSavedSub": "Estimated from hits",
|
||||
"autoRefresh": "Auto-refreshes every {seconds}s",
|
||||
"hits": "Hits",
|
||||
"misses": "Misses",
|
||||
"total": "Total",
|
||||
"behaviorDeterministic": "Only non-streaming requests with temperature=0 are cached.",
|
||||
"behaviorBypass": "Bypass with header {header}.",
|
||||
"behaviorTwoTier": "Two-tier storage: in-memory LRU (fast) + SQLite (persistent across restarts).",
|
||||
"behaviorTtl": "Default TTL: 30 minutes. Configure via {envVar}.",
|
||||
"activeDedupKeys": "Active Dedup Keys",
|
||||
"dedupWindow": "Dedup Window"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,7 +185,9 @@
|
||||
"agents": "Agenti",
|
||||
"cliToolsShort": "Strumenti",
|
||||
"autoCombo": "Auto Combo",
|
||||
"searchTools": "Search Tools"
|
||||
"searchTools": "Search Tools",
|
||||
"cache": "Cache",
|
||||
"cacheShort": "Cache"
|
||||
},
|
||||
"themesPage": {
|
||||
"title": "Themes",
|
||||
@@ -2847,5 +2849,37 @@
|
||||
"userInitial": "Ho bisogno di aiuto con",
|
||||
"userFollowUp": "Puoi approfondire questo argomento?"
|
||||
}
|
||||
},
|
||||
"cache": {
|
||||
"title": "Cache Management",
|
||||
"description": "Monitor and manage semantic response cache, hit rates, and token savings.",
|
||||
"refresh": "Refresh",
|
||||
"clearAll": "Clear All",
|
||||
"memoryEntries": "Memory Entries",
|
||||
"dbEntries": "DB Entries",
|
||||
"cacheHits": "Cache Hits",
|
||||
"tokensSaved": "Tokens Saved",
|
||||
"hitRate": "Hit Rate",
|
||||
"performance": "Cache Performance",
|
||||
"behavior": "Cache Behavior",
|
||||
"idempotency": "Idempotency Layer",
|
||||
"clearSuccess": "Cache cleared. {count} expired entries removed.",
|
||||
"clearError": "Failed to clear cache.",
|
||||
"unavailable": "Cache unavailable",
|
||||
"unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.",
|
||||
"memoryEntriesSub": "In-memory LRU",
|
||||
"dbEntriesSub": "Persisted (SQLite)",
|
||||
"cacheHitsSub": "of {total} total",
|
||||
"tokensSavedSub": "Estimated from hits",
|
||||
"autoRefresh": "Auto-refreshes every {seconds}s",
|
||||
"hits": "Hits",
|
||||
"misses": "Misses",
|
||||
"total": "Total",
|
||||
"behaviorDeterministic": "Only non-streaming requests with temperature=0 are cached.",
|
||||
"behaviorBypass": "Bypass with header {header}.",
|
||||
"behaviorTwoTier": "Two-tier storage: in-memory LRU (fast) + SQLite (persistent across restarts).",
|
||||
"behaviorTtl": "Default TTL: 30 minutes. Configure via {envVar}.",
|
||||
"activeDedupKeys": "Active Dedup Keys",
|
||||
"dedupWindow": "Dedup Window"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,7 +185,9 @@
|
||||
"agents": "エージェント",
|
||||
"cliToolsShort": "ツール",
|
||||
"autoCombo": "Auto Combo",
|
||||
"searchTools": "Search Tools"
|
||||
"searchTools": "Search Tools",
|
||||
"cache": "Cache",
|
||||
"cacheShort": "Cache"
|
||||
},
|
||||
"themesPage": {
|
||||
"title": "Themes",
|
||||
@@ -2847,5 +2849,37 @@
|
||||
"userInitial": "助けが必要です",
|
||||
"userFollowUp": "それについて詳しく教えてもらえますか?"
|
||||
}
|
||||
},
|
||||
"cache": {
|
||||
"title": "Cache Management",
|
||||
"description": "Monitor and manage semantic response cache, hit rates, and token savings.",
|
||||
"refresh": "Refresh",
|
||||
"clearAll": "Clear All",
|
||||
"memoryEntries": "Memory Entries",
|
||||
"dbEntries": "DB Entries",
|
||||
"cacheHits": "Cache Hits",
|
||||
"tokensSaved": "Tokens Saved",
|
||||
"hitRate": "Hit Rate",
|
||||
"performance": "Cache Performance",
|
||||
"behavior": "Cache Behavior",
|
||||
"idempotency": "Idempotency Layer",
|
||||
"clearSuccess": "Cache cleared. {count} expired entries removed.",
|
||||
"clearError": "Failed to clear cache.",
|
||||
"unavailable": "Cache unavailable",
|
||||
"unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.",
|
||||
"memoryEntriesSub": "In-memory LRU",
|
||||
"dbEntriesSub": "Persisted (SQLite)",
|
||||
"cacheHitsSub": "of {total} total",
|
||||
"tokensSavedSub": "Estimated from hits",
|
||||
"autoRefresh": "Auto-refreshes every {seconds}s",
|
||||
"hits": "Hits",
|
||||
"misses": "Misses",
|
||||
"total": "Total",
|
||||
"behaviorDeterministic": "Only non-streaming requests with temperature=0 are cached.",
|
||||
"behaviorBypass": "Bypass with header {header}.",
|
||||
"behaviorTwoTier": "Two-tier storage: in-memory LRU (fast) + SQLite (persistent across restarts).",
|
||||
"behaviorTtl": "Default TTL: 30 minutes. Configure via {envVar}.",
|
||||
"activeDedupKeys": "Active Dedup Keys",
|
||||
"dedupWindow": "Dedup Window"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,7 +185,9 @@
|
||||
"agents": "에이전트",
|
||||
"cliToolsShort": "도구",
|
||||
"autoCombo": "Auto Combo",
|
||||
"searchTools": "Search Tools"
|
||||
"searchTools": "Search Tools",
|
||||
"cache": "Cache",
|
||||
"cacheShort": "Cache"
|
||||
},
|
||||
"themesPage": {
|
||||
"title": "Themes",
|
||||
@@ -2847,5 +2849,37 @@
|
||||
"userInitial": "도움이 필요해요",
|
||||
"userFollowUp": "그것에 대해 자세히 설명해주실 수 있나요?"
|
||||
}
|
||||
},
|
||||
"cache": {
|
||||
"title": "Cache Management",
|
||||
"description": "Monitor and manage semantic response cache, hit rates, and token savings.",
|
||||
"refresh": "Refresh",
|
||||
"clearAll": "Clear All",
|
||||
"memoryEntries": "Memory Entries",
|
||||
"dbEntries": "DB Entries",
|
||||
"cacheHits": "Cache Hits",
|
||||
"tokensSaved": "Tokens Saved",
|
||||
"hitRate": "Hit Rate",
|
||||
"performance": "Cache Performance",
|
||||
"behavior": "Cache Behavior",
|
||||
"idempotency": "Idempotency Layer",
|
||||
"clearSuccess": "Cache cleared. {count} expired entries removed.",
|
||||
"clearError": "Failed to clear cache.",
|
||||
"unavailable": "Cache unavailable",
|
||||
"unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.",
|
||||
"memoryEntriesSub": "In-memory LRU",
|
||||
"dbEntriesSub": "Persisted (SQLite)",
|
||||
"cacheHitsSub": "of {total} total",
|
||||
"tokensSavedSub": "Estimated from hits",
|
||||
"autoRefresh": "Auto-refreshes every {seconds}s",
|
||||
"hits": "Hits",
|
||||
"misses": "Misses",
|
||||
"total": "Total",
|
||||
"behaviorDeterministic": "Only non-streaming requests with temperature=0 are cached.",
|
||||
"behaviorBypass": "Bypass with header {header}.",
|
||||
"behaviorTwoTier": "Two-tier storage: in-memory LRU (fast) + SQLite (persistent across restarts).",
|
||||
"behaviorTtl": "Default TTL: 30 minutes. Configure via {envVar}.",
|
||||
"activeDedupKeys": "Active Dedup Keys",
|
||||
"dedupWindow": "Dedup Window"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,7 +185,9 @@
|
||||
"agents": "Ejen",
|
||||
"cliToolsShort": "Alat",
|
||||
"autoCombo": "Auto Combo",
|
||||
"searchTools": "Search Tools"
|
||||
"searchTools": "Search Tools",
|
||||
"cache": "Cache",
|
||||
"cacheShort": "Cache"
|
||||
},
|
||||
"themesPage": {
|
||||
"title": "Themes",
|
||||
@@ -2847,5 +2849,37 @@
|
||||
"userInitial": "Saya perlukan bantuan",
|
||||
"userFollowUp": "Bolehkah anda menghuraikan perkara itu?"
|
||||
}
|
||||
},
|
||||
"cache": {
|
||||
"title": "Cache Management",
|
||||
"description": "Monitor and manage semantic response cache, hit rates, and token savings.",
|
||||
"refresh": "Refresh",
|
||||
"clearAll": "Clear All",
|
||||
"memoryEntries": "Memory Entries",
|
||||
"dbEntries": "DB Entries",
|
||||
"cacheHits": "Cache Hits",
|
||||
"tokensSaved": "Tokens Saved",
|
||||
"hitRate": "Hit Rate",
|
||||
"performance": "Cache Performance",
|
||||
"behavior": "Cache Behavior",
|
||||
"idempotency": "Idempotency Layer",
|
||||
"clearSuccess": "Cache cleared. {count} expired entries removed.",
|
||||
"clearError": "Failed to clear cache.",
|
||||
"unavailable": "Cache unavailable",
|
||||
"unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.",
|
||||
"memoryEntriesSub": "In-memory LRU",
|
||||
"dbEntriesSub": "Persisted (SQLite)",
|
||||
"cacheHitsSub": "of {total} total",
|
||||
"tokensSavedSub": "Estimated from hits",
|
||||
"autoRefresh": "Auto-refreshes every {seconds}s",
|
||||
"hits": "Hits",
|
||||
"misses": "Misses",
|
||||
"total": "Total",
|
||||
"behaviorDeterministic": "Only non-streaming requests with temperature=0 are cached.",
|
||||
"behaviorBypass": "Bypass with header {header}.",
|
||||
"behaviorTwoTier": "Two-tier storage: in-memory LRU (fast) + SQLite (persistent across restarts).",
|
||||
"behaviorTtl": "Default TTL: 30 minutes. Configure via {envVar}.",
|
||||
"activeDedupKeys": "Active Dedup Keys",
|
||||
"dedupWindow": "Dedup Window"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,7 +185,9 @@
|
||||
"agents": "Agenten",
|
||||
"cliToolsShort": "Gereedschap",
|
||||
"autoCombo": "Auto Combo",
|
||||
"searchTools": "Search Tools"
|
||||
"searchTools": "Search Tools",
|
||||
"cache": "Cache",
|
||||
"cacheShort": "Cache"
|
||||
},
|
||||
"themesPage": {
|
||||
"title": "Themes",
|
||||
@@ -2847,5 +2849,37 @@
|
||||
"userInitial": "Ik heb hulp nodig bij",
|
||||
"userFollowUp": "Kunt u dat nader toelichten?"
|
||||
}
|
||||
},
|
||||
"cache": {
|
||||
"title": "Cache Management",
|
||||
"description": "Monitor and manage semantic response cache, hit rates, and token savings.",
|
||||
"refresh": "Refresh",
|
||||
"clearAll": "Clear All",
|
||||
"memoryEntries": "Memory Entries",
|
||||
"dbEntries": "DB Entries",
|
||||
"cacheHits": "Cache Hits",
|
||||
"tokensSaved": "Tokens Saved",
|
||||
"hitRate": "Hit Rate",
|
||||
"performance": "Cache Performance",
|
||||
"behavior": "Cache Behavior",
|
||||
"idempotency": "Idempotency Layer",
|
||||
"clearSuccess": "Cache cleared. {count} expired entries removed.",
|
||||
"clearError": "Failed to clear cache.",
|
||||
"unavailable": "Cache unavailable",
|
||||
"unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.",
|
||||
"memoryEntriesSub": "In-memory LRU",
|
||||
"dbEntriesSub": "Persisted (SQLite)",
|
||||
"cacheHitsSub": "of {total} total",
|
||||
"tokensSavedSub": "Estimated from hits",
|
||||
"autoRefresh": "Auto-refreshes every {seconds}s",
|
||||
"hits": "Hits",
|
||||
"misses": "Misses",
|
||||
"total": "Total",
|
||||
"behaviorDeterministic": "Only non-streaming requests with temperature=0 are cached.",
|
||||
"behaviorBypass": "Bypass with header {header}.",
|
||||
"behaviorTwoTier": "Two-tier storage: in-memory LRU (fast) + SQLite (persistent across restarts).",
|
||||
"behaviorTtl": "Default TTL: 30 minutes. Configure via {envVar}.",
|
||||
"activeDedupKeys": "Active Dedup Keys",
|
||||
"dedupWindow": "Dedup Window"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,7 +185,9 @@
|
||||
"agents": "Agenter",
|
||||
"cliToolsShort": "Verktøy",
|
||||
"autoCombo": "Auto Combo",
|
||||
"searchTools": "Search Tools"
|
||||
"searchTools": "Search Tools",
|
||||
"cache": "Cache",
|
||||
"cacheShort": "Cache"
|
||||
},
|
||||
"themesPage": {
|
||||
"title": "Themes",
|
||||
@@ -2847,5 +2849,37 @@
|
||||
"userInitial": "Jeg trenger hjelp med",
|
||||
"userFollowUp": "Kan du utdype det?"
|
||||
}
|
||||
},
|
||||
"cache": {
|
||||
"title": "Cache Management",
|
||||
"description": "Monitor and manage semantic response cache, hit rates, and token savings.",
|
||||
"refresh": "Refresh",
|
||||
"clearAll": "Clear All",
|
||||
"memoryEntries": "Memory Entries",
|
||||
"dbEntries": "DB Entries",
|
||||
"cacheHits": "Cache Hits",
|
||||
"tokensSaved": "Tokens Saved",
|
||||
"hitRate": "Hit Rate",
|
||||
"performance": "Cache Performance",
|
||||
"behavior": "Cache Behavior",
|
||||
"idempotency": "Idempotency Layer",
|
||||
"clearSuccess": "Cache cleared. {count} expired entries removed.",
|
||||
"clearError": "Failed to clear cache.",
|
||||
"unavailable": "Cache unavailable",
|
||||
"unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.",
|
||||
"memoryEntriesSub": "In-memory LRU",
|
||||
"dbEntriesSub": "Persisted (SQLite)",
|
||||
"cacheHitsSub": "of {total} total",
|
||||
"tokensSavedSub": "Estimated from hits",
|
||||
"autoRefresh": "Auto-refreshes every {seconds}s",
|
||||
"hits": "Hits",
|
||||
"misses": "Misses",
|
||||
"total": "Total",
|
||||
"behaviorDeterministic": "Only non-streaming requests with temperature=0 are cached.",
|
||||
"behaviorBypass": "Bypass with header {header}.",
|
||||
"behaviorTwoTier": "Two-tier storage: in-memory LRU (fast) + SQLite (persistent across restarts).",
|
||||
"behaviorTtl": "Default TTL: 30 minutes. Configure via {envVar}.",
|
||||
"activeDedupKeys": "Active Dedup Keys",
|
||||
"dedupWindow": "Dedup Window"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,7 +185,9 @@
|
||||
"agents": "Mga Agent",
|
||||
"cliToolsShort": "Mga Tool",
|
||||
"autoCombo": "Auto Combo",
|
||||
"searchTools": "Search Tools"
|
||||
"searchTools": "Search Tools",
|
||||
"cache": "Cache",
|
||||
"cacheShort": "Cache"
|
||||
},
|
||||
"themesPage": {
|
||||
"title": "Themes",
|
||||
@@ -2847,5 +2849,37 @@
|
||||
"userInitial": "Kailangan ko ng tulong sa",
|
||||
"userFollowUp": "Maaari mo bang ipaliwanag iyon?"
|
||||
}
|
||||
},
|
||||
"cache": {
|
||||
"title": "Cache Management",
|
||||
"description": "Monitor and manage semantic response cache, hit rates, and token savings.",
|
||||
"refresh": "Refresh",
|
||||
"clearAll": "Clear All",
|
||||
"memoryEntries": "Memory Entries",
|
||||
"dbEntries": "DB Entries",
|
||||
"cacheHits": "Cache Hits",
|
||||
"tokensSaved": "Tokens Saved",
|
||||
"hitRate": "Hit Rate",
|
||||
"performance": "Cache Performance",
|
||||
"behavior": "Cache Behavior",
|
||||
"idempotency": "Idempotency Layer",
|
||||
"clearSuccess": "Cache cleared. {count} expired entries removed.",
|
||||
"clearError": "Failed to clear cache.",
|
||||
"unavailable": "Cache unavailable",
|
||||
"unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.",
|
||||
"memoryEntriesSub": "In-memory LRU",
|
||||
"dbEntriesSub": "Persisted (SQLite)",
|
||||
"cacheHitsSub": "of {total} total",
|
||||
"tokensSavedSub": "Estimated from hits",
|
||||
"autoRefresh": "Auto-refreshes every {seconds}s",
|
||||
"hits": "Hits",
|
||||
"misses": "Misses",
|
||||
"total": "Total",
|
||||
"behaviorDeterministic": "Only non-streaming requests with temperature=0 are cached.",
|
||||
"behaviorBypass": "Bypass with header {header}.",
|
||||
"behaviorTwoTier": "Two-tier storage: in-memory LRU (fast) + SQLite (persistent across restarts).",
|
||||
"behaviorTtl": "Default TTL: 30 minutes. Configure via {envVar}.",
|
||||
"activeDedupKeys": "Active Dedup Keys",
|
||||
"dedupWindow": "Dedup Window"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,7 +185,9 @@
|
||||
"agents": "Agenci",
|
||||
"cliToolsShort": "Narzędzia",
|
||||
"autoCombo": "Auto Combo",
|
||||
"searchTools": "Search Tools"
|
||||
"searchTools": "Search Tools",
|
||||
"cache": "Cache",
|
||||
"cacheShort": "Cache"
|
||||
},
|
||||
"themesPage": {
|
||||
"title": "Themes",
|
||||
@@ -2847,5 +2849,37 @@
|
||||
"userInitial": "Potrzebuję pomocy",
|
||||
"userFollowUp": "Możesz to rozwinąć?"
|
||||
}
|
||||
},
|
||||
"cache": {
|
||||
"title": "Cache Management",
|
||||
"description": "Monitor and manage semantic response cache, hit rates, and token savings.",
|
||||
"refresh": "Refresh",
|
||||
"clearAll": "Clear All",
|
||||
"memoryEntries": "Memory Entries",
|
||||
"dbEntries": "DB Entries",
|
||||
"cacheHits": "Cache Hits",
|
||||
"tokensSaved": "Tokens Saved",
|
||||
"hitRate": "Hit Rate",
|
||||
"performance": "Cache Performance",
|
||||
"behavior": "Cache Behavior",
|
||||
"idempotency": "Idempotency Layer",
|
||||
"clearSuccess": "Cache cleared. {count} expired entries removed.",
|
||||
"clearError": "Failed to clear cache.",
|
||||
"unavailable": "Cache unavailable",
|
||||
"unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.",
|
||||
"memoryEntriesSub": "In-memory LRU",
|
||||
"dbEntriesSub": "Persisted (SQLite)",
|
||||
"cacheHitsSub": "of {total} total",
|
||||
"tokensSavedSub": "Estimated from hits",
|
||||
"autoRefresh": "Auto-refreshes every {seconds}s",
|
||||
"hits": "Hits",
|
||||
"misses": "Misses",
|
||||
"total": "Total",
|
||||
"behaviorDeterministic": "Only non-streaming requests with temperature=0 are cached.",
|
||||
"behaviorBypass": "Bypass with header {header}.",
|
||||
"behaviorTwoTier": "Two-tier storage: in-memory LRU (fast) + SQLite (persistent across restarts).",
|
||||
"behaviorTtl": "Default TTL: 30 minutes. Configure via {envVar}.",
|
||||
"activeDedupKeys": "Active Dedup Keys",
|
||||
"dedupWindow": "Dedup Window"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,7 +185,9 @@
|
||||
"playground": "Playground",
|
||||
"agents": "Agentes",
|
||||
"cliToolsShort": "Ferramentas",
|
||||
"searchTools": "Search Tools"
|
||||
"searchTools": "Search Tools",
|
||||
"cache": "Cache",
|
||||
"cacheShort": "Cache"
|
||||
},
|
||||
"themesPage": {
|
||||
"title": "Themes",
|
||||
@@ -2865,5 +2867,37 @@
|
||||
"userInitial": "preciso de ajuda com",
|
||||
"userFollowUp": "Você pode explicar isso?"
|
||||
}
|
||||
},
|
||||
"cache": {
|
||||
"title": "Cache Management",
|
||||
"description": "Monitor and manage semantic response cache, hit rates, and token savings.",
|
||||
"refresh": "Refresh",
|
||||
"clearAll": "Clear All",
|
||||
"memoryEntries": "Memory Entries",
|
||||
"dbEntries": "DB Entries",
|
||||
"cacheHits": "Cache Hits",
|
||||
"tokensSaved": "Tokens Saved",
|
||||
"hitRate": "Hit Rate",
|
||||
"performance": "Cache Performance",
|
||||
"behavior": "Cache Behavior",
|
||||
"idempotency": "Idempotency Layer",
|
||||
"clearSuccess": "Cache cleared. {count} expired entries removed.",
|
||||
"clearError": "Failed to clear cache.",
|
||||
"unavailable": "Cache unavailable",
|
||||
"unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.",
|
||||
"memoryEntriesSub": "In-memory LRU",
|
||||
"dbEntriesSub": "Persisted (SQLite)",
|
||||
"cacheHitsSub": "of {total} total",
|
||||
"tokensSavedSub": "Estimated from hits",
|
||||
"autoRefresh": "Auto-refreshes every {seconds}s",
|
||||
"hits": "Hits",
|
||||
"misses": "Misses",
|
||||
"total": "Total",
|
||||
"behaviorDeterministic": "Only non-streaming requests with temperature=0 are cached.",
|
||||
"behaviorBypass": "Bypass with header {header}.",
|
||||
"behaviorTwoTier": "Two-tier storage: in-memory LRU (fast) + SQLite (persistent across restarts).",
|
||||
"behaviorTtl": "Default TTL: 30 minutes. Configure via {envVar}.",
|
||||
"activeDedupKeys": "Active Dedup Keys",
|
||||
"dedupWindow": "Dedup Window"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,7 +185,9 @@
|
||||
"agents": "Agentes",
|
||||
"cliToolsShort": "Ferramentas",
|
||||
"autoCombo": "Auto Combo",
|
||||
"searchTools": "Search Tools"
|
||||
"searchTools": "Search Tools",
|
||||
"cache": "Cache",
|
||||
"cacheShort": "Cache"
|
||||
},
|
||||
"themesPage": {
|
||||
"title": "Themes",
|
||||
@@ -2847,5 +2849,37 @@
|
||||
"userInitial": "preciso de ajuda com",
|
||||
"userFollowUp": "Você pode explicar isso?"
|
||||
}
|
||||
},
|
||||
"cache": {
|
||||
"title": "Cache Management",
|
||||
"description": "Monitor and manage semantic response cache, hit rates, and token savings.",
|
||||
"refresh": "Refresh",
|
||||
"clearAll": "Clear All",
|
||||
"memoryEntries": "Memory Entries",
|
||||
"dbEntries": "DB Entries",
|
||||
"cacheHits": "Cache Hits",
|
||||
"tokensSaved": "Tokens Saved",
|
||||
"hitRate": "Hit Rate",
|
||||
"performance": "Cache Performance",
|
||||
"behavior": "Cache Behavior",
|
||||
"idempotency": "Idempotency Layer",
|
||||
"clearSuccess": "Cache cleared. {count} expired entries removed.",
|
||||
"clearError": "Failed to clear cache.",
|
||||
"unavailable": "Cache unavailable",
|
||||
"unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.",
|
||||
"memoryEntriesSub": "In-memory LRU",
|
||||
"dbEntriesSub": "Persisted (SQLite)",
|
||||
"cacheHitsSub": "of {total} total",
|
||||
"tokensSavedSub": "Estimated from hits",
|
||||
"autoRefresh": "Auto-refreshes every {seconds}s",
|
||||
"hits": "Hits",
|
||||
"misses": "Misses",
|
||||
"total": "Total",
|
||||
"behaviorDeterministic": "Only non-streaming requests with temperature=0 are cached.",
|
||||
"behaviorBypass": "Bypass with header {header}.",
|
||||
"behaviorTwoTier": "Two-tier storage: in-memory LRU (fast) + SQLite (persistent across restarts).",
|
||||
"behaviorTtl": "Default TTL: 30 minutes. Configure via {envVar}.",
|
||||
"activeDedupKeys": "Active Dedup Keys",
|
||||
"dedupWindow": "Dedup Window"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,7 +185,9 @@
|
||||
"agents": "Agenți",
|
||||
"cliToolsShort": "Instrumente",
|
||||
"autoCombo": "Auto Combo",
|
||||
"searchTools": "Search Tools"
|
||||
"searchTools": "Search Tools",
|
||||
"cache": "Cache",
|
||||
"cacheShort": "Cache"
|
||||
},
|
||||
"themesPage": {
|
||||
"title": "Themes",
|
||||
@@ -2847,5 +2849,37 @@
|
||||
"userInitial": "Am nevoie de ajutor cu",
|
||||
"userFollowUp": "Puteți detalia asta?"
|
||||
}
|
||||
},
|
||||
"cache": {
|
||||
"title": "Cache Management",
|
||||
"description": "Monitor and manage semantic response cache, hit rates, and token savings.",
|
||||
"refresh": "Refresh",
|
||||
"clearAll": "Clear All",
|
||||
"memoryEntries": "Memory Entries",
|
||||
"dbEntries": "DB Entries",
|
||||
"cacheHits": "Cache Hits",
|
||||
"tokensSaved": "Tokens Saved",
|
||||
"hitRate": "Hit Rate",
|
||||
"performance": "Cache Performance",
|
||||
"behavior": "Cache Behavior",
|
||||
"idempotency": "Idempotency Layer",
|
||||
"clearSuccess": "Cache cleared. {count} expired entries removed.",
|
||||
"clearError": "Failed to clear cache.",
|
||||
"unavailable": "Cache unavailable",
|
||||
"unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.",
|
||||
"memoryEntriesSub": "In-memory LRU",
|
||||
"dbEntriesSub": "Persisted (SQLite)",
|
||||
"cacheHitsSub": "of {total} total",
|
||||
"tokensSavedSub": "Estimated from hits",
|
||||
"autoRefresh": "Auto-refreshes every {seconds}s",
|
||||
"hits": "Hits",
|
||||
"misses": "Misses",
|
||||
"total": "Total",
|
||||
"behaviorDeterministic": "Only non-streaming requests with temperature=0 are cached.",
|
||||
"behaviorBypass": "Bypass with header {header}.",
|
||||
"behaviorTwoTier": "Two-tier storage: in-memory LRU (fast) + SQLite (persistent across restarts).",
|
||||
"behaviorTtl": "Default TTL: 30 minutes. Configure via {envVar}.",
|
||||
"activeDedupKeys": "Active Dedup Keys",
|
||||
"dedupWindow": "Dedup Window"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,7 +185,9 @@
|
||||
"agents": "Агенты",
|
||||
"cliToolsShort": "Инструменты",
|
||||
"autoCombo": "Auto Combo",
|
||||
"searchTools": "Search Tools"
|
||||
"searchTools": "Search Tools",
|
||||
"cache": "Cache",
|
||||
"cacheShort": "Cache"
|
||||
},
|
||||
"themesPage": {
|
||||
"title": "Темы",
|
||||
@@ -2847,5 +2849,37 @@
|
||||
"userInitial": "мне нужна помощь с",
|
||||
"userFollowUp": "Можете ли вы рассказать об этом подробнее?"
|
||||
}
|
||||
},
|
||||
"cache": {
|
||||
"title": "Cache Management",
|
||||
"description": "Monitor and manage semantic response cache, hit rates, and token savings.",
|
||||
"refresh": "Refresh",
|
||||
"clearAll": "Clear All",
|
||||
"memoryEntries": "Memory Entries",
|
||||
"dbEntries": "DB Entries",
|
||||
"cacheHits": "Cache Hits",
|
||||
"tokensSaved": "Tokens Saved",
|
||||
"hitRate": "Hit Rate",
|
||||
"performance": "Cache Performance",
|
||||
"behavior": "Cache Behavior",
|
||||
"idempotency": "Idempotency Layer",
|
||||
"clearSuccess": "Cache cleared. {count} expired entries removed.",
|
||||
"clearError": "Failed to clear cache.",
|
||||
"unavailable": "Cache unavailable",
|
||||
"unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.",
|
||||
"memoryEntriesSub": "In-memory LRU",
|
||||
"dbEntriesSub": "Persisted (SQLite)",
|
||||
"cacheHitsSub": "of {total} total",
|
||||
"tokensSavedSub": "Estimated from hits",
|
||||
"autoRefresh": "Auto-refreshes every {seconds}s",
|
||||
"hits": "Hits",
|
||||
"misses": "Misses",
|
||||
"total": "Total",
|
||||
"behaviorDeterministic": "Only non-streaming requests with temperature=0 are cached.",
|
||||
"behaviorBypass": "Bypass with header {header}.",
|
||||
"behaviorTwoTier": "Two-tier storage: in-memory LRU (fast) + SQLite (persistent across restarts).",
|
||||
"behaviorTtl": "Default TTL: 30 minutes. Configure via {envVar}.",
|
||||
"activeDedupKeys": "Active Dedup Keys",
|
||||
"dedupWindow": "Dedup Window"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,7 +185,9 @@
|
||||
"agents": "Agenti",
|
||||
"cliToolsShort": "Nástroje",
|
||||
"autoCombo": "Auto Combo",
|
||||
"searchTools": "Search Tools"
|
||||
"searchTools": "Search Tools",
|
||||
"cache": "Cache",
|
||||
"cacheShort": "Cache"
|
||||
},
|
||||
"themesPage": {
|
||||
"title": "Themes",
|
||||
@@ -2847,5 +2849,37 @@
|
||||
"userInitial": "Potrebujem pomoc s",
|
||||
"userFollowUp": "Môžete to upresniť?"
|
||||
}
|
||||
},
|
||||
"cache": {
|
||||
"title": "Cache Management",
|
||||
"description": "Monitor and manage semantic response cache, hit rates, and token savings.",
|
||||
"refresh": "Refresh",
|
||||
"clearAll": "Clear All",
|
||||
"memoryEntries": "Memory Entries",
|
||||
"dbEntries": "DB Entries",
|
||||
"cacheHits": "Cache Hits",
|
||||
"tokensSaved": "Tokens Saved",
|
||||
"hitRate": "Hit Rate",
|
||||
"performance": "Cache Performance",
|
||||
"behavior": "Cache Behavior",
|
||||
"idempotency": "Idempotency Layer",
|
||||
"clearSuccess": "Cache cleared. {count} expired entries removed.",
|
||||
"clearError": "Failed to clear cache.",
|
||||
"unavailable": "Cache unavailable",
|
||||
"unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.",
|
||||
"memoryEntriesSub": "In-memory LRU",
|
||||
"dbEntriesSub": "Persisted (SQLite)",
|
||||
"cacheHitsSub": "of {total} total",
|
||||
"tokensSavedSub": "Estimated from hits",
|
||||
"autoRefresh": "Auto-refreshes every {seconds}s",
|
||||
"hits": "Hits",
|
||||
"misses": "Misses",
|
||||
"total": "Total",
|
||||
"behaviorDeterministic": "Only non-streaming requests with temperature=0 are cached.",
|
||||
"behaviorBypass": "Bypass with header {header}.",
|
||||
"behaviorTwoTier": "Two-tier storage: in-memory LRU (fast) + SQLite (persistent across restarts).",
|
||||
"behaviorTtl": "Default TTL: 30 minutes. Configure via {envVar}.",
|
||||
"activeDedupKeys": "Active Dedup Keys",
|
||||
"dedupWindow": "Dedup Window"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,7 +185,9 @@
|
||||
"agents": "Agenter",
|
||||
"cliToolsShort": "Verktyg",
|
||||
"autoCombo": "Auto Combo",
|
||||
"searchTools": "Search Tools"
|
||||
"searchTools": "Search Tools",
|
||||
"cache": "Cache",
|
||||
"cacheShort": "Cache"
|
||||
},
|
||||
"themesPage": {
|
||||
"title": "Themes",
|
||||
@@ -2847,5 +2849,37 @@
|
||||
"userInitial": "Jag behöver hjälp med",
|
||||
"userFollowUp": "Kan du utveckla det?"
|
||||
}
|
||||
},
|
||||
"cache": {
|
||||
"title": "Cache Management",
|
||||
"description": "Monitor and manage semantic response cache, hit rates, and token savings.",
|
||||
"refresh": "Refresh",
|
||||
"clearAll": "Clear All",
|
||||
"memoryEntries": "Memory Entries",
|
||||
"dbEntries": "DB Entries",
|
||||
"cacheHits": "Cache Hits",
|
||||
"tokensSaved": "Tokens Saved",
|
||||
"hitRate": "Hit Rate",
|
||||
"performance": "Cache Performance",
|
||||
"behavior": "Cache Behavior",
|
||||
"idempotency": "Idempotency Layer",
|
||||
"clearSuccess": "Cache cleared. {count} expired entries removed.",
|
||||
"clearError": "Failed to clear cache.",
|
||||
"unavailable": "Cache unavailable",
|
||||
"unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.",
|
||||
"memoryEntriesSub": "In-memory LRU",
|
||||
"dbEntriesSub": "Persisted (SQLite)",
|
||||
"cacheHitsSub": "of {total} total",
|
||||
"tokensSavedSub": "Estimated from hits",
|
||||
"autoRefresh": "Auto-refreshes every {seconds}s",
|
||||
"hits": "Hits",
|
||||
"misses": "Misses",
|
||||
"total": "Total",
|
||||
"behaviorDeterministic": "Only non-streaming requests with temperature=0 are cached.",
|
||||
"behaviorBypass": "Bypass with header {header}.",
|
||||
"behaviorTwoTier": "Two-tier storage: in-memory LRU (fast) + SQLite (persistent across restarts).",
|
||||
"behaviorTtl": "Default TTL: 30 minutes. Configure via {envVar}.",
|
||||
"activeDedupKeys": "Active Dedup Keys",
|
||||
"dedupWindow": "Dedup Window"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,7 +185,9 @@
|
||||
"agents": "เอเจนต์",
|
||||
"cliToolsShort": "เครื่องมือ",
|
||||
"autoCombo": "Auto Combo",
|
||||
"searchTools": "Search Tools"
|
||||
"searchTools": "Search Tools",
|
||||
"cache": "Cache",
|
||||
"cacheShort": "Cache"
|
||||
},
|
||||
"themesPage": {
|
||||
"title": "ธีมส์",
|
||||
@@ -2847,5 +2849,37 @@
|
||||
"userInitial": "ฉันต้องการความช่วยเหลือเกี่ยวกับ",
|
||||
"userFollowUp": "คุณช่วยอธิบายรายละเอียดเกี่ยวกับเรื่องนั้นได้ไหม?"
|
||||
}
|
||||
},
|
||||
"cache": {
|
||||
"title": "Cache Management",
|
||||
"description": "Monitor and manage semantic response cache, hit rates, and token savings.",
|
||||
"refresh": "Refresh",
|
||||
"clearAll": "Clear All",
|
||||
"memoryEntries": "Memory Entries",
|
||||
"dbEntries": "DB Entries",
|
||||
"cacheHits": "Cache Hits",
|
||||
"tokensSaved": "Tokens Saved",
|
||||
"hitRate": "Hit Rate",
|
||||
"performance": "Cache Performance",
|
||||
"behavior": "Cache Behavior",
|
||||
"idempotency": "Idempotency Layer",
|
||||
"clearSuccess": "Cache cleared. {count} expired entries removed.",
|
||||
"clearError": "Failed to clear cache.",
|
||||
"unavailable": "Cache unavailable",
|
||||
"unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.",
|
||||
"memoryEntriesSub": "In-memory LRU",
|
||||
"dbEntriesSub": "Persisted (SQLite)",
|
||||
"cacheHitsSub": "of {total} total",
|
||||
"tokensSavedSub": "Estimated from hits",
|
||||
"autoRefresh": "Auto-refreshes every {seconds}s",
|
||||
"hits": "Hits",
|
||||
"misses": "Misses",
|
||||
"total": "Total",
|
||||
"behaviorDeterministic": "Only non-streaming requests with temperature=0 are cached.",
|
||||
"behaviorBypass": "Bypass with header {header}.",
|
||||
"behaviorTwoTier": "Two-tier storage: in-memory LRU (fast) + SQLite (persistent across restarts).",
|
||||
"behaviorTtl": "Default TTL: 30 minutes. Configure via {envVar}.",
|
||||
"activeDedupKeys": "Active Dedup Keys",
|
||||
"dedupWindow": "Dedup Window"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,7 +185,9 @@
|
||||
"agents": "Агенти",
|
||||
"cliToolsShort": "Інструменти",
|
||||
"autoCombo": "Auto Combo",
|
||||
"searchTools": "Search Tools"
|
||||
"searchTools": "Search Tools",
|
||||
"cache": "Cache",
|
||||
"cacheShort": "Cache"
|
||||
},
|
||||
"themesPage": {
|
||||
"title": "Themes",
|
||||
@@ -2847,5 +2849,37 @@
|
||||
"userInitial": "Мені потрібна допомога з",
|
||||
"userFollowUp": "Чи можете ви розповісти про це?"
|
||||
}
|
||||
},
|
||||
"cache": {
|
||||
"title": "Cache Management",
|
||||
"description": "Monitor and manage semantic response cache, hit rates, and token savings.",
|
||||
"refresh": "Refresh",
|
||||
"clearAll": "Clear All",
|
||||
"memoryEntries": "Memory Entries",
|
||||
"dbEntries": "DB Entries",
|
||||
"cacheHits": "Cache Hits",
|
||||
"tokensSaved": "Tokens Saved",
|
||||
"hitRate": "Hit Rate",
|
||||
"performance": "Cache Performance",
|
||||
"behavior": "Cache Behavior",
|
||||
"idempotency": "Idempotency Layer",
|
||||
"clearSuccess": "Cache cleared. {count} expired entries removed.",
|
||||
"clearError": "Failed to clear cache.",
|
||||
"unavailable": "Cache unavailable",
|
||||
"unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.",
|
||||
"memoryEntriesSub": "In-memory LRU",
|
||||
"dbEntriesSub": "Persisted (SQLite)",
|
||||
"cacheHitsSub": "of {total} total",
|
||||
"tokensSavedSub": "Estimated from hits",
|
||||
"autoRefresh": "Auto-refreshes every {seconds}s",
|
||||
"hits": "Hits",
|
||||
"misses": "Misses",
|
||||
"total": "Total",
|
||||
"behaviorDeterministic": "Only non-streaming requests with temperature=0 are cached.",
|
||||
"behaviorBypass": "Bypass with header {header}.",
|
||||
"behaviorTwoTier": "Two-tier storage: in-memory LRU (fast) + SQLite (persistent across restarts).",
|
||||
"behaviorTtl": "Default TTL: 30 minutes. Configure via {envVar}.",
|
||||
"activeDedupKeys": "Active Dedup Keys",
|
||||
"dedupWindow": "Dedup Window"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,7 +185,9 @@
|
||||
"agents": "Tác nhân",
|
||||
"cliToolsShort": "Công cụ",
|
||||
"autoCombo": "Auto Combo",
|
||||
"searchTools": "Search Tools"
|
||||
"searchTools": "Search Tools",
|
||||
"cache": "Cache",
|
||||
"cacheShort": "Cache"
|
||||
},
|
||||
"themesPage": {
|
||||
"title": "Themes",
|
||||
@@ -2847,5 +2849,37 @@
|
||||
"userInitial": "Tôi cần giúp đỡ với",
|
||||
"userFollowUp": "Bạn có thể giải thích về điều đó?"
|
||||
}
|
||||
},
|
||||
"cache": {
|
||||
"title": "Cache Management",
|
||||
"description": "Monitor and manage semantic response cache, hit rates, and token savings.",
|
||||
"refresh": "Refresh",
|
||||
"clearAll": "Clear All",
|
||||
"memoryEntries": "Memory Entries",
|
||||
"dbEntries": "DB Entries",
|
||||
"cacheHits": "Cache Hits",
|
||||
"tokensSaved": "Tokens Saved",
|
||||
"hitRate": "Hit Rate",
|
||||
"performance": "Cache Performance",
|
||||
"behavior": "Cache Behavior",
|
||||
"idempotency": "Idempotency Layer",
|
||||
"clearSuccess": "Cache cleared. {count} expired entries removed.",
|
||||
"clearError": "Failed to clear cache.",
|
||||
"unavailable": "Cache unavailable",
|
||||
"unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.",
|
||||
"memoryEntriesSub": "In-memory LRU",
|
||||
"dbEntriesSub": "Persisted (SQLite)",
|
||||
"cacheHitsSub": "of {total} total",
|
||||
"tokensSavedSub": "Estimated from hits",
|
||||
"autoRefresh": "Auto-refreshes every {seconds}s",
|
||||
"hits": "Hits",
|
||||
"misses": "Misses",
|
||||
"total": "Total",
|
||||
"behaviorDeterministic": "Only non-streaming requests with temperature=0 are cached.",
|
||||
"behaviorBypass": "Bypass with header {header}.",
|
||||
"behaviorTwoTier": "Two-tier storage: in-memory LRU (fast) + SQLite (persistent across restarts).",
|
||||
"behaviorTtl": "Default TTL: 30 minutes. Configure via {envVar}.",
|
||||
"activeDedupKeys": "Active Dedup Keys",
|
||||
"dedupWindow": "Dedup Window"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,7 +185,9 @@
|
||||
"themeViolet": "紫罗兰",
|
||||
"themeOrange": "橙色",
|
||||
"themeCyan": "青色",
|
||||
"cliToolsShort": "工具"
|
||||
"cliToolsShort": "工具",
|
||||
"cache": "Cache",
|
||||
"cacheShort": "Cache"
|
||||
},
|
||||
"themesPage": {
|
||||
"title": "主题",
|
||||
@@ -2843,5 +2845,37 @@
|
||||
"userInitial": "我需要帮助来处理",
|
||||
"userFollowUp": "你可以再详细说明一下吗?"
|
||||
}
|
||||
},
|
||||
"cache": {
|
||||
"title": "Cache Management",
|
||||
"description": "Monitor and manage semantic response cache, hit rates, and token savings.",
|
||||
"refresh": "Refresh",
|
||||
"clearAll": "Clear All",
|
||||
"memoryEntries": "Memory Entries",
|
||||
"dbEntries": "DB Entries",
|
||||
"cacheHits": "Cache Hits",
|
||||
"tokensSaved": "Tokens Saved",
|
||||
"hitRate": "Hit Rate",
|
||||
"performance": "Cache Performance",
|
||||
"behavior": "Cache Behavior",
|
||||
"idempotency": "Idempotency Layer",
|
||||
"clearSuccess": "Cache cleared. {count} expired entries removed.",
|
||||
"clearError": "Failed to clear cache.",
|
||||
"unavailable": "Cache unavailable",
|
||||
"unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.",
|
||||
"memoryEntriesSub": "In-memory LRU",
|
||||
"dbEntriesSub": "Persisted (SQLite)",
|
||||
"cacheHitsSub": "of {total} total",
|
||||
"tokensSavedSub": "Estimated from hits",
|
||||
"autoRefresh": "Auto-refreshes every {seconds}s",
|
||||
"hits": "Hits",
|
||||
"misses": "Misses",
|
||||
"total": "Total",
|
||||
"behaviorDeterministic": "Only non-streaming requests with temperature=0 are cached.",
|
||||
"behaviorBypass": "Bypass with header {header}.",
|
||||
"behaviorTwoTier": "Two-tier storage: in-memory LRU (fast) + SQLite (persistent across restarts).",
|
||||
"behaviorTtl": "Default TTL: 30 minutes. Configure via {envVar}.",
|
||||
"activeDedupKeys": "Active Dedup Keys",
|
||||
"dedupWindow": "Dedup Window"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import { validateProviderApiKey } from "@/lib/providers/validation";
|
||||
import { getProviderCredentials } from "@/sse/services/auth";
|
||||
import { getModelInfo } from "@/sse/services/model";
|
||||
|
||||
const SOFT_REACHABILITY_STATUSES = new Set([400, 405, 406, 409, 422]);
|
||||
|
||||
export function buildComboTestRequestBody(modelStr: string) {
|
||||
return {
|
||||
model: modelStr,
|
||||
messages: [{ role: "user", content: "Reply with OK only." }],
|
||||
// Some gateway-routed models reject ultra-tiny budgets during smoke tests.
|
||||
max_tokens: 16,
|
||||
stream: false,
|
||||
};
|
||||
}
|
||||
|
||||
export function shouldProbeComboTestReachability(statusCode: number) {
|
||||
return SOFT_REACHABILITY_STATUSES.has(Number(statusCode));
|
||||
}
|
||||
|
||||
type ProbeDeps = {
|
||||
getModelInfo?: typeof getModelInfo;
|
||||
getProviderCredentials?: typeof getProviderCredentials;
|
||||
validateProviderApiKey?: typeof validateProviderApiKey;
|
||||
};
|
||||
|
||||
export async function probeComboModelReachability(modelStr: string, deps: ProbeDeps = {}) {
|
||||
const resolveModel = deps.getModelInfo || getModelInfo;
|
||||
const loadCredentials = deps.getProviderCredentials || getProviderCredentials;
|
||||
const validateKey = deps.validateProviderApiKey || validateProviderApiKey;
|
||||
|
||||
const modelInfo = await resolveModel(modelStr);
|
||||
if (!modelInfo?.provider) {
|
||||
return { reachable: false, reason: "unresolved_model" };
|
||||
}
|
||||
|
||||
const credentials = await loadCredentials(
|
||||
modelInfo.provider,
|
||||
null,
|
||||
null,
|
||||
modelInfo.model || modelStr
|
||||
);
|
||||
if (!credentials || credentials.allRateLimited) {
|
||||
return { reachable: false, reason: "credentials_unavailable" };
|
||||
}
|
||||
|
||||
const apiKey = credentials.apiKey || credentials.accessToken;
|
||||
if (typeof apiKey !== "string" || apiKey.trim().length === 0) {
|
||||
return { reachable: false, reason: "missing_auth_material" };
|
||||
}
|
||||
|
||||
const providerSpecificData =
|
||||
credentials.providerSpecificData && typeof credentials.providerSpecificData === "object"
|
||||
? { ...credentials.providerSpecificData }
|
||||
: {};
|
||||
|
||||
if (!providerSpecificData.validationModelId && modelInfo.model) {
|
||||
providerSpecificData.validationModelId = modelInfo.model;
|
||||
}
|
||||
|
||||
const validation = await validateKey({
|
||||
provider: modelInfo.provider,
|
||||
apiKey,
|
||||
providerSpecificData,
|
||||
});
|
||||
|
||||
return {
|
||||
reachable: Boolean(validation?.valid),
|
||||
provider: modelInfo.provider,
|
||||
model: modelInfo.model || null,
|
||||
method: validation?.method || null,
|
||||
warning: validation?.warning || null,
|
||||
};
|
||||
}
|
||||
@@ -1,16 +1,18 @@
|
||||
import initializeCloudSync from "@/shared/services/initializeCloudSync";
|
||||
import { startModelSyncScheduler } from "@/shared/services/modelSyncScheduler";
|
||||
import "@/lib/tokenHealthCheck"; // Proactive token health-check scheduler
|
||||
|
||||
// Initialize cloud sync when this module is imported
|
||||
// Initialize background sync services when this module is imported
|
||||
let initialized = false;
|
||||
|
||||
export async function ensureCloudSyncInitialized() {
|
||||
if (!initialized) {
|
||||
try {
|
||||
await initializeCloudSync();
|
||||
startModelSyncScheduler();
|
||||
initialized = true;
|
||||
} catch (error) {
|
||||
console.error("[ServerInit] Error initializing cloud sync:", error);
|
||||
console.error("[ServerInit] Error initializing background sync services:", error);
|
||||
}
|
||||
}
|
||||
return initialized;
|
||||
|
||||
@@ -5,6 +5,7 @@ import { getSettings } from "./lib/localDb";
|
||||
import { isPublicRoute, verifyAuth, isAuthRequired } from "./shared/utils/apiAuth";
|
||||
import { checkBodySize, getBodySizeLimit } from "./shared/middleware/bodySizeGuard";
|
||||
import { isDraining } from "./lib/gracefulShutdown";
|
||||
import { isModelSyncInternalRequest } from "./shared/services/modelSyncScheduler";
|
||||
|
||||
const SECRET = new TextEncoder().encode(process.env.JWT_SECRET || "");
|
||||
|
||||
@@ -43,6 +44,14 @@ export async function proxy(request) {
|
||||
return response;
|
||||
}
|
||||
|
||||
// Allow the model auto-sync scheduler to reach only its internal provider routes.
|
||||
if (
|
||||
isModelSyncInternalRequest(request) &&
|
||||
/^\/api\/providers\/[^/]+\/(sync-models|models)$/.test(pathname)
|
||||
) {
|
||||
return response;
|
||||
}
|
||||
|
||||
// Check if auth is required at all (respects requireLogin setting)
|
||||
const authRequired = await isAuthRequired();
|
||||
if (!authRequired) {
|
||||
|
||||
@@ -21,6 +21,7 @@ const navItemDefs = [
|
||||
{ href: "/dashboard/costs", i18nKey: "costs", icon: "account_balance_wallet" },
|
||||
{ href: "/dashboard/analytics", i18nKey: "analytics", icon: "analytics" },
|
||||
{ href: "/dashboard/limits", i18nKey: "limits", icon: "tune" },
|
||||
{ href: "/dashboard/cache", i18nKey: "cache", icon: "cached" },
|
||||
];
|
||||
|
||||
const cliItemDefs = [
|
||||
|
||||
@@ -661,6 +661,7 @@ export const USAGE_SUPPORTED_PROVIDERS = [
|
||||
"codex",
|
||||
"claude",
|
||||
"kimi-coding",
|
||||
"glm",
|
||||
];
|
||||
|
||||
// ── Zod validation at module load (Phase 7.2) ──
|
||||
|
||||
@@ -8,13 +8,53 @@
|
||||
* Pattern mirrors cloudSyncScheduler.ts for consistency.
|
||||
*/
|
||||
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { getSettings, updateSettings } from "@/lib/localDb";
|
||||
import { getRuntimePorts } from "@/lib/runtime/ports";
|
||||
|
||||
const DEFAULT_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
||||
const MODEL_SYNC_SETTING_KEY = "model_sync_last_run";
|
||||
const MODEL_SYNC_INTERNAL_AUTH_HEADER = "x-model-sync-internal-auth";
|
||||
|
||||
const { dashboardPort } = getRuntimePorts();
|
||||
|
||||
const INTERNAL_BASE_URL =
|
||||
process.env.BASE_URL ||
|
||||
process.env.NEXT_PUBLIC_BASE_URL ||
|
||||
process.env.NEXT_PUBLIC_APP_URL ||
|
||||
`http://localhost:${dashboardPort}`;
|
||||
|
||||
const globalState = globalThis as typeof globalThis & {
|
||||
__omnirouteModelSyncInternalAuthToken?: string;
|
||||
};
|
||||
|
||||
let schedulerTimer: NodeJS.Timeout | null = null;
|
||||
let isRunning = false;
|
||||
let internalAuthToken: string | null = null;
|
||||
|
||||
function getInternalAuthToken(): string {
|
||||
if (!internalAuthToken) {
|
||||
internalAuthToken = globalState.__omnirouteModelSyncInternalAuthToken || randomUUID();
|
||||
globalState.__omnirouteModelSyncInternalAuthToken = internalAuthToken;
|
||||
}
|
||||
return internalAuthToken;
|
||||
}
|
||||
|
||||
export function getModelSyncInternalAuthHeaderName(): string {
|
||||
return MODEL_SYNC_INTERNAL_AUTH_HEADER;
|
||||
}
|
||||
|
||||
export function buildModelSyncInternalHeaders(): Record<string, string> {
|
||||
return { [MODEL_SYNC_INTERNAL_AUTH_HEADER]: getInternalAuthToken() };
|
||||
}
|
||||
|
||||
export function isModelSyncInternalRequest(request: Request): boolean {
|
||||
if (!internalAuthToken && globalState.__omnirouteModelSyncInternalAuthToken) {
|
||||
internalAuthToken = globalState.__omnirouteModelSyncInternalAuthToken;
|
||||
}
|
||||
const headerToken = request.headers.get(MODEL_SYNC_INTERNAL_AUTH_HEADER);
|
||||
return Boolean(headerToken && internalAuthToken && headerToken === internalAuthToken);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all provider connections that have autoSync enabled.
|
||||
@@ -50,7 +90,10 @@ async function syncConnectionModels(
|
||||
try {
|
||||
const res = await fetch(`${baseUrl}/api/providers/${connectionId}/sync-models`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", "x-internal": "model-sync-scheduler" },
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...buildModelSyncInternalHeaders(),
|
||||
},
|
||||
});
|
||||
if (!res.ok) {
|
||||
console.warn(
|
||||
@@ -121,7 +164,7 @@ async function runSyncCycle(apiBaseUrl: string): Promise<void> {
|
||||
* @param intervalMs — sync interval in milliseconds (default: 24h)
|
||||
*/
|
||||
export function startModelSyncScheduler(
|
||||
apiBaseUrl = "http://localhost:20128",
|
||||
apiBaseUrl = INTERNAL_BASE_URL,
|
||||
intervalMs = DEFAULT_INTERVAL_MS
|
||||
): void {
|
||||
if (schedulerTimer) {
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
const { buildComboTestRequestBody, shouldProbeComboTestReachability, probeComboModelReachability } =
|
||||
await import("../../src/lib/combos/testHealth.ts");
|
||||
|
||||
test("combo test helper builds a realistic smoke payload", () => {
|
||||
const body = buildComboTestRequestBody("openrouter/openai/gpt-5.4");
|
||||
|
||||
assert.equal(body.model, "openrouter/openai/gpt-5.4");
|
||||
assert.equal(body.messages[0].content, "Reply with OK only.");
|
||||
assert.equal(body.max_tokens, 16);
|
||||
assert.equal(body.stream, false);
|
||||
});
|
||||
|
||||
test("combo test helper probes only soft 4xx responses", () => {
|
||||
assert.equal(shouldProbeComboTestReachability(400), true);
|
||||
assert.equal(shouldProbeComboTestReachability(422), true);
|
||||
assert.equal(shouldProbeComboTestReachability(401), false);
|
||||
assert.equal(shouldProbeComboTestReachability(404), false);
|
||||
assert.equal(shouldProbeComboTestReachability(429), false);
|
||||
});
|
||||
|
||||
test("combo reachability probe reuses resolved provider credentials and model id", async () => {
|
||||
let validationInput = null;
|
||||
|
||||
const result = await probeComboModelReachability("openrouter/openai/gpt-5.4", {
|
||||
getModelInfo: async () => ({ provider: "openrouter", model: "openai/gpt-5.4" }),
|
||||
getProviderCredentials: async () => ({
|
||||
apiKey: "test-key",
|
||||
providerSpecificData: { baseUrl: "https://openrouter.ai/api/v1" },
|
||||
}),
|
||||
validateProviderApiKey: async (input) => {
|
||||
validationInput = input;
|
||||
return { valid: true, method: "models_endpoint" };
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(result.reachable, true);
|
||||
assert.equal(result.provider, "openrouter");
|
||||
assert.equal(result.model, "openai/gpt-5.4");
|
||||
assert.equal(result.method, "models_endpoint");
|
||||
assert.deepEqual(validationInput, {
|
||||
provider: "openrouter",
|
||||
apiKey: "test-key",
|
||||
providerSpecificData: {
|
||||
baseUrl: "https://openrouter.ai/api/v1",
|
||||
validationModelId: "openai/gpt-5.4",
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
test("modelSyncScheduler: internal auth headers validate only for scheduler requests", async () => {
|
||||
const {
|
||||
buildModelSyncInternalHeaders,
|
||||
getModelSyncInternalAuthHeaderName,
|
||||
isModelSyncInternalRequest,
|
||||
} = await import("../../src/shared/services/modelSyncScheduler.ts");
|
||||
|
||||
const internalRequest = new Request("http://localhost/api/providers/test/sync-models", {
|
||||
method: "POST",
|
||||
headers: buildModelSyncInternalHeaders(),
|
||||
});
|
||||
assert.equal(isModelSyncInternalRequest(internalRequest), true);
|
||||
|
||||
const externalRequest = new Request("http://localhost/api/providers/test/sync-models", {
|
||||
method: "POST",
|
||||
headers: { [getModelSyncInternalAuthHeaderName()]: "invalid-token" },
|
||||
});
|
||||
assert.equal(isModelSyncInternalRequest(externalRequest), false);
|
||||
});
|
||||
|
||||
test("initCloudSync: startup initialization also starts model sync scheduler", () => {
|
||||
const filePath = path.join(process.cwd(), "src/lib/initCloudSync.ts");
|
||||
const source = fs.readFileSync(filePath, "utf8");
|
||||
|
||||
assert.match(source, /startModelSyncScheduler\s*\(/);
|
||||
});
|
||||
|
||||
test("proxy: internal model sync token is only allowed for provider model sync routes", () => {
|
||||
const filePath = path.join(process.cwd(), "src/proxy.ts");
|
||||
const source = fs.readFileSync(filePath, "utf8");
|
||||
|
||||
assert.match(source, /isModelSyncInternalRequest/);
|
||||
assert.match(source, /sync-models\|models/);
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
const providerLimitUtils =
|
||||
await import("../../src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.tsx");
|
||||
|
||||
test("provider plan fallbacks normalize to Unknown instead of repeating provider labels", () => {
|
||||
const tier = providerLimitUtils.normalizePlanTier("Claude Code");
|
||||
|
||||
assert.equal(tier.key, "unknown");
|
||||
assert.equal(tier.label, "Unknown");
|
||||
});
|
||||
|
||||
test("paid individual tiers use non-gray badge variants", () => {
|
||||
assert.equal(providerLimitUtils.normalizePlanTier("Plus").variant, "success");
|
||||
assert.equal(providerLimitUtils.normalizePlanTier("Pro").variant, "success");
|
||||
assert.equal(providerLimitUtils.normalizePlanTier("Student").variant, "success");
|
||||
assert.equal(providerLimitUtils.normalizePlanTier("Free").variant, "default");
|
||||
});
|
||||
|
||||
test("Codex workspacePlanType is used when live plan is missing or unknown", () => {
|
||||
const resolvedPlan = providerLimitUtils.resolvePlanValue("unknown", {
|
||||
workspacePlanType: "plus",
|
||||
});
|
||||
|
||||
assert.equal(resolvedPlan, "plus");
|
||||
const tier = providerLimitUtils.normalizePlanTier(resolvedPlan);
|
||||
assert.equal(tier.key, "plus");
|
||||
assert.equal(tier.variant, "success");
|
||||
});
|
||||
|
||||
test("remaining percentage helpers reflect remaining quota and stale resets refill to 100", () => {
|
||||
assert.equal(providerLimitUtils.calculatePercentage(0, 100), 100);
|
||||
assert.equal(providerLimitUtils.calculatePercentage(17, 100), 83);
|
||||
assert.equal(providerLimitUtils.calculatePercentage(60, 100), 40);
|
||||
|
||||
const past = new Date(Date.now() - 60_000).toISOString();
|
||||
const parsed = providerLimitUtils.parseQuotaData("codex", {
|
||||
quotas: {
|
||||
session: { used: 83, total: 100, resetAt: past },
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(parsed.length, 1);
|
||||
assert.equal(providerLimitUtils.calculatePercentage(parsed[0].used, parsed[0].total), 100);
|
||||
});
|
||||
@@ -2,7 +2,8 @@ import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
const { checkFallbackError } = await import("../../open-sse/services/accountFallback.ts");
|
||||
const { handleComboChat } = await import("../../open-sse/services/combo.ts");
|
||||
const { handleComboChat, shouldFallbackComboBadRequest } =
|
||||
await import("../../open-sse/services/combo.ts");
|
||||
const { resetAllCircuitBreakers } = await import("../../src/shared/utils/circuitBreaker.ts");
|
||||
|
||||
test.beforeEach(() => {
|
||||
@@ -139,3 +140,43 @@ test("T24: all inactive accounts return 503 service_unavailable (not 406)", asyn
|
||||
const body = await result.json();
|
||||
assert.equal(body.error?.code, "ALL_ACCOUNTS_INACTIVE");
|
||||
});
|
||||
|
||||
test("combo falls through provider-scoped 400s and reaches the next model", async () => {
|
||||
const log = createLog();
|
||||
|
||||
const result = await handleComboChat({
|
||||
body: {},
|
||||
combo: {
|
||||
name: "t24-provider-scoped-400",
|
||||
strategy: "priority",
|
||||
models: [
|
||||
{ model: "free/gemini-3.1-pro-preview", weight: 0 },
|
||||
{ model: "aio/gemini-3.1-pro-preview-thinking-high", weight: 0 },
|
||||
{ model: "openrouter/google/gemini-3.1-pro-preview", weight: 0 },
|
||||
],
|
||||
},
|
||||
handleSingleModel: createStatusSequenceHandler([
|
||||
{ status: 429, message: "No capacity available for model gemini-3.1-pro-preview" },
|
||||
{ status: 400, message: "request blocked by Gemini API: PROHIBITED_CONTENT" },
|
||||
{ status: 200 },
|
||||
]),
|
||||
isModelAvailable: () => true,
|
||||
log,
|
||||
settings: null,
|
||||
allCombos: null,
|
||||
});
|
||||
|
||||
assert.equal(result.ok, true);
|
||||
const badRequestLog = log.entries.find((entry) => entry.msg.includes("provider-scoped 400"));
|
||||
assert.ok(badRequestLog);
|
||||
});
|
||||
|
||||
test("combo bad-request fallback helper keeps generic 400s terminal", () => {
|
||||
assert.equal(shouldFallbackComboBadRequest(400, "request blocked by Gemini API"), true);
|
||||
assert.equal(
|
||||
shouldFallbackComboBadRequest(400, "One or more of the provided message roles is not valid"),
|
||||
true
|
||||
);
|
||||
assert.equal(shouldFallbackComboBadRequest(400, "bad request"), false);
|
||||
assert.equal(shouldFallbackComboBadRequest(422, "request blocked by Gemini API"), false);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user