diff --git a/packages/api-client/src/endpoints/api-key-permission-profiles.ts b/packages/api-client/src/endpoints/api-key-permission-profiles.ts
index 7edf6475..c50965dc 100644
--- a/packages/api-client/src/endpoints/api-key-permission-profiles.ts
+++ b/packages/api-client/src/endpoints/api-key-permission-profiles.ts
@@ -112,6 +112,7 @@ export function applyApiKeyPermissionProfile(
"daily-limit": 0,
"total-quota": 0,
"spending-limit": 0,
+ "daily-spending-limit": 0,
"concurrency-limit": 0,
"rpm-limit": 0,
"tpm-limit": 0,
diff --git a/packages/api-client/src/endpoints/api-keys.ts b/packages/api-client/src/endpoints/api-keys.ts
index 429787fb..62475637 100644
--- a/packages/api-client/src/endpoints/api-keys.ts
+++ b/packages/api-client/src/endpoints/api-keys.ts
@@ -8,6 +8,7 @@ export interface ApiKeyEntry {
"daily-limit"?: number;
"total-quota"?: number;
"spending-limit"?: number;
+ "daily-spending-limit"?: number;
"concurrency-limit"?: number;
"rpm-limit"?: number;
"tpm-limit"?: number;
diff --git a/packages/i18n/src/locales/en.json b/packages/i18n/src/locales/en.json
index 25573835..dfc076fe 100644
--- a/packages/i18n/src/locales/en.json
+++ b/packages/i18n/src/locales/en.json
@@ -2609,7 +2609,9 @@
"col_daily_limit": "Daily limit",
"col_total_quota": "Total quota",
"col_spending_limit": "Spending limit",
+ "col_daily_spending_limit": "Daily spending limit",
"spending_limit_help": "Maximum cumulative API key cost in USD. Empty or 0 means unlimited.",
+ "daily_spending_limit_help": "Maximum daily API key cost in USD. Resets at midnight (project timezone). Empty or 0 means unlimited. Requires model pricing to be configured.",
"col_models": "Models",
"col_channel_groups": "Channel groups",
"col_channels": "Channels",
@@ -2650,6 +2652,8 @@
"form_permission_profile_desc": "Permission configs contain limits, channel/model restrictions, and prompt text. Empty uses the unrestricted default.",
"permission_profile_unrestricted": "Default: unrestricted",
"permission_profile_custom_keep": "Current custom config (keep)",
+ "form_spending_limit": "Spending limit ($)",
+ "form_daily_spending_limit": "Daily spending limit ($)",
"form_daily_limit": "Daily request limit",
"form_total_quota": "Total request quota",
"form_concurrency_limit": "Concurrent request limit",
diff --git a/packages/i18n/src/locales/zh-CN.json b/packages/i18n/src/locales/zh-CN.json
index 3e0286f1..a8ed4541 100644
--- a/packages/i18n/src/locales/zh-CN.json
+++ b/packages/i18n/src/locales/zh-CN.json
@@ -2878,7 +2878,9 @@
"col_daily_limit": "每日限额",
"col_total_quota": "总配额",
"col_spending_limit": "消费限额",
+ "col_daily_spending_limit": "每日消费限额",
"spending_limit_help": "该 API 密钥允许累计消费的美元上限,留空或 0 表示不限制。",
+ "daily_spending_limit_help": "该 API 密钥每天允许消费的美元上限,按天重置,留空或 0 表示不限制。需要配置模型定价。",
"col_models": "模型",
"col_channel_groups": "渠道分组",
"col_channels": "渠道",
@@ -2919,6 +2921,8 @@
"form_permission_profile_desc": "权限配置包含限额、渠道/模型权限和提示词;不选择则使用默认无限制。",
"permission_profile_unrestricted": "默认:不限制",
"permission_profile_custom_keep": "当前自定义配置(保留)",
+ "form_spending_limit": "累计消费上限 ($)",
+ "form_daily_spending_limit": "每日消费上限 ($)",
"form_daily_limit": "每日请求限额",
"form_total_quota": "总请求配额",
"form_concurrency_limit": "并发请求限制",
diff --git a/pages/api-keys/ApiKeysPage.tsx b/pages/api-keys/ApiKeysPage.tsx
index dab1b650..ef271fa7 100644
--- a/pages/api-keys/ApiKeysPage.tsx
+++ b/pages/api-keys/ApiKeysPage.tsx
@@ -16,7 +16,7 @@ import {
import { ccSwitchImportConfigsApi } from "@code-proxy/api-client/endpoints/ccswitch-import-configs";
import { detectApiBaseFromLocation } from "@code-proxy/api-client";
import { useOptionalAuth } from "@app/providers/AuthProvider";
-import { generateApiKey, makeEmptyApiKeyForm, maskApiKey } from "./apiKeyPageUtils";
+import { generateApiKey, makeEmptyApiKeyForm, maskApiKey, parseSpendingLimit } from "./apiKeyPageUtils";
import { createApiKeyColumns } from "./components/ApiKeyColumns";
import { DeleteApiKeyModal } from "./components/DeleteApiKeyModal";
import { copyTextToClipboard } from "@code-proxy/ui";
@@ -285,6 +285,7 @@ export function ApiKeysPage() {
dailyLimit: entry["daily-limit"]?.toString() || "",
totalQuota: entry["total-quota"]?.toString() || "",
spendingLimit: entry["spending-limit"]?.toString() || "",
+ dailySpendingLimit: entry["daily-spending-limit"]?.toString() || "",
concurrencyLimit: entry["concurrency-limit"]?.toString() || "",
rpmLimit: entry["rpm-limit"]?.toString() || "",
tpmLimit: entry["tpm-limit"]?.toString() || "",
@@ -312,25 +313,33 @@ export function ApiKeysPage() {
}
setSaving(true);
try {
+ const useCustomLimits =
+ form.permissionProfileId === CUSTOM_PERMISSION_PROFILE_ID ||
+ form.permissionProfileId === "";
await apiKeyEntriesApi.update({
id: entries[editIndex].id,
index: editIndex,
value: {
...(newKey !== originalKey ? { key: newKey } : {}),
name: form.name.trim(),
- ...(form.permissionProfileId === CUSTOM_PERMISSION_PROFILE_ID
+ ...(useCustomLimits
? {
- "permission-profile-id": entries[editIndex]["permission-profile-id"] ?? "",
- "daily-limit": entries[editIndex]["daily-limit"] ?? 0,
- "total-quota": entries[editIndex]["total-quota"] ?? 0,
- "spending-limit": entries[editIndex]["spending-limit"] ?? 0,
- "concurrency-limit": entries[editIndex]["concurrency-limit"] ?? 0,
- "rpm-limit": entries[editIndex]["rpm-limit"] ?? 0,
- "tpm-limit": entries[editIndex]["tpm-limit"] ?? 0,
- "allowed-models": entries[editIndex]["allowed-models"] ?? [],
- "allowed-channels": entries[editIndex]["allowed-channels"] ?? [],
- "allowed-channel-groups": entries[editIndex]["allowed-channel-groups"] ?? [],
- "system-prompt": entries[editIndex]["system-prompt"] ?? "",
+ ...(form.permissionProfileId === ""
+ ? applyApiKeyPermissionProfile({} as ApiKeyEntry, null)
+ : {
+ "permission-profile-id": entries[editIndex]["permission-profile-id"] ?? "",
+ "daily-limit": entries[editIndex]["daily-limit"] ?? 0,
+ "total-quota": entries[editIndex]["total-quota"] ?? 0,
+ "concurrency-limit": entries[editIndex]["concurrency-limit"] ?? 0,
+ "rpm-limit": entries[editIndex]["rpm-limit"] ?? 0,
+ "tpm-limit": entries[editIndex]["tpm-limit"] ?? 0,
+ "allowed-models": entries[editIndex]["allowed-models"] ?? [],
+ "allowed-channels": entries[editIndex]["allowed-channels"] ?? [],
+ "allowed-channel-groups": entries[editIndex]["allowed-channel-groups"] ?? [],
+ "system-prompt": entries[editIndex]["system-prompt"] ?? "",
+ }),
+ "spending-limit": parseSpendingLimit(form.spendingLimit),
+ "daily-spending-limit": parseSpendingLimit(form.dailySpendingLimit),
}
: applyApiKeyPermissionProfile(
{} as ApiKeyEntry,
diff --git a/pages/api-keys/apiKeyPageUtils.tsx b/pages/api-keys/apiKeyPageUtils.tsx
index 7130673c..ddd23a5e 100644
--- a/pages/api-keys/apiKeyPageUtils.tsx
+++ b/pages/api-keys/apiKeyPageUtils.tsx
@@ -60,6 +60,7 @@ export const makeEmptyApiKeyForm = (key = ""): ApiKeyFormValues => ({
dailyLimit: "",
totalQuota: "",
spendingLimit: "",
+ dailySpendingLimit: "",
concurrencyLimit: "",
rpmLimit: "",
tpmLimit: "",
@@ -114,6 +115,11 @@ export const formatApiKeySpendingLimit = (limit: number | undefined) => {
}).format(limit);
};
+export const parseSpendingLimit = (value: string): number => {
+ const parsed = Number.parseFloat(value);
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
+};
+
export const normalizeChannelKey = (value: string) => value.trim().toLowerCase();
export const readAuthFileChannelName = (file: AuthFileItem): string => {
diff --git a/pages/api-keys/components/ApiKeyColumns.tsx b/pages/api-keys/components/ApiKeyColumns.tsx
index f83fc10a..e9679e89 100644
--- a/pages/api-keys/components/ApiKeyColumns.tsx
+++ b/pages/api-keys/components/ApiKeyColumns.tsx
@@ -151,6 +151,32 @@ export const createApiKeyColumns = ({
),
},
+ {
+ key: "dailySpendingLimit",
+ label: t("api_keys_page.col_daily_spending_limit"),
+ width: "w-[148px] min-w-[148px]",
+ cellClassName: "whitespace-nowrap text-slate-700 dark:text-white/70",
+ headerRender: () => (
+