mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-31 08:25:35 +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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user