@@ -35,11 +35,20 @@ public static class TierRoutingAnalyzer
3535 /// Run the analyzer over a snapshot of entries. The caller is responsible for
3636 /// any time-window filtering before calling — this method aggregates whatever it gets.
3737 /// </summary>
38+ /// <param name="highTierCostFloorMultiplier">
39+ /// Routing cost floor. For threshold-scan and flagged-cluster cost projections, the High
40+ /// tier is priced at no less than this multiple of the Balanced tier's real rate. Prevents
41+ /// the dream from reading a Balanced→High shift as zero-cost — and ratcheting balancedCeiling
42+ /// to its floor — when High currently shares Balanced's model. <c>Math.Max</c> means a
43+ /// genuinely premium High model keeps its real (higher) price. 1.0 disables the floor.
44+ /// Does NOT affect <see cref="TierRoutingProjectedCost"/>, which always reports real spend.
45+ /// </param>
3846 public static TierRoutingAnalysis Analyze (
3947 IReadOnlyList < TierRoutingEntry > entries ,
4048 TierSelectorConfig ? currentConfig = null ,
4149 IReadOnlyList < LlmPricingRow > ? pricing = null ,
42- IReadOnlyDictionary < ModelTier , string ? > ? tierModelMap = null )
50+ IReadOnlyDictionary < ModelTier , string ? > ? tierModelMap = null ,
51+ double highTierCostFloorMultiplier = 1.0 )
4352 {
4453 var lowCeiling = currentConfig ? . LowCeiling ?? DefaultLowCeiling ;
4554 var balancedCeiling = currentConfig ? . BalancedCeiling ?? DefaultBalancedCeiling ;
@@ -65,9 +74,9 @@ public static TierRoutingAnalysis Analyze(
6574
6675 var globalStats = BuildGlobalStats ( entries , fallbackCount ) ;
6776 var clusters = BuildClusters ( quality ) ;
68- var flagged = FlagClusters ( clusters , pricing , tierModelMap ) ;
77+ var flagged = FlagClusters ( clusters , pricing , tierModelMap , highTierCostFloorMultiplier ) ;
6978 var keywordCandidates = BuildKeywordCandidates ( quality ) ;
70- var scans = BuildThresholdScans ( quality , lowCeiling , balancedCeiling , pricing , tierModelMap ) ;
79+ var scans = BuildThresholdScans ( quality , lowCeiling , balancedCeiling , pricing , tierModelMap , highTierCostFloorMultiplier ) ;
7180 var projectedCost = BuildProjectedCost ( entries , pricing ) ;
7281
7382 return new TierRoutingAnalysis (
@@ -183,7 +192,8 @@ private static string BuildSignature(TierRoutingEntry e)
183192 private static IReadOnlyList < TierRoutingFlaggedCluster > FlagClusters (
184193 IReadOnlyList < TierRoutingCluster > clusters ,
185194 IReadOnlyList < LlmPricingRow > ? pricing ,
186- IReadOnlyDictionary < ModelTier , string ? > ? tierModelMap )
195+ IReadOnlyDictionary < ModelTier , string ? > ? tierModelMap ,
196+ double highTierCostFloorMultiplier )
187197 {
188198 var flagged = new List < TierRoutingFlaggedCluster > ( ) ;
189199
@@ -195,7 +205,7 @@ private static IReadOnlyList<TierRoutingFlaggedCluster> FlagClusters(
195205 {
196206 flagged . Add ( BuildFlagged ( c , "panicEscalation" ,
197207 $ "Low tier averaging { c . AvgToolCalls : F1} tool calls across { c . Count } entries — model likely struggling.",
198- ModelTier . Balanced , pricing , tierModelMap ) ) ;
208+ ModelTier . Balanced , pricing , tierModelMap , highTierCostFloorMultiplier ) ) ;
199209 continue ;
200210 }
201211
@@ -204,7 +214,7 @@ private static IReadOnlyList<TierRoutingFlaggedCluster> FlagClusters(
204214 {
205215 flagged . Add ( BuildFlagged ( c , "tokenSurprise" ,
206216 $ "Score { c . AvgComplexityScore : F2} but post-injection tokens { p } — context inflation, not user complexity. Informational only.",
207- alternateTier : null , pricing , tierModelMap ) ) ;
217+ alternateTier : null , pricing , tierModelMap , highTierCostFloorMultiplier ) ) ;
208218 continue ;
209219 }
210220
@@ -213,7 +223,7 @@ private static IReadOnlyList<TierRoutingFlaggedCluster> FlagClusters(
213223 {
214224 flagged . Add ( BuildFlagged ( c , "lowOutputAtHigh" ,
215225 $ "High tier producing only { o } avg output tokens across { c . Count } entries — possible over-routing.",
216- ModelTier . Balanced , pricing , tierModelMap ) ) ;
226+ ModelTier . Balanced , pricing , tierModelMap , highTierCostFloorMultiplier ) ) ;
217227 }
218228 }
219229
@@ -222,18 +232,19 @@ private static IReadOnlyList<TierRoutingFlaggedCluster> FlagClusters(
222232
223233 private static TierRoutingFlaggedCluster BuildFlagged (
224234 TierRoutingCluster cluster , string flag , string rationale , ModelTier ? alternateTier ,
225- IReadOnlyList < LlmPricingRow > ? pricing , IReadOnlyDictionary < ModelTier , string ? > ? tierModelMap )
235+ IReadOnlyList < LlmPricingRow > ? pricing , IReadOnlyDictionary < ModelTier , string ? > ? tierModelMap ,
236+ double highTierCostFloorMultiplier )
226237 {
227238 decimal ? currentCost = null ;
228239 decimal ? alternateCost = null ;
229240 if ( pricing is not null && tierModelMap is not null
230241 && cluster . AvgInputTokens is long ai && cluster . AvgOutputTokens is long ao )
231242 {
232- var perCallCurrent = TryPriceCall ( tierModelMap . GetValueOrDefault ( cluster . Tier ) , ai , ao , pricing ) ;
243+ var perCallCurrent = TryPriceCallForTier ( cluster . Tier , ai , ao , tierModelMap , pricing , highTierCostFloorMultiplier ) ;
233244 currentCost = perCallCurrent * cluster . Count ;
234245 if ( alternateTier is ModelTier alt )
235246 {
236- var perCallAlt = TryPriceCall ( tierModelMap . GetValueOrDefault ( alt ) , ai , ao , pricing ) ;
247+ var perCallAlt = TryPriceCallForTier ( alt , ai , ao , tierModelMap , pricing , highTierCostFloorMultiplier ) ;
237248 alternateCost = perCallAlt * cluster . Count ;
238249 }
239250 }
@@ -325,14 +336,15 @@ private static IReadOnlyList<TierRoutingThresholdScan> BuildThresholdScans(
325336 IReadOnlyList < TierRoutingEntry > entries ,
326337 double lowCeiling , double balancedCeiling ,
327338 IReadOnlyList < LlmPricingRow > ? pricing ,
328- IReadOnlyDictionary < ModelTier , string ? > ? tierModelMap )
339+ IReadOnlyDictionary < ModelTier , string ? > ? tierModelMap ,
340+ double highTierCostFloorMultiplier )
329341 {
330342 return new [ ]
331343 {
332- BuildScan ( entries , "lowCeiling" , + ThresholdScanDelta , lowCeiling , balancedCeiling , pricing , tierModelMap ) ,
333- BuildScan ( entries , "lowCeiling" , - ThresholdScanDelta , lowCeiling , balancedCeiling , pricing , tierModelMap ) ,
334- BuildScan ( entries , "balancedCeiling" , + ThresholdScanDelta , lowCeiling , balancedCeiling , pricing , tierModelMap ) ,
335- BuildScan ( entries , "balancedCeiling" , - ThresholdScanDelta , lowCeiling , balancedCeiling , pricing , tierModelMap )
344+ BuildScan ( entries , "lowCeiling" , + ThresholdScanDelta , lowCeiling , balancedCeiling , pricing , tierModelMap , highTierCostFloorMultiplier ) ,
345+ BuildScan ( entries , "lowCeiling" , - ThresholdScanDelta , lowCeiling , balancedCeiling , pricing , tierModelMap , highTierCostFloorMultiplier ) ,
346+ BuildScan ( entries , "balancedCeiling" , + ThresholdScanDelta , lowCeiling , balancedCeiling , pricing , tierModelMap , highTierCostFloorMultiplier ) ,
347+ BuildScan ( entries , "balancedCeiling" , - ThresholdScanDelta , lowCeiling , balancedCeiling , pricing , tierModelMap , highTierCostFloorMultiplier )
336348 } ;
337349 }
338350
@@ -341,7 +353,8 @@ private static TierRoutingThresholdScan BuildScan(
341353 string threshold , double delta ,
342354 double lowCeiling , double balancedCeiling ,
343355 IReadOnlyList < LlmPricingRow > ? pricing ,
344- IReadOnlyDictionary < ModelTier , string ? > ? tierModelMap )
356+ IReadOnlyDictionary < ModelTier , string ? > ? tierModelMap ,
357+ double highTierCostFloorMultiplier )
345358 {
346359 // Round shifted thresholds to 4 decimals — bare double subtraction yields
347360 // 0.15 - 0.05 = 0.09999999999999998, which makes boundary entries (score 0.10)
@@ -370,8 +383,8 @@ private static TierRoutingThresholdScan BuildScan(
370383 foreach ( var ( e , from , to ) in flips )
371384 {
372385 if ( e . InputTokens is not long it || e . OutputTokens is not long ot ) continue ;
373- var fromCost = TryPriceCall ( tierModelMap . GetValueOrDefault ( from ) , it , ot , pricing ) ;
374- var toCost = TryPriceCall ( tierModelMap . GetValueOrDefault ( to ) , it , ot , pricing ) ;
386+ var fromCost = TryPriceCallForTier ( from , it , ot , tierModelMap , pricing , highTierCostFloorMultiplier ) ;
387+ var toCost = TryPriceCallForTier ( to , it , ot , tierModelMap , pricing , highTierCostFloorMultiplier ) ;
375388 if ( fromCost is null || toCost is null ) continue ;
376389 accum += toCost . Value - fromCost . Value ;
377390 any = true ;
@@ -442,11 +455,67 @@ private static TierRoutingProjectedCost BuildProjectedCost(
442455 EntriesUnpricedCount : unpriced ) ;
443456 }
444457
458+ /// <summary>
459+ /// Prices a call by raw model ID at real pricing — no cost floor. Used for
460+ /// <see cref="BuildProjectedCost"/>, which reports actual spend.
461+ /// </summary>
445462 private static decimal ? TryPriceCall ( string ? modelId , long inputTokens , long outputTokens , IReadOnlyList < LlmPricingRow > pricing )
446463 {
447- if ( string . IsNullOrEmpty ( modelId ) ) return null ;
448- var row = pricing . FirstOrDefault ( p => modelId . Contains ( p . Prefix , StringComparison . OrdinalIgnoreCase ) ) ;
464+ var row = MatchRow ( modelId , pricing ) ;
449465 if ( row is null ) return null ;
450466 return ( inputTokens * row . InputPerM + outputTokens * row . OutputPerM ) / 1_000_000m ;
451467 }
468+
469+ /// <summary>
470+ /// Prices a call for a specific <see cref="ModelTier"/> with the routing cost floor applied
471+ /// (see <see cref="EffectiveTierRates"/>). Used by the threshold-scan and flagged-cluster
472+ /// projections that drive tuning decisions — NOT by real-spend reporting.
473+ /// </summary>
474+ private static decimal ? TryPriceCallForTier (
475+ ModelTier tier , long inputTokens , long outputTokens ,
476+ IReadOnlyDictionary < ModelTier , string ? > tierModelMap ,
477+ IReadOnlyList < LlmPricingRow > pricing ,
478+ double highTierCostFloorMultiplier )
479+ {
480+ var rates = EffectiveTierRates ( tier , tierModelMap , pricing , highTierCostFloorMultiplier ) ;
481+ if ( rates is null ) return null ;
482+ var ( inPerM , outPerM ) = rates . Value ;
483+ return ( inputTokens * inPerM + outputTokens * outPerM ) / 1_000_000m ;
484+ }
485+
486+ /// <summary>
487+ /// Effective per-million-token rates for a tier. For the High tier, rates are floored at
488+ /// <paramref name="highTierCostFloorMultiplier"/> × the Balanced tier's real rates so a
489+ /// Balanced→High shift never projects as zero-cost when High shares Balanced's model.
490+ /// <c>Math.Max</c> keeps a genuinely premium High model at its real (higher) price.
491+ /// Returns <c>null</c> when the tier's model has no pricing row.
492+ /// </summary>
493+ private static ( decimal InputPerM , decimal OutputPerM ) ? EffectiveTierRates (
494+ ModelTier tier ,
495+ IReadOnlyDictionary < ModelTier , string ? > tierModelMap ,
496+ IReadOnlyList < LlmPricingRow > pricing ,
497+ double highTierCostFloorMultiplier )
498+ {
499+ var row = MatchRow ( tierModelMap . GetValueOrDefault ( tier ) , pricing ) ;
500+ if ( row is null ) return null ;
501+
502+ if ( tier == ModelTier . High && highTierCostFloorMultiplier > 1.0 )
503+ {
504+ var balanced = MatchRow ( tierModelMap . GetValueOrDefault ( ModelTier . Balanced ) , pricing ) ;
505+ if ( balanced is not null )
506+ {
507+ var m = ( decimal ) highTierCostFloorMultiplier ;
508+ return ( Math . Max ( row . InputPerM , balanced . InputPerM * m ) ,
509+ Math . Max ( row . OutputPerM , balanced . OutputPerM * m ) ) ;
510+ }
511+ }
512+
513+ return ( row . InputPerM , row . OutputPerM ) ;
514+ }
515+
516+ private static LlmPricingRow ? MatchRow ( string ? modelId , IReadOnlyList < LlmPricingRow > pricing )
517+ {
518+ if ( string . IsNullOrEmpty ( modelId ) ) return null ;
519+ return pricing . FirstOrDefault ( p => modelId . Contains ( p . Prefix , StringComparison . OrdinalIgnoreCase ) ) ;
520+ }
452521}
0 commit comments