fix(billing): 默认开启 OpenAI 长上下文计费

This commit is contained in:
benjamin committed 2026-07-13 17:55:01 +08:00
1 parent 0d9c140bc2
commit a0ac5e0240
22 files changed
+1517 -66

No files matched your search

@@ -3708,7 +3708,7 @@ const fillHeaderOverrideTemplate = () => {
const interceptWarmupRequests = ref(false)
const autoPauseOnExpired = ref(true)
const openaiPassthroughEnabled = ref(false)
const openAILongContextBillingEnabled = ref(false)
const openAILongContextBillingEnabled = ref(true)
const openAICompactMode = ref<OpenAICompactMode>('auto')
const openAIResponsesMode = ref<OpenAIResponsesMode>('auto')
const openAIEndpointCapabilities = ref<OpenAIEndpointCapability[]>(['chat_completions', 'embeddings'])
@@ -4563,7 +4563,7 @@ const resetForm = () => {
interceptWarmupRequests.value = false
autoPauseOnExpired.value = true
openaiPassthroughEnabled.value = false
openAILongContextBillingEnabled.value = false
openAILongContextBillingEnabled.value = true
openAICompactMode.value = 'auto'
openAIResponsesMode.value = 'auto'
openAIEndpointCapabilities.value = ['chat_completions', 'embeddings']
@@ -4646,11 +4646,7 @@ const buildOpenAIExtra = (base?: Record<string, unknown>): Record<string, unknow
delete extra.openai_passthrough
delete extra.openai_oauth_passthrough
}
if (openAILongContextBillingEnabled.value) {
extra.openai_long_context_billing_enabled = true
} else {
delete extra.openai_long_context_billing_enabled
}
extra.openai_long_context_billing_enabled = openAILongContextBillingEnabled.value
if (accountCategory.value === 'oauth-based' && codexCLIOnlyEnabled.value) {
extra.codex_cli_only = true
@@ -4686,6 +4682,15 @@ const buildOpenAIExtra = (base?: Record<string, unknown>): Record<string, unknow
return Object.keys(extra).length > 0 ? extra : undefined
}
const buildOpenAICodexImportExtra = (): Record<string, unknown> | undefined => {
const extra = buildOpenAIExtra()
if (!extra) {
return undefined
}
delete extra.openai_long_context_billing_enabled
return Object.keys(extra).length > 0 ? extra : undefined
}
const buildAnthropicExtra = (base?: Record<string, unknown>): Record<string, unknown> | undefined => {
if (form.platform !== 'anthropic' || accountCategory.value !== 'apikey') {
return base
@@ -5371,7 +5376,7 @@ const handleOpenAIImportCodexSession = async (content: string) => {
oauthClient.error.value = ''
try {
const extra = buildOpenAIExtra()
const extra = buildOpenAICodexImportExtra()
const result = await adminAPI.accounts.importCodexSession({
content: trimmed,
name: form.name,
@@ -5449,7 +5454,7 @@ const handleOpenAIImportCodexPAT = async (accessToken: string) => {
oauthClient.error.value = ''
try {
const extra = buildOpenAIExtra()
const extra = buildOpenAICodexImportExtra()
await adminAPI.accounts.createOpenAICodexPAT({
access_token: trimmed,
name: form.name,
@@ -2794,7 +2794,7 @@ const customBaseUrl = ref('')
// OpenAI 自动透传开关(OAuth/API Key)
const openaiPassthroughEnabled = ref(false)
const openAILongContextBillingEnabled = ref(false)
const openAILongContextBillingEnabled = ref(true)
const openAICompactMode = ref<OpenAICompactMode>('auto')
const openAIResponsesMode = ref<OpenAIResponsesMode>('auto')
const openAIEndpointCapabilities = ref<OpenAIEndpointCapability[]>(['chat_completions', 'embeddings'])
@@ -3222,7 +3222,7 @@ const syncFormFromAccount = (newAccount: Account | null) => {
// Load OpenAI passthrough toggle (OpenAI OAuth/SetupToken/API Key)
openaiPassthroughEnabled.value = false
openAILongContextBillingEnabled.value = false
openAILongContextBillingEnabled.value = true
openAICompactMode.value = 'auto'
openAIResponsesMode.value = 'auto'
openAIEndpointCapabilities.value = ['chat_completions', 'embeddings']
@@ -3237,7 +3237,8 @@ const syncFormFromAccount = (newAccount: Account | null) => {
webSearchEmulationMode.value = 'default'
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
openAILongContextBillingEnabled.value = extra?.openai_long_context_billing_enabled === true
const longContextBillingValue = extra?.openai_long_context_billing_enabled
openAILongContextBillingEnabled.value = longContextBillingValue === undefined || longContextBillingValue === true
openAICompactMode.value = (extra?.openai_compact_mode as OpenAICompactMode) || 'auto'
if (newAccount.type === 'apikey') {
openAIResponsesMode.value = normalizeOpenAIResponsesMode(extra?.openai_responses_mode)
@@ -0,0 +1,178 @@
import { defineComponent } from 'vue'
import { flushPromises, mount } from '@vue/test-utils'
import { beforeEach, describe, expect, it, vi } from 'vitest'
const {
createAccountMock,
importCodexSessionMock,
createOpenAICodexPATMock,
} = vi.hoisted(() => ({
createAccountMock: vi.fn(),
importCodexSessionMock: vi.fn(),
createOpenAICodexPATMock: vi.fn(),
}))
vi.mock('@/stores/app', () => ({
useAppStore: () => ({
showError: vi.fn(),
showSuccess: vi.fn(),
showWarning: vi.fn(),
}),
}))
vi.mock('@/stores/auth', () => ({
useAuthStore: () => ({ isSimpleMode: true }),
}))
vi.mock('@/api/admin', () => ({
adminAPI: {
accounts: {
create: createAccountMock,
checkMixedChannelRisk: vi.fn().mockResolvedValue({ has_risk: false }),
importCodexSession: importCodexSessionMock,
createOpenAICodexPAT: createOpenAICodexPATMock,
},
settings: {
getWebSearchEmulationConfig: vi.fn().mockResolvedValue({ enabled: false, providers: [] }),
getSettings: vi.fn().mockResolvedValue({}),
},
tlsFingerprintProfiles: {
list: vi.fn().mockResolvedValue([]),
},
},
}))
vi.mock('@/api/admin/accounts', () => ({
getAntigravityDefaultModelMapping: vi.fn().mockResolvedValue([]),
}))
vi.mock('vue-i18n', async () => {
const actual = await vi.importActual<typeof import('vue-i18n')>('vue-i18n')
return {
...actual,
useI18n: () => ({ t: (key: string) => key }),
}
})
import CreateAccountModal from '../CreateAccountModal.vue'
const BaseDialogStub = defineComponent({
name: 'BaseDialog',
props: { show: { type: Boolean, default: false } },
template: '<div v-if="show"><slot /><slot name="footer" /></div>',
})
const OAuthAuthorizationFlowStub = defineComponent({
name: 'OAuthAuthorizationFlow',
emits: ['import-codex-session', 'import-codex-pat'],
template: `
<div>
<button data-testid="import-codex-session" @click="$emit('import-codex-session', 'session-json')">session</button>
<button data-testid="import-codex-pat" @click="$emit('import-codex-pat', 'pat-token')">pat</button>
</div>
`,
})
function mountModal() {
return mount(CreateAccountModal, {
props: { show: true, proxies: [], groups: [] },
global: {
stubs: {
BaseDialog: BaseDialogStub,
OAuthAuthorizationFlow: OAuthAuthorizationFlowStub,
ConfirmDialog: true,
Select: true,
Icon: true,
PlatformIcon: true,
ProxySelector: true,
ProxyAdBanner: true,
GroupSelector: true,
ModelWhitelistSelector: true,
QuotaLimitCard: true,
},
},
})
}
async function selectButtonByText(wrapper: ReturnType<typeof mountModal>, text: string) {
const button = wrapper.findAll('button').find((candidate) => candidate.text().includes(text))
expect(button).toBeDefined()
await button?.trigger('click')
}
async function submitApiKeyAccount(platform: 'openai' | 'anthropic', disableLongContextBilling = false) {
const wrapper = mountModal()
await selectButtonByText(wrapper, platform === 'openai' ? 'OpenAI' : 'admin.accounts.claudeConsole')
if (platform === 'openai') {
await selectButtonByText(wrapper, 'API Key')
}
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) {
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() {
const wrapper = mountModal()
await selectButtonByText(wrapper, 'OpenAI')
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
}
describe('CreateAccountModal OpenAI long-context billing', () => {
beforeEach(() => {
createAccountMock.mockReset().mockResolvedValue({})
importCodexSessionMock.mockReset().mockResolvedValue({
created: 1,
updated: 0,
skipped: 0,
failed: 0,
errors: [],
warnings: [],
})
createOpenAICodexPATMock.mockReset().mockResolvedValue({})
})
it('sends true 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)
})
it('sends false explicitly when OpenAI long-context billing is disabled', async () => {
await submitApiKeyAccount('openai', true)
expect(createAccountMock).toHaveBeenCalledTimes(1)
expect(createAccountMock.mock.calls[0]?.[0]?.extra?.openai_long_context_billing_enabled).toBe(false)
})
it('omits the OpenAI setting for non-OpenAI account creation', async () => {
await submitApiKeyAccount('anthropic')
expect(createAccountMock).toHaveBeenCalledTimes(1)
expect(createAccountMock.mock.calls[0]?.[0]?.extra?.openai_long_context_billing_enabled).toBeUndefined()
})
it('leaves Codex session import billing ownership to the backend', async () => {
const wrapper = await openCodexImportStep()
await wrapper.get('[data-testid="import-codex-session"]').trigger('click')
await flushPromises()
expect(importCodexSessionMock).toHaveBeenCalledTimes(1)
expect(importCodexSessionMock.mock.calls[0]?.[0]?.extra?.openai_long_context_billing_enabled).toBeUndefined()
})
it('leaves Codex PAT import billing ownership to the backend', async () => {
const wrapper = await openCodexImportStep()
await wrapper.get('[data-testid="import-codex-pat"]').trigger('click')
await flushPromises()
expect(createOpenAICodexPATMock).toHaveBeenCalledTimes(1)
expect(createOpenAICodexPATMock.mock.calls[0]?.[0]?.extra?.openai_long_context_billing_enabled).toBeUndefined()
})
})
@@ -416,6 +416,63 @@ 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 () => {
const account = buildAccount()
updateAccountMock.mockReset()
checkMixedChannelRiskMock.mockReset()
checkMixedChannelRiskMock.mockResolvedValue({ has_risk: false })
updateAccountMock.mockResolvedValue(account)
const wrapper = mountModal(account)
const toggle = wrapper.get('[data-testid="openai-long-context-billing-toggle"]')
expect(toggle.attributes('aria-checked')).toBe('true')
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)
})
it('preserves an explicit OpenAI long-context billing opt-out', async () => {
const account = buildAccount()
account.extra = {
openai_long_context_billing_enabled: false
}
updateAccountMock.mockReset()
checkMixedChannelRiskMock.mockReset()
checkMixedChannelRiskMock.mockResolvedValue({ has_risk: false })
updateAccountMock.mockResolvedValue(account)
const wrapper = mountModal(account)
const toggle = wrapper.get('[data-testid="openai-long-context-billing-toggle"]')
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(false)
})
it('fails closed for malformed OpenAI long-context billing values', async () => {
const account = buildAccount()
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.get('[data-testid="openai-long-context-billing-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(false)
})
it('loads and submits Grok OAuth model mapping edits', async () => {
const account = buildGrokOAuthAccount()
updateAccountMock.mockReset()