@@ -11,6 +11,12 @@ defmodule CodexPooler.Accounting.PricingResolution do
1111 @ default_price_bucket "default"
1212 @ long_context_price_bucket "long_context"
1313
14+ @ typep suffix_candidate :: % {
15+ from: String . t ( ) ,
16+ to: String . t ( ) ,
17+ distance: pos_integer ( )
18+ }
19+
1420 @ spec lookup ( Model . t ( ) , String . t ( ) , map ( ) , map ( ) , DateTime . t ( ) ) :: map ( )
1521 def lookup ( % Model { } = model , requested_model , payload , opts , timestamp ) do
1622 requested_tier = requested_service_tier ( payload , opts )
@@ -92,7 +98,7 @@ defmodule CodexPooler.Accounting.PricingResolution do
9298 def metadata ( pricing ) do
9399 snapshot = pricing . snapshot
94100
95- % {
101+ pricing_metadata = % {
96102 "status" => pricing . status ,
97103 "requested_service_tier" => pricing . requested_service_tier ,
98104 "actual_service_tier" => pricing . actual_service_tier ,
@@ -110,13 +116,15 @@ defmodule CodexPooler.Accounting.PricingResolution do
110116 else: nil
111117 )
112118 }
119+
120+ put_serialized_alias_metadata ( pricing_metadata , Map . get ( pricing , :alias ) )
113121 end
114122
115123 @ spec details ( map ( ) ) :: map ( )
116124 def details ( pricing ) do
117125 snapshot = pricing . snapshot
118126
119- % {
127+ details = % {
120128 "pricing_status" => pricing . status ,
121129 "requested_service_tier" => pricing . requested_service_tier ,
122130 "actual_service_tier" => pricing . actual_service_tier ,
@@ -126,6 +134,8 @@ defmodule CodexPooler.Accounting.PricingResolution do
126134 "batch_usage" => pricing . batch_usage ,
127135 "price_version" => snapshot && snapshot . price_version
128136 }
137+
138+ put_serialized_alias_metadata ( details , Map . get ( pricing , :alias ) )
129139 end
130140
131141 @ spec update_request_metadata ( map ( ) | nil , map ( ) ) :: map ( )
@@ -216,15 +226,39 @@ defmodule CodexPooler.Accounting.PricingResolution do
216226 priced_snapshot ( fallback , requested_tier , actual_tier , service_tier , batch_usage? )
217227
218228 nil ->
219- missing_pricing_snapshot (
220- model ,
221- identifiers ,
222- requested_tier ,
223- actual_tier ,
224- service_tier ,
225- timestamp ,
226- batch_usage?
227- )
229+ case suffix_inference_pricing_snapshot (
230+ identifiers ,
231+ service_tier ,
232+ price_bucket ,
233+ timestamp
234+ ) ||
235+ fallback_suffix_inference_pricing_snapshot (
236+ identifiers ,
237+ service_tier ,
238+ price_bucket ,
239+ timestamp
240+ ) do
241+ { % PricingSnapshot { } = inferred , alias_metadata } ->
242+ priced_snapshot (
243+ inferred ,
244+ requested_tier ,
245+ actual_tier ,
246+ service_tier ,
247+ batch_usage? ,
248+ alias_metadata
249+ )
250+
251+ nil ->
252+ missing_pricing_snapshot (
253+ model ,
254+ identifiers ,
255+ requested_tier ,
256+ actual_tier ,
257+ service_tier ,
258+ timestamp ,
259+ batch_usage?
260+ )
261+ end
228262 end
229263 end
230264 end
@@ -252,6 +286,190 @@ defmodule CodexPooler.Accounting.PricingResolution do
252286
253287 defp fallback_pricing_snapshot ( _identifiers , _service_tier , _price_bucket , _timestamp ) , do: nil
254288
289+ @ spec fallback_suffix_inference_pricing_snapshot (
290+ [ String . t ( ) ] ,
291+ String . t ( ) ,
292+ String . t ( ) ,
293+ DateTime . t ( )
294+ ) :: { % PricingSnapshot { } , map ( ) } | nil
295+ defp fallback_suffix_inference_pricing_snapshot (
296+ identifiers ,
297+ service_tier ,
298+ @ long_context_price_bucket ,
299+ timestamp
300+ ) ,
301+ do:
302+ suffix_inference_pricing_snapshot (
303+ identifiers ,
304+ service_tier ,
305+ @ default_price_bucket ,
306+ timestamp
307+ )
308+
309+ defp fallback_suffix_inference_pricing_snapshot (
310+ _identifiers ,
311+ _service_tier ,
312+ _price_bucket ,
313+ _timestamp
314+ ) ,
315+ do: nil
316+
317+ @ spec suffix_inference_pricing_snapshot ( [ String . t ( ) ] , String . t ( ) , String . t ( ) , DateTime . t ( ) ) ::
318+ { % PricingSnapshot { } , map ( ) } | nil
319+ defp suffix_inference_pricing_snapshot ( identifiers , service_tier , price_bucket , timestamp ) do
320+ candidates = suffix_inference_candidates ( identifiers )
321+ candidate_identifiers = candidates |> Enum . map ( & & 1 . to ) |> Enum . uniq ( )
322+
323+ snapshots_by_identifier =
324+ pricing_snapshots_by_identifier (
325+ candidate_identifiers ,
326+ service_tier ,
327+ price_bucket ,
328+ timestamp
329+ )
330+
331+ candidates
332+ |> Enum . flat_map ( & candidate_snapshot_match ( & 1 , snapshots_by_identifier ) )
333+ |> nearest_suffix_inference_match ( )
334+ end
335+
336+ @ spec pricing_snapshots_by_identifier ( [ String . t ( ) ] , String . t ( ) , String . t ( ) , DateTime . t ( ) ) ::
337+ map ( )
338+ defp pricing_snapshots_by_identifier ( [ ] , _service_tier , _price_bucket , _timestamp ) , do: % { }
339+
340+ defp pricing_snapshots_by_identifier ( identifiers , service_tier , price_bucket , timestamp ) do
341+ PricingSnapshot
342+ |> where (
343+ [ ps ] ,
344+ ps . model_identifier in ^ identifiers and ps . effective_at <= ^ timestamp and
345+ fragment ( "?->>'service_tier'" , ps . config ) == ^ service_tier and
346+ fragment ( "?->>'price_bucket'" , ps . config ) == ^ price_bucket and
347+ fragment ( "?->>'pricing_type'" , ps . config ) == "per_1m_tokens"
348+ )
349+ |> order_by ( [ ps ] ,
350+ asc: ps . model_identifier ,
351+ desc: ps . effective_at ,
352+ desc: ps . captured_at ,
353+ desc: ps . id
354+ )
355+ |> Repo . all ( )
356+ |> Enum . reduce ( % { } , fn snapshot , snapshots ->
357+ Map . put_new ( snapshots , snapshot . model_identifier , snapshot )
358+ end )
359+ end
360+
361+ @ spec suffix_inference_candidates ( [ String . t ( ) ] ) :: [ suffix_candidate ( ) ]
362+ defp suffix_inference_candidates ( identifiers ) do
363+ exact_identifiers = MapSet . new ( identifiers )
364+
365+ identifiers
366+ |> Enum . flat_map ( & suffix_inference_candidates_for_identifier / 1 )
367+ |> Enum . reject ( & MapSet . member? ( exact_identifiers , & 1 . to ) )
368+ |> Enum . uniq_by ( fn candidate -> { candidate . from , candidate . to , candidate . distance } end )
369+ end
370+
371+ @ spec suffix_inference_candidates_for_identifier ( term ( ) ) :: [ suffix_candidate ( ) ]
372+ defp suffix_inference_candidates_for_identifier ( identifier ) when is_binary ( identifier ) do
373+ normalized_identifier = String . trim ( identifier )
374+ segments = String . split ( normalized_identifier , "-" , trim: true )
375+
376+ case segments do
377+ [ _single ] ->
378+ [ ]
379+
380+ [ ] ->
381+ [ ]
382+
383+ _segments ->
384+ do_suffix_inference_candidates ( segments , normalized_identifier )
385+ end
386+ end
387+
388+ defp suffix_inference_candidates_for_identifier ( _identifier ) , do: [ ]
389+
390+ @ spec do_suffix_inference_candidates ( [ String . t ( ) ] , String . t ( ) ) :: [ suffix_candidate ( ) ]
391+ defp do_suffix_inference_candidates ( segments , original_identifier ) do
392+ segment_count = length ( segments )
393+
394+ 1 .. ( segment_count - 1 )
395+ |> Enum . reduce_while ( { [ ] , 0 } , fn distance , { candidates , arbitrary_trim_count } ->
396+ trimmed_segment = Enum . at ( segments , - distance )
397+
398+ arbitrary_trim_count =
399+ if date_or_version_suffix? ( trimmed_segment ) ,
400+ do: arbitrary_trim_count ,
401+ else: arbitrary_trim_count + 1
402+
403+ if arbitrary_trim_count > 1 do
404+ { :halt , { candidates , arbitrary_trim_count } }
405+ else
406+ candidate_identifier =
407+ segments
408+ |> Enum . take ( segment_count - distance )
409+ |> Enum . join ( "-" )
410+
411+ candidate =
412+ if blank? ( candidate_identifier ) or candidate_identifier == original_identifier do
413+ nil
414+ else
415+ % {
416+ from: original_identifier ,
417+ to: candidate_identifier ,
418+ distance: distance
419+ }
420+ end
421+
422+ candidates = if candidate , do: [ candidate | candidates ] , else: candidates
423+
424+ { :cont , { candidates , arbitrary_trim_count } }
425+ end
426+ end )
427+ |> elem ( 0 )
428+ |> Enum . reverse ( )
429+ end
430+
431+ @ spec date_or_version_suffix? ( term ( ) ) :: boolean ( )
432+ defp date_or_version_suffix? ( segment ) when is_binary ( segment ) do
433+ Regex . match? ( ~r/ \A (?:v)?\d +(?:[._]\d +)*\z / i , String . trim ( segment ) )
434+ end
435+
436+ defp date_or_version_suffix? ( _segment ) , do: false
437+
438+ @ spec candidate_snapshot_match ( suffix_candidate ( ) , map ( ) ) :: [ map ( ) ]
439+ defp candidate_snapshot_match ( candidate , snapshots_by_identifier ) do
440+ case Map . fetch ( snapshots_by_identifier , candidate . to ) do
441+ { :ok , snapshot } -> [ Map . put ( candidate , :snapshot , snapshot ) ]
442+ :error -> [ ]
443+ end
444+ end
445+
446+ @ spec nearest_suffix_inference_match ( [ map ( ) ] ) :: { % PricingSnapshot { } , map ( ) } | nil
447+ defp nearest_suffix_inference_match ( [ ] ) , do: nil
448+
449+ defp nearest_suffix_inference_match ( matches ) do
450+ nearest_distance = matches |> Enum . map ( & & 1 . distance ) |> Enum . min ( )
451+ nearest_matches = Enum . filter ( matches , & ( & 1 . distance == nearest_distance ) )
452+
453+ case nearest_matches |> Enum . map ( & & 1 . snapshot . model_identifier ) |> Enum . uniq ( ) do
454+ [ model_identifier ] ->
455+ match = Enum . find ( nearest_matches , & ( & 1 . snapshot . model_identifier == model_identifier ) )
456+
457+ { match . snapshot , suffix_alias_metadata ( match . from , model_identifier ) }
458+
459+ _ambiguous_or_missing ->
460+ nil
461+ end
462+ end
463+
464+ @ spec suffix_alias_metadata ( String . t ( ) , String . t ( ) ) :: map ( )
465+ defp suffix_alias_metadata ( from_identifier , to_identifier ) do
466+ % {
467+ "source" => "suffix_inference" ,
468+ "from" => from_identifier ,
469+ "to" => to_identifier
470+ }
471+ end
472+
255473 defp pricing_identifiers ( model , requested_model ) do
256474 Enum . uniq (
257475 Enum . reject (
@@ -285,8 +503,15 @@ defmodule CodexPooler.Accounting.PricingResolution do
285503 unpriced_snapshot ( status , requested_tier , actual_tier , service_tier , batch_usage? )
286504 end
287505
288- defp priced_snapshot ( snapshot , requested_tier , actual_tier , service_tier , batch_usage? ) do
289- % {
506+ defp priced_snapshot (
507+ snapshot ,
508+ requested_tier ,
509+ actual_tier ,
510+ service_tier ,
511+ batch_usage? ,
512+ alias_metadata \\ nil
513+ ) do
514+ pricing = % {
290515 snapshot: snapshot ,
291516 status: "priced" ,
292517 requested_service_tier: requested_tier ,
@@ -296,6 +521,8 @@ defmodule CodexPooler.Accounting.PricingResolution do
296521 pricing_type: snapshot . config [ "pricing_type" ] ,
297522 batch_usage: batch_usage?
298523 }
524+
525+ put_pricing_alias_metadata ( pricing , alias_metadata )
299526 end
300527
301528 defp unpriced_snapshot ( status , requested_tier , actual_tier , service_tier , batch_usage? ) do
@@ -466,5 +693,15 @@ defmodule CodexPooler.Accounting.PricingResolution do
466693 defp blank_to_nil ( value ) , do: if ( blank? ( value ) , do: nil , else: value )
467694 defp truthy? ( value ) , do: value in [ true , "true" , "1" , 1 , "yes" , "batch" ]
468695
696+ defp put_pricing_alias_metadata ( pricing , nil ) , do: pricing
697+
698+ defp put_pricing_alias_metadata ( pricing , alias_metadata ) ,
699+ do: Map . put ( pricing , :alias , alias_metadata )
700+
701+ defp put_serialized_alias_metadata ( serialized , nil ) , do: serialized
702+
703+ defp put_serialized_alias_metadata ( serialized , alias_metadata ) ,
704+ do: Map . put ( serialized , "alias" , alias_metadata )
705+
469706 defp now , do: DateTime . utc_now ( ) |> DateTime . truncate ( :microsecond )
470707end
0 commit comments