feat(openai): port /responses/compact account support flow (PR #1555)
将 vansour/sub2api#1555 的 OpenAI compact 能力建模手工移植到当前 main:账号 级 compact 状态/auto-force_on-force_off 模式、compact-only 模型映射、调度器 tier 分层(已支持 > 未知 > 已知不支持)、管理后台 compact 主动探测,以及对应 i18n/状态徽章。普通 /responses 流量行为不变,无数据库迁移。
This commit is contained in:
@@ -122,7 +122,7 @@ describe('AccountStatusIndicator', () => {
|
||||
}
|
||||
})
|
||||
|
||||
expect(wrapper.text()).toContain('account.creditsExhausted')
|
||||
expect(wrapper.text()).toContain('admin.accounts.status.creditsExhausted')
|
||||
})
|
||||
|
||||
it('模型限流 + overages 启用 + AICredits key 生效 → 普通限流样式(积分耗尽,无 ⚡)', () => {
|
||||
@@ -157,6 +157,6 @@ describe('AccountStatusIndicator', () => {
|
||||
expect(wrapper.text()).toContain('CSon45')
|
||||
expect(wrapper.text()).not.toContain('⚡')
|
||||
// AICredits 积分耗尽状态应显示
|
||||
expect(wrapper.text()).toContain('account.creditsExhausted')
|
||||
expect(wrapper.text()).toContain('admin.accounts.status.creditsExhausted')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest'
|
||||
import { flushPromises, mount } from '@vue/test-utils'
|
||||
import { defineComponent } from 'vue'
|
||||
import AccountTestModal from '../AccountTestModal.vue'
|
||||
|
||||
const { getAvailableModelsMock } = vi.hoisted(() => ({
|
||||
getAvailableModelsMock: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('@/api/admin', () => ({
|
||||
adminAPI: {
|
||||
accounts: {
|
||||
getAvailableModels: getAvailableModelsMock
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/useClipboard', () => ({
|
||||
useClipboard: () => ({
|
||||
copyToClipboard: vi.fn()
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('vue-i18n', async () => {
|
||||
const actual = await vi.importActual<typeof import('vue-i18n')>('vue-i18n')
|
||||
return {
|
||||
...actual,
|
||||
useI18n: () => ({
|
||||
t: (key: string) => key
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const BaseDialogStub = defineComponent({
|
||||
name: 'BaseDialog',
|
||||
props: { show: { type: Boolean, default: false } },
|
||||
template: '<div v-if="show"><slot /><slot name="footer" /></div>'
|
||||
})
|
||||
|
||||
const SelectStub = defineComponent({
|
||||
name: 'SelectStub',
|
||||
props: {
|
||||
modelValue: { type: [String, Number, Boolean, null], default: '' },
|
||||
options: { type: Array, default: () => [] },
|
||||
valueKey: { type: String, default: 'value' },
|
||||
labelKey: { type: String, default: 'label' }
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
template: `
|
||||
<select
|
||||
v-bind="$attrs"
|
||||
:value="modelValue"
|
||||
@change="$emit('update:modelValue', $event.target.value)"
|
||||
>
|
||||
<option
|
||||
v-for="option in options"
|
||||
:key="option[valueKey]"
|
||||
:value="option[valueKey]"
|
||||
>
|
||||
{{ option[labelKey] }}
|
||||
</option>
|
||||
</select>
|
||||
`
|
||||
})
|
||||
|
||||
const TextAreaStub = defineComponent({
|
||||
name: 'TextArea',
|
||||
props: {
|
||||
modelValue: { type: String, default: '' }
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
template: `
|
||||
<textarea
|
||||
v-bind="$attrs"
|
||||
:value="modelValue"
|
||||
@input="$emit('update:modelValue', $event.target.value)"
|
||||
/>
|
||||
`
|
||||
})
|
||||
|
||||
function buildAccount() {
|
||||
return {
|
||||
id: 1,
|
||||
name: 'OpenAI OAuth',
|
||||
platform: 'openai',
|
||||
type: 'oauth',
|
||||
status: 'active',
|
||||
credentials: {},
|
||||
extra: {},
|
||||
concurrency: 1,
|
||||
priority: 1,
|
||||
proxy_id: null,
|
||||
auto_pause_on_expired: false
|
||||
} as any
|
||||
}
|
||||
|
||||
describe('AccountTestModal', () => {
|
||||
const originalFetch = global.fetch
|
||||
|
||||
beforeEach(() => {
|
||||
getAvailableModelsMock.mockReset()
|
||||
getAvailableModelsMock.mockResolvedValue([
|
||||
{ id: 'gpt-5.4', display_name: 'GPT-5.4' }
|
||||
])
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
body: {
|
||||
getReader: () => ({
|
||||
read: vi.fn().mockResolvedValue({ done: true, value: undefined })
|
||||
})
|
||||
}
|
||||
} as any)
|
||||
localStorage.setItem('auth_token', 'test-token')
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
global.fetch = originalFetch
|
||||
localStorage.clear()
|
||||
})
|
||||
|
||||
it('posts compact mode for OpenAI compact probe', async () => {
|
||||
const wrapper = mount(AccountTestModal, {
|
||||
props: {
|
||||
show: true,
|
||||
account: buildAccount()
|
||||
},
|
||||
global: {
|
||||
stubs: {
|
||||
BaseDialog: BaseDialogStub,
|
||||
Select: SelectStub,
|
||||
TextArea: TextAreaStub,
|
||||
Icon: true
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
await flushPromises()
|
||||
;(wrapper.vm as any).selectedModelId = 'gpt-5.4'
|
||||
;(wrapper.vm as any).testMode = 'compact'
|
||||
await (wrapper.vm as any).startTest()
|
||||
await flushPromises()
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledTimes(1)
|
||||
const [, options] = (global.fetch as any).mock.calls[0]
|
||||
expect(JSON.parse(options.body)).toMatchObject({
|
||||
model_id: 'gpt-5.4',
|
||||
mode: 'compact'
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -26,6 +26,13 @@ vi.mock('@/api/admin', () => ({
|
||||
accounts: {
|
||||
update: updateAccountMock,
|
||||
checkMixedChannelRisk: checkMixedChannelRiskMock
|
||||
},
|
||||
settings: {
|
||||
getWebSearchEmulationConfig: vi.fn().mockResolvedValue({ enabled: false, providers: [] }),
|
||||
getSettings: vi.fn().mockResolvedValue({})
|
||||
},
|
||||
tlsFingerprintProfiles: {
|
||||
list: vi.fn().mockResolvedValue([])
|
||||
}
|
||||
}
|
||||
}))
|
||||
@@ -82,6 +89,32 @@ const ModelWhitelistSelectorStub = defineComponent({
|
||||
`
|
||||
})
|
||||
|
||||
const SelectStub = defineComponent({
|
||||
name: 'SelectStub',
|
||||
props: {
|
||||
modelValue: {
|
||||
type: [String, Number, Boolean, null],
|
||||
default: ''
|
||||
},
|
||||
options: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
template: `
|
||||
<select
|
||||
v-bind="$attrs"
|
||||
:value="modelValue"
|
||||
@change="$emit('update:modelValue', $event.target.value)"
|
||||
>
|
||||
<option v-for="option in options" :key="option.value" :value="option.value">
|
||||
{{ option.label }}
|
||||
</option>
|
||||
</select>
|
||||
`
|
||||
})
|
||||
|
||||
function buildAccount() {
|
||||
return {
|
||||
id: 1,
|
||||
@@ -119,7 +152,7 @@ function mountModal(account = buildAccount()) {
|
||||
global: {
|
||||
stubs: {
|
||||
BaseDialog: BaseDialogStub,
|
||||
Select: true,
|
||||
Select: SelectStub,
|
||||
Icon: true,
|
||||
ProxySelector: true,
|
||||
GroupSelector: true,
|
||||
@@ -156,4 +189,31 @@ describe('EditAccountModal', () => {
|
||||
'gpt-5.2': 'gpt-5.2'
|
||||
})
|
||||
})
|
||||
|
||||
it('submits OpenAI compact mode and compact-only model mapping', async () => {
|
||||
const account = buildAccount()
|
||||
account.extra = {
|
||||
openai_compact_mode: 'force_on'
|
||||
}
|
||||
account.credentials = {
|
||||
...account.credentials,
|
||||
compact_model_mapping: {
|
||||
'gpt-5.4': 'gpt-5.4-openai-compact'
|
||||
}
|
||||
}
|
||||
updateAccountMock.mockReset()
|
||||
checkMixedChannelRiskMock.mockReset()
|
||||
checkMixedChannelRiskMock.mockResolvedValue({ has_risk: false })
|
||||
updateAccountMock.mockResolvedValue(account)
|
||||
|
||||
const wrapper = mountModal(account)
|
||||
|
||||
await wrapper.get('form#edit-account-form').trigger('submit.prevent')
|
||||
|
||||
expect(updateAccountMock).toHaveBeenCalledTimes(1)
|
||||
expect(updateAccountMock.mock.calls[0]?.[1]?.extra?.openai_compact_mode).toBe('force_on')
|
||||
expect(updateAccountMock.mock.calls[0]?.[1]?.credentials?.compact_model_mapping).toEqual({
|
||||
'gpt-5.4': 'gpt-5.4-openai-compact'
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user