From 7ab75dd15a1460c1a11e136c924f28cd0e8dafef Mon Sep 17 00:00:00 2001 From: Chris Staley Date: Mon, 30 Mar 2026 14:18:02 -0600 Subject: [PATCH 1/8] fix: use gemini-cli/ as model prefix instead of gc/ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gemini CLI clients use bare model names, not provider-prefixed IDs. The gc/ alias was opaque — gemini-cli/ is self-documenting. Since alias now equals provider ID, the dual-prefix duplication logic naturally skips Gemini CLI (no duplicate gemini-cli/ entries). --- open-sse/config/providerRegistry.ts | 2 +- src/app/(dashboard)/dashboard/combos/page.tsx | 2 +- src/app/api/v1/models/catalog.ts | 1 - src/lib/pricingSync.ts | 4 ++-- src/shared/constants/pricing.ts | 2 +- src/shared/constants/providers.ts | 2 +- 6 files changed, 6 insertions(+), 7 deletions(-) diff --git a/open-sse/config/providerRegistry.ts b/open-sse/config/providerRegistry.ts index 0228412b9f..0e831d3b37 100644 --- a/open-sse/config/providerRegistry.ts +++ b/open-sse/config/providerRegistry.ts @@ -212,7 +212,7 @@ export const REGISTRY: Record = { "gemini-cli": { id: "gemini-cli", - alias: "gc", + alias: "gemini-cli", format: "gemini-cli", executor: "gemini-cli", baseUrl: "https://cloudcode-pa.googleapis.com/v1internal", diff --git a/src/app/(dashboard)/dashboard/combos/page.tsx b/src/app/(dashboard)/dashboard/combos/page.tsx index 952b50276e..181cded1fb 100644 --- a/src/app/(dashboard)/dashboard/combos/page.tsx +++ b/src/app/(dashboard)/dashboard/combos/page.tsx @@ -1439,7 +1439,7 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders }) { }; const FREE_STACK_PRESET_MODELS = [ - { model: "gc/gemini-3-flash-preview", weight: 0 }, + { model: "gemini-cli/gemini-3-flash-preview", weight: 0 }, { model: "kr/claude-sonnet-4.5", weight: 0 }, { model: "if/kimi-k2-thinking", weight: 0 }, { model: "if/qwen3-coder-plus", weight: 0 }, diff --git a/src/app/api/v1/models/catalog.ts b/src/app/api/v1/models/catalog.ts index 3be16b456e..5aeebeb20c 100644 --- a/src/app/api/v1/models/catalog.ts +++ b/src/app/api/v1/models/catalog.ts @@ -25,7 +25,6 @@ const FALLBACK_ALIAS_TO_PROVIDER = { cl: "cline", cu: "cursor", cx: "codex", - gc: "gemini-cli", gh: "github", if: "iflow", kc: "kilocode", diff --git a/src/lib/pricingSync.ts b/src/lib/pricingSync.ts index f3836eee36..b6f1fe65c6 100644 --- a/src/lib/pricingSync.ts +++ b/src/lib/pricingSync.ts @@ -75,9 +75,9 @@ const LITELLM_PRICING_URL = const LITELLM_PROVIDER_MAP: Record = { openai: ["openai", "cx"], anthropic: ["anthropic", "cc"], - vertex_ai: ["gemini", "gc"], + vertex_ai: ["gemini", "gemini-cli"], "vertex_ai-anthropic_models": ["anthropic"], - google: ["gemini", "gc"], + google: ["gemini", "gemini-cli"], deepseek: ["if"], groq: ["groq"], together_ai: ["openrouter"], diff --git a/src/shared/constants/pricing.ts b/src/shared/constants/pricing.ts index 789ae0a06a..10fcff078e 100644 --- a/src/shared/constants/pricing.ts +++ b/src/shared/constants/pricing.ts @@ -1299,7 +1299,7 @@ type TokenUsage = Record; /** * Get pricing for a specific provider and model - * @param {string} provider - Provider ID (e.g., "openai", "cc", "gc") + * @param {string} provider - Provider ID (e.g., "openai", "cc", "gemini-cli") * @param {string} model - Model ID * @returns {object|null} Pricing object or null if not found */ diff --git a/src/shared/constants/providers.ts b/src/shared/constants/providers.ts index 266dc08234..13dfc12ed5 100644 --- a/src/shared/constants/providers.ts +++ b/src/shared/constants/providers.ts @@ -6,7 +6,7 @@ export const FREE_PROVIDERS = { qwen: { id: "qwen", alias: "qw", name: "Qwen Code", icon: "psychology", color: "#10B981" }, "gemini-cli": { id: "gemini-cli", - alias: "gc", + alias: "gemini-cli", name: "Gemini CLI", icon: "terminal", color: "#4285F4", From ff73de5716016a3dcadbb2d52999190a4f58568e Mon Sep 17 00:00:00 2001 From: "R.D." Date: Mon, 30 Mar 2026 17:05:19 -0400 Subject: [PATCH 2/8] fix openrouter available models sync --- README.md | 2 + docs/ARCHITECTURE.md | 38 ++--- docs/USER_GUIDE.md | 5 + .../dashboard/providers/[id]/page.tsx | 137 ++++++++++++------ .../api/providers/[id]/sync-models/route.ts | 19 ++- .../providerModels/managedAvailableModels.ts | 91 ++++++++++++ src/shared/utils/providerModelAliases.ts | 68 +++++++++ tests/unit/managed-available-models.test.mjs | 69 +++++++++ 8 files changed, 363 insertions(+), 66 deletions(-) create mode 100644 src/lib/providerModels/managedAvailableModels.ts create mode 100644 src/shared/utils/providerModelAliases.ts create mode 100644 tests/unit/managed-available-models.test.mjs diff --git a/README.md b/README.md index 3b2fe3a90d..36ad3aeb89 100644 --- a/README.md +++ b/README.md @@ -1541,6 +1541,8 @@ Models: **Models:** Access 100+ models from all major providers through a single API key. +**Dashboard behavior:** OpenRouter models are managed from **Available Models**. Manual add, import, and auto-sync all update the same list. +
diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 80960c7212..421b19615f 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -686,25 +686,25 @@ Additional processing layers in the translation pipeline: ## Supported API Endpoints -| Endpoint | Format | Handler | -| -------------------------------------------------- | ------------------ | ---------------------------------------------------- | -| `POST /v1/chat/completions` | OpenAI Chat | `src/sse/handlers/chat.ts` | -| `POST /v1/messages` | Claude Messages | Same handler (auto-detected) | -| `POST /v1/responses` | OpenAI Responses | `open-sse/handlers/responsesHandler.ts` | -| `POST /v1/embeddings` | OpenAI Embeddings | `open-sse/handlers/embeddings.ts` | -| `GET /v1/embeddings` | Model listing | API route | -| `POST /v1/images/generations` | OpenAI Images | `open-sse/handlers/imageGeneration.ts` | -| `GET /v1/images/generations` | Model listing | API route | -| `POST /v1/providers/{provider}/chat/completions` | OpenAI Chat | Dedicated per-provider with model validation | -| `POST /v1/providers/{provider}/embeddings` | OpenAI Embeddings | Dedicated per-provider with model validation | -| `POST /v1/providers/{provider}/images/generations` | OpenAI Images | Dedicated per-provider with model validation | -| `POST /v1/messages/count_tokens` | Claude Token Count | API route | -| `GET /v1/models` | OpenAI Models list | API route (chat + embedding + image + custom models) | -| `GET /api/models/catalog` | Catalog | All models grouped by provider + type | -| `POST /v1beta/models/*:streamGenerateContent` | Gemini native | API route | -| `GET/PUT/DELETE /api/settings/proxy` | Proxy Config | Network proxy configuration | -| `POST /api/settings/proxy/test` | Proxy Connectivity | Proxy health/connectivity test endpoint | -| `GET/POST/DELETE /api/provider-models` | Custom Models | Custom model management per provider | +| Endpoint | Format | Handler | +| -------------------------------------------------- | ------------------ | ------------------------------------------------------------------- | +| `POST /v1/chat/completions` | OpenAI Chat | `src/sse/handlers/chat.ts` | +| `POST /v1/messages` | Claude Messages | Same handler (auto-detected) | +| `POST /v1/responses` | OpenAI Responses | `open-sse/handlers/responsesHandler.ts` | +| `POST /v1/embeddings` | OpenAI Embeddings | `open-sse/handlers/embeddings.ts` | +| `GET /v1/embeddings` | Model listing | API route | +| `POST /v1/images/generations` | OpenAI Images | `open-sse/handlers/imageGeneration.ts` | +| `GET /v1/images/generations` | Model listing | API route | +| `POST /v1/providers/{provider}/chat/completions` | OpenAI Chat | Dedicated per-provider with model validation | +| `POST /v1/providers/{provider}/embeddings` | OpenAI Embeddings | Dedicated per-provider with model validation | +| `POST /v1/providers/{provider}/images/generations` | OpenAI Images | Dedicated per-provider with model validation | +| `POST /v1/messages/count_tokens` | Claude Token Count | API route | +| `GET /v1/models` | OpenAI Models list | API route (chat + embedding + image + custom models) | +| `GET /api/models/catalog` | Catalog | All models grouped by provider + type | +| `POST /v1beta/models/*:streamGenerateContent` | Gemini native | API route | +| `GET/PUT/DELETE /api/settings/proxy` | Proxy Config | Network proxy configuration | +| `POST /api/settings/proxy/test` | Proxy Connectivity | Proxy health/connectivity test endpoint | +| `GET/POST/DELETE /api/provider-models` | Provider Models | Provider model metadata backing custom and managed available models | ## Bypass Handler diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md index 3f47a4a6e6..ed35a0ba16 100644 --- a/docs/USER_GUIDE.md +++ b/docs/USER_GUIDE.md @@ -596,6 +596,11 @@ curl -X POST http://localhost:20128/api/provider-models \ Or use Dashboard: **Providers → [Provider] → Custom Models**. +Notes: + +- OpenRouter and OpenAI/Anthropic-compatible providers are managed from **Available Models** only. Manual add, import, and auto-sync all land in the same available-model list, so there is no separate Custom Models section for those providers. +- The **Custom Models** section is intended for providers that do not expose managed available-model imports. + ### Dedicated Provider Routes Route requests directly to a specific provider with model validation: diff --git a/src/app/(dashboard)/dashboard/providers/[id]/page.tsx b/src/app/(dashboard)/dashboard/providers/[id]/page.tsx index ca1a3a1f4b..53c6e7a0df 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/page.tsx +++ b/src/app/(dashboard)/dashboard/providers/[id]/page.tsx @@ -36,6 +36,7 @@ import { MODEL_COMPAT_PROTOCOL_KEYS, type ModelCompatProtocolKey, } from "@/shared/constants/modelCompat"; +import { resolveManagedModelAlias } from "@/shared/utils/providerModelAliases"; type CompatByProtocolMap = Partial< Record< @@ -331,6 +332,10 @@ interface CompatibleModelsSectionProps { providerStorageAlias: string; providerDisplayAlias: string; modelAliases: Record; + fallbackModels?: CompatModelRow[]; + description: string; + inputLabel: string; + inputPlaceholder: string; copied?: string; onCopy: (text: string, key: string) => void; onSetAlias: (modelId: string, alias: string, providerStorageAlias?: string) => Promise; @@ -850,6 +855,7 @@ export default function ProviderDetailPage() { const isOpenAICompatible = isOpenAICompatibleProvider(providerId); const isAnthropicCompatible = isAnthropicCompatibleProvider(providerId); const isCompatible = isOpenAICompatible || isAnthropicCompatible; + const isManagedAvailableModelsProvider = isCompatible || providerId === "openrouter"; const isSearchProvider = providerId.endsWith("-search"); const providerStorageAlias = isCompatible ? providerId : providerAlias; @@ -1469,7 +1475,10 @@ export default function ProviderDetailPage() { logs: [ t("foundModelsStartingImport", { count: newModels.length }), ...(newModels.length < fetchedModels.length - ? [t("skippingExistingModels", { count: fetchedModels.length - newModels.length }) || `Skipping ${fetchedModels.length - newModels.length} existing models`] + ? [ + t("skippingExistingModels", { count: fetchedModels.length - newModels.length }) || + `Skipping ${fetchedModels.length - newModels.length} existing models`, + ] : []), ], })); @@ -1664,6 +1673,14 @@ export default function ProviderDetailPage() { }; const [clearingModels, setClearingModels] = useState(false); + const providerAliasEntries = useMemo( + () => + Object.entries(modelAliases).filter(([, model]) => + (model as string).startsWith(`${providerStorageAlias}/`) + ), + [modelAliases, providerStorageAlias] + ); + const handleClearAllModels = async () => { if (clearingModels) return; if (!confirm(t("clearAllModelsConfirm"))) return; @@ -1675,11 +1692,8 @@ export default function ProviderDetailPage() { ); if (res.ok) { // Also delete all aliases that belong to this provider - const aliasEntries = Object.entries(modelAliases).filter(([, model]) => - (model as string).startsWith(`${providerStorageAlias}/`) - ); await Promise.all( - aliasEntries.map(([alias]) => + providerAliasEntries.map(([alias]) => fetch(`/api/models/alias?alias=${encodeURIComponent(alias)}`, { method: "DELETE", }).catch(() => {}) @@ -1806,7 +1820,8 @@ export default function ProviderDetailPage() { ); - const clearAllButton = modelMeta.customModels.length > 0 && ( + const clearAllButton = (modelMeta.customModels.length > 0 || + providerAliasEntries.length > 0) && ( - - - {metrics ? ( -
- {/* Overview Stats */} -
-
-

Total Requests

-

{metrics.totalRequests}

-
-
-

With Cache Control

-

{metrics.requestsWithCacheControl}

-
+ +
+
+
+ +

Prompt Cache Metrics

- - {/* Token Stats */} -
-
-

Input Tokens

-

- {metrics.totalInputTokens.toLocaleString()} -

-
-
-

Cached Tokens (Read)

-

- {metrics.totalCachedTokens.toLocaleString()} -

-
-
-

Cache Creation (Write)

-

- {metrics.totalCacheCreationTokens.toLocaleString()} -

-
-
- - {/* Cache Ratio */} -
-
-
-

Cache Reuse Ratio

-

Cached tokens / Total input tokens

-
-

{cacheHitRate.toFixed(1)}%

-
- {/* Progress bar */} -
-
-
-
- - {/* Savings */} -
-
-

Tokens Saved

-

- {metrics.tokensSaved.toLocaleString()} -

-
-
-

Est. Cost Saved

-

- ${metrics.estimatedCostSaved.toFixed(4)} -

-
-
- - {/* By Provider */} - {Object.keys(metrics.byProvider).length > 0 && ( -
-

By Provider

-
- {Object.entries(metrics.byProvider).map(([provider, stats]) => { - const providerCacheRate = - stats.inputTokens > 0 ? (stats.cachedTokens / stats.inputTokens) * 100 : 0; - return ( -
-
- {provider} - {stats.requests} reqs -
-
- - In: {stats.inputTokens.toLocaleString()} - - - Cached: {stats.cachedTokens.toLocaleString()} - - - Write: {stats.cacheCreationTokens.toLocaleString()} - - - {providerCacheRate.toFixed(0)}% - -
-
- ); - })} -
-
- )} +
- ) : ( -

Loading cache metrics...

- )} + + {metrics ? ( +
+ {/* Overview Stats */} +
+
+

Total Requests

+

{metrics.totalRequests}

+
+
+

With Cache Control

+

+ {metrics.requestsWithCacheControl} +

+
+
+ + {/* Token Stats */} +
+
+

Input Tokens

+

+ {metrics.totalInputTokens.toLocaleString()} +

+
+
+

Cached Tokens (Read)

+

+ {metrics.totalCachedTokens.toLocaleString()} +

+
+
+

Cache Creation (Write)

+

+ {metrics.totalCacheCreationTokens.toLocaleString()} +

+
+
+ + {/* Cache Ratio */} +
+
+
+

Cache Reuse Ratio

+

Cached tokens / Total input tokens

+
+

{cacheHitRate.toFixed(1)}%

+
+ {/* Progress bar */} +
+
+
+
+ + {/* Savings */} +
+
+

Tokens Saved

+

+ {metrics.tokensSaved.toLocaleString()} +

+
+
+

Est. Cost Saved

+

+ ${metrics.estimatedCostSaved.toFixed(4)} +

+
+
+ + {/* By Provider */} + {Object.keys(metrics.byProvider).length > 0 && ( +
+

By Provider

+
+ {Object.entries(metrics.byProvider).map(([provider, stats]) => { + const providerCacheRate = + stats.inputTokens > 0 ? (stats.cachedTokens / stats.inputTokens) * 100 : 0; + return ( +
+
+ {provider} + {stats.requests} reqs +
+
+ + In: {stats.inputTokens.toLocaleString()} + + + Cached: {stats.cachedTokens.toLocaleString()} + + + Write: {stats.cacheCreationTokens.toLocaleString()} + + + {providerCacheRate.toFixed(0)}% + +
+
+ ); + })} +
+
+ )} +
+ ) : ( +

Loading cache metrics...

+ )} +
); } From f99c90dc85f4a91cfee648aa45fbbc3da66e8e57 Mon Sep 17 00:00:00 2001 From: oyi77 Date: Tue, 31 Mar 2026 03:55:30 +0700 Subject: [PATCH 5/8] feat(cache): add OpenAI prompt_cache_key and Gemini cachedContent support Provider-specific caching enhancements: OpenAI: - Auto-generate prompt_cache_key from message prefix hash - Key format: omni-{prefix_hash_32chars} - Preserves client-provided keys (doesn't override) Gemini: - Preserve cachedContent ID if provided by client - Enables explicit Gemini caching for long prompts - cachedContentTokenCount already tracked in responses --- open-sse/handlers/chatCore.ts | 39 ++++++++++++++++--- .../translator/request/openai-to-gemini.ts | 6 +++ src/lib/promptCache/index.ts | 2 +- src/lib/promptCache/prefixAnalyzer.ts | 8 ++++ 4 files changed, 49 insertions(+), 6 deletions(-) diff --git a/open-sse/handlers/chatCore.ts b/open-sse/handlers/chatCore.ts index c60b379a0e..5c3312ab87 100644 --- a/open-sse/handlers/chatCore.ts +++ b/open-sse/handlers/chatCore.ts @@ -950,11 +950,24 @@ export async function handleChatCore({ const executeProviderRequest = async (modelToCall = effectiveModel, allowDedup = false) => { const execute = async () => { - const bodyToSend = + let bodyToSend = translatedBody.model === modelToCall ? translatedBody : { ...translatedBody, model: modelToCall }; + // Inject prompt_cache_key for OpenAI providers if not already set + if ( + targetFormat === FORMATS.OPENAI && + !bodyToSend.prompt_cache_key && + Array.isArray(bodyToSend.messages) + ) { + const { generatePromptCacheKey } = await import("@/lib/promptCache"); + const cacheKey = generatePromptCacheKey(bodyToSend.messages); + if (cacheKey) { + bodyToSend = { ...bodyToSend, prompt_cache_key: cacheKey }; + } + } + const rawResult = await withRateLimit(provider, connectionId, modelToCall, () => executor.execute({ model: modelToCall, @@ -1444,11 +1457,19 @@ export async function handleChatCore({ const cachedTokens = toPositiveNumber( usage.cache_read_input_tokens ?? usage.cached_tokens ?? - ((usage as Record).prompt_tokens_details as Record | undefined)?.cached_tokens + ( + (usage as Record).prompt_tokens_details as + | Record + | undefined + )?.cached_tokens ); const cacheCreationTokens = toPositiveNumber( usage.cache_creation_input_tokens ?? - ((usage as Record).prompt_tokens_details as Record | undefined)?.cache_creation_tokens + ( + (usage as Record).prompt_tokens_details as + | Record + | undefined + )?.cache_creation_tokens ); saveRequestUsage({ @@ -1604,11 +1625,19 @@ export async function handleChatCore({ const cachedTokens = toPositiveNumber( streamUsage.cache_read_input_tokens ?? streamUsage.cached_tokens ?? - ((streamUsage as Record).prompt_tokens_details as Record | undefined)?.cached_tokens + ( + (streamUsage as Record).prompt_tokens_details as + | Record + | undefined + )?.cached_tokens ); const cacheCreationTokens = toPositiveNumber( streamUsage.cache_creation_input_tokens ?? - ((streamUsage as Record).prompt_tokens_details as Record | undefined)?.cache_creation_tokens + ( + (streamUsage as Record).prompt_tokens_details as + | Record + | undefined + )?.cache_creation_tokens ); saveRequestUsage({ diff --git a/open-sse/translator/request/openai-to-gemini.ts b/open-sse/translator/request/openai-to-gemini.ts index 4aa3bba95f..8d4d2b4047 100644 --- a/open-sse/translator/request/openai-to-gemini.ts +++ b/open-sse/translator/request/openai-to-gemini.ts @@ -52,6 +52,7 @@ type GeminiRequest = { safetySettings: unknown; systemInstruction?: GeminiContent; tools?: Array<{ functionDeclarations: GeminiFunctionDeclaration[] }>; + cachedContent?: string; }; type CloudCodeEnvelope = { @@ -82,6 +83,11 @@ function openaiToGeminiBase(model, body, stream) { safetySettings: DEFAULT_SAFETY_SETTINGS, }; + // Preserve cachedContent if provided by client (for explicit Gemini caching) + if (body.cachedContent) { + result.cachedContent = body.cachedContent; + } + // Generation config if (body.temperature !== undefined) { result.generationConfig.temperature = body.temperature; diff --git a/src/lib/promptCache/index.ts b/src/lib/promptCache/index.ts index f4ee1fd501..2d1446f864 100644 --- a/src/lib/promptCache/index.ts +++ b/src/lib/promptCache/index.ts @@ -1 +1 @@ -export { analyzePrefix, shouldInjectCacheControl } from "./prefixAnalyzer"; +export { analyzePrefix, shouldInjectCacheControl, generatePromptCacheKey } from "./prefixAnalyzer"; diff --git a/src/lib/promptCache/prefixAnalyzer.ts b/src/lib/promptCache/prefixAnalyzer.ts index 43f622c87f..6f8e86e12e 100644 --- a/src/lib/promptCache/prefixAnalyzer.ts +++ b/src/lib/promptCache/prefixAnalyzer.ts @@ -75,3 +75,11 @@ export function analyzePrefix(messages: Message[]): PrefixAnalysis { export function shouldInjectCacheControl(analysis: PrefixAnalysis, minTokens = 1024): boolean { return analysis.prefixTokens >= minTokens && analysis.confidence >= 0.7; } + +export function generatePromptCacheKey(messages: Message[]): string { + const analysis = analyzePrefix(messages); + if (analysis.prefixHash) { + return `omni-${analysis.prefixHash.slice(0, 32)}`; + } + return ""; +} From 5437d691b5ffb101563dda85262fcf52652b077f Mon Sep 17 00:00:00 2001 From: oyi77 Date: Tue, 31 Mar 2026 04:03:54 +0700 Subject: [PATCH 6/8] fix(cache): address code review issues - Add authentication to /api/cache/entries (GET and DELETE) - Add authentication to /api/cache (GET and DELETE) - Validate trendHours: clamp to 1-720 range - Fix estimatedCostSaved bug: use calculated value instead of hardcoded 0 --- src/app/api/cache/entries/route.ts | 9 +++++++++ src/app/api/cache/route.ts | 12 +++++++++++- src/lib/db/settings.ts | 2 +- 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/app/api/cache/entries/route.ts b/src/app/api/cache/entries/route.ts index 26f30b02a9..97ff51ca9a 100644 --- a/src/app/api/cache/entries/route.ts +++ b/src/app/api/cache/entries/route.ts @@ -1,5 +1,6 @@ import { NextRequest, NextResponse } from "next/server"; import { getDbInstance } from "@/lib/db/core"; +import { isAuthenticated } from "@/shared/utils/apiAuth"; interface CacheEntry { id: string; @@ -12,6 +13,10 @@ interface CacheEntry { } export async function GET(req: NextRequest) { + if (!(await isAuthenticated(req))) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + try { const { searchParams } = new URL(req.url); const page = Math.max(1, parseInt(searchParams.get("page") || "1", 10)); @@ -71,6 +76,10 @@ export async function GET(req: NextRequest) { } export async function DELETE(req: NextRequest) { + if (!(await isAuthenticated(req))) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + try { const { searchParams } = new URL(req.url); const signature = searchParams.get("signature"); diff --git a/src/app/api/cache/route.ts b/src/app/api/cache/route.ts index d1bca53891..dd7364f929 100644 --- a/src/app/api/cache/route.ts +++ b/src/app/api/cache/route.ts @@ -9,15 +9,21 @@ import { } from "@/lib/semanticCache"; import { getIdempotencyStats } from "@/lib/idempotencyLayer"; import { getCacheMetrics, getCacheTrend } from "@/lib/db/settings"; +import { isAuthenticated } from "@/shared/utils/apiAuth"; function errorMessage(error: unknown): string { return error instanceof Error ? error.message : String(error); } export async function GET(req: NextRequest) { + if (!(await isAuthenticated(req))) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + try { const { searchParams } = new URL(req.url); - const trendHours = parseInt(searchParams.get("trendHours") || "24", 10); + const rawHours = parseInt(searchParams.get("trendHours") || "24", 10); + const trendHours = Math.min(720, Math.max(1, Number.isNaN(rawHours) ? 24 : rawHours)); const cacheStats = getCacheStats(); const idempotencyStats = getIdempotencyStats(); @@ -36,6 +42,10 @@ export async function GET(req: NextRequest) { } export async function DELETE(req: NextRequest) { + if (!(await isAuthenticated(req))) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + try { const { searchParams } = new URL(req.url); const model = searchParams.get("model"); diff --git a/src/lib/db/settings.ts b/src/lib/db/settings.ts index 333d71379f..5173e81e14 100644 --- a/src/lib/db/settings.ts +++ b/src/lib/db/settings.ts @@ -630,7 +630,7 @@ export async function getCacheMetrics() { totalCachedTokens: totalsRow?.totalCachedTokens || 0, totalCacheCreationTokens: totalsRow?.totalCacheCreationTokens || 0, tokensSaved, - estimatedCostSaved: 0, // Would need pricing data to calculate + estimatedCostSaved, byProvider, byStrategy, lastUpdated: new Date().toISOString(), From a315ab29bc465858d3cf5dd162d920481003b919 Mon Sep 17 00:00:00 2001 From: oyi77 Date: Tue, 31 Mar 2026 04:27:40 +0700 Subject: [PATCH 7/8] fix(debug/sidebar): make debug toggle control debug section visibility and fix sidebar hidden items tracking - Sync debugMode with showDebug in Sidebar (was using enableRequestLogs env var) - Only render debug-section sidebar toggles in AppearanceTab when debugMode=true - Sidebar filters debug-section items based on debugMode (was already correct) - Debug toggle now triggers omniroute:settings-updated event for instant sidebar update EOF --- .../dashboard/settings/components/AppearanceTab.tsx | 5 ++++- src/shared/components/Sidebar.tsx | 6 +++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/app/(dashboard)/dashboard/settings/components/AppearanceTab.tsx b/src/app/(dashboard)/dashboard/settings/components/AppearanceTab.tsx index 3c5d685530..bf173b8b4b 100644 --- a/src/app/(dashboard)/dashboard/settings/components/AppearanceTab.tsx +++ b/src/app/(dashboard)/dashboard/settings/components/AppearanceTab.tsx @@ -106,7 +106,10 @@ export default function AppearanceTab() { { id: "cyan", color: COLOR_THEMES.cyan, label: t("themeCyan") }, ]; - const sidebarSections = SIDEBAR_SECTIONS.map((section) => ({ + const showDebug = settings.debugMode === true; + const sidebarSections = SIDEBAR_SECTIONS.filter( + (section) => section.visibility !== "debug" || showDebug + ).map((section) => ({ ...section, title: getSidebarLabel(section.titleKey, section.titleFallback), items: section.items.map((item) => ({ ...item, label: tSidebar(item.i18nKey) })), diff --git a/src/shared/components/Sidebar.tsx b/src/shared/components/Sidebar.tsx index fb77a3017a..2eeb5a3b65 100644 --- a/src/shared/components/Sidebar.tsx +++ b/src/shared/components/Sidebar.tsx @@ -40,7 +40,7 @@ export default function Sidebar({ useEffect(() => { const applySettings = (data) => { - setShowDebug(data?.enableRequestLogs === true); + setShowDebug(data?.debugMode === true); setHiddenSidebarItems(normalizeHiddenSidebarItems(data?.[HIDDEN_SIDEBAR_ITEMS_SETTING_KEY])); }; @@ -52,8 +52,8 @@ export default function Sidebar({ const handleSettingsUpdated = (event: Event) => { const detail = (event as CustomEvent>).detail || {}; - if ("enableRequestLogs" in detail) { - setShowDebug(detail.enableRequestLogs === true); + if ("debugMode" in detail) { + setShowDebug(detail.debugMode === true); } if (HIDDEN_SIDEBAR_ITEMS_SETTING_KEY in detail) { From 67b9a3bc0eb0e564cb5fa43a751dd2f0ab3d5796 Mon Sep 17 00:00:00 2001 From: tombii Date: Tue, 31 Mar 2026 00:32:37 +0200 Subject: [PATCH 8/8] fix(ui): internationalize CacheStatsCard and add auto-refresh - Add 10s auto-refresh interval matching cache page pattern - Replace all hardcoded English strings with translation keys - Add 13 new i18n keys to cache namespace for metrics display - Reorganize header layout with auto-refresh indicator and reset button Addresses Gemini Code Assist feedback on PR #835: - Fixed internationalization (all text now uses t()) - Maintains distinct metrics (cumulative/resettable vs real-time rolling) Co-Authored-By: Claude Sonnet 4.6 --- .../settings/components/CacheStatsCard.tsx | 76 +++++++++++-------- src/i18n/messages/en.json | 13 ++++ 2 files changed, 58 insertions(+), 31 deletions(-) diff --git a/src/app/(dashboard)/dashboard/settings/components/CacheStatsCard.tsx b/src/app/(dashboard)/dashboard/settings/components/CacheStatsCard.tsx index 7876a67fdf..4798fbba2e 100644 --- a/src/app/(dashboard)/dashboard/settings/components/CacheStatsCard.tsx +++ b/src/app/(dashboard)/dashboard/settings/components/CacheStatsCard.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useEffect } from "react"; +import { useState, useEffect, useCallback } from "react"; import { Card } from "@/shared/components"; import { useTranslations } from "next-intl"; @@ -33,19 +33,26 @@ interface CacheMetrics { lastUpdated: string; } +const REFRESH_INTERVAL_MS = 10_000; +const REFRESH_INTERVAL_SECONDS = REFRESH_INTERVAL_MS / 1000; + export default function CacheStatsCard() { const [metrics, setMetrics] = useState(null); const [resetting, setResetting] = useState(false); - const t = useTranslations("settings"); + const t = useTranslations("cache"); - const fetchMetrics = () => { + const fetchMetrics = useCallback(() => { fetch("/api/settings/cache-metrics") .then((r) => r.json()) .then(setMetrics) .catch(() => {}); - }; + }, []); - useEffect(fetchMetrics, []); + useEffect(() => { + void fetchMetrics(); + const id = setInterval(() => void fetchMetrics(), REFRESH_INTERVAL_MS); + return () => clearInterval(id); + }, [fetchMetrics]); const handleReset = async () => { setResetting(true); @@ -73,15 +80,20 @@ export default function CacheStatsCard() { > insights -

Prompt Cache Metrics

+

{t("cacheMetrics")}

+
+
+ + {t("autoRefresh", { seconds: REFRESH_INTERVAL_SECONDS })} + +
-
{metrics ? ( @@ -89,11 +101,11 @@ export default function CacheStatsCard() { {/* Overview Stats */}
-

Total Requests

+

{t("totalRequests")}

{metrics.totalRequests}

-

With Cache Control

+

{t("withCacheControl")}

{metrics.requestsWithCacheControl}

@@ -103,19 +115,19 @@ export default function CacheStatsCard() { {/* Token Stats */}
-

Input Tokens

+

{t("inputTokens")}

{metrics.totalInputTokens.toLocaleString()}

-

Cached Tokens (Read)

+

{t("cachedTokensRead")}

{metrics.totalCachedTokens.toLocaleString()}

-

Cache Creation (Write)

+

{t("cacheCreationWrite")}

{metrics.totalCacheCreationTokens.toLocaleString()}

@@ -126,8 +138,8 @@ export default function CacheStatsCard() {
-

Cache Reuse Ratio

-

Cached tokens / Total input tokens

+

{t("cacheReuseRatio")}

+

{t("cacheReuseRatioDesc")}

{cacheHitRate.toFixed(1)}%

@@ -143,13 +155,13 @@ export default function CacheStatsCard() { {/* Savings */}
-

Tokens Saved

+

{t("tokensSaved")}

{metrics.tokensSaved.toLocaleString()}

-

Est. Cost Saved

+

{t("estCostSaved")}

${metrics.estimatedCostSaved.toFixed(4)}

@@ -159,7 +171,7 @@ export default function CacheStatsCard() { {/* By Provider */} {Object.keys(metrics.byProvider).length > 0 && (
-

By Provider

+

{t("byProvider")}

{Object.entries(metrics.byProvider).map(([provider, stats]) => { const providerCacheRate = @@ -171,17 +183,19 @@ export default function CacheStatsCard() { >
{provider} - {stats.requests} reqs + + {stats.requests} {t("requestsShort")} +
- - In: {stats.inputTokens.toLocaleString()} + + {t("inputShort")}: {stats.inputTokens.toLocaleString()} - - Cached: {stats.cachedTokens.toLocaleString()} + + {t("cachedShort")}: {stats.cachedTokens.toLocaleString()} - - Write: {stats.cacheCreationTokens.toLocaleString()} + + {t("writeShort")}: {stats.cacheCreationTokens.toLocaleString()} {providerCacheRate.toFixed(0)}% @@ -195,7 +209,7 @@ export default function CacheStatsCard() { )}
) : ( -

Loading cache metrics...

+

{t("loading")}

)}
diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index 43463d7cba..0a1ce25ac6 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -2937,6 +2937,19 @@ "cacheHitRate": "Cache Hit Rate", "cachedTokens": "Cached Tokens", "cacheCreationTokens": "Cache Creation Tokens", + "cacheMetrics": "Prompt Cache Metrics", + "withCacheControl": "With Cache Control", + "cachedTokensRead": "Cached Tokens (Read)", + "cacheCreationWrite": "Cache Creation (Write)", + "cacheReuseRatio": "Cache Reuse Ratio", + "cacheReuseRatioDesc": "Cached tokens / Total input tokens", + "estCostSaved": "Est. Cost Saved", + "requestsShort": "reqs", + "inputShort": "In", + "cachedShort": "Cached", + "writeShort": "Write", + "resetting": "Resetting...", + "resetMetrics": "Reset Metrics", "byProvider": "Breakdown by Provider", "provider": "Provider", "requests": "Requests",