pi-smart-router 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/.pi/extensions/smart-router/delegation-runtime.ts +12 -2
  2. package/README.md +55 -17
  3. package/dist/api/explain/router-explain.d.ts +4 -1
  4. package/dist/api/explain/router-explain.d.ts.map +1 -1
  5. package/dist/api/explain/router-explain.js +9 -9
  6. package/dist/api/explain/router-explain.js.map +1 -1
  7. package/dist/config/defaults.d.ts +4 -1
  8. package/dist/config/defaults.d.ts.map +1 -1
  9. package/dist/config/defaults.js +10 -0
  10. package/dist/config/defaults.js.map +1 -1
  11. package/dist/domain/delegation/delegation-context.d.ts +23 -1
  12. package/dist/domain/delegation/delegation-context.d.ts.map +1 -1
  13. package/dist/domain/delegation/delegation-context.js +93 -0
  14. package/dist/domain/delegation/delegation-context.js.map +1 -1
  15. package/dist/domain/pinning/cache-breakeven.d.ts +51 -0
  16. package/dist/domain/pinning/cache-breakeven.d.ts.map +1 -0
  17. package/dist/domain/pinning/cache-breakeven.js +124 -0
  18. package/dist/domain/pinning/cache-breakeven.js.map +1 -0
  19. package/dist/domain/pinning/saar-session-state.d.ts +36 -0
  20. package/dist/domain/pinning/saar-session-state.d.ts.map +1 -0
  21. package/dist/domain/pinning/saar-session-state.js +74 -0
  22. package/dist/domain/pinning/saar-session-state.js.map +1 -0
  23. package/dist/domain/pinning/session-pinner.d.ts +35 -3
  24. package/dist/domain/pinning/session-pinner.d.ts.map +1 -1
  25. package/dist/domain/pinning/session-pinner.js +149 -1
  26. package/dist/domain/pinning/session-pinner.js.map +1 -1
  27. package/dist/domain/pipeline/router-pipeline.d.ts +15 -1
  28. package/dist/domain/pipeline/router-pipeline.d.ts.map +1 -1
  29. package/dist/domain/pipeline/router-pipeline.js +118 -6
  30. package/dist/domain/pipeline/router-pipeline.js.map +1 -1
  31. package/dist/domain/routing/tool-history-guard.d.ts +21 -5
  32. package/dist/domain/routing/tool-history-guard.d.ts.map +1 -1
  33. package/dist/domain/routing/tool-history-guard.js +77 -10
  34. package/dist/domain/routing/tool-history-guard.js.map +1 -1
  35. package/dist/domain/types/entities.d.ts +48 -0
  36. package/dist/domain/types/entities.d.ts.map +1 -1
  37. package/dist/domain/types/index.d.ts +1 -1
  38. package/dist/domain/types/index.d.ts.map +1 -1
  39. package/dist/domain/types/schemas.d.ts +24 -0
  40. package/dist/domain/types/schemas.d.ts.map +1 -1
  41. package/dist/domain/types/schemas.js +54 -0
  42. package/dist/domain/types/schemas.js.map +1 -1
  43. package/dist/infra/gemini-provider.d.ts.map +1 -1
  44. package/dist/infra/gemini-provider.js +2 -1
  45. package/dist/infra/gemini-provider.js.map +1 -1
  46. package/dist/infrastructure/persistence/sqlite-store.d.ts.map +1 -1
  47. package/dist/infrastructure/persistence/sqlite-store.js +3 -0
  48. package/dist/infrastructure/persistence/sqlite-store.js.map +1 -1
  49. package/dist/infrastructure/telemetry/routing-telemetry.d.ts +30 -2
  50. package/dist/infrastructure/telemetry/routing-telemetry.d.ts.map +1 -1
  51. package/dist/infrastructure/telemetry/routing-telemetry.js +221 -3
  52. package/dist/infrastructure/telemetry/routing-telemetry.js.map +1 -1
  53. package/package.json +5 -4
  54. package/src/api/explain/router-explain.ts +14 -10
  55. package/src/config/defaults.ts +18 -1
  56. package/src/domain/delegation/delegation-context.ts +124 -0
  57. package/src/domain/pinning/cache-breakeven.ts +219 -0
  58. package/src/domain/pinning/saar-session-state.ts +93 -0
  59. package/src/domain/pinning/session-pinner.ts +244 -3
  60. package/src/domain/pipeline/router-pipeline.ts +159 -7
  61. package/src/domain/routing/tool-history-guard.ts +108 -10
  62. package/src/domain/types/entities.ts +54 -0
  63. package/src/domain/types/index.ts +4 -0
  64. package/src/domain/types/schemas.ts +69 -0
  65. package/src/infra/gemini-provider.ts +2 -1
  66. package/src/infrastructure/persistence/sqlite-store.ts +6 -0
  67. package/src/infrastructure/telemetry/routing-telemetry.ts +327 -7
