Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions tests/e2e/core/utils/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,15 @@ export const Selectors = {
saveBtn: '[data-testid="custom-provider-save-btn"]',
cancelBtn: '[data-testid="custom-provider-cancel-btn"]',
},

// Pricing tab
pricingTab: '[data-testid="provider-tab-pricing"]',
pricingForm: {
container: '[data-testid="provider-pricing-form"]',
jsonInput: '[data-testid="provider-pricing-overrides-json-input"]',
resetBtn: '[data-testid="provider-pricing-overrides-reset-button"]',
saveBtn: '[data-testid="provider-pricing-overrides-save-button"]',
},
},

// Virtual Keys Page
Expand Down
59 changes: 57 additions & 2 deletions tests/e2e/features/providers/pages/providers.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -352,14 +352,15 @@ export class ProvidersPage extends BasePage {
/**
* Select a configuration tab
*/
async selectConfigTab(tabName: 'network' | 'proxy' | 'performance' | 'governance'): Promise<void> {
async selectConfigTab(tabName: 'network' | 'proxy' | 'performance' | 'governance' | 'pricing'): Promise<void> {
await this.openConfigSheet()

const tabLabels: Record<string, string> = {
network: 'Network config',
proxy: 'Proxy config',
performance: 'Performance tuning',
governance: 'Governance',
pricing: 'Pricing',
}

const tab = this.page.getByRole('tab', { name: tabLabels[tabName] })
Expand All @@ -370,12 +371,13 @@ export class ProvidersPage extends BasePage {
/**
* Get the save button for the current config tab
*/
getConfigSaveBtn(configType: 'network' | 'proxy' | 'performance' | 'governance'): Locator {
getConfigSaveBtn(configType: 'network' | 'proxy' | 'performance' | 'governance' | 'pricing'): Locator {
const buttonNames: Record<string, string> = {
network: 'Save Network Configuration',
proxy: 'Save Proxy Configuration',
performance: 'Save Performance Configuration',
governance: 'Save Governance Configuration',
pricing: 'Save Pricing Overrides',
}
return this.page.getByRole('button', { name: buttonNames[configType] })
}
Expand Down Expand Up @@ -610,4 +612,57 @@ export class ProvidersPage extends BasePage {
const tab = this.page.getByRole('tab', { name: 'Governance' })
return await tab.isVisible().catch(() => false)
}

// ============================================
// Pricing Configuration (Pricing Overrides)
// ============================================

/**
* Get pricing overrides JSON textarea input
*/
getPricingJsonInput(): Locator {
return this.page.getByTestId('provider-pricing-overrides-json-input')
}

/**
* Get pricing overrides reset button
*/
getPricingResetBtn(): Locator {
return this.page.getByTestId('provider-pricing-overrides-reset-button')
}

/**
* Get pricing overrides save button
*/
getPricingSaveBtn(): Locator {
return this.page.getByTestId('provider-pricing-overrides-save-button')
}

/**
* Save pricing configuration and wait for success toast
*/
async savePricingConfig(): Promise<void> {
const saveBtn = this.getConfigSaveBtn('pricing')
await saveBtn.click()
await this.waitForSuccessToast()
}

/**
* Check if pricing tab is visible
*/
async isPricingTabVisible(): Promise<boolean> {
await this.openConfigSheet()
const tab = this.page.getByRole('tab', { name: 'Pricing' })
return await tab.isVisible().catch(() => false)
}

/**
* Set pricing overrides JSON value
*/
async setPricingOverridesJson(json: string): Promise<void> {
await this.selectConfigTab('pricing')
const input = this.getPricingJsonInput()
await input.clear()
await input.fill(json)
}
}
89 changes: 89 additions & 0 deletions tests/e2e/features/providers/pricing.api.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/**
* API integration tests for Pricing Overrides
* These tests validate the backend API integration for pricing overrides
*/

import { expect } from '@playwright/test'
import { test } from '../../core/fixtures/base.fixture'

/**
* Validate that pricing overrides are properly sent to and received from the API
*/
test.describe('Pricing Overrides API Integration', () => {
test.describe.configure({ mode: 'serial' })

test.beforeEach(async ({ providersPage }) => {
await providersPage.goto()
})

test('api: pricing overrides are persisted via provider update API', async ({ providersPage, page }) => {
// Listen for API requests
const apiRequests: { url: string; method: string; body: unknown }[] = []

page.on('request', (request) => {
if (request.url().includes('/api/providers/') && request.method() === 'PUT') {
apiRequests.push({
url: request.url(),
method: request.method(),
body: request.postDataJSON(),
})
}
})

// Select OpenAI and set pricing
await providersPage.selectProvider('openai')

const pricingOverride = JSON.stringify([{
model_pattern: 'api-test-*',
match_type: 'wildcard',
request_types: ['chat_completion'],
input_cost_per_token: 0.000001,
output_cost_per_token: 0.000002,
}], null, 2)

await providersPage.setPricingOverridesJson(pricingOverride)
await providersPage.savePricingConfig()

// Verify the API was called with pricing_overrides
await expect.poll(() => apiRequests.length).toBeGreaterThan(0)

const lastRequest = apiRequests[apiRequests.length - 1]
expect(lastRequest.body).toHaveProperty('pricing_overrides')
expect(Array.isArray((lastRequest.body as { pricing_overrides: unknown[] }).pricing_overrides)).toBe(true)

// Cleanup
await providersPage.setPricingOverridesJson('[]')
await providersPage.savePricingConfig()
})

test('api: malformed pricing overrides are rejected', async ({ providersPage, page }) => {
// Listen for API responses
let errorResponse: { status: number; body: unknown } | null = null

page.on('response', async (response) => {
if (response.url().includes('/api/providers/') && response.status() >= 400) {
errorResponse = {
status: response.status(),
body: await response.json().catch(() => null),
}
}
})

await providersPage.selectProvider('openai')
await providersPage.selectConfigTab('pricing')

// Try to save with invalid pricing structure (UI validation should prevent this,
// but we test the API behavior directly)
const jsonInput = providersPage.getPricingJsonInput()

// Clear and enter invalid JSON
await jsonInput.fill('{ invalid }')

// Attempt to save (save button should be disabled due to validation)
const saveBtn = providersPage.getPricingSaveBtn()
const isDisabled = await saveBtn.isDisabled()

// Verify UI validation prevents the save
expect(isDisabled).toBe(true)
})
})
100 changes: 100 additions & 0 deletions tests/e2e/features/providers/pricing.types.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/**
* Type validation tests for Pricing Overrides
* These tests ensure the type definitions match the expected schemas
*/

import { expect } from '@playwright/test'
import { test } from '../../core/fixtures/base.fixture'
import { ProviderPricingOverride } from '../../../../ui/lib/types/config'

/**
* Validate that the ProviderPricingOverride type has all required fields
* This test ensures type safety for the pricing overrides feature
*/
test.describe('Pricing Overrides Type Validation', () => {
test('ProviderPricingOverride type has required fields', async () => {
// Create a valid pricing override object
const validOverride: ProviderPricingOverride = {
model_pattern: 'gpt-4o*',
match_type: 'wildcard',
request_types: ['chat_completion'],
input_cost_per_token: 0.000005,
output_cost_per_token: 0.000015,
}

// Verify the object structure
expect(validOverride.model_pattern).toBe('gpt-4o*')
expect(validOverride.match_type).toBe('wildcard')
expect(validOverride.request_types).toContain('chat_completion')
expect(validOverride.input_cost_per_token).toBe(0.000005)
expect(validOverride.output_cost_per_token).toBe(0.000015)
})

test('ProviderPricingOverride supports all match types', async () => {
const exactMatch: ProviderPricingOverride = {
model_pattern: 'gpt-4o',
match_type: 'exact',
request_types: ['chat_completion'],
}

const wildcardMatch: ProviderPricingOverride = {
model_pattern: 'gpt-4o*',
match_type: 'wildcard',
request_types: ['chat_completion'],
}

const regexMatch: ProviderPricingOverride = {
model_pattern: '^gpt-4.*$',
match_type: 'regex',
request_types: ['chat_completion'],
}

expect(exactMatch.match_type).toBe('exact')
expect(wildcardMatch.match_type).toBe('wildcard')
expect(regexMatch.match_type).toBe('regex')
})

test('ProviderPricingOverride supports all request types', async () => {
const allRequestTypes: ProviderPricingOverride['request_types'] = [
'text_completion',
'text_completion_stream',
'chat_completion',
'chat_completion_stream',
'responses',
'responses_stream',
'embedding',
'rerank',
'speech',
'speech_stream',
'transcription',
'transcription_stream',
'image_generation',
'image_generation_stream',
]

const override: ProviderPricingOverride = {
model_pattern: 'test-*',
match_type: 'wildcard',
request_types: allRequestTypes,
}

expect(override.request_types).toHaveLength(14)
})

test('ProviderPricingOverride supports advanced pricing fields', async () => {
const advancedOverride: ProviderPricingOverride = {
model_pattern: 'claude-3-opus',
match_type: 'exact',
request_types: ['chat_completion'],
input_cost_per_token: 0.000015,
output_cost_per_token: 0.000075,
input_cost_per_token_above_128k_tokens: 0.00003,
output_cost_per_token_above_128k_tokens: 0.00015,
cache_read_input_token_cost: 0.0000015,
cache_creation_input_token_cost: 0.00001875,
}

expect(advancedOverride.input_cost_per_token_above_128k_tokens).toBe(0.00003)
expect(advancedOverride.cache_read_input_token_cost).toBe(0.0000015)
})
})
Loading