fix(billing): 默认关闭 OpenAI 长上下文计费

This commit is contained in:
benjamin
2026-07-13 23:32:16 +08:00
parent f63d168ae0
commit e9fb5983cd
19 changed files with 182 additions and 73 deletions
@@ -2840,7 +2840,7 @@
data-testid="openai-long-context-billing-toggle"
role="switch"
:aria-checked="openAILongContextBillingEnabled"
@click="openAILongContextBillingEnabled = !openAILongContextBillingEnabled"
@click="toggleOpenAILongContextBilling"
:class="[
'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2',
openAILongContextBillingEnabled ? 'bg-primary-600' : 'bg-gray-200 dark:bg-dark-600'
@@ -3708,7 +3708,8 @@ const fillHeaderOverrideTemplate = () => {
const interceptWarmupRequests = ref(false)
const autoPauseOnExpired = ref(true)
const openaiPassthroughEnabled = ref(false)
const openAILongContextBillingEnabled = ref(true)
const openAILongContextBillingEnabled = ref(false)
const openAILongContextBillingTouched = ref(false)
const openAICompactMode = ref<OpenAICompactMode>('auto')
const openAIResponsesMode = ref<OpenAIResponsesMode>('auto')
const openAIEndpointCapabilities = ref<OpenAIEndpointCapability[]>(['chat_completions', 'embeddings'])
@@ -3721,6 +3722,11 @@ const anthropicPassthroughEnabled = ref(false)
const anthropicAPIKeyAuthScheme = ref<AnthropicAPIKeyAuthScheme>('x_api_key')
const webSearchEmulationMode = ref('default')
const webSearchGlobalEnabled = ref(false)
const toggleOpenAILongContextBilling = () => {
openAILongContextBillingEnabled.value = !openAILongContextBillingEnabled.value
openAILongContextBillingTouched.value = true
}
const {
globalEnabled: quotaNotifyGlobalEnabled,
state: quotaNotifyState,
@@ -4563,7 +4569,8 @@ const resetForm = () => {
interceptWarmupRequests.value = false
autoPauseOnExpired.value = true
openaiPassthroughEnabled.value = false
openAILongContextBillingEnabled.value = true
openAILongContextBillingEnabled.value = false
openAILongContextBillingTouched.value = false
openAICompactMode.value = 'auto'
openAIResponsesMode.value = 'auto'
openAIEndpointCapabilities.value = ['chat_completions', 'embeddings']
@@ -4687,7 +4694,9 @@ const buildOpenAICodexImportExtra = (): Record<string, unknown> | undefined => {
if (!extra) {
return undefined
}
delete extra.openai_long_context_billing_enabled
if (!openAILongContextBillingTouched.value) {
delete extra.openai_long_context_billing_enabled
}
return Object.keys(extra).length > 0 ? extra : undefined
}
@@ -1786,9 +1786,9 @@
/>
</div>
<!-- OpenAI OAuth Codex 官方客户端限制开关 -->
<!-- OpenAI API 长上下文计费开关 -->
<div
v-if="account?.platform === 'openai' && (account?.type === 'oauth' || account?.type === 'setup-token' || account?.type === 'apikey')"
v-if="account?.platform === 'openai' && !isSparkShadow && (account?.type === 'oauth' || account?.type === 'setup-token' || account?.type === 'apikey')"
class="border-t border-gray-200 pt-4 dark:border-dark-600"
>
<div class="flex items-center justify-between gap-4">
@@ -2815,7 +2815,7 @@ const customBaseUrl = ref('')
// OpenAI 自动透传开关(OAuth/API Key)
const openaiPassthroughEnabled = ref(false)
const openAILongContextBillingEnabled = ref(true)
const openAILongContextBillingEnabled = ref(false)
// OpenAI 订阅档位(Plus/Pro/Free)手动覆盖值,存于 credentials.plan_type;'' 表示清空/自动识别
const editPlanType = ref<string>('')
const openAICompactMode = ref<OpenAICompactMode>('auto')
@@ -3249,7 +3249,7 @@ const syncFormFromAccount = (newAccount: Account | null) => {
// Load OpenAI passthrough toggle (OpenAI OAuth/SetupToken/API Key)
openaiPassthroughEnabled.value = false
openAILongContextBillingEnabled.value = true
openAILongContextBillingEnabled.value = false
editPlanType.value = ''
openAICompactMode.value = 'auto'
openAIResponsesMode.value = 'auto'
@@ -3266,7 +3266,7 @@ const syncFormFromAccount = (newAccount: Account | null) => {
if (newAccount.platform === 'openai' && (newAccount.type === 'oauth' || newAccount.type === 'setup-token' || newAccount.type === 'apikey')) {
openaiPassthroughEnabled.value = extra?.openai_passthrough === true || extra?.openai_oauth_passthrough === true
const longContextBillingValue = extra?.openai_long_context_billing_enabled
openAILongContextBillingEnabled.value = longContextBillingValue === undefined || longContextBillingValue === true
openAILongContextBillingEnabled.value = longContextBillingValue === true
// plan_type 手动覆盖仅 OAuth 有实际调度语义(IsOpenAIChatGPTSubscription 要求 oauth),故只对 oauth 回填
editPlanType.value = newAccount.type === 'oauth'
? readPlanType(newAccount.credentials as Record<string, unknown> | undefined)
@@ -4437,7 +4437,11 @@ const handleSubmit = async () => {
delete newExtra.openai_passthrough
delete newExtra.openai_oauth_passthrough
}
newExtra.openai_long_context_billing_enabled = openAILongContextBillingEnabled.value
if (isSparkShadow.value) {
delete newExtra.openai_long_context_billing_enabled
} else {
newExtra.openai_long_context_billing_enabled = openAILongContextBillingEnabled.value
}
if (openAICompactMode.value === 'auto') {
delete newExtra.openai_compact_mode
} else {
@@ -100,7 +100,7 @@ async function selectButtonByText(wrapper: ReturnType<typeof mountModal>, text:
await button?.trigger('click')
}
async function submitApiKeyAccount(platform: 'openai' | 'anthropic', disableLongContextBilling = false) {
async function submitApiKeyAccount(platform: 'openai' | 'anthropic', enableLongContextBilling = false) {
const wrapper = mountModal()
await selectButtonByText(wrapper, platform === 'openai' ? 'OpenAI' : 'admin.accounts.claudeConsole')
if (platform === 'openai') {
@@ -108,16 +108,19 @@ async function submitApiKeyAccount(platform: 'openai' | 'anthropic', disableLong
}
await wrapper.get('form#create-account-form input[type="text"]').setValue(`${platform} account`)
await wrapper.get('form#create-account-form input[type="password"]').setValue('test-api-key')
if (disableLongContextBilling) {
if (enableLongContextBilling) {
await wrapper.get('[data-testid="openai-long-context-billing-toggle"]').trigger('click')
}
await wrapper.get('form#create-account-form').trigger('submit.prevent')
await flushPromises()
}
async function openCodexImportStep() {
async function openCodexImportStep(toggleClicks = 0) {
const wrapper = mountModal()
await selectButtonByText(wrapper, 'OpenAI')
for (let click = 0; click < toggleClicks; click += 1) {
await wrapper.get('[data-testid="openai-long-context-billing-toggle"]').trigger('click')
}
await wrapper.get('form#create-account-form input[type="text"]').setValue('Codex import')
await wrapper.get('form#create-account-form').trigger('submit.prevent')
return wrapper
@@ -137,18 +140,18 @@ describe('CreateAccountModal OpenAI long-context billing', () => {
createOpenAICodexPATMock.mockReset().mockResolvedValue({})
})
it('sends true explicitly for normal OpenAI account creation by default', async () => {
it('sends false explicitly for normal OpenAI account creation by default', async () => {
await submitApiKeyAccount('openai')
expect(createAccountMock).toHaveBeenCalledTimes(1)
expect(createAccountMock.mock.calls[0]?.[0]?.extra?.openai_long_context_billing_enabled).toBe(true)
expect(createAccountMock.mock.calls[0]?.[0]?.extra?.openai_long_context_billing_enabled).toBe(false)
})
it('sends false explicitly when OpenAI long-context billing is disabled', async () => {
it('sends true explicitly when OpenAI long-context billing is enabled', async () => {
await submitApiKeyAccount('openai', true)
expect(createAccountMock).toHaveBeenCalledTimes(1)
expect(createAccountMock.mock.calls[0]?.[0]?.extra?.openai_long_context_billing_enabled).toBe(false)
expect(createAccountMock.mock.calls[0]?.[0]?.extra?.openai_long_context_billing_enabled).toBe(true)
})
it('omits the OpenAI setting for non-OpenAI account creation', async () => {
@@ -175,4 +178,36 @@ describe('CreateAccountModal OpenAI long-context billing', () => {
expect(createOpenAICodexPATMock).toHaveBeenCalledTimes(1)
expect(createOpenAICodexPATMock.mock.calls[0]?.[0]?.extra?.openai_long_context_billing_enabled).toBeUndefined()
})
it('sends explicit true for Codex session import after the toggle is enabled', async () => {
const wrapper = await openCodexImportStep(1)
await wrapper.get('[data-testid="import-codex-session"]').trigger('click')
await flushPromises()
expect(importCodexSessionMock.mock.calls[0]?.[0]?.extra?.openai_long_context_billing_enabled).toBe(true)
})
it('sends explicit false for Codex session import after the toggle is changed back', async () => {
const wrapper = await openCodexImportStep(2)
await wrapper.get('[data-testid="import-codex-session"]').trigger('click')
await flushPromises()
expect(importCodexSessionMock.mock.calls[0]?.[0]?.extra?.openai_long_context_billing_enabled).toBe(false)
})
it('sends explicit true for Codex PAT import after the toggle is enabled', async () => {
const wrapper = await openCodexImportStep(1)
await wrapper.get('[data-testid="import-codex-pat"]').trigger('click')
await flushPromises()
expect(createOpenAICodexPATMock.mock.calls[0]?.[0]?.extra?.openai_long_context_billing_enabled).toBe(true)
})
it('sends explicit false for Codex PAT import after the toggle is changed back', async () => {
const wrapper = await openCodexImportStep(2)
await wrapper.get('[data-testid="import-codex-pat"]').trigger('click')
await flushPromises()
expect(createOpenAICodexPATMock.mock.calls[0]?.[0]?.extra?.openai_long_context_billing_enabled).toBe(false)
})
})
@@ -416,7 +416,7 @@ describe('EditAccountModal', () => {
expect(updateAccountMock.mock.calls[0]?.[1]?.extra?.openai_long_context_billing_enabled).toBe(false)
})
it('defaults legacy OpenAI accounts to long-context billing enabled', async () => {
it('defaults legacy OpenAI accounts to long-context billing disabled', async () => {
const account = buildAccount()
updateAccountMock.mockReset()
checkMixedChannelRiskMock.mockReset()
@@ -425,12 +425,33 @@ describe('EditAccountModal', () => {
const wrapper = mountModal(account)
const toggle = wrapper.get('[data-testid="openai-long-context-billing-toggle"]')
expect(toggle.attributes('aria-checked')).toBe('true')
expect(toggle.attributes('aria-checked')).toBe('false')
await wrapper.get('form#edit-account-form').trigger('submit.prevent')
expect(updateAccountMock).toHaveBeenCalledTimes(1)
expect(updateAccountMock.mock.calls[0]?.[1]?.extra?.openai_long_context_billing_enabled).toBe(true)
expect(updateAccountMock.mock.calls[0]?.[1]?.extra?.openai_long_context_billing_enabled).toBe(false)
})
it('does not render or submit the long-context billing toggle for Spark shadow accounts', async () => {
const account = buildOpenAISparkShadowAccount()
account.extra = {
openai_long_context_billing_enabled: false
}
updateAccountMock.mockReset()
checkMixedChannelRiskMock.mockReset()
checkMixedChannelRiskMock.mockResolvedValue({ has_risk: false })
updateAccountMock.mockResolvedValue(account)
const wrapper = mountModal(account)
expect(wrapper.find('[data-testid="openai-long-context-billing-toggle"]').exists()).toBe(false)
await wrapper.get('form#edit-account-form').trigger('submit.prevent')
expect(updateAccountMock).toHaveBeenCalledTimes(1)
expect(updateAccountMock.mock.calls[0]?.[1]?.extra).not.toHaveProperty(
'openai_long_context_billing_enabled'
)
})
it('preserves an explicit OpenAI long-context billing opt-out', async () => {
@@ -404,7 +404,7 @@ export default {
'When enabled, this OpenAI account uses automatic passthrough: the gateway forwards request/response as-is and only swaps auth, while keeping billing/concurrency/audit and necessary safety filtering.',
longContextBilling: 'API long-context pricing',
longContextBillingDesc:
'Enabled by default. Disable only when this account\'s upstream does not charge OpenAI API long-context rates above the model threshold.',
'Disabled by default. Enable only when this account\'s upstream charges OpenAI API long-context rates above the model threshold.',
responsesWebsocketsV2: 'Responses WebSocket v2',
responsesWebsocketsV2Desc:
'Disabled by default. Enable to allow responses_websockets_v2 capability (still gated by global and account-type switches).',
@@ -506,7 +506,7 @@ export default {
oauthPassthroughDesc:
'开启后,该 OpenAI 账号将自动透传请求与响应,仅替换认证并保留计费/并发/审计及必要安全过滤;如遇兼容性问题可随时关闭回滚。',
longContextBilling: 'API 长上下文计费',
longContextBillingDesc: '默认开启。仅当该账号的上游不会按模型阈值收取 OpenAI API 长上下文费率时关闭。',
longContextBillingDesc: '默认关闭。仅当该账号的上游会按模型阈值收取 OpenAI API 长上下文费率时开启。',
responsesWebsocketsV2: 'Responses WebSocket v2',
responsesWebsocketsV2Desc:
'默认关闭。开启后可启用 responses_websockets_v2 协议能力(受网关全局开关与账号类型开关约束)。',