@@ -16,7 +16,11 @@ import {
16
16
  } from '../../domain/routing/context-fit.js';
17
17
  import { CLUSTER_REASON_CODE_PREFIX } from '../../config/routing-clusters-loader.js';
18
18
  import type { ClusterMatcher } from '../../domain/matching/cluster-matcher.js';
19
+ import { evaluateModelSwitchBreakeven } from '../../domain/pinning/session-pinner.js';
20
+ import { selectLowestCostModel } from '../../domain/pinning/sub-route-policy.js';
21
+ import type { SessionPinner } from '../../domain/pinning/session-pinner.js';
19
22
  import type {
23
+ BreakevenObservability,
20
24
  ClusterMatchTableEntry,
21
25
  ContextFitObservability,
22
26
  ContextFitRejectedEntry,
@@ -27,6 +31,9 @@ import type {
27
31
  RoutingDecision,
28
32
  RoutingRequest,
29
33
  RoutingTelemetry,
34
+ SaarConfig,
35
+ SaarObservability,
36
+ Tier,
30
37
  TierFeatureSummary,
31
38
  TierSelectionObservability,
32
39
  } from '../../domain/types/index.js';
@@ -47,6 +54,26 @@ export const HIGH_INTENSITY_STRUCTURAL = 'high_intensity_structural' as const;
47
54
  export const P_SUCCESS_CHEAP = 'p_success_cheap' as const;
48
55
  export const P_SUCCESS_UNCERTAIN = 'p_success_uncertain' as const;
49
56
 
57
+ export const BREAKEVEN_BLOCKED = 'breakeven_blocked' as const;
58
+ export const BREAKEVEN_PASS = 'breakeven_pass' as const;
59
+ export const SAAR_BUFFER_ACTIVE = 'saar_buffer_active' as const;
60
+ export const SAAR_HARD_LOCK = 'saar_hard_lock' as const;
61
+
62
+ const TURN_ENVELOPE_TIER_MAP: Readonly<Record<string, Tier | null>> = {
63
+ planning: 'frontier-cloud',
64
+ tool_result: 'economical-cloud',
65
+ subagent: 'economical-cloud',
66
+ main_loop: null,
67
+ unknown: null,
68
+ };
69
+
70
+ const SAAR_DECISION_REASON_CODES = new Set<string>([
71
+ SAAR_BUFFER_ACTIVE,
72
+ SAAR_HARD_LOCK,
73
+ 'saar_tier_upgrade',
74
+ 'saar_idle_reopen',
75
+ ]);
76
+
50
77
  const EXPECTED_COST_PREFIX = 'expected_cost_';
51
78
  const EXPECTED_COST_DEFER_CODES = new Set<string>([
52
79
  'expected_cost_price_delta_insufficient',
@@ -88,6 +115,8 @@ export interface TelemetryEmitterOptions {
88
115
  readonly onRecord?: (record: RoutingTelemetry) => void;
89
116
  readonly fleet?: readonly ModelProfile[];
90
117
  readonly contextFitConfig?: ContextFitConfig;
118
+ readonly sessionPinner?: SessionPinner;
119
+ readonly saarConfig?: SaarConfig;
91
120
  }
92
121
 
93
122
  export interface ContextFitObservabilityInput {
@@ -467,6 +496,223 @@ export function buildTierSelectionObservability(
467
496
  };
468
497
  }
469
498
 
499
+ export interface PinEconomicsObservabilityInput {
500
+ readonly request: RoutingRequest;
501
+ readonly decision: RoutingDecision;
502
+ readonly fleet?: readonly ModelProfile[] | undefined;
503
+ readonly sessionPinner?: SessionPinner | undefined;
504
+ readonly saarConfig?: SaarConfig | undefined;
505
+ }
506
+
507
+ function resolveTurnEnvelopeTargetTier(request: RoutingRequest): Tier | null {
508
+ const turnType = request.turn_type ?? 'unknown';
509
+ return TURN_ENVELOPE_TIER_MAP[turnType] ?? null;
510
+ }
511
+
512
+ function isSaarPlanningBufferActive(
513
+ request: RoutingRequest,
514
+ sessionPinner: SessionPinner | undefined,
515
+ saarConfig: SaarConfig | undefined,
516
+ ): boolean {
517
+ if (!saarConfig || !sessionPinner || request.turn_type !== 'planning') {
518
+ return false;
519
+ }
520
+
521
+ if (!sessionPinner.getPin(request.session_id)) {
522
+ return false;
523
+ }
524
+
525
+ const saarState = sessionPinner.getSaarState(request.session_id);
526
+ const turnIndex = saarState?.turn_index ?? 0;
527
+
528
+ return turnIndex < saarConfig.planning_turn_buffer;
529
+ }
530
+
531
+ function resolveSaarReasonCode(decision: RoutingDecision): string | null {
532
+ if (SAAR_DECISION_REASON_CODES.has(decision.reason_code)) {
533
+ return decision.reason_code;
534
+ }
535
+
536
+ return null;
537
+ }
538
+
539
+ /** Build privacy-safe SAAR pin state for explain and telemetry (SP-126). */
540
+ export function buildSaarObservability(
541
+ input: PinEconomicsObservabilityInput,
542
+ ): SaarObservability | null {
543
+ const { request, decision, sessionPinner, saarConfig } = input;
544
+ if (!saarConfig) {
545
+ return null;
546
+ }
547
+
548
+ const saarState = sessionPinner?.getSaarState(request.session_id) ?? null;
549
+ const pin = sessionPinner?.getPin(request.session_id) ?? null;
550
+
551
+ if (!pin && !saarState && !resolveSaarReasonCode(decision)) {
552
+ return null;
553
+ }
554
+
555
+ const turnIndex = saarState?.turn_index ?? (pin ? 0 : null);
556
+
557
+ return {
558
+ buffer_active:
559
+ turnIndex !== null ? turnIndex < saarConfig.planning_turn_buffer : false,
560
+ hard_lock: saarState?.hard_lock ?? false,
561
+ turn_index_in_session: turnIndex,
562
+ planning_turn_buffer: saarConfig.planning_turn_buffer,
563
+ idle_timeout_seconds: saarConfig.idle_timeout_seconds,
564
+ saar_reason_code: resolveSaarReasonCode(decision),
565
+ };
566
+ }
567
+
568
+ /** Build cache breakeven breakdown when a pin would switch tiers (SP-126). */
569
+ export function buildBreakevenObservability(
570
+ input: PinEconomicsObservabilityInput,
571
+ ): BreakevenObservability | null {
572
+ const { request, sessionPinner, saarConfig, fleet } = input;
573
+ if (!fleet || !sessionPinner) {
574
+ return null;
575
+ }
576
+
577
+ const pin = sessionPinner.getPin(request.session_id);
578
+ if (!pin) {
579
+ return null;
580
+ }
581
+
582
+ const targetTier = resolveTurnEnvelopeTargetTier(request);
583
+ if (!targetTier) {
584
+ return null;
585
+ }
586
+
587
+ if (isSaarPlanningBufferActive(request, sessionPinner, saarConfig)) {
588
+ return null;
589
+ }
590
+
591
+ const pinnedModel = fleet.find(
592
+ (model) => model.id === pin.pinned_model_id && model.healthy !== false,
593
+ );
594
+ const candidate = selectLowestCostModel(
595
+ fleet.filter((model) => model.tier === targetTier && model.healthy !== false),
596
+ );
597
+
598
+ if (!pinnedModel || !candidate || pinnedModel.id === candidate.id) {
599
+ return null;
600
+ }
601
+
602
+ const tokenEstimate =
603
+ request.estimated_input_tokens ?? request.prompt_text.length;
604
+ const breakeven = evaluateModelSwitchBreakeven(
605
+ pinnedModel,
606
+ candidate,
607
+ tokenEstimate,
608
+ tokenEstimate,
609
+ saarConfig,
610
+ );
611
+
612
+ return {
613
+ marginal_savings: breakeven.marginal_savings,
614
+ future_cache_value: breakeven.future_cache_value,
615
+ cache_reprime_cost: breakeven.cache_reprime_cost,
616
+ decision: breakeven.shouldSwitch ? 'pass' : 'blocked',
617
+ breakeven_reason_code: breakeven.shouldSwitch ? BREAKEVEN_PASS : BREAKEVEN_BLOCKED,
618
+ };
619
+ }
620
+
621
+ function defaultBreakevenTelemetry(): Pick<
622
+ RoutingTelemetry,
623
+ | 'marginal_savings'
624
+ | 'future_cache_value'
625
+ | 'cache_reprime_cost'
626
+ | 'breakeven_decision'
627
+ | 'breakeven_reason_code'
628
+ > {
629
+ return {
630
+ marginal_savings: null,
631
+ future_cache_value: null,
632
+ cache_reprime_cost: null,
633
+ breakeven_decision: null,
634
+ breakeven_reason_code: null,
635
+ };
636
+ }
637
+
638
+ function defaultSaarTelemetry(): Pick<
639
+ RoutingTelemetry,
640
+ | 'saar_buffer_active'
641
+ | 'saar_hard_lock'
642
+ | 'turn_index_in_session'
643
+ | 'saar_reason_code'
644
+ > {
645
+ return {
646
+ saar_buffer_active: false,
647
+ saar_hard_lock: false,
648
+ turn_index_in_session: null,
649
+ saar_reason_code: null,
650
+ };
651
+ }
652
+
653
+ /** Default breakeven telemetry scalars for tests and legacy store reads. */
654
+ export const DEFAULT_BREAKEVEN_TELEMETRY_FIELDS = defaultBreakevenTelemetry();
655
+
656
+ /** Default SAAR telemetry scalars for tests and legacy store reads. */
657
+ export const DEFAULT_SAAR_TELEMETRY_FIELDS = defaultSaarTelemetry();
658
+
659
+ function pinEconomicsTelemetryFromInput(
660
+ input: PinEconomicsObservabilityInput,
661
+ ): ReturnType<typeof defaultBreakevenTelemetry> &
662
+ ReturnType<typeof defaultSaarTelemetry> {
663
+ const breakeven = buildBreakevenObservability(input);
664
+ const saar = buildSaarObservability(input);
665
+
666
+ return {
667
+ ...(breakeven
668
+ ? {
669
+ marginal_savings: breakeven.marginal_savings,
670
+ future_cache_value: breakeven.future_cache_value,
671
+ cache_reprime_cost: breakeven.cache_reprime_cost,
672
+ breakeven_decision: breakeven.decision,
673
+ breakeven_reason_code: breakeven.breakeven_reason_code,
674
+ }
675
+ : defaultBreakevenTelemetry()),
676
+ saar_buffer_active: saar?.buffer_active ?? false,
677
+ saar_hard_lock: saar?.hard_lock ?? false,
678
+ turn_index_in_session: saar?.turn_index_in_session ?? null,
679
+ saar_reason_code: saar?.saar_reason_code ?? null,
680
+ };
681
+ }
682
+
683
+ /** Attach breakeven and SAAR observability to routing decision features (SP-126). */
684
+ export function enrichRoutingDecisionWithPinEconomics(
685
+ request: RoutingRequest,
686
+ decision: RoutingDecision,
687
+ options?: Omit<PinEconomicsObservabilityInput, 'request' | 'decision'>,
688
+ ): RoutingDecision {
689
+ const input: PinEconomicsObservabilityInput = {
690
+ request,
691
+ decision,
692
+ ...(options?.fleet !== undefined ? { fleet: options.fleet } : {}),
693
+ ...(options?.sessionPinner !== undefined
694
+ ? { sessionPinner: options.sessionPinner }
695
+ : {}),
696
+ ...(options?.saarConfig !== undefined ? { saarConfig: options.saarConfig } : {}),
697
+ };
698
+
699
+ const breakeven = buildBreakevenObservability(input);
700
+ const saar = buildSaarObservability(input);
701
+
702
+ if (!breakeven && !saar) {
703
+ return decision;
704
+ }
705
+
706
+ return {
707
+ ...decision,
708
+ features: {
709
+ ...(decision.features ?? emptyFeatureSidecar()),
710
+ ...(breakeven ? { breakeven } : {}),
711
+ ...(saar ? { saar } : {}),
712
+ },
713
+ };
714
+ }
715
+
470
716
  function emptyFeatureSidecar() {
471
717
  return {
472
718
  triage: null,
@@ -508,6 +754,8 @@ export interface ExplainEnrichmentOptions {
508
754
  readonly fleet?: readonly ModelProfile[];
509
755
  readonly contextFitConfig?: ContextFitConfig;
510
756
  readonly clusterMatcher?: ClusterMatcher;
757
+ readonly sessionPinner?: SessionPinner;
758
+ readonly saarConfig?: SaarConfig;
511
759
  }
512
760
 
513
761
  /** Attach context-fit and tier-selection observability for explain responses (SP-110, SP-113). */
@@ -532,7 +780,23 @@ export async function enrichRoutingDecisionForExplain(
532
780
  }
533
781
  }
534
782
 
535
- return enrichRoutingDecisionWithTierSelection(withContextFit, clusterMatchTable);
783
+ return enrichRoutingDecisionWithPinEconomics(
784
+ request,
785
+ enrichRoutingDecisionWithTierSelection(withContextFit, clusterMatchTable),
786
+ pinEconomicsOptionsFromExplain(options),
787
+ );
788
+ }
789
+
790
+ function pinEconomicsOptionsFromExplain(
791
+ options?: ExplainEnrichmentOptions,
792
+ ): Omit<PinEconomicsObservabilityInput, 'request' | 'decision'> {
793
+ return {
794
+ ...(options?.fleet !== undefined ? { fleet: options.fleet } : {}),
795
+ ...(options?.sessionPinner !== undefined
796
+ ? { sessionPinner: options.sessionPinner }
797
+ : {}),
798
+ ...(options?.saarConfig !== undefined ? { saarConfig: options.saarConfig } : {}),
799
+ };
536
800
  }
537
801
 
538
802
  /** Attach context-fit observability to a routing decision features sidecar (SP-110). */
@@ -575,17 +839,32 @@ export function buildRoutingDecisionLogPayload(
575
839
  delegate?: RoutingDecisionLogDelegate,
576
840
  fleet?: readonly ModelProfile[],
577
841
  contextFitConfig?: ContextFitConfig,
842
+ pinEconomics?: Omit<PinEconomicsObservabilityInput, 'request' | 'decision' | 'fleet'>,
578
843
  ): Record<string, unknown> {
579
- const enriched = enrichRoutingDecisionWithTierSelection(
580
- enrichRoutingDecisionWithContextFit(
581
- request,
582
- decision,
583
- fleet,
584
- contextFitConfig,
844
+ const enriched = enrichRoutingDecisionWithPinEconomics(
845
+ request,
846
+ enrichRoutingDecisionWithTierSelection(
847
+ enrichRoutingDecisionWithContextFit(
848
+ request,
849
+ decision,
850
+ fleet,
851
+ contextFitConfig,
852
+ ),
585
853
  ),
854
+ {
855
+ ...(fleet !== undefined ? { fleet } : {}),
856
+ ...(pinEconomics?.sessionPinner !== undefined
857
+ ? { sessionPinner: pinEconomics.sessionPinner }
858
+ : {}),
859
+ ...(pinEconomics?.saarConfig !== undefined
860
+ ? { saarConfig: pinEconomics.saarConfig }
861
+ : {}),
862
+ },
586
863
  );
587
864
 
588
865
  const tierSelection = enriched.features?.tier_selection;
866
+ const breakeven = enriched.features?.breakeven;
867
+ const saar = enriched.features?.saar;
589
868
 
590
869
  return {
591
870
  request_id: enriched.request_id,
@@ -606,6 +885,25 @@ export function buildRoutingDecisionLogPayload(
606
885
  p_success_cheap: tierSelection.p_success_cheap,
607
886
  }
608
887
  : null,
888
+ breakeven_summary: breakeven
889
+ ? {
890
+ marginal_savings: breakeven.marginal_savings,
891
+ future_cache_value: breakeven.future_cache_value,
892
+ cache_reprime_cost: breakeven.cache_reprime_cost,
893
+ decision: breakeven.decision,
894
+ breakeven_reason_code: breakeven.breakeven_reason_code,
895
+ }
896
+ : null,
897
+ saar_summary: saar
898
+ ? {
899
+ buffer_active: saar.buffer_active,
900
+ hard_lock: saar.hard_lock,
901
+ turn_index_in_session: saar.turn_index_in_session,
902
+ planning_turn_buffer: saar.planning_turn_buffer,
903
+ idle_timeout_seconds: saar.idle_timeout_seconds,
904
+ saar_reason_code: saar.saar_reason_code,
905
+ }
906
+ : null,
609
907
  delegate,
610
908
  };
611
909
  }
@@ -728,6 +1026,8 @@ export class RoutingTelemetryEmitter {
728
1026
  private readonly onRecord: ((record: RoutingTelemetry) => void) | undefined;
729
1027
  private readonly fleet: readonly ModelProfile[] | undefined;
730
1028
  private readonly contextFitConfig: ContextFitConfig | undefined;
1029
+ private readonly sessionPinner: SessionPinner | undefined;
1030
+ private readonly saarConfig: SaarConfig | undefined;
731
1031
 
732
1032
  constructor(options?: TelemetryEmitterOptions) {
733
1033
  this.maxEntries = options?.maxEntries ?? TELEMETRY_MAX_ENTRIES;
@@ -736,6 +1036,8 @@ export class RoutingTelemetryEmitter {
736
1036
  this.onRecord = options?.onRecord;
737
1037
  this.fleet = options?.fleet;
738
1038
  this.contextFitConfig = options?.contextFitConfig;
1039
+ this.sessionPinner = options?.sessionPinner;
1040
+ this.saarConfig = options?.saarConfig;
739
1041
  }
740
1042
 
741
1043
  /**
@@ -778,6 +1080,15 @@ export class RoutingTelemetryEmitter {
778
1080
  });
779
1081
  const contextFitFields = contextFit ?? defaultContextFitTelemetry();
780
1082
  const tierSelectionFields = tierSelectionTelemetryFromDecision(decision);
1083
+ const pinEconomicsFields = pinEconomicsTelemetryFromInput({
1084
+ request,
1085
+ decision,
1086
+ ...(this.fleet !== undefined ? { fleet: this.fleet } : {}),
1087
+ ...(this.sessionPinner !== undefined
1088
+ ? { sessionPinner: this.sessionPinner }
1089
+ : {}),
1090
+ ...(this.saarConfig !== undefined ? { saarConfig: this.saarConfig } : {}),
1091
+ });
781
1092
 
782
1093
  const record: RoutingTelemetry = {
783
1094
  timestamp: this.clock(),
@@ -804,6 +1115,15 @@ export class RoutingTelemetryEmitter {
804
1115
  p_success_cheap: tierSelectionFields.p_success_cheap,
805
1116
  local_eligible_reason: tierSelectionFields.local_eligible_reason,
806
1117
  tier_selection_reason_code: tierSelectionFields.tier_selection_reason_code,
1118
+ marginal_savings: pinEconomicsFields.marginal_savings,
1119
+ future_cache_value: pinEconomicsFields.future_cache_value,
1120
+ cache_reprime_cost: pinEconomicsFields.cache_reprime_cost,
1121
+ breakeven_decision: pinEconomicsFields.breakeven_decision,
1122
+ breakeven_reason_code: pinEconomicsFields.breakeven_reason_code,
1123
+ saar_buffer_active: pinEconomicsFields.saar_buffer_active,
1124
+ saar_hard_lock: pinEconomicsFields.saar_hard_lock,
1125
+ turn_index_in_session: pinEconomicsFields.turn_index_in_session,
1126
+ saar_reason_code: pinEconomicsFields.saar_reason_code,
807
1127
  };
808
1128
 
809
1129
  this.entries.push(record);