@@ -340,16 +340,15 @@ func (r *Router) SetClassifierMinLen(n int) {
340340}
341341
342342func (r * Router ) Chat (ctx context.Context , messages []Message , systemPrompt string , tools []Tool ) (Response , error ) {
343- provider := r .pick (ctx , messages )
344- primaryKey := r .keyFor (provider )
343+ provider , role := r .pick (ctx , messages )
345344 r .mu .Lock ()
346345 r .lastRouted = provider .Name ()
347- r .lastRoutedKey = primaryKey
346+ r .lastRoutedKey = r . keyFor ( provider )
348347 r .mu .Unlock ()
349348
350349 start := time .Now ()
351350 resp , err := provider .Chat (ctx , messages , systemPrompt , tools )
352- r .recordUsage (ctx , provider , primaryKey , resp , err , time .Since (start ))
351+ r .recordUsage (ctx , provider , role , resp , err , time .Since (start ))
353352
354353 if err != nil && isUnavailable (err ) {
355354 // Build fallback chain: override → default → fallback.
@@ -403,6 +402,7 @@ func (r *Router) recordUsage(ctx context.Context, p Provider, role string, resp
403402 CompletionTokens : resp .Usage .CompletionTokens ,
404403 CachedPromptTokens : resp .Usage .CachedPromptTokens ,
405404 ReasoningTokens : resp .Usage .ReasoningTokens ,
405+ Cost : resp .Usage .Cost ,
406406 LatencyMs : int (latency / time .Millisecond ),
407407 Success : callErr == nil ,
408408 ErrorClass : ClassifyErrorClass (callErr ),
@@ -435,27 +435,26 @@ func splitProviderName(name string) (provider, model string) {
435435// ChatStream returns a streaming channel if the current provider supports it.
436436// Falls back to wrapping a synchronous Chat() in a single-chunk channel.
437437func (r * Router ) ChatStream (ctx context.Context , messages []Message , systemPrompt string , tools []Tool ) (<- chan StreamChunk , error ) {
438- provider := r .pick (ctx , messages )
439- primaryKey := r .keyFor (provider )
438+ provider , role := r .pick (ctx , messages )
440439 r .mu .Lock ()
441440 r .lastRouted = provider .Name ()
442- r .lastRoutedKey = primaryKey
441+ r .lastRoutedKey = r . keyFor ( provider )
443442 r .mu .Unlock ()
444443 if sp , ok := provider .(StreamProvider ); ok {
445444 ch , err := sp .ChatStream (ctx , messages , systemPrompt , tools )
446445 if err != nil {
447446 // Even a start-of-stream failure deserves a usage record with
448447 // error_class set so dashboards see it.
449- r .recordUsage (ctx , provider , primaryKey , Response {}, err , 0 )
448+ r .recordUsage (ctx , provider , role , Response {}, err , 0 )
450449 if isUnavailable (err ) {
451450 return r .syncFallbackStream (ctx , provider , messages , systemPrompt , tools , err )
452451 }
453452 return nil , err
454453 }
455- return r .instrumentedStream (ctx , provider , primaryKey , ch ), nil
454+ return r .instrumentedStream (ctx , provider , role , ch ), nil
456455 }
457456 // Provider does not stream — wrap synchronous call.
458- return r .syncStream (ctx , provider , messages , systemPrompt , tools )
457+ return r .syncStream (ctx , provider , role , messages , systemPrompt , tools )
459458}
460459
461460// instrumentedStream tees the provider's stream to the caller and records a
@@ -484,8 +483,7 @@ func (r *Router) instrumentedStream(ctx context.Context, provider Provider, role
484483// syncStream wraps a synchronous Chat() call in a channel. Usage is recorded
485484// via the same path as Router.Chat so streaming callers that hit non-stream
486485// providers still produce UsageLog rows.
487- func (r * Router ) syncStream (ctx context.Context , provider Provider , messages []Message , systemPrompt string , tools []Tool ) (<- chan StreamChunk , error ) {
488- role := r .keyFor (provider )
486+ func (r * Router ) syncStream (ctx context.Context , provider Provider , role string , messages []Message , systemPrompt string , tools []Tool ) (<- chan StreamChunk , error ) {
489487 start := time .Now ()
490488 resp , err := provider .Chat (ctx , messages , systemPrompt , tools )
491489 r .recordUsage (ctx , provider , role , resp , err , time .Since (start ))
@@ -522,21 +520,22 @@ func (r *Router) syncFallbackStream(ctx context.Context, failed Provider, messag
522520 r .recordUsage (ctx , next , "fallback" , Response {}, err , 0 )
523521 }
524522 // Otherwise sync.
525- return r .syncStream (ctx , next , messages , systemPrompt , tools )
523+ return r .syncStream (ctx , next , "fallback" , messages , systemPrompt , tools )
526524 }
527525 return nil , origErr
528526}
529527
530528// SupportsStreaming returns true if the currently active provider implements StreamProvider.
531529func (r * Router ) SupportsStreaming () bool {
532- provider := r .pick (context .Background (), nil )
530+ provider , _ := r .pick (context .Background (), nil )
533531 _ , ok := provider .(StreamProvider )
534532 return ok
535533}
536534
537535// Name returns the name of the currently active provider (respecting override).
538536func (r * Router ) Name () string {
539- return r .pick (context .Background (), nil ).Name ()
537+ p , _ := r .pick (context .Background (), nil )
538+ return p .Name ()
540539}
541540
542541// SetOverride sets a named model override. Pass "" to clear. Returns error if name is unknown.
@@ -590,10 +589,16 @@ func (r *Router) ProviderNames() []string {
590589 return names
591590}
592591
593- func (r * Router ) pick (ctx context.Context , messages []Message ) Provider {
592+ // pick selects the provider for this request AND returns the logical routing
593+ // role that drove the decision (simple/default/complex/multimodal/override/
594+ // tool-continuation). Role is what ends up in usage_log — conceptually
595+ // useful for analytics. Separate from the provider's slot key (e.g. the same
596+ // "simple" role may be backed by different slot names across deployments).
597+ func (r * Router ) pick (ctx context.Context , messages []Message ) (Provider , string ) {
594598 r .mu .RLock ()
595599 cfg := r .cfg
596600 override := r .override
601+ lastRoutedKey := r .lastRoutedKey
597602 r .mu .RUnlock ()
598603
599604 multimodal := hasMultimodalContent (messages )
@@ -602,7 +607,7 @@ func (r *Router) pick(ctx context.Context, messages []Message) Provider {
602607 if p := r .get (override ); p != nil {
603608 if ! multimodal || supportsVision (p ) {
604609 slog .Info ("routing" , "reason" , "override" , "provider" , p .Name ())
605- return p
610+ return p , "override"
606611 }
607612 // Override doesn't support vision — fall through to multimodal
608613 }
@@ -612,23 +617,23 @@ func (r *Router) pick(ctx context.Context, messages []Message) Provider {
612617 if multimodal {
613618 if p := r .get (cfg .Simple ); p != nil && supportsVision (p ) {
614619 slog .Info ("routing" , "reason" , "simple+vision" , "provider" , p .Name ())
615- return p
620+ return p , "simple"
616621 }
617622 if p := r .get (cfg .Default ); p != nil && supportsVision (p ) {
618623 slog .Info ("routing" , "reason" , "default+vision" , "provider" , p .Name ())
619- return p
624+ return p , "default"
620625 }
621626 if p := r .get (cfg .Multimodal ); p != nil {
622627 slog .Info ("routing" , "reason" , "multimodal" , "provider" , p .Name ())
623- return p
628+ return p , "multimodal"
624629 }
625630 }
626631
627632 // Tool continuation — keep using the same provider that started the tool loop.
628- if hasToolMessages (messages ) && r . lastRoutedKey != "" {
629- if last := r .get (r . lastRoutedKey ); last != nil {
633+ if hasToolMessages (messages ) && lastRoutedKey != "" {
634+ if last := r .get (lastRoutedKey ); last != nil {
630635 slog .Info ("routing" , "reason" , "tool-continuation" , "provider" , last .Name ())
631- return last
636+ return last , "tool-continuation"
632637 }
633638 }
634639
@@ -644,12 +649,12 @@ func (r *Router) pick(ctx context.Context, messages []Message) Provider {
644649 case 1 :
645650 if p := r .get (cfg .Simple ); p != nil {
646651 slog .Info ("routing" , "reason" , "classifier→simple" , "level" , 1 , "provider" , p .Name ())
647- return p
652+ return p , "simple"
648653 }
649654 case 3 :
650655 if p := r .get (cfg .Complex ); p != nil {
651656 slog .Info ("routing" , "reason" , "classifier→complex" , "level" , 3 , "provider" , p .Name ())
652- return p
657+ return p , "complex"
653658 }
654659 }
655660 // level 2 or fallback → default
@@ -658,13 +663,13 @@ func (r *Router) pick(ctx context.Context, messages []Message) Provider {
658663
659664 if p := r .get (cfg .Default ); p != nil {
660665 slog .Info ("routing" , "reason" , "default" , "provider" , p .Name ())
661- return p
666+ return p , "default"
662667 }
663668 // Should never happen if config is valid
664669 for _ , p := range r .providers {
665- return p
670+ return p , "default"
666671 }
667- return nil
672+ return nil , ""
668673}
669674
670675func (r * Router ) get (key string ) Provider {
0 commit comments