feat(billing-probe): extend upstream billing probe to all API-key platforms

/v1/sub2api/billing is a key-scoped sub2api convention: any API-key
account whose base_url points at a sub2api-compatible upstream answers
it regardless of the account platform. Widen probe eligibility from
platform=openai to every API-key account (OAuth/Bedrock stay excluded:
no static key to present).

- central predicate exported as IsUpstreamBillingProbeIdentity; runner,
  manual probe, SetAccountEnabled, admin create/update/bulk validation
  and CRS reconcile all follow it
- probe target resolution reads credentials.api_key/base_url directly.
  OpenAI keeps its official-default base URL and openai transport
  profile. Other platforms whose base_url is empty or points at an
  official provider API domain (anthropic.com, googleapis.com, x.ai,
  grok.com, openai.com - matched on the normalized hostname, port and
  trailing dot stripped, as the exact host or any subdomain) persist
  "unsupported" without sending a request: the create form fills empty
  base_url with official defaults and offers official regional presets
  (e.g. us-east-1.api.x.ai), and official APIs cannot answer
  /v1/sub2api/billing, so probing would only send the account key to a
  nonexistent official path
- due-scan SQL, BulkUpdate probe WHERE, UpdateCredentials stale-snapshot
  CASE and proxy-change invalidation drop their platform filters
  (type='apikey' retained)
- frontend: rate cell, edit/create/bulk modals gate on type==='apikey';
  the antigravity upstream create flow (its own helper and form section)
  shows the auto-probe toggle and passes upstream_billing_probe_enabled;
  bulk WS-mode section stays OpenAI-only; settings copy de-scoped
- tests: multiplatform service unit tests (relay success, official/empty
  base_url unsupported without request, normalized official-host matrix,
  OpenAI defaults preserved), real-PostgreSQL due-scan coverage for
  openai/anthropic/grok plus oauth/disabled exclusion, updated
  sqlmock/CRS/frontend specs; probe stays opt-in per account
This commit is contained in:
Brisbanehuang committed 2026-08-01 02:31:04 -04:00
1 parent b22f73e725
commit f3a3d86845
20 files changed
+481 -42

No files matched your search

