From 198121d106ad2e3333f20c6f227664e6e7ce9291 Mon Sep 17 00:00:00 2001 From: panzeyu2013 <1971614652@qq.com> Date: Wed, 27 May 2026 00:51:57 +0800 Subject: [PATCH 1/5] feat: allow per-model price configuration and auto-provision for aggregate API models - Auto-create model_price_rules (price=0) when syncing aggregate API models - Only insert on first sync, never overwrite existing user-set prices - Add RPC endpoints: quota/modelPriceRules/list, read, upsert - Add price input fields (input/cached/output per 1M tokens) to model edit modal - Register Tauri commands for desktop support - User-created rules use priority=20000,ID=user-{slug} to override official seeds --- apps/src-tauri/src/commands/apikey.rs | 24 +++++ apps/src-tauri/src/commands/registry.rs | 3 + apps/src/app/models/page.tsx | 2 + .../components/modals/model-catalog-modal.tsx | 78 +++++++++++++++- apps/src/hooks/useManagedModels.ts | 5 ++ apps/src/lib/api/account-client.ts | 48 ++++++++++ apps/src/lib/api/transport-web-commands.ts | 10 +++ crates/core/src/rpc/types.rs | 54 +++++++++++ crates/service/src/apikey/apikey_models.rs | 62 ++++++++++++- crates/service/src/quota/read.rs | 90 ++++++++++++++++++- crates/service/src/rpc_dispatch/quota.rs | 33 ++++++- 11 files changed, 403 insertions(+), 6 deletions(-) diff --git a/apps/src-tauri/src/commands/apikey.rs b/apps/src-tauri/src/commands/apikey.rs index 94ce312d6..aadd3bb75 100644 --- a/apps/src-tauri/src/commands/apikey.rs +++ b/apps/src-tauri/src/commands/apikey.rs @@ -347,3 +347,27 @@ pub async fn service_apikey_enable( let params = serde_json::json!({ "id": key_id }); rpc_call_in_background("apikey/enable", addr, Some(params)).await } + +#[tauri::command] +pub async fn service_model_price_rules_list( + addr: Option, +) -> Result { + rpc_call_in_background("quota/modelPriceRules/list", addr, None).await +} + +#[tauri::command] +pub async fn service_model_price_rule_read( + addr: Option, + model_pattern: String, +) -> Result { + let params = serde_json::json!({ "modelPattern": model_pattern }); + rpc_call_in_background("quota/modelPriceRule/read", addr, Some(params)).await +} + +#[tauri::command] +pub async fn service_model_price_rule_upsert( + addr: Option, + payload: serde_json::Value, +) -> Result { + rpc_call_in_background("quota/modelPriceRule/upsert", addr, Some(payload)).await +} diff --git a/apps/src-tauri/src/commands/registry.rs b/apps/src-tauri/src/commands/registry.rs index 9507dfdbf..589ee193d 100644 --- a/apps/src-tauri/src/commands/registry.rs +++ b/apps/src-tauri/src/commands/registry.rs @@ -132,6 +132,9 @@ macro_rules! invoke_handler { crate::commands::apikey::service_model_source_model_save, crate::commands::apikey::service_model_source_mapping_save, crate::commands::apikey::service_model_source_mapping_delete, + crate::commands::apikey::service_model_price_rules_list, + crate::commands::apikey::service_model_price_rule_read, + crate::commands::apikey::service_model_price_rule_upsert, crate::commands::apikey::service_apikey_usage_stats, crate::commands::apikey::service_apikey_update_model, crate::commands::apikey::service_apikey_delete, diff --git a/apps/src/app/models/page.tsx b/apps/src/app/models/page.tsx index 7e173a983..4323a2646 100644 --- a/apps/src/app/models/page.tsx +++ b/apps/src/app/models/page.tsx @@ -133,6 +133,7 @@ export default function ModelsPage() { isServiceReady, refreshRemote, saveModel, + saveModelPriceRule, deleteModel, deleteModels, exportCodexCache, @@ -1410,6 +1411,7 @@ export default function ModelsPage() { nextSortIndex={nextSortIndex} isSaving={isSaving} onSave={saveModel} + onSavePriceRule={saveModelPriceRule} /> ) : null} diff --git a/apps/src/components/modals/model-catalog-modal.tsx b/apps/src/components/modals/model-catalog-modal.tsx index 7172ca169..e6660b749 100644 --- a/apps/src/components/modals/model-catalog-modal.tsx +++ b/apps/src/components/modals/model-catalog-modal.tsx @@ -24,7 +24,7 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; -import { ManagedModelPayload } from "@/lib/api/account-client"; +import { ManagedModelPayload, ModelPriceRuleUpsertPayload } from "@/lib/api/account-client"; import { useI18n } from "@/lib/i18n/provider"; import { ManagedModelInfo } from "@/types"; @@ -35,6 +35,7 @@ interface ModelCatalogModalProps { nextSortIndex: number; isSaving?: boolean; onSave: (payload: ManagedModelPayload) => Promise; + onSavePriceRule?: (payload: ModelPriceRuleUpsertPayload) => Promise; } interface ModelCatalogDraft { @@ -49,6 +50,9 @@ interface ModelCatalogDraft { visibility: string; defaultReasoningLevel: string; advancedJson: string; + inputPricePer1m: string; + cachedInputPricePer1m: string; + outputPricePer1m: string; } const EDITABLE_ADVANCED_KEYS = [ @@ -208,6 +212,9 @@ function buildDraft( visibility: normalizeVisibilityValue(model?.visibility), defaultReasoningLevel: model?.defaultReasoningLevel || "", advancedJson: buildAdvancedJson(model), + inputPricePer1m: "", + cachedInputPricePer1m: "", + outputPricePer1m: "", }; } @@ -261,6 +268,7 @@ export function ModelCatalogModal({ nextSortIndex, isSaving = false, onSave, + onSavePriceRule, }: ModelCatalogModalProps) { const { t } = useI18n(); const [draft, setDraft] = useState(() => @@ -320,6 +328,23 @@ export function ModelCatalogModal({ model: nextModel, }); if (saved) { + if (onSavePriceRule && slug) { + const ip = draft.inputPricePer1m.trim(); + const cp = draft.cachedInputPricePer1m.trim(); + const op = draft.outputPricePer1m.trim(); + if (ip !== "" || cp !== "" || op !== "") { + try { + await onSavePriceRule({ + modelPattern: slug, + inputPricePer1m: ip !== "" ? Number(ip) : null, + cachedInputPricePer1m: cp !== "" ? Number(cp) : null, + outputPricePer1m: op !== "" ? Number(op) : null, + }); + } catch { + // price save is non-fatal + } + } + } onOpenChange(false); } }; @@ -500,6 +525,57 @@ export function ModelCatalogModal({ +
+ +

+ {t("零表示不计费,价格将用于请求成本估算。")} +

+
+
+
+ + + updateDraft("inputPricePer1m", event.target.value) + } + placeholder="0" + /> +
+
+ + + updateDraft("cachedInputPricePer1m", event.target.value) + } + placeholder="0" + /> +
+
+ + + updateDraft("outputPricePer1m", event.target.value) + } + placeholder="0" + /> +
+
+