mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-30 00:21:34 +00:00
feat(grok): 账单绝对金额、调度阈值 UI、搜索/Voice 计费与 /v1/web_search
- BillingSummary 输出 prepaid/monthly_used/on_demand 绝对金额,并映射官方 7d/30d 进度条 - 账号编辑页支持 credentials.account_scheduling_threshold 覆盖;Settings 文案细化 - groups.search_price_per_1k + 管理端/缓存全链路;SearchCount/AudioUsage 请求级计费 - Voice TTS/STT/Realtime 成功路径 RecordUsage;独立 /v1/web_search 原生 Grok 搜索
This commit is contained in:
@@ -365,7 +365,7 @@
|
||||
{{ grokEntitlementLabel }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="grokLocalUsage" class="mb-0.5 flex items-center">
|
||||
<div v-if="showGrokStandaloneLocalStats && grokLocalUsage" class="mb-0.5 flex items-center">
|
||||
<div class="flex items-center gap-1.5 text-[9px] text-gray-500 dark:text-gray-400">
|
||||
<span class="rounded bg-gray-100 px-1.5 py-0.5 dark:bg-gray-800">
|
||||
{{ formatWindowRequests(grokLocalUsage) }} req
|
||||
@@ -405,6 +405,13 @@
|
||||
v-if="grokBillingMoneySummary"
|
||||
class="flex flex-wrap items-center gap-1 text-[10px] text-gray-500 dark:text-gray-400"
|
||||
>
|
||||
<span
|
||||
v-if="grokBillingMoneySummary.prepaid != null"
|
||||
class="rounded bg-emerald-50 px-1 py-0.5 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-300"
|
||||
:title="t('admin.accounts.usageWindow.grokPrepaid')"
|
||||
>
|
||||
{{ t('admin.accounts.usageWindow.grokPrepaid') }} ${{ grokBillingMoneySummary.prepaid }}
|
||||
</span>
|
||||
<span :title="t('admin.accounts.usageWindow.grokMonthlyLimit')">
|
||||
{{ t('admin.accounts.usageWindow.grokUsed') }}
|
||||
{{ grokBillingMoneySummary.used }}/{{ grokBillingMoneySummary.limit }}
|
||||
@@ -415,6 +422,14 @@
|
||||
>
|
||||
{{ grokBillingMoneySummary.usedPercent }}%
|
||||
</span>
|
||||
<span
|
||||
v-if="grokBillingMoneySummary.showOverage"
|
||||
class="rounded bg-amber-50 px-1 py-0.5 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300"
|
||||
:title="t('admin.accounts.usageWindow.grokOverage')"
|
||||
>
|
||||
{{ t('admin.accounts.usageWindow.grokOverageShort') }}
|
||||
{{ grokBillingMoneySummary.onDemandUsed }}/{{ grokBillingMoneySummary.onDemandCap }}
|
||||
</span>
|
||||
</div>
|
||||
<UsageProgressBar
|
||||
v-if="!grokWeeklyBillingBar && !grokIsFree && grokRequestQuotaBar"
|
||||
@@ -1155,22 +1170,42 @@ const grokMonthlyBillingBar = computed((): GrokQuotaBarInfo | null => {
|
||||
resetsAt: billing.billing_period_end || billing.period_end || null
|
||||
}
|
||||
})
|
||||
const formatGrokMoneyFromCents = (cents?: number | null) => {
|
||||
if (cents == null || Number.isNaN(cents)) return '0'
|
||||
const dollars = cents / 100
|
||||
if (dollars >= 1000) return formatCompactNumber(dollars)
|
||||
if (dollars >= 100) return dollars.toFixed(0)
|
||||
if (dollars >= 10) return dollars.toFixed(1)
|
||||
return dollars.toFixed(2)
|
||||
const formatGrokMoney = (value?: number | null) => {
|
||||
if (value == null || Number.isNaN(value)) return '0'
|
||||
if (value >= 1000) return formatCompactNumber(value)
|
||||
if (value >= 100) return value.toFixed(0)
|
||||
if (value >= 10) return value.toFixed(1)
|
||||
return value.toFixed(2)
|
||||
}
|
||||
// Absolute monthly used/limit derived from cents fields already on GrokBillingSummary.
|
||||
// (personal-dev has separate prepaid/on_demand fields; HEAD only has cents.)
|
||||
// Absolute money: prefer prepaid/monthly_used fields; fall back to cents/100.
|
||||
const grokBillingMoneySummary = computed(() => {
|
||||
const billing = grokBilling.value
|
||||
if (!billing) return null
|
||||
const limit = billing.monthly_limit_cents
|
||||
const used = billing.used_cents
|
||||
if ((limit == null || limit <= 0) && (used == null || used <= 0)) return null
|
||||
const prepaid =
|
||||
billing.prepaid_balance != null
|
||||
? billing.prepaid_balance
|
||||
: null
|
||||
const used =
|
||||
billing.monthly_used != null
|
||||
? billing.monthly_used
|
||||
: billing.used_cents != null
|
||||
? billing.used_cents / 100
|
||||
: null
|
||||
const limit =
|
||||
billing.monthly_limit != null
|
||||
? billing.monthly_limit
|
||||
: billing.monthly_limit_cents != null
|
||||
? billing.monthly_limit_cents / 100
|
||||
: null
|
||||
const onDemandUsed = billing.on_demand_used ?? null
|
||||
const onDemandCap = billing.on_demand_cap ?? null
|
||||
const hasAny =
|
||||
(prepaid ?? 0) > 0 ||
|
||||
(used ?? 0) > 0 ||
|
||||
(limit ?? 0) > 0 ||
|
||||
(onDemandCap ?? 0) > 0 ||
|
||||
(onDemandUsed ?? 0) > 0
|
||||
if (!hasAny) return null
|
||||
let usedPercent: number | null = null
|
||||
if (billing.used_percent != null && Number.isFinite(billing.used_percent)) {
|
||||
usedPercent = Math.round(Math.min(100, Math.max(0, billing.used_percent)))
|
||||
@@ -1178,8 +1213,12 @@ const grokBillingMoneySummary = computed(() => {
|
||||
usedPercent = Math.round(Math.min(100, Math.max(0, (used / limit) * 100)))
|
||||
}
|
||||
return {
|
||||
used: formatGrokMoneyFromCents(used ?? 0),
|
||||
limit: formatGrokMoneyFromCents(limit ?? 0),
|
||||
prepaid: prepaid != null ? formatGrokMoney(prepaid) : null,
|
||||
used: formatGrokMoney(used ?? 0),
|
||||
limit: formatGrokMoney(limit ?? 0),
|
||||
onDemandUsed: formatGrokMoney(onDemandUsed ?? 0),
|
||||
onDemandCap: formatGrokMoney(onDemandCap ?? 0),
|
||||
showOverage: (onDemandCap ?? 0) > 0 || (onDemandUsed ?? 0) > 0,
|
||||
usedPercent
|
||||
}
|
||||
})
|
||||
@@ -1208,6 +1247,10 @@ const grokIsFree = computed(() => {
|
||||
return billing != null
|
||||
})
|
||||
const grokFreeQuotaUsage = computed(() => usageInfo.value?.grok_local_usage_24h || null)
|
||||
const showGrokStandaloneLocalStats = computed(() => {
|
||||
// Official 7d/30d bars already embed window_stats — avoid duplicate local row.
|
||||
return !usageInfo.value?.seven_day && !usageInfo.value?.thirty_day
|
||||
})
|
||||
const grokLocalUsage = computed(() => {
|
||||
if (grokIsFree.value) return grokFreeQuotaUsage.value
|
||||
return props.todayStats ||
|
||||
|
||||
@@ -1375,6 +1375,40 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div
|
||||
v-if="supportsAccountSchedulingThresholdOverride"
|
||||
class="border-t border-gray-200 pt-4 dark:border-dark-600"
|
||||
data-testid="account-scheduling-threshold-section"
|
||||
>
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<div>
|
||||
<label class="input-label mb-0">{{ t('admin.accounts.accountSchedulingThresholdOverride') }}</label>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('admin.accounts.accountSchedulingThresholdOverrideHint') }}
|
||||
</p>
|
||||
</div>
|
||||
<input
|
||||
v-model="accountSchedulingThresholdOverrideEnabled"
|
||||
data-testid="account-scheduling-threshold-override-enabled"
|
||||
type="checkbox"
|
||||
class="h-4 w-4 rounded border-gray-300 text-primary-600 focus:ring-primary-500"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="accountSchedulingThresholdOverrideEnabled">
|
||||
<label class="input-label">{{ t('admin.accounts.accountSchedulingThresholdOverrideValue') }}</label>
|
||||
<input
|
||||
v-model.number="accountSchedulingThresholdOverrideValue"
|
||||
data-testid="account-scheduling-threshold-override-value"
|
||||
type="number"
|
||||
min="1"
|
||||
max="100"
|
||||
class="input"
|
||||
/>
|
||||
<p class="input-hint">{{ t('admin.accounts.accountSchedulingThresholdOverrideDisabledHint') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Intercept Warmup Requests (Anthropic/Antigravity) -->
|
||||
<div
|
||||
v-if="account?.platform === 'anthropic' || account?.platform === 'antigravity'"
|
||||
@@ -2867,6 +2901,12 @@ const antigravityWhitelistModels = ref<string[]>([])
|
||||
const antigravityModelMappings = ref<ModelMapping[]>([])
|
||||
const isSyncingAntigravityUpstream = ref(false)
|
||||
const tempUnschedEnabled = ref(false)
|
||||
const accountSchedulingThresholdOverrideEnabled = ref(false)
|
||||
const accountSchedulingThresholdOverrideValue = ref(100)
|
||||
const ACCOUNT_SCHEDULING_THRESHOLD_CREDENTIAL_KEY = 'account_scheduling_threshold'
|
||||
const supportsAccountSchedulingThresholdOverride = computed(() =>
|
||||
supportsAccountSchedulingThresholdOverridePlatform(props.account?.platform)
|
||||
)
|
||||
const tempUnschedRules = ref<TempUnschedRuleForm[]>([])
|
||||
const getModelMappingKey = createStableObjectKeyResolver<ModelMapping>('edit-model-mapping')
|
||||
const getOpenAICompactModelMappingKey = createStableObjectKeyResolver<ModelMapping>('edit-openai-compact-model-mapping')
|
||||
@@ -3512,6 +3552,7 @@ const syncFormFromAccount = (newAccount: Account | null) => {
|
||||
loadQuotaControlSettings(newAccount)
|
||||
|
||||
loadTempUnschedRules(credentials)
|
||||
loadAccountSchedulingThresholdOverride(newAccount.platform, credentials)
|
||||
|
||||
// Load header override state (anthropic/openai apikey + grok apikey/oauth)
|
||||
headerOverrideEnabled.value = false
|
||||
@@ -3874,6 +3915,67 @@ const applyTempUnschedConfig = (credentials: Record<string, unknown>) => {
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
const supportsAccountSchedulingThresholdOverridePlatform = (platform: Account['platform'] | undefined) =>
|
||||
platform === 'openai' || platform === 'anthropic' || platform === 'grok'
|
||||
|
||||
const normalizeAccountSchedulingThresholdOverride = (value: unknown): number | null => {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return null
|
||||
}
|
||||
const numeric = Number(value)
|
||||
if (!Number.isFinite(numeric)) {
|
||||
return null
|
||||
}
|
||||
const integer = Math.trunc(numeric)
|
||||
if (integer < 1 || integer > 100) {
|
||||
return null
|
||||
}
|
||||
return integer
|
||||
}
|
||||
|
||||
const clampAccountSchedulingThresholdOverride = (value: unknown): number =>
|
||||
Math.min(100, Math.max(1, Math.trunc(Number(value) || 100)))
|
||||
|
||||
const loadAccountSchedulingThresholdOverride = (
|
||||
platform: Account['platform'] | undefined,
|
||||
credentials: Record<string, unknown> | undefined
|
||||
) => {
|
||||
if (!supportsAccountSchedulingThresholdOverridePlatform(platform)) {
|
||||
accountSchedulingThresholdOverrideEnabled.value = false
|
||||
accountSchedulingThresholdOverrideValue.value = 100
|
||||
return
|
||||
}
|
||||
const value = normalizeAccountSchedulingThresholdOverride(
|
||||
credentials?.[ACCOUNT_SCHEDULING_THRESHOLD_CREDENTIAL_KEY]
|
||||
)
|
||||
accountSchedulingThresholdOverrideEnabled.value = value !== null
|
||||
accountSchedulingThresholdOverrideValue.value = value ?? 100
|
||||
}
|
||||
|
||||
const applyAccountSchedulingThresholdOverridePatch = (
|
||||
credentials: Record<string, unknown>,
|
||||
currentCredentials: Record<string, unknown>,
|
||||
platform: Account['platform'] | undefined = props.account?.platform
|
||||
) => {
|
||||
if (!supportsAccountSchedulingThresholdOverridePlatform(platform)) {
|
||||
return
|
||||
}
|
||||
const current = normalizeAccountSchedulingThresholdOverride(
|
||||
currentCredentials[ACCOUNT_SCHEDULING_THRESHOLD_CREDENTIAL_KEY]
|
||||
)
|
||||
if (!accountSchedulingThresholdOverrideEnabled.value) {
|
||||
if (current !== null) {
|
||||
credentials[ACCOUNT_SCHEDULING_THRESHOLD_CREDENTIAL_KEY] = null
|
||||
}
|
||||
return
|
||||
}
|
||||
const next = clampAccountSchedulingThresholdOverride(accountSchedulingThresholdOverrideValue.value)
|
||||
if (current !== next) {
|
||||
credentials[ACCOUNT_SCHEDULING_THRESHOLD_CREDENTIAL_KEY] = next
|
||||
}
|
||||
}
|
||||
|
||||
function loadTempUnschedRules(credentials?: Record<string, unknown>) {
|
||||
tempUnschedEnabled.value = credentials?.temp_unschedulable_enabled === true
|
||||
const rawRules = credentials?.temp_unschedulable_rules
|
||||
@@ -4233,6 +4335,7 @@ const handleSubmit = async () => {
|
||||
|
||||
// Add intercept warmup requests setting
|
||||
applyInterceptWarmup(newCredentials, interceptWarmupRequests.value, 'edit')
|
||||
applyAccountSchedulingThresholdOverridePatch(newCredentials, currentCredentials)
|
||||
if (!applyTempUnschedConfig(newCredentials)) {
|
||||
return
|
||||
}
|
||||
@@ -4251,6 +4354,7 @@ const handleSubmit = async () => {
|
||||
// Add intercept warmup requests setting
|
||||
applyInterceptWarmup(newCredentials, interceptWarmupRequests.value, 'edit')
|
||||
|
||||
applyAccountSchedulingThresholdOverridePatch(newCredentials, currentCredentials)
|
||||
if (!applyTempUnschedConfig(newCredentials)) {
|
||||
return
|
||||
}
|
||||
@@ -4299,6 +4403,7 @@ const handleSubmit = async () => {
|
||||
}
|
||||
|
||||
applyInterceptWarmup(newCredentials, interceptWarmupRequests.value, 'edit')
|
||||
applyAccountSchedulingThresholdOverridePatch(newCredentials, currentCredentials)
|
||||
if (!applyTempUnschedConfig(newCredentials)) {
|
||||
return
|
||||
}
|
||||
@@ -4356,6 +4461,7 @@ const handleSubmit = async () => {
|
||||
}
|
||||
|
||||
applyInterceptWarmup(newCredentials, interceptWarmupRequests.value, 'edit')
|
||||
applyAccountSchedulingThresholdOverridePatch(newCredentials, currentCredentials)
|
||||
if (!applyTempUnschedConfig(newCredentials)) {
|
||||
return
|
||||
}
|
||||
@@ -4367,6 +4473,7 @@ const handleSubmit = async () => {
|
||||
const newCredentials: Record<string, unknown> = { ...currentCredentials }
|
||||
|
||||
applyInterceptWarmup(newCredentials, interceptWarmupRequests.value, 'edit')
|
||||
applyAccountSchedulingThresholdOverridePatch(newCredentials, currentCredentials)
|
||||
if (!applyTempUnschedConfig(newCredentials)) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -131,6 +131,10 @@ export default {
|
||||
rateLimited: 'Rate Limited',
|
||||
overloaded: 'Overloaded',
|
||||
tempUnschedulable: 'Temp Unschedulable',
|
||||
accountSchedulingThresholdOverride: 'Account Auto-Pause Threshold Override',
|
||||
accountSchedulingThresholdOverrideHint: 'Override the platform auto-pause threshold for this account only. Disable to use platform settings.',
|
||||
accountSchedulingThresholdOverrideValue: 'Account threshold percent',
|
||||
accountSchedulingThresholdOverrideDisabledHint: 'Use 1-100. The account becomes temporarily unschedulable after reaching this usage percent; 100 disables it for this account.',
|
||||
quotaExceeded: 'Quota Exceeded',
|
||||
unschedulable: 'Unschedulable',
|
||||
rateLimitedUntil: 'Rate limited and removed from scheduling. Auto resumes at {time}',
|
||||
@@ -1358,7 +1362,11 @@ export default {
|
||||
grokFreeQuota24hHint: 'Estimated from local token usage over the rolling 24-hour window ({limit} limit)',
|
||||
grokWeeklyUsage: 'Weekly {percent}%',
|
||||
grokUsed: 'Used $',
|
||||
grokMonthlyLimit: 'Monthly used / limit (USD from billing cents)',
|
||||
grokBalance: 'Bal $',
|
||||
grokPrepaid: 'Prepaid balance',
|
||||
grokMonthlyLimit: 'Monthly used / limit (USD)',
|
||||
grokOverage: 'Overage onDemandUsed/onDemandCap',
|
||||
grokOverageShort: 'OD $',
|
||||
grokUnknown: 'Grok quota is unknown until the first upstream response includes xAI rate-limit headers.',
|
||||
grokRetryAfter: 'Retry after {time}',
|
||||
grokProbe: 'Probe',
|
||||
|
||||
@@ -994,6 +994,12 @@ export default {
|
||||
finalPricePreview: 'Final per-second price preview',
|
||||
notConfigured: 'Not configured'
|
||||
},
|
||||
explicitPricing: {
|
||||
title: 'Grok Search & Voice Pricing',
|
||||
description: 'Optional per-group prices for web_search (per 1k calls) and Voice realtime / TTS / STT (USD). Leave empty if unused.',
|
||||
searchPricePer1k: 'Search price per 1k calls (USD)',
|
||||
pricePlaceholder: 'optional'
|
||||
},
|
||||
voicePricing: {
|
||||
title: 'Grok Voice Pricing',
|
||||
description: 'Optional per-group prices for Voice realtime / TTS / STT (USD). Leave empty to leave unpriced.',
|
||||
|
||||
@@ -411,10 +411,10 @@ export default {
|
||||
allowUngroupedKey: 'Allow Ungrouped Key Scheduling',
|
||||
allowUngroupedKeyHint: 'When disabled, API Keys not assigned to any group cannot make requests (403 Forbidden). Keep disabled to ensure all Keys belong to a specific group.',
|
||||
accountSchedulingThresholdsTitle: 'Platform Account Auto-Pause Thresholds',
|
||||
accountSchedulingThresholdsDescription: 'Set per-platform thresholds that automatically pause scheduling for accounts on that platform when their current native usage window reaches the configured percentage.',
|
||||
accountSchedulingThresholdsGlobalHint: 'This is a system-wide global setting and applies to all accounts on that platform.',
|
||||
accountSchedulingThresholdsDisabledHint: 'A value of 100 disables the auto-pause threshold for that platform.',
|
||||
accountSchedulingThresholdsRangeHint: 'Range 1-100, entered as a percentage.'
|
||||
accountSchedulingThresholdsDescription: 'When an account\'s current native usage window (OpenAI Codex/Anthropic session, or Grok request/token utilization) reaches this percent, Sub2API temporarily removes it from scheduling until the window resets. Use 100 to disable.',
|
||||
accountSchedulingThresholdsGlobalHint: 'System-wide default for every account on that platform. Individual accounts can still override this in the account editor.',
|
||||
accountSchedulingThresholdsDisabledHint: '100 disables platform auto-pause. Values 1–99 pause scheduling once utilization reaches that percent.',
|
||||
accountSchedulingThresholdsRangeHint: 'Integer 1–100 (percent). OpenAI/Anthropic/Grok only.'
|
||||
},
|
||||
upstreamBillingProbe: {
|
||||
title: 'Upstream Rate Auto Detection',
|
||||
|
||||
@@ -334,6 +334,10 @@ export default {
|
||||
rateLimited: '限流中',
|
||||
overloaded: '过载中',
|
||||
tempUnschedulable: '临时不可调度',
|
||||
accountSchedulingThresholdOverride: '账号自动停调阈值覆盖',
|
||||
accountSchedulingThresholdOverrideHint: '仅对当前账号覆盖平台级自动停调阈值;关闭后使用平台设置。',
|
||||
accountSchedulingThresholdOverrideValue: '账号阈值百分比',
|
||||
accountSchedulingThresholdOverrideDisabledHint: '1-100,达到该用量百分比后临时不可调度;100 表示禁用当前账号自动停调。',
|
||||
quotaExceeded: '配额超限',
|
||||
unschedulable: '不可调度',
|
||||
rateLimitedUntil: '限流中,当前不参与调度,预计 {time} 自动恢复',
|
||||
@@ -402,7 +406,11 @@ export default {
|
||||
grokFreeQuota24hHint: '按 sub2api 近 24 小时本地 Token 用量估算(上限 {limit})',
|
||||
grokWeeklyUsage: '周额度已用 {percent}%',
|
||||
grokUsed: '已用 $',
|
||||
grokMonthlyLimit: '月度已用/上限(由 billing cents 换算 USD)',
|
||||
grokBalance: '余额 $',
|
||||
grokPrepaid: '预付余额',
|
||||
grokMonthlyLimit: '月度已用/上限(USD)',
|
||||
grokOverage: '超额 onDemandUsed/onDemandCap',
|
||||
grokOverageShort: '超额 $',
|
||||
grokUnknown: 'Grok 配额需等待首次上游响应返回 xAI rate-limit 头后显示。',
|
||||
grokRetryAfter: '{time} 后重试',
|
||||
grokProbe: '探测',
|
||||
|
||||
@@ -991,6 +991,12 @@ export default {
|
||||
finalPricePreview: '最终每秒价格预览',
|
||||
notConfigured: '未配置'
|
||||
},
|
||||
explicitPricing: {
|
||||
title: 'Grok 搜索与 Voice 定价',
|
||||
description: '分组级 web_search(每千次)与 Voice realtime / TTS / STT 单价(USD)。留空表示未配置。',
|
||||
searchPricePer1k: '搜索每千次价格(USD)',
|
||||
pricePlaceholder: '可选'
|
||||
},
|
||||
voicePricing: {
|
||||
title: 'Grok Voice 定价',
|
||||
description: '分组级 Voice realtime / TTS / STT 单价(USD)。留空表示未配置。',
|
||||
|
||||
@@ -404,10 +404,10 @@ export default {
|
||||
allowUngroupedKey: '允许未分组 Key 调度',
|
||||
allowUngroupedKeyHint: '关闭后,未分配到任何分组的 API Key 将无法发起请求(返回 403)。建议保持关闭以确保所有 Key 都归属明确的分组。',
|
||||
accountSchedulingThresholdsTitle: '平台账号自动停调阈值',
|
||||
accountSchedulingThresholdsDescription: '按平台设置账号自动停调阈值,当账号当前原生使用窗口达到配置百分比时,自动停调该平台账号。',
|
||||
accountSchedulingThresholdsGlobalHint: '这是系统级全局设置,对该平台全部账号生效。',
|
||||
accountSchedulingThresholdsDisabledHint: '100 表示禁用该平台的自动停调阈值。',
|
||||
accountSchedulingThresholdsRangeHint: '范围 1-100,按百分比填写。'
|
||||
accountSchedulingThresholdsDescription: '当账号当前原生用量窗口(OpenAI Codex/Anthropic 会话,或 Grok 请求/Token 利用率)达到该百分比时,Sub2API 会临时将其移出调度,直到窗口重置。填 100 表示禁用。',
|
||||
accountSchedulingThresholdsGlobalHint: '系统级默认值,作用于该平台全部账号。可在账号编辑页对单个账号覆盖。',
|
||||
accountSchedulingThresholdsDisabledHint: '100 表示禁用该平台自动停调;1–99 表示达到该利用率后暂停调度。',
|
||||
accountSchedulingThresholdsRangeHint: '整数 1–100(百分比)。仅 OpenAI / Anthropic / Grok。'
|
||||
},
|
||||
upstreamBillingProbe: {
|
||||
title: '上游倍率自动探测',
|
||||
|
||||
@@ -573,6 +573,7 @@ export interface Group {
|
||||
// Codex 网页搜索单次价格(USD/次);null 表示使用默认价 0.01
|
||||
web_search_price_per_call: number | null
|
||||
// Grok Voice 显式定价(分组级)
|
||||
search_price_per_1k: number | null
|
||||
audio_realtime_price_per_min: number | null
|
||||
audio_tts_price_per_million_chars: number | null
|
||||
audio_stt_price_per_hour: number | null
|
||||
@@ -776,6 +777,7 @@ export interface CreateGroupRequest {
|
||||
video_price_1080p?: number | null
|
||||
video_model_prices?: VideoModelPrices
|
||||
web_search_price_per_call?: number | null
|
||||
search_price_per_1k?: number | null
|
||||
audio_realtime_price_per_min?: number | null
|
||||
audio_tts_price_per_million_chars?: number | null
|
||||
audio_stt_price_per_hour?: number | null
|
||||
@@ -835,6 +837,7 @@ export interface UpdateGroupRequest {
|
||||
video_price_1080p?: number | null
|
||||
video_model_prices?: VideoModelPrices
|
||||
web_search_price_per_call?: number | null
|
||||
search_price_per_1k?: number | null
|
||||
audio_realtime_price_per_min?: number | null
|
||||
audio_tts_price_per_million_chars?: number | null
|
||||
audio_stt_price_per_hour?: number | null
|
||||
@@ -1276,6 +1279,14 @@ export interface GrokBillingSummary {
|
||||
billing_period_start?: string
|
||||
billing_period_end?: string
|
||||
used_percent?: number | null
|
||||
/** Absolute USD money from billing probes */
|
||||
prepaid_balance?: number | null
|
||||
monthly_limit?: number | null
|
||||
monthly_used?: number | null
|
||||
on_demand_cap?: number | null
|
||||
on_demand_used?: number | null
|
||||
top_up_method?: string
|
||||
is_unified_billing_user?: boolean
|
||||
plan?: string
|
||||
status_code?: number
|
||||
source?: string
|
||||
@@ -1294,6 +1305,7 @@ export interface AccountUsageInfo {
|
||||
seven_day: UsageProgress | null
|
||||
seven_day_sonnet: UsageProgress | null
|
||||
seven_day_fable?: UsageProgress | null
|
||||
thirty_day?: UsageProgress | null
|
||||
gemini_shared_daily?: UsageProgress | null
|
||||
gemini_pro_daily?: UsageProgress | null
|
||||
gemini_flash_daily?: UsageProgress | null
|
||||
|
||||
@@ -1490,12 +1490,24 @@
|
||||
class="border-t border-gray-200 dark:border-dark-400 pt-4 mt-4"
|
||||
>
|
||||
<h4 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
{{ t("admin.groups.voicePricing.title") }}
|
||||
{{ t("admin.groups.explicitPricing.title") }}
|
||||
</h4>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mb-3">
|
||||
{{ t("admin.groups.voicePricing.description") }}
|
||||
{{ t("admin.groups.explicitPricing.description") }}
|
||||
</p>
|
||||
<div class="grid grid-cols-1 gap-3 md:grid-cols-3">
|
||||
<div>
|
||||
<label class="input-label">{{ t("admin.groups.explicitPricing.searchPricePer1k") }}</label>
|
||||
<input
|
||||
v-model.number="createForm.search_price_per_1k"
|
||||
type="number"
|
||||
step="0.000001"
|
||||
min="0"
|
||||
class="input"
|
||||
:placeholder="t('admin.groups.explicitPricing.pricePlaceholder')"
|
||||
data-testid="create-search-price"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="input-label">{{ t("admin.groups.voicePricing.audioRealtimePerMin") }}</label>
|
||||
<input
|
||||
@@ -3181,12 +3193,24 @@
|
||||
class="border-t border-gray-200 dark:border-dark-400 pt-4 mt-4"
|
||||
>
|
||||
<h4 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
{{ t("admin.groups.voicePricing.title") }}
|
||||
{{ t("admin.groups.explicitPricing.title") }}
|
||||
</h4>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mb-3">
|
||||
{{ t("admin.groups.voicePricing.description") }}
|
||||
{{ t("admin.groups.explicitPricing.description") }}
|
||||
</p>
|
||||
<div class="grid grid-cols-1 gap-3 md:grid-cols-3">
|
||||
<div>
|
||||
<label class="input-label">{{ t("admin.groups.explicitPricing.searchPricePer1k") }}</label>
|
||||
<input
|
||||
v-model.number="editForm.search_price_per_1k"
|
||||
type="number"
|
||||
step="0.000001"
|
||||
min="0"
|
||||
class="input"
|
||||
:placeholder="t('admin.groups.explicitPricing.pricePlaceholder')"
|
||||
data-testid="edit-search-price"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="input-label">{{ t("admin.groups.voicePricing.audioRealtimePerMin") }}</label>
|
||||
<input
|
||||
@@ -4908,6 +4932,7 @@ const createForm = reactive({
|
||||
video_model_prices: createVideoModelPricesForm(),
|
||||
// Codex 网页搜索按次计费(仅 openai 平台使用);null = 使用默认价 0.01
|
||||
web_search_price_per_call: null as number | null,
|
||||
search_price_per_1k: null as number | null,
|
||||
audio_realtime_price_per_min: null as number | null,
|
||||
audio_tts_price_per_million_chars: null as number | null,
|
||||
audio_stt_price_per_hour: null as number | null,
|
||||
@@ -5266,6 +5291,7 @@ const editForm = reactive({
|
||||
video_model_prices: createVideoModelPricesForm(),
|
||||
// Codex 网页搜索按次计费(仅 openai 平台使用);null = 使用默认价 0.01
|
||||
web_search_price_per_call: null as number | null,
|
||||
search_price_per_1k: null as number | null,
|
||||
audio_realtime_price_per_min: null as number | null,
|
||||
audio_tts_price_per_million_chars: null as number | null,
|
||||
audio_stt_price_per_hour: null as number | null,
|
||||
@@ -5719,6 +5745,7 @@ const closeCreateModal = () => {
|
||||
createForm.video_price_1080p = null;
|
||||
createForm.video_model_prices = createVideoModelPricesForm();
|
||||
createForm.web_search_price_per_call = null;
|
||||
createForm.search_price_per_1k = null;
|
||||
createForm.audio_realtime_price_per_min = null;
|
||||
createForm.audio_tts_price_per_million_chars = null;
|
||||
createForm.audio_stt_price_per_hour = null;
|
||||
@@ -5886,6 +5913,9 @@ const handleCreateGroup = async () => {
|
||||
requestData.video_price_480p = emptyToNull(requestData.video_price_480p);
|
||||
requestData.video_price_720p = emptyToNull(requestData.video_price_720p);
|
||||
requestData.video_price_1080p = emptyToNull(requestData.video_price_1080p);
|
||||
requestData.search_price_per_1k = emptyToNull(
|
||||
requestData.search_price_per_1k,
|
||||
);
|
||||
requestData.audio_realtime_price_per_min = emptyToNull(
|
||||
requestData.audio_realtime_price_per_min,
|
||||
);
|
||||
@@ -5955,6 +5985,7 @@ const handleEdit = async (group: AdminGroup) => {
|
||||
group.video_model_prices,
|
||||
);
|
||||
editForm.web_search_price_per_call = group.web_search_price_per_call ?? null;
|
||||
editForm.search_price_per_1k = group.search_price_per_1k ?? null;
|
||||
editForm.audio_realtime_price_per_min = group.audio_realtime_price_per_min ?? null;
|
||||
editForm.audio_tts_price_per_million_chars = group.audio_tts_price_per_million_chars ?? null;
|
||||
editForm.audio_stt_price_per_hour = group.audio_stt_price_per_hour ?? null;
|
||||
@@ -6039,6 +6070,7 @@ const closeEditModal = () => {
|
||||
editForm.video_price_1080p = null;
|
||||
editForm.video_model_prices = createVideoModelPricesForm();
|
||||
editForm.web_search_price_per_call = null;
|
||||
editForm.search_price_per_1k = null;
|
||||
editForm.audio_realtime_price_per_min = null;
|
||||
editForm.audio_tts_price_per_million_chars = null;
|
||||
editForm.audio_stt_price_per_hour = null;
|
||||
@@ -6147,6 +6179,9 @@ const handleUpdateGroup = async () => {
|
||||
payload.video_price_480p = emptyPriceToClear(payload.video_price_480p);
|
||||
payload.video_price_720p = emptyPriceToClear(payload.video_price_720p);
|
||||
payload.video_price_1080p = emptyPriceToClear(payload.video_price_1080p);
|
||||
payload.search_price_per_1k = emptyPriceToClear(
|
||||
payload.search_price_per_1k,
|
||||
);
|
||||
payload.audio_realtime_price_per_min = emptyPriceToClear(
|
||||
payload.audio_realtime_price_per_min,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user