Compare commits

..

7 Commits

Author SHA1 Message Date
Diego Rodrigues de Sa e Souza 6637f294df chore: release v3.2.0 (#722)
Build Electron Desktop App / Validate version (push) Failing after 28s
Build Electron Desktop App / Build Electron (macos-arm64) (push) Has been skipped
Build Electron Desktop App / Build Electron (linux) (push) Has been skipped
Build Electron Desktop App / Build Electron (macos-intel) (push) Has been skipped
Build Electron Desktop App / Build Electron (windows) (push) Has been skipped
Build Electron Desktop App / Create Release (push) Has been skipped
Build Electron Desktop App / Publish to npm (push) Has been skipped
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-03-28 20:45:18 -03:00
dependabot[bot] ad8a444105 deps: bump path-to-regexp from 8.3.0 to 8.4.0 (#715)
Bumps [path-to-regexp](https://github.com/pillarjs/path-to-regexp) from 8.3.0 to 8.4.0.
- [Release notes](https://github.com/pillarjs/path-to-regexp/releases)
- [Changelog](https://github.com/pillarjs/path-to-regexp/blob/master/History.md)
- [Commits](https://github.com/pillarjs/path-to-regexp/compare/v8.3.0...v8.4.0)

---
updated-dependencies:
- dependency-name: path-to-regexp
  dependency-version: 8.4.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-28 20:39:29 -03:00
Chris 877cfa0071 feat: add GLM Coding usage/quota tracking with Z.AI session quota (#698)
* feat: add GLM Coding usage/quota tracking with Z.AI session quota

Add GLM to the usage tracking pipeline: usage API route, Z.AI quota
fetcher (TOKENS_LIMIT percentage-based), quota parser, and Provider
Limits UI. Adds API region dropdown (International/China) to Add/Edit
connection modals. Displays session quota with plan level.

* fix: address PR review feedback for GLM usage tracking

- Remove explicit `any` types from getGlmUsage (fix lint budget)
- Fix empty string fallback for plan level
- Remove duplicate `case "glm"` in quota parser (identical to default)
- Skip OAuth refresh flow for GLM (API key auth) in usage route

* fix: upgrade path-to-regexp to fix ReDoS vulnerability (GHSA-j3q9-mxjg-w52f, GHSA-27v5-c462-wpq7)

---------

Co-authored-by: Chris Staley <christopher-s@users.noreply.github.com>
2026-03-28 20:39:24 -03:00
Paijo e6f0a780b7 feat(dashboard): add Cache Management page with stats, hit rate, and targeted invalidation (#701)
Adds a new /dashboard/cache page that surfaces the existing but UI-less
semantic cache infrastructure.

Changes:
- New page: src/app/(dashboard)/dashboard/cache/page.tsx
  - Live stats: memory entries, DB entries, cache hits, tokens saved
  - Hit rate progress bar with color coding (green/yellow/red)
  - Hits/Misses/Total breakdown
  - Idempotency layer stats (active dedup keys + window)
  - Cache behavior info panel
  - Clear All button
  - Auto-refresh every 10s
- Enhanced API: src/app/api/cache/route.ts
  - DELETE ?model=<name> — invalidate by model
  - DELETE ?signature=<hex> — invalidate single entry
  - DELETE ?staleMs=<ms> — invalidate entries older than N ms
  - DELETE (no params) — clear all (existing behavior)
- Sidebar: added Cache nav item (icon: cached)
- i18n: added cache + sidebar.cache keys for all 31 supported locales

No new dependencies. All functionality builds on existing semanticCache.ts,
cacheLayer.ts, and idempotencyLayer.ts modules.

Co-authored-by: oyi77 <oyi77@users.noreply.github.com>
2026-03-28 20:39:20 -03:00
Randi dd9de2efa9 fix: harden combo fallback and health checks (#704) 2026-03-28 20:39:16 -03:00
Randi f6b0811f78 [codex] fix provider limits ui (#718)
* fix provider limits ui

* restore remaining quota progress bars

* address provider limits review feedback
2026-03-28 20:39:06 -03:00
Randi eba9d854a9 fix model auto-sync startup and auth (#719) 2026-03-28 20:39:02 -03:00
58 changed files with 2179 additions and 111 deletions
+19
View File
@@ -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
+3 -2
View File
@@ -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
View File
@@ -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,
+33 -2
View File
@@ -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) {
+56 -1
View File
@@ -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}` };
}
+11 -11
View File
@@ -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
View File
@@ -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
View File
@@ -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>
);
}
+18 -7
View File
@@ -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 (
+67 -8
View File
@@ -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 });
}
}
+31 -7
View File
@@ -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(),
},
});
+14 -2
View File
@@ -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);
+35 -1
View File
@@ -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"
}
}
+35 -1
View File
@@ -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"
}
}
+35 -1
View File
@@ -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"
}
}
+35 -1
View File
@@ -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"
}
}
+35 -1
View File
@@ -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"
}
}
+35 -1
View File
@@ -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."
}
}
+35 -1
View File
@@ -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"
}
}
+35 -1
View File
@@ -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"
}
}
+35 -1
View File
@@ -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"
}
}
+35 -1
View File
@@ -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"
}
}
+35 -1
View File
@@ -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"
}
}
+35 -1
View File
@@ -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"
}
}
+35 -1
View File
@@ -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"
}
}
+35 -1
View File
@@ -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"
}
}
+35 -1
View File
@@ -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"
}
}
+35 -1
View File
@@ -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"
}
}
+35 -1
View File
@@ -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"
}
}
+35 -1
View File
@@ -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"
}
}
+35 -1
View File
@@ -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"
}
}
+35 -1
View File
@@ -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"
}
}
+35 -1
View File
@@ -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"
}
}
+35 -1
View File
@@ -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"
}
}
+35 -1
View File
@@ -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"
}
}
+35 -1
View File
@@ -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"
}
}
+35 -1
View File
@@ -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"
}
}
+35 -1
View File
@@ -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"
}
}
+35 -1
View File
@@ -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"
}
}
+35 -1
View File
@@ -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"
}
}
+35 -1
View File
@@ -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"
}
}
+35 -1
View File
@@ -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"
}
}
+35 -1
View File
@@ -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"
}
}
+35 -1
View File
@@ -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"
}
}
+74
View File
@@ -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,
};
}
+4 -2
View File
@@ -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;
+9
View File
@@ -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) {
+1
View File
@@ -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 = [
+1
View File
@@ -661,6 +661,7 @@ export const USAGE_SUPPORTED_PROVIDERS = [
"codex",
"claude",
"kimi-coding",
"glm",
];
// ── Zod validation at module load (Phase 7.2) ──
+45 -2
View File
@@ -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) {
+51
View File
@@ -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",
},
});
});
+39
View File
@@ -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/);
});
+46
View File
@@ -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);
});