@@ -174,6 +174,165 @@ describe("ModelPricingRegistry", () => {
174174 } ) ;
175175 } ) ;
176176
177+ describe ( "prefix stripping" , ( ) => {
178+ it ( "should match gateway-prefixed model names" , ( ) => {
179+ const result = registry . match ( "openai/gpt-4o" ) ;
180+ expect ( result ) . not . toBeNull ( ) ;
181+ expect ( result ! . modelName ) . toBe ( "gpt-4o" ) ;
182+ } ) ;
183+
184+ it ( "should match openrouter-prefixed model names with date suffix" , ( ) => {
185+ const result = registry . match ( "openai/gpt-4o-2024-08-06" ) ;
186+ expect ( result ) . not . toBeNull ( ) ;
187+ expect ( result ! . modelName ) . toBe ( "gpt-4o" ) ;
188+ } ) ;
189+
190+ it ( "should return null for prefixed unknown model" , ( ) => {
191+ const result = registry . match ( "xai/unknown-model" ) ;
192+ expect ( result ) . toBeNull ( ) ;
193+ } ) ;
194+ } ) ;
195+
196+ describe ( "tier matching" , ( ) => {
197+ const multiTierModel : LlmModelWithPricing = {
198+ id : "model-gemini-pro" ,
199+ friendlyId : "llm_model_gemini_pro" ,
200+ modelName : "gemini-2.5-pro" ,
201+ matchPattern : "^gemini-2\\.5-pro$" ,
202+ startDate : null ,
203+ pricingTiers : [
204+ {
205+ id : "tier-large-context" ,
206+ name : "Large Context" ,
207+ isDefault : false ,
208+ priority : 0 ,
209+ conditions : [
210+ { usageDetailPattern : "input" , operator : "gt" as const , value : 200000 } ,
211+ ] ,
212+ prices : [
213+ { usageType : "input" , price : 0.0000025 } ,
214+ { usageType : "output" , price : 0.00001 } ,
215+ ] ,
216+ } ,
217+ {
218+ id : "tier-standard" ,
219+ name : "Standard" ,
220+ isDefault : true ,
221+ priority : 1 ,
222+ conditions : [ ] ,
223+ prices : [
224+ { usageType : "input" , price : 0.00000125 } ,
225+ { usageType : "output" , price : 0.000005 } ,
226+ ] ,
227+ } ,
228+ ] ,
229+ } ;
230+
231+ it ( "should use conditional tier when conditions match" , ( ) => {
232+ const tieredRegistry = new TestableRegistry ( null as any ) ;
233+ tieredRegistry . loadPatterns ( [ multiTierModel ] ) ;
234+
235+ const result = tieredRegistry . calculateCost ( "gemini-2.5-pro" , {
236+ input : 250000 ,
237+ output : 1000 ,
238+ } ) ;
239+
240+ expect ( result ) . not . toBeNull ( ) ;
241+ expect ( result ! . pricingTierName ) . toBe ( "Large Context" ) ;
242+ expect ( result ! . inputCost ) . toBeCloseTo ( 0.625 ) ; // 250000 * 0.0000025
243+ } ) ;
244+
245+ it ( "should fall back to default tier when conditions do not match" , ( ) => {
246+ const tieredRegistry = new TestableRegistry ( null as any ) ;
247+ tieredRegistry . loadPatterns ( [ multiTierModel ] ) ;
248+
249+ const result = tieredRegistry . calculateCost ( "gemini-2.5-pro" , {
250+ input : 1000 ,
251+ output : 100 ,
252+ } ) ;
253+
254+ expect ( result ) . not . toBeNull ( ) ;
255+ expect ( result ! . pricingTierName ) . toBe ( "Standard" ) ;
256+ expect ( result ! . inputCost ) . toBeCloseTo ( 0.00125 ) ; // 1000 * 0.00000125
257+ } ) ;
258+
259+ it ( "should not let unconditional tier win over conditional match" , ( ) => {
260+ // Model where unconditional tier has lower priority than conditional
261+ const model : LlmModelWithPricing = {
262+ ...multiTierModel ,
263+ pricingTiers : [
264+ {
265+ id : "tier-unconditional" ,
266+ name : "Unconditional" ,
267+ isDefault : false ,
268+ priority : 0 ,
269+ conditions : [ ] ,
270+ prices : [ { usageType : "input" , price : 0.001 } ] ,
271+ } ,
272+ {
273+ id : "tier-conditional" ,
274+ name : "Conditional" ,
275+ isDefault : false ,
276+ priority : 1 ,
277+ conditions : [
278+ { usageDetailPattern : "input" , operator : "gt" as const , value : 100 } ,
279+ ] ,
280+ prices : [ { usageType : "input" , price : 0.0001 } ] ,
281+ } ,
282+ {
283+ id : "tier-default" ,
284+ name : "Default" ,
285+ isDefault : true ,
286+ priority : 2 ,
287+ conditions : [ ] ,
288+ prices : [ { usageType : "input" , price : 0.01 } ] ,
289+ } ,
290+ ] ,
291+ } ;
292+
293+ const tieredRegistry = new TestableRegistry ( null as any ) ;
294+ tieredRegistry . loadPatterns ( [ model ] ) ;
295+
296+ // Condition matches — conditional tier should win, not the unconditional one
297+ const result = tieredRegistry . calculateCost ( "gemini-2.5-pro" , { input : 500 } ) ;
298+ expect ( result ) . not . toBeNull ( ) ;
299+ expect ( result ! . pricingTierName ) . toBe ( "Conditional" ) ;
300+ } ) ;
301+
302+ it ( "should fall back to isDefault tier when no conditions match" , ( ) => {
303+ const model : LlmModelWithPricing = {
304+ ...multiTierModel ,
305+ pricingTiers : [
306+ {
307+ id : "tier-conditional" ,
308+ name : "Conditional" ,
309+ isDefault : false ,
310+ priority : 0 ,
311+ conditions : [
312+ { usageDetailPattern : "input" , operator : "gt" as const , value : 999999 } ,
313+ ] ,
314+ prices : [ { usageType : "input" , price : 0.001 } ] ,
315+ } ,
316+ {
317+ id : "tier-default" ,
318+ name : "Default" ,
319+ isDefault : true ,
320+ priority : 1 ,
321+ conditions : [ ] ,
322+ prices : [ { usageType : "input" , price : 0.0001 } ] ,
323+ } ,
324+ ] ,
325+ } ;
326+
327+ const tieredRegistry = new TestableRegistry ( null as any ) ;
328+ tieredRegistry . loadPatterns ( [ model ] ) ;
329+
330+ const result = tieredRegistry . calculateCost ( "gemini-2.5-pro" , { input : 100 } ) ;
331+ expect ( result ) . not . toBeNull ( ) ;
332+ expect ( result ! . pricingTierName ) . toBe ( "Default" ) ;
333+ } ) ;
334+ } ) ;
335+
177336 describe ( "defaultModelPrices (Langfuse JSON)" , ( ) => {
178337 it ( "should load all models from the JSON file" , ( ) => {
179338 expect ( defaultModelPrices . length ) . toBeGreaterThan ( 100 ) ;
0 commit comments