diff --git a/messages/en/dashboard.json b/messages/en/dashboard.json
index 0590ae221..5e4a8a88f 100644
--- a/messages/en/dashboard.json
+++ b/messages/en/dashboard.json
@@ -158,6 +158,7 @@
"nonBilling": "Non-Billing",
"skipped": "Skipped",
"specialSettings": "Special",
+ "anthropicEffort": "Effort: {effort}",
"times": "times",
"loadedCount": "Loaded {count} records",
"loadingMore": "Loading more...",
diff --git a/messages/en/myUsage.json b/messages/en/myUsage.json
index 0e39e8496..851da6c1d 100644
--- a/messages/en/myUsage.json
+++ b/messages/en/myUsage.json
@@ -58,7 +58,8 @@
"next": "Next",
"noLogs": "No logs",
"unknownModel": "Unknown model",
- "billingModel": "Billing: {model}"
+ "billingModel": "Billing: {model}",
+ "anthropicEffort": "Effort: {effort}"
},
"expiration": {
"title": "Expiration",
diff --git a/messages/ja/dashboard.json b/messages/ja/dashboard.json
index 4e3b946cd..697746a6e 100644
--- a/messages/ja/dashboard.json
+++ b/messages/ja/dashboard.json
@@ -158,6 +158,7 @@
"nonBilling": "非課金",
"skipped": "スキップ",
"specialSettings": "特殊設定",
+ "anthropicEffort": "Effort: {effort}",
"times": "回",
"loadedCount": "{count} 件のレコードを読み込みました",
"loadingMore": "読み込み中...",
diff --git a/messages/ja/myUsage.json b/messages/ja/myUsage.json
index 901e10ab6..80761d0a8 100644
--- a/messages/ja/myUsage.json
+++ b/messages/ja/myUsage.json
@@ -58,7 +58,8 @@
"next": "次へ",
"noLogs": "ログがありません",
"unknownModel": "不明なモデル",
- "billingModel": "課金: {model}"
+ "billingModel": "課金: {model}",
+ "anthropicEffort": "Effort: {effort}"
},
"expiration": {
"title": "有効期限",
diff --git a/messages/ru/dashboard.json b/messages/ru/dashboard.json
index 1af0a96f2..9afd185e7 100644
--- a/messages/ru/dashboard.json
+++ b/messages/ru/dashboard.json
@@ -158,6 +158,7 @@
"nonBilling": "Не тарифицируется",
"skipped": "Пропущено",
"specialSettings": "Особые",
+ "anthropicEffort": "Effort: {effort}",
"times": "раз",
"loadedCount": "Загружено {count} записей",
"loadingMore": "Загрузка...",
diff --git a/messages/ru/myUsage.json b/messages/ru/myUsage.json
index 5ccfec871..c85dd01ea 100644
--- a/messages/ru/myUsage.json
+++ b/messages/ru/myUsage.json
@@ -58,7 +58,8 @@
"next": "Вперед",
"noLogs": "Нет записей",
"unknownModel": "Неизвестная модель",
- "billingModel": "Биллинг: {model}"
+ "billingModel": "Биллинг: {model}",
+ "anthropicEffort": "Effort: {effort}"
},
"expiration": {
"title": "Срок действия",
diff --git a/messages/zh-CN/dashboard.json b/messages/zh-CN/dashboard.json
index b4cea4419..3e366dad4 100644
--- a/messages/zh-CN/dashboard.json
+++ b/messages/zh-CN/dashboard.json
@@ -158,6 +158,7 @@
"nonBilling": "非计费",
"skipped": "已跳过",
"specialSettings": "特殊设置",
+ "anthropicEffort": "Effort: {effort}",
"times": "次",
"loadedCount": "已加载 {count} 条记录",
"loadingMore": "加载更多中...",
diff --git a/messages/zh-CN/myUsage.json b/messages/zh-CN/myUsage.json
index 6cf939337..5b230d9df 100644
--- a/messages/zh-CN/myUsage.json
+++ b/messages/zh-CN/myUsage.json
@@ -58,7 +58,8 @@
"next": "下一页",
"noLogs": "暂无日志",
"unknownModel": "未知模型",
- "billingModel": "计费:{model}"
+ "billingModel": "计费:{model}",
+ "anthropicEffort": "Effort: {effort}"
},
"expiration": {
"title": "过期时间",
diff --git a/messages/zh-TW/dashboard.json b/messages/zh-TW/dashboard.json
index cecc8daa7..0f81ba706 100644
--- a/messages/zh-TW/dashboard.json
+++ b/messages/zh-TW/dashboard.json
@@ -158,6 +158,7 @@
"nonBilling": "非計費",
"skipped": "已跳過",
"specialSettings": "特殊設定",
+ "anthropicEffort": "Effort: {effort}",
"times": "次數",
"loadedCount": "已載入 {count} 筆記錄",
"loadingMore": "載入更多中...",
diff --git a/messages/zh-TW/myUsage.json b/messages/zh-TW/myUsage.json
index f803a617b..41be1b6e8 100644
--- a/messages/zh-TW/myUsage.json
+++ b/messages/zh-TW/myUsage.json
@@ -58,7 +58,8 @@
"next": "下一頁",
"noLogs": "暫無日誌",
"unknownModel": "未知的模型",
- "billingModel": "計費:{model}"
+ "billingModel": "計費:{model}",
+ "anthropicEffort": "Effort: {effort}"
},
"expiration": {
"title": "到期時間",
diff --git a/src/actions/my-usage.ts b/src/actions/my-usage.ts
index 9b5f7e439..ed3ba8340 100644
--- a/src/actions/my-usage.ts
+++ b/src/actions/my-usage.ts
@@ -152,6 +152,7 @@ export interface MyUsageLogEntry {
createdAt: Date | null;
model: string | null;
billingModel: string | null;
+ anthropicEffort?: string | null;
modelRedirect: string | null;
inputTokens: number;
outputTokens: number;
@@ -506,6 +507,7 @@ export async function getMyUsageLogs(
createdAt: log.createdAt,
model: log.model,
billingModel,
+ anthropicEffort: log.anthropicEffort ?? null,
modelRedirect,
inputTokens: log.inputTokens ?? 0,
outputTokens: log.outputTokens ?? 0,
diff --git a/src/app/[locale]/dashboard/logs/_components/model-display-with-redirect.tsx b/src/app/[locale]/dashboard/logs/_components/model-display-with-redirect.tsx
index c59553a11..6700aaddc 100644
--- a/src/app/[locale]/dashboard/logs/_components/model-display-with-redirect.tsx
+++ b/src/app/[locale]/dashboard/logs/_components/model-display-with-redirect.tsx
@@ -2,8 +2,9 @@
import { ArrowRight } from "lucide-react";
import { useTranslations } from "next-intl";
-import { useCallback } from "react";
+import { type MouseEvent, useCallback } from "react";
import { toast } from "sonner";
+import { AnthropicEffortBadge } from "@/components/customs/anthropic-effort-badge";
import { ModelVendorIcon } from "@/components/customs/model-vendor-icon";
import { Badge } from "@/components/ui/badge";
import { copyTextToClipboard } from "@/lib/utils/clipboard";
@@ -13,6 +14,7 @@ interface ModelDisplayWithRedirectProps {
originalModel: string | null;
currentModel: string | null;
billingModelSource: BillingModelSource;
+ anthropicEffort?: string | null;
onRedirectClick?: () => void;
}
@@ -20,10 +22,11 @@ export function ModelDisplayWithRedirect({
originalModel,
currentModel,
billingModelSource,
+ anthropicEffort,
onRedirectClick,
}: ModelDisplayWithRedirectProps) {
- const t = useTranslations("common");
-
+ const tCommon = useTranslations("common");
+ const tDashboard = useTranslations("dashboard");
// 判断是否发生重定向
const isRedirected = originalModel && currentModel && originalModel !== currentModel;
@@ -31,44 +34,60 @@ export function ModelDisplayWithRedirect({
const billingModel = billingModelSource === "original" ? originalModel : currentModel;
const handleCopyModel = useCallback(
- (e: React.MouseEvent) => {
+ (e: MouseEvent) => {
e.stopPropagation();
if (!billingModel) return;
void copyTextToClipboard(billingModel).then((ok) => {
- if (ok) toast.success(t("copySuccess"));
+ if (ok) toast.success(tCommon("copySuccess"));
});
},
- [billingModel, t]
+ [billingModel, tCommon]
);
+ const effortBadge = anthropicEffort ? (
+
+ ) : null;
+
if (!isRedirected) {
return (
-
- {billingModel ?
: null}
-
- {billingModel || "-"}
-
+
+
+ {billingModel ? : null}
+
+ {billingModel || "-"}
+
+
+ {effortBadge}
);
}
// 计费模型 + 重定向标记(只显示图标)
return (
-
- {billingModel ?
: null}
-
- {billingModel}
-
-
{
- e.stopPropagation();
- onRedirectClick?.();
- }}
- >
-
-
+
+
+ {billingModel ?
: null}
+
+ {billingModel}
+
+
{
+ e.stopPropagation();
+ onRedirectClick?.();
+ }}
+ >
+
+
+
+ {effortBadge}
);
}
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 dd9065476..e09415d1d 100644
--- a/src/app/[locale]/dashboard/logs/_components/usage-logs-table.tsx
+++ b/src/app/[locale]/dashboard/logs/_components/usage-logs-table.tsx
@@ -263,11 +263,12 @@ export function UsageLogsTable({
-
+
setDialogState({ logId: log.id, scrollToRedirect: true })
}
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 10288f03a..d2153c8ae 100644
--- a/src/app/[locale]/dashboard/logs/_components/virtualized-logs-table.tsx
+++ b/src/app/[locale]/dashboard/logs/_components/virtualized-logs-table.tsx
@@ -538,6 +538,7 @@ export function VirtualizedLogsTable({
originalModel={log.originalModel}
currentModel={log.model}
billingModelSource={billingModelSource}
+ anthropicEffort={log.anthropicEffort}
onRedirectClick={() =>
setDialogState({ logId: log.id, scrollToRedirect: true })
}
diff --git a/src/app/[locale]/my-usage/_components/usage-logs-table.tsx b/src/app/[locale]/my-usage/_components/usage-logs-table.tsx
index cf492e857..2274dff0c 100644
--- a/src/app/[locale]/my-usage/_components/usage-logs-table.tsx
+++ b/src/app/[locale]/my-usage/_components/usage-logs-table.tsx
@@ -5,6 +5,7 @@ import { useTimeZone, useTranslations } from "next-intl";
import { useCallback } from "react";
import { toast } from "sonner";
import type { MyUsageLogEntry } from "@/actions/my-usage";
+import { AnthropicEffortBadge } from "@/components/customs/anthropic-effort-badge";
import { ModelVendorIcon } from "@/components/customs/model-vendor-icon";
import { Badge } from "@/components/ui/badge";
import { Skeleton } from "@/components/ui/skeleton";
@@ -122,6 +123,12 @@ export function UsageLogsTable({
{t("billingModel", { model: log.billingModel })}
) : null}
+ {log.anthropicEffort ? (
+
+ ) : null}
diff --git a/src/app/v1/_lib/proxy/message-service.ts b/src/app/v1/_lib/proxy/message-service.ts
index 1c67cacb1..27afd7cd2 100644
--- a/src/app/v1/_lib/proxy/message-service.ts
+++ b/src/app/v1/_lib/proxy/message-service.ts
@@ -1,3 +1,4 @@
+import { extractAnthropicEffortFromRequestBody } from "@/lib/utils/anthropic-effort";
import { createMessageRequest } from "@/repository/message";
import type { ProxySession } from "./session";
@@ -31,6 +32,24 @@ export class ProxyMessageService {
session.setOriginalModel(currentModel);
}
+ const isAnthropicProvider =
+ provider.providerType === "claude" || provider.providerType === "claude-auth";
+ const hasAnthropicEffortAudit = session
+ .getSpecialSettings()
+ ?.some((setting) => setting.type === "anthropic_effort");
+
+ if (isAnthropicProvider && !hasAnthropicEffortAudit) {
+ const anthropicEffort = extractAnthropicEffortFromRequestBody(session.request.message);
+ if (anthropicEffort) {
+ session.addSpecialSetting({
+ type: "anthropic_effort",
+ scope: "request",
+ hit: true,
+ effort: anthropicEffort,
+ });
+ }
+ }
+
const messageRequest = await createMessageRequest({
provider_id: provider.id,
user_id: authState.user.id,
diff --git a/src/components/customs/anthropic-effort-badge.tsx b/src/components/customs/anthropic-effort-badge.tsx
new file mode 100644
index 000000000..a93ea4c61
--- /dev/null
+++ b/src/components/customs/anthropic-effort-badge.tsx
@@ -0,0 +1,39 @@
+import { Badge } from "@/components/ui/badge";
+import { cn } from "@/lib/utils";
+
+const ANTHROPIC_EFFORT_BADGE_STYLES: Record = {
+ auto: "border-sky-300 bg-gradient-to-r from-cyan-50 via-sky-50 to-indigo-50 text-sky-800 dark:border-sky-700 dark:from-cyan-950/40 dark:via-sky-950/40 dark:to-indigo-950/40 dark:text-sky-200",
+ low: "border-slate-200 bg-slate-50 text-slate-700 dark:border-slate-700 dark:bg-slate-900/40 dark:text-slate-300",
+ medium:
+ "border-amber-200 bg-amber-50 text-amber-700 dark:border-amber-800 dark:bg-amber-950/30 dark:text-amber-300",
+ high: "border-rose-200 bg-rose-50 text-rose-700 dark:border-rose-800 dark:bg-rose-950/30 dark:text-rose-300",
+ max: "border-red-300 bg-red-100 text-red-800 dark:border-red-700 dark:bg-red-950/40 dark:text-red-200",
+};
+
+const DEFAULT_BADGE_STYLE =
+ "border-muted-foreground/20 bg-muted/40 text-muted-foreground dark:border-muted-foreground/30 dark:bg-muted/20";
+
+export function getAnthropicEffortBadgeClassName(effort: string): string {
+ return ANTHROPIC_EFFORT_BADGE_STYLES[effort.trim().toLowerCase()] ?? DEFAULT_BADGE_STYLE;
+}
+
+interface AnthropicEffortBadgeProps {
+ effort: string;
+ label: string;
+ className?: string;
+}
+
+export function AnthropicEffortBadge({ effort, label, className }: AnthropicEffortBadgeProps) {
+ return (
+
+ {label}
+
+ );
+}
diff --git a/src/lib/utils/anthropic-effort.ts b/src/lib/utils/anthropic-effort.ts
new file mode 100644
index 000000000..66e903c70
--- /dev/null
+++ b/src/lib/utils/anthropic-effort.ts
@@ -0,0 +1,44 @@
+import type { SpecialSetting } from "@/types/special-settings";
+
+function normalizeAnthropicEffort(value: unknown): string | null {
+ if (typeof value !== "string") {
+ return null;
+ }
+
+ const trimmed = value.trim();
+ return trimmed.length > 0 ? trimmed : null;
+}
+
+export function extractAnthropicEffortFromRequestBody(requestBody: unknown): string | null {
+ if (!requestBody || typeof requestBody !== "object" || Array.isArray(requestBody)) {
+ return null;
+ }
+
+ const outputConfig = (requestBody as Record).output_config;
+ if (!outputConfig || typeof outputConfig !== "object" || Array.isArray(outputConfig)) {
+ return null;
+ }
+
+ return normalizeAnthropicEffort((outputConfig as Record).effort);
+}
+
+export function extractAnthropicEffortFromSpecialSettings(
+ specialSettings: SpecialSetting[] | null | undefined
+): string | null {
+ if (!Array.isArray(specialSettings)) {
+ return null;
+ }
+
+ for (const setting of specialSettings) {
+ if (setting.type !== "anthropic_effort") {
+ continue;
+ }
+
+ const normalized = normalizeAnthropicEffort(setting.effort);
+ if (normalized) {
+ return normalized;
+ }
+ }
+
+ return null;
+}
diff --git a/src/lib/utils/special-settings.ts b/src/lib/utils/special-settings.ts
index 0b9e2e2e4..d75380406 100644
--- a/src/lib/utils/special-settings.ts
+++ b/src/lib/utils/special-settings.ts
@@ -50,6 +50,8 @@ function buildSettingKey(setting: SpecialSetting): string {
]);
case "guard_intercept":
return JSON.stringify([setting.type, setting.guard, setting.action, setting.statusCode]);
+ case "anthropic_effort":
+ return JSON.stringify([setting.type, setting.hit, setting.effort]);
case "anthropic_cache_ttl_header_override":
return JSON.stringify([setting.type, setting.ttl]);
case "anthropic_context_1m_header_override":
diff --git a/src/repository/usage-logs.ts b/src/repository/usage-logs.ts
index 7bf3d48c0..708503b16 100644
--- a/src/repository/usage-logs.ts
+++ b/src/repository/usage-logs.ts
@@ -5,6 +5,7 @@ import { db } from "@/drizzle/db";
import { keys as keysTable, messageRequest, providers, usageLedger, users } from "@/drizzle/schema";
import { TTLMap } from "@/lib/cache/ttl-map";
import { isLedgerOnlyMode } from "@/lib/ledger-fallback";
+import { extractAnthropicEffortFromSpecialSettings } from "@/lib/utils/anthropic-effort";
import { buildUnifiedSpecialSettings } from "@/lib/utils/special-settings";
import type { ProviderChainItem } from "@/types/message";
import type { SpecialSetting } from "@/types/special-settings";
@@ -68,6 +69,7 @@ export interface UsageLogRow {
swapCacheTtlApplied: boolean | null; // 是否启用了swap cache TTL billing
specialSettings: SpecialSetting[] | null; // 特殊设置(审计/展示)
_liveChain?: { chain: ProviderChainItem[]; phase: string; updatedAt: number } | null;
+ anthropicEffort?: string | null;
}
export interface UsageLogSummary {
@@ -221,6 +223,7 @@ export async function findUsageLogsBatch(
cacheTtlApplied: row.cacheTtlApplied,
context1mApplied: row.context1mApplied,
});
+ const anthropicEffort = extractAnthropicEffortFromSpecialSettings(unifiedSpecialSettings);
return {
...row,
@@ -233,6 +236,7 @@ export async function findUsageLogsBatch(
providerChain: row.providerChain as ProviderChainItem[] | null,
endpoint: row.endpoint,
specialSettings: unifiedSpecialSettings,
+ anthropicEffort,
};
});
@@ -423,6 +427,43 @@ interface UsageLogSlimRow {
cacheCreation5mInputTokens: number | null;
cacheCreation1hInputTokens: number | null;
cacheTtlApplied: string | null;
+ anthropicEffort?: string | null;
+}
+
+function mapUsageLogSlimRow(row: {
+ id: number;
+ createdAt: Date | null;
+ model: string | null;
+ originalModel: string | null;
+ endpoint: string | null;
+ statusCode: number | null;
+ inputTokens: number | null;
+ outputTokens: number | null;
+ costUsd: string | null | { toString(): string };
+ durationMs: number | null;
+ cacheCreationInputTokens: number | null;
+ cacheReadInputTokens: number | null;
+ cacheCreation5mInputTokens: number | null;
+ cacheCreation1hInputTokens: number | null;
+ cacheTtlApplied: string | null;
+ specialSettings?: SpecialSetting[] | null;
+}): UsageLogSlimRow {
+ const { specialSettings, ...rest } = row;
+ const unifiedSpecialSettings = buildUnifiedSpecialSettings({
+ existing: Array.isArray(specialSettings) ? specialSettings : null,
+ blockedBy: null,
+ blockedReason: null,
+ statusCode: rest.statusCode,
+ cacheTtlApplied: rest.cacheTtlApplied,
+ context1mApplied: null,
+ });
+ const anthropicEffort = extractAnthropicEffortFromSpecialSettings(unifiedSpecialSettings);
+
+ return {
+ ...rest,
+ costUsd: rest.costUsd?.toString() ?? null,
+ anthropicEffort,
+ };
}
// my-usage logs: short TTL cache for total count to avoid repeated COUNT(*) on pagination/polling.
@@ -474,6 +515,7 @@ export async function findUsageLogsForKeySlim(
cacheCreation5mInputTokens: messageRequest.cacheCreation5mInputTokens,
cacheCreation1hInputTokens: messageRequest.cacheCreation1hInputTokens,
cacheTtlApplied: messageRequest.cacheTtlApplied,
+ specialSettings: messageRequest.specialSettings,
})
.from(messageRequest)
.where(and(...conditions))
@@ -553,7 +595,11 @@ export async function findUsageLogsForKeySlim(
if (cachedTotal !== undefined) {
ledgerTotal = Math.max(cachedTotal, ledgerTotal);
return {
- logs: ledgerPageRows.map((row) => ({ ...row, costUsd: row.costUsd?.toString() ?? null })),
+ logs: ledgerPageRows.map((row) => ({
+ ...row,
+ costUsd: row.costUsd?.toString() ?? null,
+ anthropicEffort: null,
+ })),
total: ledgerTotal,
};
}
@@ -575,6 +621,7 @@ export async function findUsageLogsForKeySlim(
const ledgerLogs: UsageLogSlimRow[] = ledgerPageRows.map((row) => ({
...row,
costUsd: row.costUsd?.toString() ?? null,
+ anthropicEffort: null,
}));
usageLogSlimTotalCache.set(totalCacheKey, ledgerTotal);
@@ -587,7 +634,7 @@ export async function findUsageLogsForKeySlim(
if (cachedTotal !== undefined) {
total = Math.max(cachedTotal, total);
return {
- logs: pageRows.map((row) => ({ ...row, costUsd: row.costUsd?.toString() ?? null })),
+ logs: pageRows.map((row) => mapUsageLogSlimRow(row)),
total,
};
}
@@ -606,10 +653,7 @@ export async function findUsageLogsForKeySlim(
total = countResults[0]?.totalRows ?? 0;
}
- const logs: UsageLogSlimRow[] = pageRows.map((row) => ({
- ...row,
- costUsd: row.costUsd?.toString() ?? null,
- }));
+ const logs: UsageLogSlimRow[] = pageRows.map((row) => mapUsageLogSlimRow(row));
usageLogSlimTotalCache.set(totalCacheKey, total);
return { logs, total };
@@ -823,6 +867,7 @@ export async function findUsageLogsWithDetails(filters: UsageLogFilters): Promis
cacheTtlApplied: row.cacheTtlApplied,
context1mApplied: row.context1mApplied,
});
+ const anthropicEffort = extractAnthropicEffortFromSpecialSettings(unifiedSpecialSettings);
return {
...row,
@@ -835,6 +880,7 @@ export async function findUsageLogsWithDetails(filters: UsageLogFilters): Promis
providerChain: row.providerChain as ProviderChainItem[] | null,
endpoint: row.endpoint,
specialSettings: unifiedSpecialSettings,
+ anthropicEffort,
};
});
diff --git a/src/types/special-settings.ts b/src/types/special-settings.ts
index e8ad72cfa..eaf8570f4 100644
--- a/src/types/special-settings.ts
+++ b/src/types/special-settings.ts
@@ -14,6 +14,7 @@ export type SpecialSetting =
| BillingHeaderRectifierSpecialSetting
| CodexSessionIdCompletionSpecialSetting
| ClaudeMetadataUserIdInjectionSpecialSetting
+ | AnthropicEffortSpecialSetting
| AnthropicCacheTtlHeaderOverrideSpecialSetting
| AnthropicContext1mHeaderOverrideSpecialSetting
| GeminiGoogleSearchOverrideSpecialSetting
@@ -71,6 +72,19 @@ export type GuardInterceptSpecialSetting = {
reason: string | null;
};
+/**
+ * Anthropic effort 请求参数审计
+ *
+ * 用于记录原始 Anthropic 请求体中的 output_config.effort,
+ * 便于在使用记录中以标签形式展示。
+ */
+export type AnthropicEffortSpecialSetting = {
+ type: "anthropic_effort";
+ scope: "request";
+ hit: boolean;
+ effort: string;
+};
+
/**
* Anthropic 缓存 TTL 相关标头覆写审计
*