mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-08 02:17:46 +08:00
fix(models): correct fast pricing and online sync
This commit is contained in:
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
|
||||
|
||||
+278
-49
@@ -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()
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user