diff --git a/drizzle/0044_add_anthropic_warmup_intercept.sql b/drizzle/0044_add_anthropic_warmup_intercept.sql new file mode 100644 index 000000000..e87fb0f78 --- /dev/null +++ b/drizzle/0044_add_anthropic_warmup_intercept.sql @@ -0,0 +1,3 @@ +ALTER TABLE "system_settings" +ADD COLUMN IF NOT EXISTS "enable_anthropic_warmup_intercept" boolean NOT NULL DEFAULT false; + diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index cc660ce02..0f81ea51d 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -309,6 +309,13 @@ "when": 1767362174718, "tag": "0043_faithful_mother_askani", "breakpoints": true + }, + { + "idx": 44, + "version": "7", + "when": 1767461538553, + "tag": "0044_add_anthropic_warmup_intercept", + "breakpoints": true } ] -} \ No newline at end of file +} diff --git a/messages/en/dashboard.json b/messages/en/dashboard.json index 2e64ac5e3..c395fb28b 100644 --- a/messages/en/dashboard.json +++ b/messages/en/dashboard.json @@ -127,6 +127,7 @@ "nextPage": "Next Page", "blocked": "Blocked", "nonBilling": "Non-Billing", + "skipped": "Skipped", "times": "times", "loadedCount": "Loaded {count} records", "loadingMore": "Loading more...", @@ -156,6 +157,7 @@ "title": "Blocking Information", "type": "Blocking Type", "sensitiveWord": "Sensitive Word Blocking", + "warmupIntercept": "Warmup Intercept (CCH)", "word": "Sensitive Word", "matchType": "Match Type", "matchTypeContains": "Contains Match", diff --git a/messages/en/settings.json b/messages/en/settings.json index 6654ba5d1..4030ad8c8 100644 --- a/messages/en/settings.json +++ b/messages/en/settings.json @@ -102,6 +102,8 @@ "verboseProviderErrorDesc": "When enabled, return detailed error messages when all providers are unavailable (including provider count, rate limit reasons, etc.); when disabled, only return a simple error code.", "enableHttp2": "Enable HTTP/2", "enableHttp2Desc": "When enabled, proxy requests will prefer HTTP/2 protocol. Automatically falls back to HTTP/1.1 on failure.", + "enableAnthropicWarmupIntercept": "Intercept Anthropic Warmup Requests", + "enableAnthropicWarmupInterceptDesc": "When enabled, CCH detects and short-circuits Claude/Anthropic warmup requests and returns a local short response to avoid unnecessary upstream calls.", "cleanupSchedule": "Cleanup Schedule", "cleanupScheduleDesc": "Select the execution schedule for automatic cleanup", "configUpdated": "System settings updated. The page will refresh to apply currency display changes.", diff --git a/messages/ja/dashboard.json b/messages/ja/dashboard.json index b1ca5e90d..397dd800a 100644 --- a/messages/ja/dashboard.json +++ b/messages/ja/dashboard.json @@ -127,6 +127,7 @@ "nextPage": "次へ", "blocked": "ブロック済み", "nonBilling": "非課金", + "skipped": "スキップ", "times": "回", "loadedCount": "{count} 件のレコードを読み込みました", "loadingMore": "読み込み中...", @@ -156,6 +157,7 @@ "title": "ブロック情報", "type": "ブロックタイプ", "sensitiveWord": "機密語句ブロック", + "warmupIntercept": "Warmup 先行応答(CCH)", "word": "機密語句", "matchType": "一致タイプ", "matchTypeContains": "部分一致", diff --git a/messages/ja/settings.json b/messages/ja/settings.json index 23d7d5107..1d915c4f5 100644 --- a/messages/ja/settings.json +++ b/messages/ja/settings.json @@ -100,6 +100,8 @@ "allowGlobalViewDesc": "無効にすると、一般ユーザーはダッシュボードで自分のキーの使用統計のみを表示できます。", "verboseProviderError": "詳細なプロバイダーエラー", "verboseProviderErrorDesc": "有効にすると、すべてのプロバイダーが利用不可の場合に詳細なエラーメッセージ(プロバイダー数、レート制限の理由など)を返します。無効の場合は簡潔なエラーコードのみを返します。", + "enableAnthropicWarmupIntercept": "Anthropic Warmup リクエストをインターセプト", + "enableAnthropicWarmupInterceptDesc": "有効にすると、CCH は Claude/Anthropic の Warmup リクエストを検出して上流への送信をスキップし、ローカルの短い応答を返します。", "cleanupSchedule": "クリーンアップスケジュール", "cleanupScheduleDesc": "自動クリーンアップの実行スケジュールを選択します", "configUpdated": "システム設定が更新されました。ページが更新され、通貨表示の変更が適用されます。", diff --git a/messages/ru/dashboard.json b/messages/ru/dashboard.json index f6f0b4737..87f8f7e6a 100644 --- a/messages/ru/dashboard.json +++ b/messages/ru/dashboard.json @@ -127,6 +127,7 @@ "nextPage": "Следующая", "blocked": "Заблокировано", "nonBilling": "Не тарифицируется", + "skipped": "Пропущено", "times": "раз", "loadedCount": "Загружено {count} записей", "loadingMore": "Загрузка...", @@ -156,6 +157,7 @@ "title": "Информация о блокировке", "type": "Тип блокировки", "sensitiveWord": "Блокировка по ключевым словам", + "warmupIntercept": "Warmup перехвачен (CCH)", "word": "Ключевое слово", "matchType": "Тип совпадения", "matchTypeContains": "Содержит", diff --git a/messages/ru/settings.json b/messages/ru/settings.json index dd248698d..70c819333 100644 --- a/messages/ru/settings.json +++ b/messages/ru/settings.json @@ -100,6 +100,8 @@ "allowGlobalViewDesc": "При отключении обычные пользователи могут видеть только статистику использования своих ключей на панели.", "verboseProviderError": "Подробные ошибки провайдеров", "verboseProviderErrorDesc": "При включении возвращает подробные сообщения об ошибках при недоступности всех провайдеров (количество провайдеров, причины ограничений и т.д.); при отключении возвращает только простой код ошибки.", + "enableAnthropicWarmupIntercept": "Перехватывать Anthropic Warmup-запросы", + "enableAnthropicWarmupInterceptDesc": "При включении CCH обнаруживает Warmup-запросы Claude/Anthropic, пропускает отправку вверх по цепочке и возвращает локальный короткий ответ.", "cleanupSchedule": "График очистки", "cleanupScheduleDesc": "Выбрать расписание автоматической очистки", "configUpdated": "Параметры системы обновлены. Страница обновится для применения изменений валюты.", diff --git a/messages/zh-CN/dashboard.json b/messages/zh-CN/dashboard.json index ff0730912..1a31c9a1b 100644 --- a/messages/zh-CN/dashboard.json +++ b/messages/zh-CN/dashboard.json @@ -127,6 +127,7 @@ "nextPage": "下一页", "blocked": "被拦截", "nonBilling": "非计费", + "skipped": "已跳过", "times": "次", "loadedCount": "已加载 {count} 条记录", "loadingMore": "加载更多中...", @@ -156,6 +157,7 @@ "title": "拦截信息", "type": "拦截类型", "sensitiveWord": "敏感词拦截", + "warmupIntercept": "Warmup 抢答(CCH)", "word": "敏感词", "matchType": "匹配类型", "matchTypeContains": "包含匹配", diff --git a/messages/zh-CN/settings.json b/messages/zh-CN/settings.json index 9f2521247..2750ba22f 100644 --- a/messages/zh-CN/settings.json +++ b/messages/zh-CN/settings.json @@ -83,6 +83,8 @@ "verboseProviderErrorDesc": "开启后,当所有供应商不可用时返回详细错误信息(包含供应商数量、限流原因等);关闭后仅返回简洁错误码。", "enableHttp2": "启用 HTTP/2", "enableHttp2Desc": "启用后,代理请求将优先使用 HTTP/2 协议。如果 HTTP/2 失败,将自动降级到 HTTP/1.1。", + "enableAnthropicWarmupIntercept": "拦截 Anthropic Warmup 请求", + "enableAnthropicWarmupInterceptDesc": "开启后,CCH 将识别并拦截 Claude/Anthropic 的 Warmup 请求,直接返回简短响应,减少不必要的供应商调用。", "saveSettings": "保存设置", "keepDays": "保留天数", "keepDaysDesc": "清理超过此天数的历史日志", diff --git a/messages/zh-TW/dashboard.json b/messages/zh-TW/dashboard.json index 1e706ff67..d33eb3e75 100644 --- a/messages/zh-TW/dashboard.json +++ b/messages/zh-TW/dashboard.json @@ -127,6 +127,7 @@ "nextPage": "下一頁", "blocked": "已攔截", "nonBilling": "非計費", + "skipped": "已跳過", "times": "次", "loadedCount": "已載入 {count} 筆記錄", "loadingMore": "載入更多中...", @@ -156,6 +157,7 @@ "title": "攔截資訊", "type": "攔截類型", "sensitiveWord": "敏感詞攔截", + "warmupIntercept": "Warmup 搶答(CCH)", "word": "敏感詞", "matchType": "匹配類型", "matchTypeContains": "包含匹配", diff --git a/messages/zh-TW/settings.json b/messages/zh-TW/settings.json index 258f023a9..33fbe5781 100644 --- a/messages/zh-TW/settings.json +++ b/messages/zh-TW/settings.json @@ -100,6 +100,8 @@ "allowGlobalViewDesc": "關閉後,普通使用者在儀表板僅能查看自己金鑰的使用統計。", "verboseProviderError": "詳細供應商錯誤資訊", "verboseProviderErrorDesc": "開啟後,當所有供應商不可用時返回詳細錯誤資訊(包含供應商數量、限流原因等);關閉後僅返回簡潔錯誤碼。", + "enableAnthropicWarmupIntercept": "攔截 Anthropic Warmup 請求", + "enableAnthropicWarmupInterceptDesc": "開啟後,CCH 會識別並攔截 Claude/Anthropic 的 Warmup 請求,直接回傳簡短回應,減少不必要的供應商呼叫。", "cleanupSchedule": "清理週期", "cleanupScheduleDesc": "選擇自動清理的執行週期", "configUpdated": "系統設定已更新,頁面將重新整理以應用貨幣顯示變更。", diff --git a/src/actions/my-usage.ts b/src/actions/my-usage.ts index 401aa7f0d..10b242f93 100644 --- a/src/actions/my-usage.ts +++ b/src/actions/my-usage.ts @@ -303,6 +303,7 @@ export async function getMyTodayStats(): Promise> { and( eq(messageRequest.key, session.key.key), isNull(messageRequest.deletedAt), + sql`${messageRequest.blockedBy} IS DISTINCT FROM 'warmup'`, gte(messageRequest.createdAt, timeRange.startTime), sql`${messageRequest.createdAt} < ${timeRange.endTime}` ) @@ -322,6 +323,7 @@ export async function getMyTodayStats(): Promise> { and( eq(messageRequest.key, session.key.key), isNull(messageRequest.deletedAt), + sql`${messageRequest.blockedBy} IS DISTINCT FROM 'warmup'`, gte(messageRequest.createdAt, timeRange.startTime), sql`${messageRequest.createdAt} < ${timeRange.endTime}` ) diff --git a/src/actions/system-config.ts b/src/actions/system-config.ts index efa265586..f81ee833c 100644 --- a/src/actions/system-config.ts +++ b/src/actions/system-config.ts @@ -37,6 +37,7 @@ export async function saveSystemSettings(formData: { enableClientVersionCheck?: boolean; verboseProviderError?: boolean; enableHttp2?: boolean; + enableAnthropicWarmupIntercept?: boolean; }): Promise> { try { const session = await getSession(); @@ -57,6 +58,7 @@ export async function saveSystemSettings(formData: { enableClientVersionCheck: validated.enableClientVersionCheck, verboseProviderError: validated.verboseProviderError, enableHttp2: validated.enableHttp2, + enableAnthropicWarmupIntercept: validated.enableAnthropicWarmupIntercept, }); // Invalidate the system settings cache so proxy requests get fresh settings diff --git a/src/app/[locale]/dashboard/logs/_components/error-details-dialog.tsx b/src/app/[locale]/dashboard/logs/_components/error-details-dialog.tsx index b365abe1c..258ec70b3 100644 --- a/src/app/[locale]/dashboard/logs/_components/error-details-dialog.tsx +++ b/src/app/[locale]/dashboard/logs/_components/error-details-dialog.tsx @@ -26,6 +26,7 @@ import { import { Link } from "@/i18n/routing"; import { cn, formatTokenAmount } from "@/lib/utils"; import { formatCurrency } from "@/lib/utils/currency"; +import { WARMUP_INTERCEPT_BLOCKED_BY } from "@/lib/utils/performance-formatter"; import { formatProviderTimeline } from "@/lib/utils/provider-chain-formatter"; import type { ProviderChainItem } from "@/types/message"; import type { BillingModelSource } from "@/types/system-config"; @@ -272,7 +273,9 @@ export function ErrorDetailsDialog({ {blockedBy === "sensitive_word" ? t("logs.details.blocked.sensitiveWord") - : blockedBy} + : blockedBy === WARMUP_INTERCEPT_BLOCKED_BY + ? t("logs.details.blocked.warmupIntercept") + : blockedBy} {parsedBlockedReason && ( diff --git a/src/app/[locale]/dashboard/logs/_components/usage-logs-table.tsx b/src/app/[locale]/dashboard/logs/_components/usage-logs-table.tsx index 559adb6dc..9b4ed532e 100644 --- a/src/app/[locale]/dashboard/logs/_components/usage-logs-table.tsx +++ b/src/app/[locale]/dashboard/logs/_components/usage-logs-table.tsx @@ -21,6 +21,7 @@ import { calculateOutputRate, formatDuration, NON_BILLING_ENDPOINT, + WARMUP_INTERCEPT_BLOCKED_BY, } from "@/lib/utils/performance-formatter"; import { formatProviderSummary } from "@/lib/utils/provider-chain-formatter"; import type { UsageLogRow } from "@/repository/usage-logs"; @@ -89,7 +90,8 @@ export function UsageLogsTable({ ) : ( logs.map((log) => { - const isNonBilling = log.endpoint === NON_BILLING_ENDPOINT; + const isWarmupIntercept = log.blockedBy === WARMUP_INTERCEPT_BLOCKED_BY; + const isNonBilling = log.endpoint === NON_BILLING_ENDPOINT || isWarmupIntercept; return ( - {isNonBilling ? ( + {isWarmupIntercept ? ( + + {t("logs.table.skipped")} + + ) : isNonBilling ? ( "-" ) : log.costUsd ? ( diff --git a/src/app/[locale]/dashboard/logs/_components/virtualized-logs-table.tsx b/src/app/[locale]/dashboard/logs/_components/virtualized-logs-table.tsx index 0f27deffe..0e92daed8 100644 --- a/src/app/[locale]/dashboard/logs/_components/virtualized-logs-table.tsx +++ b/src/app/[locale]/dashboard/logs/_components/virtualized-logs-table.tsx @@ -17,6 +17,7 @@ import { calculateOutputRate, formatDuration, NON_BILLING_ENDPOINT, + WARMUP_INTERCEPT_BLOCKED_BY, } from "@/lib/utils/performance-formatter"; import { formatProviderSummary } from "@/lib/utils/provider-chain-formatter"; import type { BillingModelSource } from "@/types/system-config"; @@ -250,7 +251,8 @@ export function VirtualizedLogsTable({ ); } - const isNonBilling = log.endpoint === NON_BILLING_ENDPOINT; + const isWarmupIntercept = log.blockedBy === WARMUP_INTERCEPT_BLOCKED_BY; + const isNonBilling = log.endpoint === NON_BILLING_ENDPOINT || isWarmupIntercept; return (
- {isNonBilling ? ( + {isWarmupIntercept ? ( + + {t("logs.table.skipped")} + + ) : isNonBilling ? ( "-" ) : log.costUsd ? ( diff --git a/src/app/[locale]/settings/config/_components/system-settings-form.tsx b/src/app/[locale]/settings/config/_components/system-settings-form.tsx index aaf2ae203..af1fc97e6 100644 --- a/src/app/[locale]/settings/config/_components/system-settings-form.tsx +++ b/src/app/[locale]/settings/config/_components/system-settings-form.tsx @@ -29,6 +29,7 @@ interface SystemSettingsFormProps { | "billingModelSource" | "verboseProviderError" | "enableHttp2" + | "enableAnthropicWarmupIntercept" >; } @@ -50,6 +51,9 @@ export function SystemSettingsForm({ initialSettings }: SystemSettingsFormProps) initialSettings.verboseProviderError ); const [enableHttp2, setEnableHttp2] = useState(initialSettings.enableHttp2); + const [enableAnthropicWarmupIntercept, setEnableAnthropicWarmupIntercept] = useState( + initialSettings.enableAnthropicWarmupIntercept + ); const [isPending, startTransition] = useTransition(); const handleSubmit = (event: React.FormEvent) => { @@ -68,6 +72,7 @@ export function SystemSettingsForm({ initialSettings }: SystemSettingsFormProps) billingModelSource, verboseProviderError, enableHttp2, + enableAnthropicWarmupIntercept, }); if (!result.ok) { @@ -82,6 +87,7 @@ export function SystemSettingsForm({ initialSettings }: SystemSettingsFormProps) setBillingModelSource(result.data.billingModelSource); setVerboseProviderError(result.data.verboseProviderError); setEnableHttp2(result.data.enableHttp2); + setEnableAnthropicWarmupIntercept(result.data.enableAnthropicWarmupIntercept); } toast.success(t("configUpdated")); @@ -192,6 +198,23 @@ export function SystemSettingsForm({ initialSettings }: SystemSettingsFormProps) />
+
+
+ +

+ {t("enableAnthropicWarmupInterceptDesc")} +

+
+ setEnableAnthropicWarmupIntercept(checked)} + disabled={isPending} + /> +
+