fix(models): correct fast pricing and online sync

This commit is contained in:
elky
2026-07-24 01:45:38 +08:00
parent a0767d957c
commit 387134ca87
11 changed files with 1269 additions and 420 deletions
File diff suppressed because it is too large Load Diff
@@ -98,7 +98,7 @@
<div class="space-y-1">
<Label class="text-xs font-medium">层级倍率(相对 Standard)</Label>
<p class="text-xs text-muted-foreground">
该层级按 Standard 的完整价格目录统一缩放。
该层级按 Standard 基准价格统一缩放。
</p>
</div>
<Button
@@ -14,6 +14,7 @@ import GlobalModelFormDialog from '../GlobalModelFormDialog.vue'
const modelsDevMocks = vi.hoisted(() => ({
getModelsDevList: vi.fn(),
refreshModelsDevList: vi.fn(),
}))
const globalModelMocks = vi.hoisted(() => ({
@@ -24,6 +25,7 @@ const globalModelMocks = vi.hoisted(() => ({
vi.mock('@/api/models-dev', () => ({
getModelsDevList: modelsDevMocks.getModelsDevList,
refreshModelsDevList: modelsDevMocks.refreshModelsDevList,
getProviderLogoUrl: (providerId: string) => `/logos/${providerId}.svg`,
}))
@@ -62,6 +64,27 @@ const stalePreset: ModelsDevModelItem = {
},
}
const alternateStalePreset: ModelsDevModelItem = {
...stalePreset,
providerId: 'azure-openai',
providerName: 'Azure OpenAI',
inputPrice: 7,
outputPrice: 8,
tieredPricing: {
tiers: [{
up_to: null,
input_price_per_1m: 7,
output_price_per_1m: 8,
}],
},
}
const unavailableStalePreset: ModelsDevModelItem = {
...stalePreset,
pricingUnsupportedFields: ['reasoning'],
tieredPricing: undefined,
}
const freshPreset: ModelsDevModelItem = {
providerId: 'openai',
providerName: 'OpenAI',
@@ -125,18 +148,26 @@ function mountDialog() {
const root = document.createElement('div')
document.body.appendChild(root)
const open = ref(false)
const editingModel = ref<GlobalModelResponse | null>(null)
const editModel = vi.fn((model: GlobalModelResponse) => {
editingModel.value = model
})
const pricingSynced = vi.fn()
const app = createApp(defineComponent({
setup() {
return () => h(GlobalModelFormDialog, {
open: open.value,
model: null,
model: editingModel.value,
onEditModel: editModel,
onPricingSynced: pricingSynced,
'onUpdate:open': (value: boolean) => { open.value = value },
})
},
}))
app.mount(root)
mountedApps.push({ app, root })
open.value = true
return { root, open }
return { root, open, editingModel, editModel, pricingSynced }
}
async function settle() {
@@ -164,6 +195,24 @@ function findExactButton(text: string): HTMLButtonElement {
return button
}
function findExistingEditButton(modelId: string): HTMLButtonElement {
const button = document.body.querySelector(
`[data-testid="edit-existing-model-${modelId}"]`,
)
if (!(button instanceof HTMLButtonElement)) {
throw new Error(`Missing existing-model edit button: ${modelId}`)
}
return button
}
function findBillingTab(value: string): HTMLButtonElement {
const button = document.body.querySelector(`button[data-value="${value}"]`)
if (!(button instanceof HTMLButtonElement)) {
throw new Error(`Missing billing tab: ${value}`)
}
return button
}
async function setInput(input: HTMLInputElement | null, value: string) {
if (!input) throw new Error('Missing input')
input.value = value
@@ -175,6 +224,8 @@ beforeEach(() => {
localStorage.clear()
modelsDevMocks.getModelsDevList.mockReset()
modelsDevMocks.getModelsDevList.mockResolvedValue([stalePreset, freshPreset, unsupportedPreset])
modelsDevMocks.refreshModelsDevList.mockReset()
modelsDevMocks.refreshModelsDevList.mockResolvedValue([stalePreset, freshPreset, unsupportedPreset])
globalModelMocks.createGlobalModel.mockReset()
globalModelMocks.createGlobalModel.mockResolvedValue({ id: 'created-model' })
globalModelMocks.listGlobalModels.mockReset()
@@ -200,6 +251,7 @@ describe('GlobalModelFormDialog preset replacement', () => {
mountDialog()
await settle()
expect(document.body.textContent).not.toContain('fresh-family')
findButton('Stale Model').click()
await settle()
@@ -325,62 +377,45 @@ describe('GlobalModelFormDialog preset replacement', () => {
expect(payload.default_tiered_pricing.processing_tiers).not.toHaveProperty('standard')
})
it('marks an existing model and updates only its online pricing after confirmation', async () => {
it('routes an already-added online model from its card action without syncing prices', async () => {
const existingStaleModel = buildExistingStaleModel()
localStorage.setItem('aether:models-dev-pricing-sources:v1', JSON.stringify({
version: 1,
models: {
[existingStaleModel.id]: {
provider_id: 'openai',
provider_name: 'OpenAI',
},
},
}))
globalModelMocks.listGlobalModels.mockResolvedValue({
models: [existingStaleModel],
total: 1,
})
mountDialog()
const { editModel } = mountDialog()
await settle()
expect(document.body.textContent).toContain('已添加')
expect(document.body.textContent).toContain('价格可更新')
expect(document.body.textContent).toContain('上次来源')
const editButton = findExistingEditButton(stalePreset.modelId)
const modelCard = findButton('Stale Model')
expect(modelCard.disabled).toBe(true)
expect(editButton.parentElement?.textContent).toContain('已添加')
expect(editButton.title).toBe('去编辑')
expect(editButton.getAttribute('aria-label')).toBe('编辑 Stale Model')
expect(document.body.textContent).not.toContain('价格可更新')
expect(document.body.textContent).not.toContain('上次来源')
expect(document.body.textContent).not.toContain('同步在线价格')
expect(document.body.textContent).not.toContain('保留当前价格')
expect(document.body.textContent).not.toContain('使用在线价格')
expect(document.body.querySelector('[data-testid="tier-input-price"]')).toBeNull()
findButton('Stale Model').click()
modelCard.click()
await settle()
expect(editModel).not.toHaveBeenCalled()
expect(document.body.textContent).toContain('创建统一模型')
editButton.click()
await settle()
expect(document.body.textContent).toContain('仅更新该模型的价格配置')
expect(document.body.querySelector<HTMLInputElement>('[data-testid="tier-input-price"]')?.value).toBe('9')
expect(document.body.querySelector('[aria-label="选择已有模型时自动应用在线价格"]')).toBeNull()
expect(findExactButton('请选择在线价格').disabled).toBe(true)
findButton('使用在线价格').click()
await settle()
expect(document.body.querySelector<HTMLInputElement>('[data-testid="tier-input-price"]')?.value).toBe('1')
findExactButton('同步价格').click()
await settle()
expect(globalModelMocks.updateGlobalModel).toHaveBeenCalledOnce()
expect(globalModelMocks.updateGlobalModel).toHaveBeenCalledWith(
existingStaleModel.id,
{ default_tiered_pricing: stalePreset.tieredPricing },
)
expect(JSON.parse(
localStorage.getItem('aether:models-dev-pricing-sources:v1') || 'null',
)).toMatchObject({
models: {
[existingStaleModel.id]: {
provider_id: 'openai',
provider_name: 'OpenAI',
},
},
})
expect(editModel).toHaveBeenCalledOnce()
expect(editModel).toHaveBeenCalledWith(existingStaleModel)
expect(document.body.textContent).toContain('编辑模型')
expect(findExactButton('保存').disabled).toBe(false)
expect(globalModelMocks.createGlobalModel).not.toHaveBeenCalled()
expect(globalModelMocks.updateGlobalModel).not.toHaveBeenCalled()
})
it('blocks manual updates when the online source has unsupported pricing dimensions', async () => {
it('routes an existing model to edit even when its online pricing is unsupported', async () => {
const existingModel = {
...buildExistingStaleModel(),
id: 'reasoning-priced-global-model',
@@ -391,15 +426,209 @@ describe('GlobalModelFormDialog preset replacement', () => {
models: [existingModel],
total: 1,
})
const { editModel } = mountDialog()
await settle()
findExistingEditButton(unsupportedPreset.modelId).click()
await settle()
expect(document.body.textContent).not.toContain('计价不兼容')
expect(document.body.textContent).not.toContain('同步在线价格')
expect(editModel).toHaveBeenCalledWith(existingModel)
expect(document.body.textContent).toContain('编辑模型')
expect(globalModelMocks.createGlobalModel).not.toHaveBeenCalled()
expect(globalModelMocks.updateGlobalModel).not.toHaveBeenCalled()
})
it('opens model editing on Token while preserving alternate billing data', async () => {
const existingModel = {
...buildExistingStaleModel(),
default_price_per_request: 0.25,
supported_capabilities: ['image_generation'],
config: {
streaming: true,
billing: {
video: {
price_per_second_by_resolution: { '720p': 0.1 },
},
},
},
}
globalModelMocks.listGlobalModels.mockResolvedValue({
models: [existingModel],
total: 1,
})
mountDialog()
await settle()
expect(document.body.textContent).toContain('计价不兼容')
findButton(unsupportedPreset.modelName).click()
findExistingEditButton(stalePreset.modelId).click()
await settle()
expect(document.body.textContent).toContain('无法独立结算推理 Token')
expect(findExactButton('暂无在线价格').disabled).toBe(true)
expect(findBillingTab('token').dataset.state).toBe('active')
expect(findBillingTab('request').dataset.state).toBe('inactive')
expect(findBillingTab('image').dataset.state).toBe('inactive')
expect(findBillingTab('video').dataset.state).toBe('inactive')
findBillingTab('request').click()
await nextTick()
expect(document.body.querySelector<HTMLInputElement>('input[placeholder="如 0.01"]')?.value)
.toBe('0.25')
})
it('refreshes and applies the latest online price from the edit dialog', async () => {
const existingStaleModel = buildExistingStaleModel()
const syncedModel = {
...existingStaleModel,
default_tiered_pricing: stalePreset.tieredPricing!,
}
globalModelMocks.updateGlobalModel.mockResolvedValue(syncedModel)
globalModelMocks.listGlobalModels.mockResolvedValue({
models: [existingStaleModel],
total: 1,
})
const { editModel, pricingSynced } = mountDialog()
await settle()
findExistingEditButton(stalePreset.modelId).click()
await settle()
const syncButton = document.body.querySelector<HTMLButtonElement>(
'[data-testid="sync-online-pricing"]',
)
if (!syncButton) throw new Error('Missing online pricing sync button')
expect(syncButton.title).toBe('同步最新在线价格')
expect(syncButton.getAttribute('aria-label')).toBe('同步最新在线价格')
syncButton.click()
await settle()
expect(modelsDevMocks.refreshModelsDevList).toHaveBeenCalledOnce()
expect(modelsDevMocks.refreshModelsDevList).toHaveBeenCalledWith(false)
expect(globalModelMocks.updateGlobalModel).toHaveBeenCalledWith(
existingStaleModel.id,
{ default_tiered_pricing: stalePreset.tieredPricing },
)
expect(pricingSynced).toHaveBeenCalledWith(syncedModel)
expect(document.body.querySelector<HTMLInputElement>('[data-testid="tier-input-price"]')?.value)
.toBe('1')
expect(document.body.textContent).toContain('编辑模型')
expect(editModel).toHaveBeenCalledWith(existingStaleModel)
expect(JSON.parse(
localStorage.getItem('aether:models-dev-pricing-sources:v1') || 'null',
)).toMatchObject({
models: {
[existingStaleModel.id]: {
provider_id: stalePreset.providerId,
provider_name: stalePreset.providerName,
},
},
})
})
it('offers a provider choice when the remembered source is unavailable', async () => {
const existingStaleModel = buildExistingStaleModel()
const syncedModel = {
...existingStaleModel,
default_tiered_pricing: alternateStalePreset.tieredPricing!,
}
modelsDevMocks.refreshModelsDevList.mockResolvedValue([
unavailableStalePreset,
alternateStalePreset,
])
localStorage.setItem('aether:models-dev-pricing-sources:v1', JSON.stringify({
version: 1,
models: {
[existingStaleModel.id]: {
provider_id: unavailableStalePreset.providerId,
provider_name: unavailableStalePreset.providerName,
},
},
}))
globalModelMocks.updateGlobalModel.mockResolvedValue(syncedModel)
const { editingModel, pricingSynced } = mountDialog()
await settle()
// Open the editor directly so there is no transient provider preference.
editingModel.value = existingStaleModel
await settle()
const syncButton = document.body.querySelector<HTMLButtonElement>(
'[data-testid="sync-online-pricing"]',
)
if (!syncButton) throw new Error('Missing online pricing sync button')
syncButton.click()
await settle()
expect(globalModelMocks.updateGlobalModel).not.toHaveBeenCalled()
expect(document.body.textContent).toContain('选择在线价格来源')
expect(document.body.textContent).toContain('OpenAI')
expect(document.body.textContent).toContain('Azure OpenAI')
const alternateSource = document.body.querySelector<HTMLButtonElement>(
'[data-testid="online-pricing-source-azure-openai"]',
)
if (!alternateSource) throw new Error('Missing alternate pricing source')
expect(document.body.querySelector<HTMLButtonElement>(
'[data-testid="online-pricing-source-openai"]',
)?.disabled).toBe(true)
expect(document.activeElement).toBe(alternateSource)
alternateSource.click()
await nextTick()
findExactButton('同步此来源').click()
await settle()
expect(globalModelMocks.updateGlobalModel).toHaveBeenCalledWith(
existingStaleModel.id,
{ default_tiered_pricing: alternateStalePreset.tieredPricing },
)
expect(pricingSynced).toHaveBeenCalledWith(syncedModel)
expect(document.body.textContent).not.toContain('选择在线价格来源')
expect(JSON.parse(
localStorage.getItem('aether:models-dev-pricing-sources:v1') || 'null',
)).toMatchObject({
models: {
[existingStaleModel.id]: {
provider_id: alternateStalePreset.providerId,
provider_name: alternateStalePreset.providerName,
},
},
})
})
it('opens the compact source popover for multiple usable providers without a remembered source', async () => {
const existingStaleModel = buildExistingStaleModel()
modelsDevMocks.refreshModelsDevList.mockResolvedValue([
stalePreset,
alternateStalePreset,
])
globalModelMocks.listGlobalModels.mockResolvedValue({
models: [existingStaleModel],
total: 1,
})
const { editingModel } = mountDialog()
await settle()
editingModel.value = existingStaleModel
await settle()
const syncButton = document.body.querySelector<HTMLButtonElement>(
'[data-testid="sync-online-pricing"]',
)
if (!syncButton) throw new Error('Missing online pricing sync button')
syncButton.click()
await settle()
expect(document.body.querySelector('[data-testid="online-pricing-source-openai"]')).not.toBeNull()
expect(document.body.querySelector('[data-testid="online-pricing-source-azure-openai"]')).not.toBeNull()
expect(document.body.querySelectorAll('.fixed.inset-0.overflow-hidden.pointer-events-none')).toHaveLength(1)
expect(globalModelMocks.updateGlobalModel).not.toHaveBeenCalled()
const cancelButton = document.body.querySelector<HTMLButtonElement>(
'[data-testid="online-pricing-source-cancel"]',
)
if (!cancelButton) throw new Error('Missing source popover cancel button')
cancelButton.click()
await settle()
expect(document.body.querySelector('[data-testid="online-pricing-source-openai"]')).toBeNull()
expect(globalModelMocks.updateGlobalModel).not.toHaveBeenCalled()
})
})