diff --git a/open-sse/handlers/chatCore.ts b/open-sse/handlers/chatCore.ts index 5c3312ab87..eea24efe4f 100644 --- a/open-sse/handlers/chatCore.ts +++ b/open-sse/handlers/chatCore.ts @@ -697,6 +697,7 @@ export async function handleChatCore({ isCombo, comboStrategy, targetProvider: provider, + targetFormat, settings: { alwaysPreserveClientCache: cacheControlMode }, }); diff --git a/open-sse/utils/cacheControlPolicy.ts b/open-sse/utils/cacheControlPolicy.ts index 3f18e2cf6d..0810674bb1 100644 --- a/open-sse/utils/cacheControlPolicy.ts +++ b/open-sse/utils/cacheControlPolicy.ts @@ -90,10 +90,19 @@ export function isClaudeCodeClient(userAgent: string | null | undefined): boolea /** * Check if a provider supports prompt caching + * Supports caching if: + * 1. Provider is in the known caching providers list, OR + * 2. Provider uses Claude protocol (detected via targetFormat) */ -export function providerSupportsCaching(provider: string | null | undefined): boolean { +export function providerSupportsCaching( + provider: string | null | undefined, + targetFormat?: string | null +): boolean { if (!provider) return false; - return CACHING_PROVIDERS.has(provider.toLowerCase()); + if (CACHING_PROVIDERS.has(provider.toLowerCase())) return true; + // All Claude-protocol providers support prompt caching + if (targetFormat === "claude") return true; + return false; } /** @@ -121,12 +130,14 @@ export function shouldPreserveCacheControl({ isCombo, comboStrategy, targetProvider, + targetFormat, settings, }: { userAgent: string | null | undefined; isCombo: boolean; comboStrategy?: RoutingStrategyValue | null; targetProvider: string | null | undefined; + targetFormat?: string | null; settings?: CacheControlSettings; }): boolean { // User override takes precedence @@ -144,7 +155,7 @@ export function shouldPreserveCacheControl({ } // Target provider must support caching - if (!providerSupportsCaching(targetProvider)) { + if (!providerSupportsCaching(targetProvider, targetFormat)) { return false; } diff --git a/src/app/(dashboard)/dashboard/settings/components/CacheStatsCard.tsx b/src/app/(dashboard)/dashboard/settings/components/CacheStatsCard.tsx deleted file mode 100644 index 4798fbba2e..0000000000 --- a/src/app/(dashboard)/dashboard/settings/components/CacheStatsCard.tsx +++ /dev/null @@ -1,217 +0,0 @@ -"use client"; - -import { useState, useEffect, useCallback } from "react"; -import { Card } from "@/shared/components"; -import { useTranslations } from "next-intl"; - -interface CacheMetrics { - totalRequests: number; - requestsWithCacheControl: number; - totalInputTokens: number; - totalCachedTokens: number; - totalCacheCreationTokens: number; - tokensSaved: number; - estimatedCostSaved: number; - byProvider: Record< - string, - { - requests: number; - inputTokens: number; - cachedTokens: number; - cacheCreationTokens: number; - } - >; - byStrategy: Record< - string, - { - requests: number; - inputTokens: number; - cachedTokens: number; - cacheCreationTokens: number; - } - >; - 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("cache"); - - const fetchMetrics = useCallback(() => { - fetch("/api/settings/cache-metrics") - .then((r) => r.json()) - .then(setMetrics) - .catch(() => {}); - }, []); - - useEffect(() => { - void fetchMetrics(); - const id = setInterval(() => void fetchMetrics(), REFRESH_INTERVAL_MS); - return () => clearInterval(id); - }, [fetchMetrics]); - - const handleReset = async () => { - setResetting(true); - try { - await fetch("/api/settings/cache-metrics", { method: "DELETE" }); - fetchMetrics(); - } finally { - setResetting(false); - } - }; - - const cacheHitRate = - metrics && metrics.totalInputTokens > 0 - ? (metrics.totalCachedTokens / metrics.totalInputTokens) * 100 - : 0; - - return ( - -
-
-
- -

{t("cacheMetrics")}

-
-
- - {t("autoRefresh", { seconds: REFRESH_INTERVAL_SECONDS })} - - -
-
- - {metrics ? ( -
- {/* Overview Stats */} -
-
-

{t("totalRequests")}

-

{metrics.totalRequests}

-
-
-

{t("withCacheControl")}

-

- {metrics.requestsWithCacheControl} -

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

{t("inputTokens")}

-

- {metrics.totalInputTokens.toLocaleString()} -

-
-
-

{t("cachedTokensRead")}

-

- {metrics.totalCachedTokens.toLocaleString()} -

-
-
-

{t("cacheCreationWrite")}

-

- {metrics.totalCacheCreationTokens.toLocaleString()} -

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

{t("cacheReuseRatio")}

-

{t("cacheReuseRatioDesc")}

-
-

{cacheHitRate.toFixed(1)}%

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

{t("tokensSaved")}

-

- {metrics.tokensSaved.toLocaleString()} -

-
-
-

{t("estCostSaved")}

-

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

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

{t("byProvider")}

-
- {Object.entries(metrics.byProvider).map(([provider, stats]) => { - const providerCacheRate = - stats.inputTokens > 0 ? (stats.cachedTokens / stats.inputTokens) * 100 : 0; - return ( -
-
- {provider} - - {stats.requests} {t("requestsShort")} - -
-
- - {t("inputShort")}: {stats.inputTokens.toLocaleString()} - - - {t("cachedShort")}: {stats.cachedTokens.toLocaleString()} - - - {t("writeShort")}: {stats.cacheCreationTokens.toLocaleString()} - - - {providerCacheRate.toFixed(0)}% - -
-
- ); - })} -
-
- )} -
- ) : ( -

{t("loading")}

- )} -
- - ); -} diff --git a/tests/unit/cache-control-claude-providers.test.mjs b/tests/unit/cache-control-claude-providers.test.mjs new file mode 100644 index 0000000000..ae437cdf89 --- /dev/null +++ b/tests/unit/cache-control-claude-providers.test.mjs @@ -0,0 +1,141 @@ +import { describe, test } from "node:test"; +import assert from "node:assert/strict"; +import { + providerSupportsCaching, + shouldPreserveCacheControl, +} from "../../open-sse/utils/cacheControlPolicy.ts"; + +describe("Cache Control Policy - Claude Protocol Providers", () => { + test("providerSupportsCaching returns true for Claude-format providers", () => { + // Known caching providers + assert.equal(providerSupportsCaching("claude", "claude"), true); + assert.equal(providerSupportsCaching("anthropic", "claude"), true); + assert.equal(providerSupportsCaching("zai", "claude"), true); + assert.equal(providerSupportsCaching("qwen", "openai"), true); + assert.equal(providerSupportsCaching("deepseek", "openai"), true); + + // Claude-protocol providers NOT in CACHING_PROVIDERS set + // These should be detected via targetFormat + assert.equal(providerSupportsCaching("bailian-coding-plan", "claude"), true); + assert.equal(providerSupportsCaching("glm", "claude"), true); + assert.equal(providerSupportsCaching("minimax", "claude"), true); + assert.equal(providerSupportsCaching("minimax-cn", "claude"), true); + assert.equal(providerSupportsCaching("kimi-coding", "claude"), true); + assert.equal(providerSupportsCaching("alicode", "claude"), true); + + // Non-Claude providers without caching support + assert.equal(providerSupportsCaching("openai", "openai"), false); + assert.equal(providerSupportsCaching("gemini", "gemini"), false); + }); + + test("shouldPreserveCacheControl preserves for Claude-format providers with Claude Code client", () => { + const claudeCodeUA = "Claude-Code/1.0.0"; + + // Claude-protocol providers should preserve cache_control + assert.equal( + shouldPreserveCacheControl({ + userAgent: claudeCodeUA, + isCombo: false, + targetProvider: "bailian-coding-plan", + targetFormat: "claude", + settings: { alwaysPreserveClientCache: "auto" }, + }), + true + ); + + assert.equal( + shouldPreserveCacheControl({ + userAgent: claudeCodeUA, + isCombo: false, + targetProvider: "glm", + targetFormat: "claude", + settings: { alwaysPreserveClientCache: "auto" }, + }), + true + ); + + assert.equal( + shouldPreserveCacheControl({ + userAgent: claudeCodeUA, + isCombo: false, + targetProvider: "zai", + targetFormat: "claude", + settings: { alwaysPreserveClientCache: "auto" }, + }), + true + ); + + assert.equal( + shouldPreserveCacheControl({ + userAgent: claudeCodeUA, + isCombo: false, + targetProvider: "minimax", + targetFormat: "claude", + settings: { alwaysPreserveClientCache: "auto" }, + }), + true + ); + }); + + test("shouldPreserveCacheControl respects user override 'always'", () => { + const regularUA = "Mozilla/5.0"; + + // Even with non-Claude Code client, 'always' should preserve + assert.equal( + shouldPreserveCacheControl({ + userAgent: regularUA, + isCombo: false, + targetProvider: "bailian-coding-plan", + targetFormat: "claude", + settings: { alwaysPreserveClientCache: "always" }, + }), + true + ); + }); + + test("shouldPreserveCacheControl respects user override 'never'", () => { + const claudeCodeUA = "Claude-Code/1.0.0"; + + // Even with Claude Code client, 'never' should not preserve + assert.equal( + shouldPreserveCacheControl({ + userAgent: claudeCodeUA, + isCombo: false, + targetProvider: "bailian-coding-plan", + targetFormat: "claude", + settings: { alwaysPreserveClientCache: "never" }, + }), + false + ); + }); + + test("shouldPreserveCacheControl does not preserve for non-Claude Code clients in auto mode", () => { + const regularUA = "Mozilla/5.0"; + + assert.equal( + shouldPreserveCacheControl({ + userAgent: regularUA, + isCombo: false, + targetProvider: "bailian-coding-plan", + targetFormat: "claude", + settings: { alwaysPreserveClientCache: "auto" }, + }), + false + ); + }); + + test("shouldPreserveCacheControl does not preserve for non-Claude format providers", () => { + const claudeCodeUA = "Claude-Code/1.0.0"; + + assert.equal( + shouldPreserveCacheControl({ + userAgent: claudeCodeUA, + isCombo: false, + targetProvider: "openai", + targetFormat: "openai", + settings: { alwaysPreserveClientCache: "auto" }, + }), + false + ); + }); +});