mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-31 11:01:36 +08:00
feat(usage): support importing model pricing from models.dev (#4079)
* feat(usage): support importing model pricing from models.dev Add an "Import from models.dev" button to the Add Pricing panel that fetches https://models.dev/api.json, lists priced models sorted by release date (newest 50 by default, full-text search across ~4800), and bulk-imports the selected entries through the same update_model_pricing command as manual entry. - Normalize imported model IDs to match the backend's clean_model_id_for_pricing rules (strip vendor prefix, lowercase, truncate ':' suffix, map '@' to '-', drop the [1m] marker) so the stored rows actually match cost-attribution lookups - Dedupe selections that collapse to the same model_id and report skipped duplicates in the success toast - Invalidate usage queries on settled (not just success) so partial import failures still refresh the pricing list - Keep ESC inside the picker's search input from closing the dialog and discarding the selection - Add i18n keys for zh/en/zh-TW/ja and unit tests for the normalization, price formatting and flattening logic Fixes #4017 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(usage): match scoped cost backfill against raw model aliases The scoped backfill selected zero-cost rows via exact SQL string match, but log columns store raw model strings (route prefixes, :free variants, date suffixes), so alias rows were skipped until the next full backfill on startup. Filter rows in Rust with the same model_pricing_candidates normalization used by the pricing lookup; pricing decision logic is untouched. Pre-existing gap from schema v11, surfaced by bulk import. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(usage): restrict models.dev pricing import to a single model Each update_model_pricing call triggers a backfill pass that loads every zero-cost usage row before filtering by model, so bulk-importing N entries scaled as selectedModels x allZeroCostLogs full scans. Re-importing pricing is rare, so drop the batch path instead of optimizing it: the picker is now single-select, one import runs exactly one update_model_pricing call and one backfill pass. This also removes the normalized-ID dedup logic and the useImportModelPricing hook in favor of the existing useUpdateModelPricing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,13 +1,14 @@
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
import { Save, Plus } from "lucide-react";
|
||||
import { Save, Plus, Globe } from "lucide-react";
|
||||
import { FullScreenPanel } from "@/components/common/FullScreenPanel";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { useUpdateModelPricing } from "@/lib/query/usage";
|
||||
import { isNonNegativeDecimalString, type ModelPricing } from "@/types/usage";
|
||||
import { ModelsDevPickerDialog } from "./ModelsDevPickerDialog";
|
||||
|
||||
interface PricingEditModalProps {
|
||||
open: boolean;
|
||||
@@ -26,6 +27,7 @@ export function PricingEditModal({
|
||||
}: PricingEditModalProps) {
|
||||
const { t } = useTranslation();
|
||||
const updatePricing = useUpdateModelPricing();
|
||||
const [isPickerOpen, setIsPickerOpen] = useState(false);
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
modelId: model.modelId,
|
||||
@@ -111,6 +113,27 @@ export function PricingEditModal({
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
{isNew && (
|
||||
<div className="mb-6 flex items-center justify-between gap-3 rounded-md border border-border/50 bg-muted/20 px-3 py-2.5">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t(
|
||||
"usage.modelsDevHint",
|
||||
"无需手动填写,可从 models.dev 选择模型定价",
|
||||
)}
|
||||
</p>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setIsPickerOpen(true)}
|
||||
className="shrink-0"
|
||||
>
|
||||
<Globe className="mr-1.5 h-4 w-4" />
|
||||
{t("usage.importFromModelsDev", "从 models.dev 导入")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form id="pricing-form" onSubmit={handleSubmit} className="space-y-6">
|
||||
{isNew && (
|
||||
<div className="space-y-2">
|
||||
@@ -220,6 +243,17 @@ export function PricingEditModal({
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{isNew && isPickerOpen && (
|
||||
<ModelsDevPickerDialog
|
||||
open={isPickerOpen}
|
||||
onClose={() => setIsPickerOpen(false)}
|
||||
onImported={() => {
|
||||
setIsPickerOpen(false);
|
||||
onClose();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</FullScreenPanel>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user