@@ -905,8 +905,8 @@
</div>
</div>
<!-- Upstream billing auto probe (OpenAI API Key only) -->
<div v-if="allOpenAIAPIKey" class="border-t border-gray-200 pt-4 dark:border-dark-600">
<!-- Upstream billing auto probe (any API-key platform) -->
<div v-if="allBillingProbeCapable" class="border-t border-gray-200 pt-4 dark:border-dark-600">
<div class="mb-3 flex items-center justify-between">
<div class="flex-1 pr-4">
<label
@@ -1401,6 +1401,15 @@ const allOpenAIAPIKey = computed(() => {
)
})
// 上游倍率自动探测已放宽到全部 API-key 平台:只要求所选类型全为 apikey,
// 平台不限(sub2api 上游即可应答 /v1/sub2api/billing)。
const allBillingProbeCapable = computed(() => {
return (
targetSelectedTypes.value.length > 0 &&
targetSelectedTypes.value.every(t => t === 'apikey')
)
})
// 是否全部为 anthropic/openai 平台的 apikey 账号(请求头覆写仅在此条件下显示)
// 所选平台 × 所选类型的全组合均需具备覆写资格(实际选中账号是该组合的子集,
// 按交叉积判定偏保守但绝不放行不合资格的账号)
@@ -888,6 +888,20 @@
/>
<p class="input-hint">{{ t('admin.accounts.upstream.apiKeyHint') }}</p>
</div>
<!-- 上游倍率自动探测:antigravity upstream 也是 API-key 账号 -->
<div class="flex items-center justify-between gap-4 border-t border-gray-200 pt-4 dark:border-dark-600">
<div>
<label class="input-label mb-0">{{ t('admin.accounts.upstreamBilling.autoProbe') }}</label>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
{{ t('admin.accounts.upstreamBilling.autoProbeHint') }}
</p>
</div>
<Toggle
v-model="upstreamBillingAutoProbeEnabled"
data-testid="upstream-billing-auto-probe-antigravity"
:aria-label="t('admin.accounts.upstreamBilling.autoProbe')"
/>
</div>
</div>
<!-- Vertex Service Account -->
@@ -1143,8 +1157,8 @@
<p v-if="apiKeyHint" class="input-hint">{{ apiKeyHint }}</p>
</div>
<!-- 上游倍率自动探测:全部 API-key 平台可用(所在区块已限定 apikey 类型) -->
<div
v-if="form.platform === 'openai'"
class="flex items-center justify-between gap-4 border-t border-gray-200 pt-4 dark:border-dark-600"
>
<div>
@@ -4608,7 +4622,6 @@ const submitCreateAccount = async (payload: CreateAccountRequest) => {
try {
const account = await adminAPI.accounts.create(withAntigravityConfirmFlag(payload))
if (
payload.platform === 'openai' &&
payload.type === 'apikey' &&
payload.upstream_billing_probe_enabled === true
) {
@@ -5164,8 +5177,7 @@ const handleSubmit = async () => {
...form,
group_ids: form.group_ids,
extra,
upstream_billing_probe_enabled:
form.platform === 'openai' ? upstreamBillingAutoProbeEnabled.value : undefined,
upstream_billing_probe_enabled: upstreamBillingAutoProbeEnabled.value,
auto_pause_on_expired: autoPauseOnExpired.value
})
}
@@ -5294,6 +5306,9 @@ const createAccountAndFinish = async (
rate_multiplier: form.rate_multiplier,
group_ids: form.group_ids,
expires_at: form.expires_at,
// 上游倍率探测对全部 API-key 平台开放(antigravity upstream 走本 helper);
// 非 apikey 类型(bedrock/oauth)不传,后端不动作。
upstream_billing_probe_enabled: type === 'apikey' ? upstreamBillingAutoProbeEnabled.value : undefined,
auto_pause_on_expired: autoPauseOnExpired.value
})
}
@@ -1650,7 +1650,7 @@
</div>
<div
v-if="account?.platform === 'openai' && account?.type === 'apikey'"
v-if="account?.type === 'apikey'"
class="flex items-center justify-between gap-4 border-t border-gray-200 pt-4 dark:border-dark-600"
>
<div>
@@ -4584,7 +4584,6 @@ const handleSubmit = async () => {
} else {
newExtra.openai_responses_mode = openAIResponsesMode.value
}
newExtra.upstream_billing_probe_enabled = upstreamBillingAutoProbeEnabled.value
}
if (autoPause5hThreshold.value != null && autoPause5hThreshold.value > 0) {
newExtra.auto_pause_5h_threshold = autoPause5hThreshold.value / 100
@@ -4649,6 +4648,11 @@ const handleSubmit = async () => {
const currentExtra = (updatePayload.extra as Record<string, unknown>) ||
(props.account.extra as Record<string, unknown>) || {}
const newExtra: Record<string, unknown> = { ...currentExtra }
// 上游倍率自动探测对全部 API-key 平台开放(sub2api 上游即可应答),
// Bedrock 凭证无静态 Key 不参与。
if (props.account.type === 'apikey') {
newExtra.upstream_billing_probe_enabled = upstreamBillingAutoProbeEnabled.value
}
// Total quota
if (editQuotaLimit.value != null && editQuotaLimit.value > 0) {
newExtra.quota_limit = editQuotaLimit.value
@@ -105,7 +105,8 @@ defineEmits<{
const { t } = useI18n()
const CLOCK_SKEW_TOLERANCE_MS = 5 * 60 * 1000
const eligible = computed(() => props.account.platform === 'openai' && props.account.type === 'apikey')
// 探测资格已放宽到全部 API-key 平台(上游是 sub2api 即可应答)。
const eligible = computed(() => props.account.type === 'apikey')
const snapshot = computed<UpstreamBillingProbeSnapshot | undefined>(() => props.account.extra?.upstream_billing_probe)
const data = computed(() => snapshot.value?.data)
const probeEnabled = computed(() => props.account.extra?.upstream_billing_probe_enabled === true)
@@ -387,6 +387,23 @@ describe('BulkEditAccountModal', () => {
})
})
it('非 OpenAI 平台的 API Key 批量编辑同样可开启上游倍率自动探测', async () => {
// 探测已放宽到全部 API-key 平台,混合平台选择只要求类型全为 apikey。
const wrapper = mountModal({
selectedPlatforms: ['grok', 'anthropic'],
selectedTypes: ['apikey']
})
await wrapper.get('#bulk-edit-upstream-billing-auto-probe-enabled').setValue(true)
await wrapper.get('#bulk-edit-account-form').trigger('submit.prevent')
await flushPromises()
expect(adminAPI.accounts.bulkUpdate).toHaveBeenCalledTimes(1)
expect(adminAPI.accounts.bulkUpdate).toHaveBeenCalledWith([1, 2], {
upstream_billing_probe_enabled: true
})
})
it('OpenAI API Key 批量编辑可统一关闭上游倍率自动探测', async () => {
const wrapper = mountModal({
selectedPlatforms: ['openai'],
@@ -254,7 +254,39 @@ describe('CreateAccountModal OpenAI long-context billing', () => {
expect(createAccountMock).toHaveBeenCalledTimes(1)
expect(createAccountMock.mock.calls[0]?.[0]?.extra?.openai_long_context_billing_enabled).toBeUndefined()
expect(createAccountMock.mock.calls[0]?.[0]?.upstream_billing_probe_enabled).toBeUndefined()
// 上游倍率探测已放宽到全部 API-key 平台:非 OpenAI 平台与 OpenAI 一致,默认开启。
expect(createAccountMock.mock.calls[0]?.[0]?.upstream_billing_probe_enabled).toBe(true)
})
it('sends an explicit disabled state when the non-OpenAI create toggle is turned off', async () => {
await submitApiKeyAccount('anthropic', false, true)
expect(createAccountMock.mock.calls[0]?.[0]?.upstream_billing_probe_enabled).toBe(false)
})
it('antigravity upstream 创建默认携带上游倍率探测开关', async () => {
// antigravity upstream 走独立创建 helper,
// 也必须与其余 API-key 平台一样默认开启探测并传递开关。
const wrapper = mountModal()
await selectButtonByText(wrapper, 'Antigravity')
await selectButtonByText(wrapper, 'admin.accounts.types.antigravityApikey')
await wrapper.get('form#create-account-form input[type="text"]').setValue('antigravity relay')
const baseInput = wrapper
.findAll('input')
.find((candidate) => candidate.attributes('placeholder') === 'https://cloudcode-pa.googleapis.com')
expect(baseInput).toBeDefined()
await baseInput?.setValue('https://relay.example')
await wrapper.get('form#create-account-form input[type="password"]').setValue('sk-upstream')
await wrapper.get('form#create-account-form').trigger('submit.prevent')
await flushPromises()
expect(createAccountMock).toHaveBeenCalledTimes(1)
const payload = createAccountMock.mock.calls[0]?.[0]
expect(payload?.platform).toBe('antigravity')
expect(payload?.type).toBe('apikey')
expect(payload?.upstream_billing_probe_enabled).toBe(true)
// 创建成功后前端立即发起一次首探(与其他 apikey 平台一致)。
expect(probeUpstreamBillingMock).toHaveBeenCalledWith(42)
})
it('leaves Codex session import billing ownership to the backend', async () => {
@@ -657,6 +657,28 @@ describe('EditAccountModal', () => {
expect(updateAccountMock.mock.calls[0]?.[1]?.extra?.upstream_billing_probe_enabled).toBe(true)
})
it('exposes the upstream billing auto-probe toggle for non-OpenAI API-key accounts', async () => {
// 探测已放宽到全部 API-key 平台:grok 账号同样能开启并保存。
const account = buildAccount()
account.platform = 'grok'
account.name = 'grok-relay'
account.credentials = { api_key: 'sk-grok', base_url: 'https://relay.example/v1' }
updateAccountMock.mockReset()
checkMixedChannelRiskMock.mockReset()
checkMixedChannelRiskMock.mockResolvedValue({ has_risk: false })
updateAccountMock.mockResolvedValue(account)
const wrapper = mountModal(account)
const toggle = wrapper.get('[data-testid="upstream-billing-auto-probe"]')
expect(toggle.attributes('aria-checked')).toBe('false')
await toggle.trigger('click')
await wrapper.get('form#edit-account-form').trigger('submit.prevent')
expect(updateAccountMock).toHaveBeenCalledTimes(1)
expect(updateAccountMock.mock.calls[0]?.[1]?.extra?.upstream_billing_probe_enabled).toBe(true)
})
it('clears OpenAI APIKey Responses override when set back to auto', async () => {
const account = buildAccount()
account.extra = {
@@ -268,6 +268,11 @@ describe('UpstreamBillingRateCell', () => {
await wrapper.get('[data-testid="upstream-billing-probe"]').trigger('click')
expect(wrapper.emitted('probe')).toHaveLength(1)
// 探测已放宽到全部 API-key 平台:grok API-key 账号同样可探测。
await wrapper.setProps({ account: makeAccount({ platform: 'grok' }) })
await wrapper.get('[data-testid="upstream-billing-probe"]').trigger('click')
expect(wrapper.emitted('probe')).toHaveLength(2)
await wrapper.setProps({ account: makeAccount({ type: 'oauth' }) })
expect(wrapper.findAll('button')).toHaveLength(0)
expect(wrapper.text()).toBe('-')
@@ -361,7 +361,7 @@ export default {
},
upstreamBillingProbe: {
title: 'Upstream Rate Auto Detection',
description: 'Periodically retrieve billing rates declared by upstream Sub2API sites connected to OpenAI API keys.',
description: 'Periodically retrieve billing rates declared by upstream Sub2API sites connected to API key accounts.',
enabled: 'Enable global auto detection',
enabledHint: 'When enabled, scheduled detection runs only for accounts that also enable automatic detection. Disabling stops all scheduled detection; manual detection remains available.',
intervalMinutes: 'Detection interval (minutes)',
@@ -354,7 +354,7 @@ export default {
},
upstreamBillingProbe: {
title: '上游倍率自动探测',
description: '定期获取 OpenAI API Key 所连接上游 Sub2API 站点声明的计费倍率。',
description: '定期获取 API Key 账号所连接上游 Sub2API 站点声明的计费倍率。',
enabled: '启用全局自动探测',
enabledHint: '开启后,仅对账号自身已启用自动检测的账号执行定时探测;关闭后停止所有定时探测,手动探测不受影响。',
intervalMinutes: '探测周期(分钟)',