diff --git a/apps/web/src/app/api/openrouter/models/[provider]/[model]/endpoints/route.ts b/apps/web/src/app/api/openrouter/models/[provider]/[model]/endpoints/route.ts index a27f01fb90..b37c9b6dec 100644 --- a/apps/web/src/app/api/openrouter/models/[provider]/[model]/endpoints/route.ts +++ b/apps/web/src/app/api/openrouter/models/[provider]/[model]/endpoints/route.ts @@ -1,7 +1,7 @@ import type { NextRequest } from 'next/server'; import { NextResponse } from 'next/server'; import { getOpenRouterModelsMetadataFromDatabase } from '@/lib/ai-gateway/providers/gateway-models-cache'; -import { getModelDisplayPricing } from '@/lib/ai-gateway/providers/openrouter'; +import { getModelDisplayPricing } from '@/lib/ai-gateway/providers/openrouter/display-pricing'; import { applyCustomPricingToPricing } from '@/lib/ai-gateway/custom-pricing'; import { isForbiddenFreeModel } from '@/lib/ai-gateway/forbidden-free-models'; diff --git a/apps/web/src/lib/ai-gateway/providers/openrouter/display-pricing.test.ts b/apps/web/src/lib/ai-gateway/providers/openrouter/display-pricing.test.ts new file mode 100644 index 0000000000..0000cab77a --- /dev/null +++ b/apps/web/src/lib/ai-gateway/providers/openrouter/display-pricing.test.ts @@ -0,0 +1,66 @@ +import { describe, expect, it } from '@jest/globals'; +import { OPENROUTER_GPT56_PROMO_MODEL_IDS } from '@/lib/ai-gateway/providers/openai'; +import { + getModelDisplayPricing, + undoPricingDiscount, +} from '@/lib/ai-gateway/providers/openrouter/display-pricing'; + +describe('undoPricingDiscount', () => { + it('reverses the discount and drops the field without exponential output', () => { + const result = undoPricingDiscount({ + prompt: '0.00000006', + completion: '0.0000006', + input_cache_read: '0.000000015', + discount: 0.7, + }); + expect(result).toEqual({ + prompt: '0.000000200000', + completion: '0.000002000000', + input_cache_read: '0.000000050000', + }); + expect('discount' in result).toBe(false); + for (const value of Object.values(result)) { + expect(value).not.toMatch(/e/i); + } + }); + + it('leaves pricing untouched when there is no discount', () => { + const pricing = { prompt: '0.000001', completion: '0.000005' }; + expect(undoPricingDiscount(pricing)).toBe(pricing); + }); + + it('leaves pricing untouched when the discount is zero', () => { + const pricing = { prompt: '0.000001', completion: '0.000005', discount: 0 }; + expect(undoPricingDiscount(pricing)).toBe(pricing); + }); + + it('drops the field when the discount cannot be reversed', () => { + const result = undoPricingDiscount({ + prompt: '0.000001', + completion: '0.000005', + discount: 1, + }); + expect(result).toEqual({ prompt: '0.000001', completion: '0.000005' }); + }); +}); + +describe('OpenRouter GPT-5.6 promotion', () => { + const discountedPricing = { + prompt: '0.000001', + completion: '0.000004', + input_cache_read: '0.0000001', + discount: 0.5, + }; + + it.each(OPENROUTER_GPT56_PROMO_MODEL_IDS)('preserves discounted pricing for %s', modelId => { + expect(getModelDisplayPricing(modelId, discountedPricing)).toBe(discountedPricing); + }); + + it('continues to undo endpoint discounts for other models', () => { + expect(getModelDisplayPricing('openai/gpt-5.6-sol', discountedPricing)).toEqual({ + prompt: '0.000002000000', + completion: '0.000008000000', + input_cache_read: '0.000000200000', + }); + }); +}); diff --git a/apps/web/src/lib/ai-gateway/providers/openrouter/display-pricing.ts b/apps/web/src/lib/ai-gateway/providers/openrouter/display-pricing.ts new file mode 100644 index 0000000000..e4327e02f1 --- /dev/null +++ b/apps/web/src/lib/ai-gateway/providers/openrouter/display-pricing.ts @@ -0,0 +1,27 @@ +import type { StoredModel } from '@kilocode/db'; +import { isOpenRouterGpt56PromoModel } from '@/lib/ai-gateway/providers/openai'; + +export type EndpointPricing = NonNullable; + +export function undoPricingDiscount(pricing: EndpointPricing): EndpointPricing { + const { discount, ...prices } = pricing; + if (discount === undefined || discount <= 0) return pricing; + const factor = 1 - discount; + if (factor <= 0) return prices; + const result = { ...prices }; + for (const key of Object.keys(prices) as (keyof typeof prices)[]) { + const value = prices[key]; + if (value !== undefined) { + result[key] = (Number.parseFloat(value) / factor).toFixed(12); + } + } + return result; +} + +export function getModelDisplayPricing( + modelId: string, + pricing: EndpointPricing | undefined +): EndpointPricing | undefined { + if (!pricing) return undefined; + return isOpenRouterGpt56PromoModel(modelId) ? pricing : undoPricingDiscount(pricing); +} diff --git a/apps/web/src/lib/ai-gateway/providers/openrouter/index.test.ts b/apps/web/src/lib/ai-gateway/providers/openrouter/index.test.ts index 1c4df57b2e..d2b992ff35 100644 --- a/apps/web/src/lib/ai-gateway/providers/openrouter/index.test.ts +++ b/apps/web/src/lib/ai-gateway/providers/openrouter/index.test.ts @@ -2,10 +2,8 @@ import { describe, it, expect, beforeEach, afterEach, jest } from '@jest/globals import { formatName, getEnhancedOpenRouterModels, - getModelDisplayPricing, getOpenRouterTranscriptionModels, shouldSuppressOpenRouterModel, - undoPricingDiscount, } from '@/lib/ai-gateway/providers/openrouter'; import { createMockResponse, mockOpenRouterModels } from '@/tests/helpers/openrouter-models.helper'; import type { OpenRouterModel } from '@/lib/organizations/organization-types'; @@ -173,66 +171,6 @@ describe('formatName', () => { }); }); -describe('undoPricingDiscount', () => { - it('reverses the discount and drops the field without exponential output', () => { - const result = undoPricingDiscount({ - prompt: '0.00000006', - completion: '0.0000006', - input_cache_read: '0.000000015', - discount: 0.7, - }); - expect(result).toEqual({ - prompt: '0.000000200000', - completion: '0.000002000000', - input_cache_read: '0.000000050000', - }); - expect('discount' in result).toBe(false); - for (const value of Object.values(result)) { - expect(value).not.toMatch(/e/i); - } - }); - - it('leaves pricing untouched when there is no discount', () => { - const pricing = { prompt: '0.000001', completion: '0.000005' }; - expect(undoPricingDiscount(pricing)).toBe(pricing); - }); - - it('leaves pricing untouched when the discount is zero', () => { - const pricing = { prompt: '0.000001', completion: '0.000005', discount: 0 }; - expect(undoPricingDiscount(pricing)).toBe(pricing); - }); - - it('drops the field when the discount cannot be reversed', () => { - const result = undoPricingDiscount({ - prompt: '0.000001', - completion: '0.000005', - discount: 1, - }); - expect(result).toEqual({ prompt: '0.000001', completion: '0.000005' }); - }); -}); - -describe('OpenRouter GPT-5.6 promotion', () => { - const discountedPricing = { - prompt: '0.000001', - completion: '0.000004', - input_cache_read: '0.0000001', - discount: 0.5, - }; - - it.each(OPENROUTER_GPT56_PROMO_MODEL_IDS)('preserves discounted pricing for %s', modelId => { - expect(getModelDisplayPricing(modelId, discountedPricing)).toBe(discountedPricing); - }); - - it('continues to undo endpoint discounts for other models', () => { - expect(getModelDisplayPricing('openai/gpt-5.6-sol', discountedPricing)).toEqual({ - prompt: '0.000002000000', - completion: '0.000008000000', - input_cache_read: '0.000000200000', - }); - }); -}); - describe('shouldSuppressOpenRouterModel', () => { it('does not suppress disabled paid Kilo-exclusive models returned by OpenRouter', () => { expect(disabledPaidModel.pricing).not.toBeNull(); diff --git a/apps/web/src/lib/ai-gateway/providers/openrouter/index.ts b/apps/web/src/lib/ai-gateway/providers/openrouter/index.ts index 8ae8923f3c..257fd81bad 100644 --- a/apps/web/src/lib/ai-gateway/providers/openrouter/index.ts +++ b/apps/web/src/lib/ai-gateway/providers/openrouter/index.ts @@ -5,7 +5,6 @@ import { } from '@/lib/ai-gateway/models'; import { isFreeModel } from '@/lib/ai-gateway/is-free-model'; import PROVIDERS from '@/lib/ai-gateway/providers/provider-definitions'; -import type { StoredModel } from '@kilocode/db'; import type { OpenRouterModel } from '@/lib/organizations/organization-types'; import { OpenRouterModelsResponseSchema, @@ -29,9 +28,15 @@ import { isFreeNemotronModel, NVIDIA_TRIAL_TOS } from '@/lib/ai-gateway/provider import { applyCustomPricingToModel } from '@/lib/ai-gateway/custom-pricing'; import { addMonths } from 'date-fns'; import { isOpenRouterGpt56PromoModel } from '@/lib/ai-gateway/providers/openai'; +import { getModelDisplayPricing } from '@/lib/ai-gateway/providers/openrouter/display-pricing'; // Re-export from shared module for backwards compatibility export { normalizeModelId } from '@/lib/ai-gateway/model-utils'; +export { + getModelDisplayPricing, + undoPricingDiscount, +} from '@/lib/ai-gateway/providers/openrouter/display-pricing'; +export type { EndpointPricing } from '@/lib/ai-gateway/providers/openrouter/display-pricing'; export function buildAutoModelCatalogEntry(m: AutoModel): OpenRouterModel { const input_modalities = ['text']; @@ -107,31 +112,6 @@ export function formatName(model: OpenRouterModel, preferredIndex: number) { return name; } -type EndpointPricing = NonNullable; - -export function undoPricingDiscount(pricing: EndpointPricing): EndpointPricing { - const { discount, ...prices } = pricing; - if (discount === undefined || discount <= 0) return pricing; - const factor = 1 - discount; - if (factor <= 0) return prices; - const result = { ...prices }; - for (const key of Object.keys(prices) as (keyof typeof prices)[]) { - const value = prices[key]; - if (value !== undefined) { - result[key] = (Number.parseFloat(value) / factor).toFixed(12); - } - } - return result; -} - -export function getModelDisplayPricing( - modelId: string, - pricing: EndpointPricing | undefined -): EndpointPricing | undefined { - if (!pricing) return undefined; - return isOpenRouterGpt56PromoModel(modelId) ? pricing : undoPricingDiscount(pricing); -} - export function shouldSuppressOpenRouterModel(model: KiloExclusiveModel): boolean { return model.status !== 'disabled' || model.pricing === null; }