Files
CC-Switch/src/components/usage/RequestDetailPanel.tsx
T
Dex Miller 785e1b5add Feat/pricing config enhancement (#781)
* feat(db): add pricing config fields to proxy_config table

- Add default_cost_multiplier field per app type
- Add pricing_model_source field (request/response)
- Add request_model field to proxy_request_logs table
- Implement schema migration v5

* feat(api): add pricing config commands and provider meta fields

- Add get/set commands for default cost multiplier
- Add get/set commands for pricing model source
- Extend ProviderMeta with cost_multiplier and pricing_model_source
- Register new commands in Tauri invoke handler

* fix(proxy): apply cost multiplier to total cost only

- Move multiplier calculation from per-item to total cost
- Add resolve_pricing_config for provider-level override
- Include request_model and cost_multiplier in usage logs
- Return new fields in get_request_logs API

* feat(ui): add pricing config UI and usage log enhancements

- Add pricing config section to provider advanced settings
- Refactor PricingConfigPanel to compact table layout
- Display all three apps (Claude/Codex/Gemini) in one view
- Add multiplier column and request model display to logs
- Add frontend API wrappers for pricing config

* feat(i18n): add pricing config translations

- Add zh/en/ja translations for pricing defaults config
- Add translations for multiplier, requestModel, responseModel
- Add provider pricing config translations

* fix(pricing): align backfill cost calculation with real-time logic

- Fix backfill to deduct cache_read_tokens from input (avoid double billing)
- Apply multiplier only to total cost, not to each item
- Add multiplier display in request detail panel with i18n support
- Use AppError::localized for backend error messages
- Fix init_proxy_config_rows to use per-app default values
- Fix silent failure in set_default_cost_multiplier/set_pricing_model_source
- Add clippy allow annotation for test mutex across await

* style: format code with cargo fmt and prettier

* fix(tests): correct error type assertions in proxy DAO tests

The tests expected AppError::InvalidInput but the DAO functions use
AppError::localized() which returns AppError::Localized variant.
Updated assertions to match the correct error type with key validation.

---------

Co-authored-by: Jason <farion1231@gmail.com>
2026-01-27 10:43:05 +08:00

286 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useTranslation } from "react-i18next";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { useRequestDetail } from "@/lib/query/usage";
interface RequestDetailPanelProps {
requestId: string;
onClose: () => void;
}
export function RequestDetailPanel({
requestId,
onClose,
}: RequestDetailPanelProps) {
const { t, i18n } = useTranslation();
const { data: request, isLoading } = useRequestDetail(requestId);
const dateLocale =
i18n.language === "zh"
? "zh-CN"
: i18n.language === "ja"
? "ja-JP"
: "en-US";
if (isLoading) {
return (
<Dialog open onOpenChange={onClose}>
<DialogContent className="max-w-2xl">
<div className="h-[400px] animate-pulse rounded bg-gray-100" />
</DialogContent>
</Dialog>
);
}
if (!request) {
return (
<Dialog open onOpenChange={onClose}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>{t("usage.requestDetail", "请求详情")}</DialogTitle>
</DialogHeader>
<div className="text-center text-muted-foreground">
{t("usage.requestNotFound", "请求未找到")}
</div>
</DialogContent>
</Dialog>
);
}
return (
<Dialog open onOpenChange={onClose}>
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>{t("usage.requestDetail", "请求详情")}</DialogTitle>
</DialogHeader>
<div className="space-y-4">
{/* 基本信息 */}
<div className="rounded-lg border p-4">
<h3 className="mb-3 font-semibold">
{t("usage.basicInfo", "基本信息")}
</h3>
<dl className="grid grid-cols-2 gap-3 text-sm">
<div>
<dt className="text-muted-foreground">
{t("usage.requestId", "请求ID")}
</dt>
<dd className="font-mono">{request.requestId}</dd>
</div>
<div>
<dt className="text-muted-foreground">
{t("usage.time", "时间")}
</dt>
<dd>
{new Date(request.createdAt * 1000).toLocaleString(
dateLocale,
)}
</dd>
</div>
<div>
<dt className="text-muted-foreground">
{t("usage.provider", "供应商")}
</dt>
<dd className="text-sm">
<span className="font-medium">
{request.providerName || t("usage.unknownProvider", "未知")}
</span>
<span className="ml-2 font-mono text-xs text-muted-foreground">
{request.providerId}
</span>
</dd>
</div>
<div>
<dt className="text-muted-foreground">
{t("usage.appType", "应用类型")}
</dt>
<dd>{request.appType}</dd>
</div>
<div>
<dt className="text-muted-foreground">
{t("usage.model", "模型")}
</dt>
<dd className="font-mono">{request.model}</dd>
</div>
<div>
<dt className="text-muted-foreground">
{t("usage.status", "状态")}
</dt>
<dd>
<span
className={`inline-flex rounded-full px-2 py-1 text-xs ${
request.statusCode >= 200 && request.statusCode < 300
? "bg-green-100 text-green-800"
: "bg-red-100 text-red-800"
}`}
>
{request.statusCode}
</span>
</dd>
</div>
</dl>
</div>
{/* Token 使用量 */}
<div className="rounded-lg border p-4">
<h3 className="mb-3 font-semibold">
{t("usage.tokenUsage", "Token 使用量")}
</h3>
<dl className="grid grid-cols-2 gap-3 text-sm">
<div>
<dt className="text-muted-foreground">
{t("usage.inputTokens", "输入 Tokens")}
</dt>
<dd className="font-mono">
{request.inputTokens.toLocaleString()}
</dd>
</div>
<div>
<dt className="text-muted-foreground">
{t("usage.outputTokens", "输出 Tokens")}
</dt>
<dd className="font-mono">
{request.outputTokens.toLocaleString()}
</dd>
</div>
<div>
<dt className="text-muted-foreground">
{t("usage.cacheReadTokens", "缓存读取")}
</dt>
<dd className="font-mono">
{request.cacheReadTokens.toLocaleString()}
</dd>
</div>
<div>
<dt className="text-muted-foreground">
{t("usage.cacheCreationTokens", "缓存写入")}
</dt>
<dd className="font-mono">
{request.cacheCreationTokens.toLocaleString()}
</dd>
</div>
<div className="col-span-2">
<dt className="text-muted-foreground">
{t("usage.totalTokens", "总计")}
</dt>
<dd className="text-lg font-semibold">
{(
request.inputTokens + request.outputTokens
).toLocaleString()}
</dd>
</div>
</dl>
</div>
{/* 成本明细 */}
<div className="rounded-lg border p-4">
<h3 className="mb-3 font-semibold">
{t("usage.costBreakdown", "成本明细")}
</h3>
<dl className="grid grid-cols-2 gap-3 text-sm">
<div>
<dt className="text-muted-foreground">
{t("usage.inputCost", "输入成本")}
<span className="ml-1 text-xs">
({t("usage.baseCost", "基础")})
</span>
</dt>
<dd className="font-mono">
${parseFloat(request.inputCostUsd).toFixed(6)}
</dd>
</div>
<div>
<dt className="text-muted-foreground">
{t("usage.outputCost", "输出成本")}
<span className="ml-1 text-xs">
({t("usage.baseCost", "基础")})
</span>
</dt>
<dd className="font-mono">
${parseFloat(request.outputCostUsd).toFixed(6)}
</dd>
</div>
<div>
<dt className="text-muted-foreground">
{t("usage.cacheReadCost", "缓存读取成本")}
<span className="ml-1 text-xs">
({t("usage.baseCost", "基础")})
</span>
</dt>
<dd className="font-mono">
${parseFloat(request.cacheReadCostUsd).toFixed(6)}
</dd>
</div>
<div>
<dt className="text-muted-foreground">
{t("usage.cacheCreationCost", "缓存写入成本")}
<span className="ml-1 text-xs">
({t("usage.baseCost", "基础")})
</span>
</dt>
<dd className="font-mono">
${parseFloat(request.cacheCreationCostUsd).toFixed(6)}
</dd>
</div>
{/* 显示成本倍率(如果不等于1) */}
{request.costMultiplier &&
parseFloat(request.costMultiplier) !== 1 && (
<div className="col-span-2 border-t pt-3">
<dt className="text-muted-foreground">
{t("usage.costMultiplier", "成本倍率")}
</dt>
<dd className="font-mono">×{request.costMultiplier}</dd>
</div>
)}
<div
className={`col-span-2 ${request.costMultiplier && parseFloat(request.costMultiplier) !== 1 ? "" : "border-t"} pt-3`}
>
<dt className="text-muted-foreground">
{t("usage.totalCost", "总成本")}
{request.costMultiplier &&
parseFloat(request.costMultiplier) !== 1 && (
<span className="ml-1 text-xs">
({t("usage.withMultiplier", "含倍率")})
</span>
)}
</dt>
<dd className="text-lg font-semibold text-primary">
${parseFloat(request.totalCostUsd).toFixed(6)}
</dd>
</div>
</dl>
</div>
{/* 性能信息 */}
<div className="rounded-lg border p-4">
<h3 className="mb-3 font-semibold">
{t("usage.performance", "性能信息")}
</h3>
<dl className="grid grid-cols-2 gap-3 text-sm">
<div>
<dt className="text-muted-foreground">
{t("usage.latency", "延迟")}
</dt>
<dd className="font-mono">{request.latencyMs}ms</dd>
</div>
</dl>
</div>
{/* 错误信息 */}
{request.errorMessage && (
<div className="rounded-lg border border-red-200 bg-red-50 p-4">
<h3 className="mb-2 font-semibold text-red-800">
{t("usage.errorMessage", "错误信息")}
</h3>
<p className="text-sm text-red-700">{request.errorMessage}</p>
</div>
)}
</div>
</DialogContent>
</Dialog>
);
}