@mupt-ai/dari-router 0.1.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 (179) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +100 -0
  3. package/dist/anonymous_actions.d.ts +55 -0
  4. package/dist/anonymous_actions.js +277 -0
  5. package/dist/anonymous_actions.js.map +1 -0
  6. package/dist/auto_router.d.ts +10 -0
  7. package/dist/auto_router.js +56 -0
  8. package/dist/auto_router.js.map +1 -0
  9. package/dist/cache_behavior.d.ts +14 -0
  10. package/dist/cache_behavior.js +112 -0
  11. package/dist/cache_behavior.js.map +1 -0
  12. package/dist/cache_scope.d.ts +5 -0
  13. package/dist/cache_scope.js +49 -0
  14. package/dist/cache_scope.js.map +1 -0
  15. package/dist/cache_switch_decision.d.ts +24 -0
  16. package/dist/cache_switch_decision.js +21 -0
  17. package/dist/cache_switch_decision.js.map +1 -0
  18. package/dist/candidate_pruning.d.ts +45 -0
  19. package/dist/candidate_pruning.js +154 -0
  20. package/dist/candidate_pruning.js.map +1 -0
  21. package/dist/candidate_resolution.d.ts +41 -0
  22. package/dist/candidate_resolution.js +111 -0
  23. package/dist/candidate_resolution.js.map +1 -0
  24. package/dist/compatibility.d.ts +22 -0
  25. package/dist/compatibility.js +76 -0
  26. package/dist/compatibility.js.map +1 -0
  27. package/dist/continuation_state.d.ts +36 -0
  28. package/dist/continuation_state.js +249 -0
  29. package/dist/continuation_state.js.map +1 -0
  30. package/dist/cost.d.ts +31 -0
  31. package/dist/cost.js +314 -0
  32. package/dist/cost.js.map +1 -0
  33. package/dist/dari_policy.d.ts +34 -0
  34. package/dist/dari_policy.js +267 -0
  35. package/dist/dari_policy.js.map +1 -0
  36. package/dist/errors.d.ts +7 -0
  37. package/dist/errors.js +14 -0
  38. package/dist/errors.js.map +1 -0
  39. package/dist/fingerprint.d.ts +13 -0
  40. package/dist/fingerprint.js +130 -0
  41. package/dist/fingerprint.js.map +1 -0
  42. package/dist/fixed_turn_cost_config.d.ts +9 -0
  43. package/dist/fixed_turn_cost_config.js +22 -0
  44. package/dist/fixed_turn_cost_config.js.map +1 -0
  45. package/dist/framework_error.d.ts +8 -0
  46. package/dist/framework_error.js +24 -0
  47. package/dist/framework_error.js.map +1 -0
  48. package/dist/framework_lifecycle.d.ts +11 -0
  49. package/dist/framework_lifecycle.js +57 -0
  50. package/dist/framework_lifecycle.js.map +1 -0
  51. package/dist/framework_models.d.ts +3 -0
  52. package/dist/framework_models.js +120 -0
  53. package/dist/framework_models.js.map +1 -0
  54. package/dist/framework_router.d.ts +2 -0
  55. package/dist/framework_router.js +384 -0
  56. package/dist/framework_router.js.map +1 -0
  57. package/dist/framework_streaming.d.ts +26 -0
  58. package/dist/framework_streaming.js +486 -0
  59. package/dist/framework_streaming.js.map +1 -0
  60. package/dist/framework_types.d.ts +272 -0
  61. package/dist/framework_types.js +5 -0
  62. package/dist/framework_types.js.map +1 -0
  63. package/dist/framework_validation.d.ts +7 -0
  64. package/dist/framework_validation.js +169 -0
  65. package/dist/framework_validation.js.map +1 -0
  66. package/dist/index.d.ts +35 -0
  67. package/dist/index.js +38 -0
  68. package/dist/index.js.map +1 -0
  69. package/dist/json.d.ts +10 -0
  70. package/dist/json.js +29 -0
  71. package/dist/json.js.map +1 -0
  72. package/dist/model_ids.d.ts +4 -0
  73. package/dist/model_ids.js +26 -0
  74. package/dist/model_ids.js.map +1 -0
  75. package/dist/pi_context.d.ts +4 -0
  76. package/dist/pi_context.js +206 -0
  77. package/dist/pi_context.js.map +1 -0
  78. package/dist/pi_options.d.ts +3 -0
  79. package/dist/pi_options.js +240 -0
  80. package/dist/pi_options.js.map +1 -0
  81. package/dist/pi_output.d.ts +4 -0
  82. package/dist/pi_output.js +328 -0
  83. package/dist/pi_output.js.map +1 -0
  84. package/dist/pi_reasoning_signature.d.ts +10 -0
  85. package/dist/pi_reasoning_signature.js +33 -0
  86. package/dist/pi_reasoning_signature.js.map +1 -0
  87. package/dist/pi_runtime.d.ts +3 -0
  88. package/dist/pi_runtime.js +155 -0
  89. package/dist/pi_runtime.js.map +1 -0
  90. package/dist/pi_types.d.ts +35 -0
  91. package/dist/pi_types.js +2 -0
  92. package/dist/pi_types.js.map +1 -0
  93. package/dist/portable_reasoning_state.d.ts +7 -0
  94. package/dist/portable_reasoning_state.js +81 -0
  95. package/dist/portable_reasoning_state.js.map +1 -0
  96. package/dist/previous_decision.d.ts +13 -0
  97. package/dist/previous_decision.js +82 -0
  98. package/dist/previous_decision.js.map +1 -0
  99. package/dist/prompts.d.ts +3 -0
  100. package/dist/prompts.js +65 -0
  101. package/dist/prompts.js.map +1 -0
  102. package/dist/protocol_anthropic.d.ts +18 -0
  103. package/dist/protocol_anthropic.js +443 -0
  104. package/dist/protocol_anthropic.js.map +1 -0
  105. package/dist/protocol_openai_chat.d.ts +25 -0
  106. package/dist/protocol_openai_chat.js +503 -0
  107. package/dist/protocol_openai_chat.js.map +1 -0
  108. package/dist/protocol_validation.d.ts +23 -0
  109. package/dist/protocol_validation.js +134 -0
  110. package/dist/protocol_validation.js.map +1 -0
  111. package/dist/route.d.ts +105 -0
  112. package/dist/route.js +226 -0
  113. package/dist/route.js.map +1 -0
  114. package/dist/routing_history.d.ts +7 -0
  115. package/dist/routing_history.js +132 -0
  116. package/dist/routing_history.js.map +1 -0
  117. package/dist/selector_input.d.ts +42 -0
  118. package/dist/selector_input.js +153 -0
  119. package/dist/selector_input.js.map +1 -0
  120. package/dist/selector_parse.d.ts +13 -0
  121. package/dist/selector_parse.js +95 -0
  122. package/dist/selector_parse.js.map +1 -0
  123. package/dist/selector_request.d.ts +26 -0
  124. package/dist/selector_request.js +85 -0
  125. package/dist/selector_request.js.map +1 -0
  126. package/dist/selector_truncation.d.ts +2 -0
  127. package/dist/selector_truncation.js +48 -0
  128. package/dist/selector_truncation.js.map +1 -0
  129. package/dist/selector_wire.d.ts +10 -0
  130. package/dist/selector_wire.js +113 -0
  131. package/dist/selector_wire.js.map +1 -0
  132. package/dist/types.d.ts +152 -0
  133. package/dist/types.js +14 -0
  134. package/dist/types.js.map +1 -0
  135. package/package.json +64 -0
  136. package/src/anonymous_actions.ts +434 -0
  137. package/src/auto_router.ts +95 -0
  138. package/src/cache_behavior.ts +130 -0
  139. package/src/cache_scope.ts +58 -0
  140. package/src/cache_switch_decision.ts +53 -0
  141. package/src/candidate_pruning.ts +257 -0
  142. package/src/candidate_resolution.ts +208 -0
  143. package/src/compatibility.ts +162 -0
  144. package/src/continuation_state.ts +368 -0
  145. package/src/cost.ts +455 -0
  146. package/src/dari_policy.ts +360 -0
  147. package/src/errors.ts +27 -0
  148. package/src/fingerprint.ts +148 -0
  149. package/src/fixed_turn_cost_config.ts +24 -0
  150. package/src/framework_error.ts +38 -0
  151. package/src/framework_lifecycle.ts +74 -0
  152. package/src/framework_models.ts +186 -0
  153. package/src/framework_router.ts +490 -0
  154. package/src/framework_streaming.ts +613 -0
  155. package/src/framework_types.ts +372 -0
  156. package/src/framework_validation.ts +235 -0
  157. package/src/index.ts +37 -0
  158. package/src/json.ts +43 -0
  159. package/src/model_ids.ts +29 -0
  160. package/src/pi_context.ts +251 -0
  161. package/src/pi_options.ts +295 -0
  162. package/src/pi_output.ts +387 -0
  163. package/src/pi_reasoning_signature.ts +41 -0
  164. package/src/pi_runtime.ts +238 -0
  165. package/src/pi_types.ts +71 -0
  166. package/src/portable_reasoning_state.ts +129 -0
  167. package/src/previous_decision.ts +109 -0
  168. package/src/prompts.ts +69 -0
  169. package/src/protocol_anthropic.ts +571 -0
  170. package/src/protocol_openai_chat.ts +645 -0
  171. package/src/protocol_validation.ts +202 -0
  172. package/src/route.ts +490 -0
  173. package/src/routing_history.ts +157 -0
  174. package/src/selector_input.ts +246 -0
  175. package/src/selector_parse.ts +138 -0
  176. package/src/selector_request.ts +130 -0
  177. package/src/selector_truncation.ts +48 -0
  178. package/src/selector_wire.ts +167 -0
  179. package/src/types.ts +231 -0
@@ -0,0 +1,10 @@
1
+ import { type DariRoutingPolicyOptions } from "./dari_policy.js";
2
+ import type { RoutingPolicy } from "./framework_types.js";
3
+ export declare const DEFAULT_AUTO_ROUTER_ENDPOINT = "https://routing.dari.dev/v1/auto-router";
4
+ export declare const DEFAULT_AUTO_ROUTER_MODEL = "dari/auto-router";
5
+ export declare const DEFAULT_AUTO_ROUTER_CONTEXT_WINDOW_CHARS = 32000;
6
+ export type CreateAutoRouterOptions<Metadata = unknown> = {
7
+ apiKey: string;
8
+ endpoint?: string;
9
+ } & Omit<DariRoutingPolicyOptions<Metadata>, "selector" | "runtime" | "selectorModel" | "selectorContextWindowChars" | "strategy" | "customConfig">;
10
+ export declare function createAutoRouter<Metadata = unknown>(options: CreateAutoRouterOptions<Metadata>): RoutingPolicy<Metadata>;
@@ -0,0 +1,56 @@
1
+ // Dari Auto Router — a hosted routing policy for self-hosted createRouter
2
+ // users. Dari bills the calling org per selection call.
3
+ import { RouterFrameworkError } from "./framework_error.js";
4
+ import { createDariRoutingPolicy } from "./dari_policy.js";
5
+ export const DEFAULT_AUTO_ROUTER_ENDPOINT = "https://routing.dari.dev/v1/auto-router";
6
+ export const DEFAULT_AUTO_ROUTER_MODEL = "dari/auto-router";
7
+ export const DEFAULT_AUTO_ROUTER_CONTEXT_WINDOW_CHARS = 32_000;
8
+ export function createAutoRouter(options) {
9
+ if (!options.apiKey?.trim()) {
10
+ throw new RouterFrameworkError("configuration", "createAutoRouter requires an apiKey.", "auto_router_api_key_missing");
11
+ }
12
+ // The hosted Auto Router serves only the default routing policy and drops
13
+ // custom rules server-side, so accepting them here would silently ignore
14
+ // them. Reject loudly instead of routing with rules the user thinks apply.
15
+ const unsupported = options;
16
+ if (unsupported.strategy !== undefined || unsupported.customConfig !== undefined) {
17
+ throw new RouterFrameworkError("configuration", "createAutoRouter does not support custom routing rules (strategy/customConfig): " +
18
+ "the hosted Dari Auto Router serves only the default policy. Use " +
19
+ "createDariRoutingPolicy with a self-hosted selector or the Dari managed " +
20
+ "platform for custom routing.", "auto_router_custom_rules_unsupported");
21
+ }
22
+ const endpoint = (options.endpoint ?? DEFAULT_AUTO_ROUTER_ENDPOINT)
23
+ .replace(/\/+$/, "")
24
+ .replace(/\/select$/, "");
25
+ const { apiKey, endpoint: _endpoint, ...policyOptions } = options;
26
+ return createDariRoutingPolicy({
27
+ ...policyOptions,
28
+ selectorModel: DEFAULT_AUTO_ROUTER_MODEL,
29
+ selectorContextWindowChars: DEFAULT_AUTO_ROUTER_CONTEXT_WINDOW_CHARS,
30
+ selector: async (request, signal) => {
31
+ let response;
32
+ try {
33
+ response = await fetch(`${endpoint}/select`, {
34
+ method: "POST",
35
+ headers: {
36
+ Authorization: `Bearer ${apiKey}`,
37
+ "Content-Type": "application/json",
38
+ },
39
+ body: JSON.stringify(request),
40
+ signal,
41
+ });
42
+ }
43
+ catch (error) {
44
+ if (error instanceof Error && error.name === "AbortError")
45
+ throw error;
46
+ throw new RouterFrameworkError("policy", `Dari Auto Router request failed: ${error instanceof Error ? error.message : String(error)}`, "auto_router_request_failed", undefined, { cause: error });
47
+ }
48
+ if (!response.ok) {
49
+ const body = await response.text().catch(() => "");
50
+ throw new RouterFrameworkError("policy", `Dari Auto Router returned HTTP ${response.status}: ${body}`, "auto_router_http_error");
51
+ }
52
+ return await response.text();
53
+ },
54
+ });
55
+ }
56
+ //# sourceMappingURL=auto_router.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auto_router.js","sourceRoot":"","sources":["../src/auto_router.ts"],"names":[],"mappings":"AAAA,0EAA0E;AAC1E,wDAAwD;AAExD,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAC5D,OAAO,EAAE,uBAAuB,EAAiC,MAAM,kBAAkB,CAAC;AAG1F,MAAM,CAAC,MAAM,4BAA4B,GAAG,yCAAyC,CAAC;AACtF,MAAM,CAAC,MAAM,yBAAyB,GAAG,kBAAkB,CAAC;AAC5D,MAAM,CAAC,MAAM,wCAAwC,GAAG,MAAM,CAAC;AAe/D,MAAM,UAAU,gBAAgB,CAC9B,OAA0C;IAE1C,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,IAAI,EAAE,EAAE,CAAC;QAC5B,MAAM,IAAI,oBAAoB,CAC5B,eAAe,EACf,sCAAsC,EACtC,6BAA6B,CAC9B,CAAC;IACJ,CAAC;IACD,0EAA0E;IAC1E,yEAAyE;IACzE,2EAA2E;IAC3E,MAAM,WAAW,GAAG,OAAyD,CAAC;IAC9E,IAAI,WAAW,CAAC,QAAQ,KAAK,SAAS,IAAI,WAAW,CAAC,YAAY,KAAK,SAAS,EAAE,CAAC;QACjF,MAAM,IAAI,oBAAoB,CAC5B,eAAe,EACf,kFAAkF;YAChF,kEAAkE;YAClE,0EAA0E;YAC1E,8BAA8B,EAChC,sCAAsC,CACvC,CAAC;IACJ,CAAC;IACD,MAAM,QAAQ,GAAG,CAAC,OAAO,CAAC,QAAQ,IAAI,4BAA4B,CAAC;SAChE,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC;SACnB,OAAO,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC;IAC5B,MAAM,EACJ,MAAM,EACN,QAAQ,EAAE,SAAS,EACnB,GAAG,aAAa,EACjB,GAAG,OAAO,CAAC;IAEZ,OAAO,uBAAuB,CAAW;QACvC,GAAG,aAAa;QAChB,aAAa,EAAE,yBAAyB;QACxC,0BAA0B,EAAE,wCAAwC;QACpE,QAAQ,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE;YAClC,IAAI,QAAkB,CAAC;YACvB,IAAI,CAAC;gBACH,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,QAAQ,SAAS,EAAE;oBAC3C,MAAM,EAAE,MAAM;oBACd,OAAO,EAAE;wBACP,aAAa,EAAE,UAAU,MAAM,EAAE;wBACjC,cAAc,EAAE,kBAAkB;qBACnC;oBACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC;oBAC7B,MAAM;iBACP,CAAC,CAAC;YACL,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,IAAI,KAAK,YAAY,KAAK,IAAI,KAAK,CAAC,IAAI,KAAK,YAAY;oBAAE,MAAM,KAAK,CAAC;gBACvE,MAAM,IAAI,oBAAoB,CAC5B,QAAQ,EACR,oCAAoC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,EAC5F,4BAA4B,EAC5B,SAAS,EACT,EAAE,KAAK,EAAE,KAAK,EAAE,CACjB,CAAC;YACJ,CAAC;YACD,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;gBACjB,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;gBACnD,MAAM,IAAI,oBAAoB,CAC5B,QAAQ,EACR,kCAAkC,QAAQ,CAAC,MAAM,KAAK,IAAI,EAAE,EAC5D,wBAAwB,CACzB,CAAC;YACJ,CAAC;YACD,OAAO,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;QAC/B,CAAC;KACF,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,14 @@
1
+ import type { RouterPrefixHit } from "./types.js";
2
+ export declare const PREFIX_WARMTH_WINDOW_MS: number;
3
+ export declare const ANTHROPIC_LOOKBACK_BLOCKS = 19;
4
+ export declare const CROSS_TOKENIZER_RATIO = 1.1;
5
+ export type PromptCacheProvider = "openai" | "anthropic" | "fireworks" | "meta";
6
+ export declare function isAnthropicFamily(model: string): boolean;
7
+ export declare function isOpenAiFamily(model: string): boolean;
8
+ export declare function promptCacheProviderForModel(model: string): PromptCacheProvider | null;
9
+ export declare function openAiCachedPrefixTokens(promptTokens: number): number;
10
+ export declare function anthropicMinCacheTokens(model: string): number;
11
+ export declare function tokenizerFamily(model: string): string;
12
+ export declare function providerMinCacheTokens(model: string): number;
13
+ export declare function warmPrefixHits(hits: readonly RouterPrefixHit[] | undefined, nowMs: number): RouterPrefixHit[];
14
+ export declare function providerCacheableTokens(model: string, provider: PromptCacheProvider, prefixTokens: number): number;
@@ -0,0 +1,112 @@
1
+ import { nativeModelId, providerForModel } from "./model_ids.js";
2
+ // Provider prompt caches stop serving a prefix roughly five minutes after it
3
+ // was last written. Stored prefix entries outlive that: a host's lookback
4
+ // keeps them long enough to still name the conversation and the decision it
5
+ // continued from. Only entries inside this window describe warmth.
6
+ export const PREFIX_WARMTH_WINDOW_MS = 5 * 60 * 1000;
7
+ // Anthropic checks 20 positions per breakpoint, counting the breakpoint
8
+ // itself as the first, so an old cached breakpoint hits only when at most
9
+ // 19 content blocks were appended after it. These are routing estimates, never
10
+ // billing.
11
+ export const ANTHROPIC_LOOKBACK_BLOCKS = 19;
12
+ export const CROSS_TOKENIZER_RATIO = 1.1;
13
+ const OPENAI_MIN_CACHE_TOKENS = 1024;
14
+ const OPENAI_CACHE_INCREMENT_TOKENS = 128;
15
+ // providerForModel() lowercases the provider prefix, so family checks must
16
+ // compare case-insensitively too. Unlike providerForModel this never throws;
17
+ // malformed ids classify as no family.
18
+ function providerPrefix(model) {
19
+ const slash = model.indexOf("/");
20
+ return slash <= 0 ? "" : model.slice(0, slash).toLowerCase();
21
+ }
22
+ export function isAnthropicFamily(model) {
23
+ return providerPrefix(model) === "anthropic";
24
+ }
25
+ export function isOpenAiFamily(model) {
26
+ return providerPrefix(model) === "openai";
27
+ }
28
+ function isFireworksFamily(model) {
29
+ return providerPrefix(model) === "fireworks";
30
+ }
31
+ function isMetaFamily(model) {
32
+ return providerPrefix(model) === "meta";
33
+ }
34
+ export function promptCacheProviderForModel(model) {
35
+ if (isOpenAiFamily(model))
36
+ return "openai";
37
+ if (isAnthropicFamily(model))
38
+ return "anthropic";
39
+ if (isFireworksFamily(model))
40
+ return "fireworks";
41
+ if (isMetaFamily(model))
42
+ return "meta";
43
+ return null;
44
+ }
45
+ export function openAiCachedPrefixTokens(promptTokens) {
46
+ if (promptTokens < OPENAI_MIN_CACHE_TOKENS)
47
+ return 0;
48
+ const extra = promptTokens - OPENAI_MIN_CACHE_TOKENS;
49
+ return (OPENAI_MIN_CACHE_TOKENS +
50
+ Math.floor(extra / OPENAI_CACHE_INCREMENT_TOKENS) * OPENAI_CACHE_INCREMENT_TOKENS);
51
+ }
52
+ // Anthropic cache minimums vary by model family. Keep this routing-side
53
+ // estimate conservative for known higher-minimum models; unknown future
54
+ // direct Anthropic models fall back to the common 1024-token threshold.
55
+ export function anthropicMinCacheTokens(model) {
56
+ if (!isAnthropicFamily(model))
57
+ return OPENAI_MIN_CACHE_TOKENS;
58
+ const native = nativeModelId(model)
59
+ .toLowerCase()
60
+ .replace(/(?<=\d)\.(?=\d)/g, "-");
61
+ if (native.includes("opus-4-6") ||
62
+ native.includes("opus-4-5") ||
63
+ native.includes("haiku-4-5")) {
64
+ return 4096;
65
+ }
66
+ if (native.includes("fable-5")) {
67
+ return 512;
68
+ }
69
+ return OPENAI_MIN_CACHE_TOKENS;
70
+ }
71
+ export function tokenizerFamily(model) {
72
+ const provider = providerForModel(model);
73
+ if (provider === "fireworks") {
74
+ const native = nativeModelId(model).toLowerCase();
75
+ const accountPrefix = "accounts/fireworks/models/";
76
+ const modelFamily = native.startsWith(accountPrefix)
77
+ ? native.slice(accountPrefix.length)
78
+ : native;
79
+ const slash = modelFamily.indexOf("/");
80
+ return `${provider}/${slash === -1 ? modelFamily : modelFamily.slice(0, slash)}`;
81
+ }
82
+ return provider;
83
+ }
84
+ export function providerMinCacheTokens(model) {
85
+ if (isAnthropicFamily(model))
86
+ return anthropicMinCacheTokens(model);
87
+ if (isFireworksFamily(model) || isMetaFamily(model))
88
+ return 0;
89
+ return OPENAI_MIN_CACHE_TOKENS;
90
+ }
91
+ // The stored entries a provider cache can still be re-reading at nowMs. This
92
+ // is the definition of "warm", and every cost estimate rests on it, so an
93
+ // unparseable timestamp reads as cold: warmth is proven, never assumed.
94
+ // nowMs is supplied by the caller because this package reads no clock.
95
+ export function warmPrefixHits(hits, nowMs) {
96
+ const cutoffMs = nowMs - PREFIX_WARMTH_WINDOW_MS;
97
+ return (hits ?? []).filter((hit) => {
98
+ const updatedAtMs = Date.parse(hit.updated_at);
99
+ return Number.isFinite(updatedAtMs) && updatedAtMs >= cutoffMs;
100
+ });
101
+ }
102
+ export function providerCacheableTokens(model, provider, prefixTokens) {
103
+ if (prefixTokens <= 0)
104
+ return 0;
105
+ if (provider === "openai")
106
+ return openAiCachedPrefixTokens(prefixTokens);
107
+ if (provider === "anthropic") {
108
+ return prefixTokens >= anthropicMinCacheTokens(model) ? Math.floor(prefixTokens) : 0;
109
+ }
110
+ return Math.floor(prefixTokens);
111
+ }
112
+ //# sourceMappingURL=cache_behavior.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache_behavior.js","sourceRoot":"","sources":["../src/cache_behavior.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ export type ReasoningCacheScope = "effort_keyed" | "shared";
2
+ export declare const DEFAULT_REASONING_CACHE_SCOPES: Readonly<Record<string, ReasoningCacheScope>>;
3
+ export type ReasoningCacheScopeLookup = (modelId: string) => ReasoningCacheScope;
4
+ export declare function reasoningCacheScope(modelId: string): ReasoningCacheScope;
5
+ export declare function cachePartitionKey(modelId: string, reasoningBucket: string | null | undefined, cacheScope?: ReasoningCacheScopeLookup): string;
@@ -0,0 +1,49 @@
1
+ import { canonicalModelId } from "./model_ids.js";
2
+ // How a model's provider prompt cache behaves when the reasoning payload
3
+ // changes between otherwise identical requests, from live-provider and
4
+ // router-path validation:
5
+ // effort_keyed - each effective reasoning effort warms its own cache
6
+ // partition; switching effort misses/rewrites, switching back
7
+ // reuses that effort's still-warm partition.
8
+ // shared - one cache partition regardless of reasoning effort.
9
+ // OpenAI reasoning models are effort_keyed (a first-ever effort missed a
10
+ // steady-warm prefix on every model tested). Anthropic models are effort_keyed
11
+ // because pi-ai places cache_control on message blocks, which Anthropic
12
+ // invalidates on thinking changes; budget-thinking Haiku would share under a
13
+ // system-block breakpoint, so revalidate its entry if pi-ai's Anthropic cache
14
+ // placement changes. Fireworks caching ignored reasoning entirely.
15
+ // Unlisted models default to effort_keyed: the safe direction is to
16
+ // under-claim warmth, never to price a cold prefix as a cache read.
17
+ export const DEFAULT_REASONING_CACHE_SCOPES = {
18
+ "openai/gpt-5.6-sol": "effort_keyed",
19
+ "openai/gpt-5.6-terra": "effort_keyed",
20
+ "openai/gpt-5.6-luna": "effort_keyed",
21
+ "openai/gpt-5.5": "effort_keyed",
22
+ "openai/gpt-4.1-mini": "shared", // single reasoning mode; nothing to key on
23
+ "anthropic/claude-opus-4-8": "effort_keyed",
24
+ "anthropic/claude-sonnet-4-6": "effort_keyed",
25
+ "anthropic/claude-haiku-4-5": "effort_keyed", // via pi-ai message-block cache_control; see note above
26
+ "fireworks/deepseek-ai/DeepSeek-V4-Pro": "shared",
27
+ "fireworks/deepseek-ai/DeepSeek-V4-Flash": "shared",
28
+ "fireworks/deepseek-ai/DeepSeek-V4-Flash-0731": "shared",
29
+ "fireworks/zai-org/GLM-5.2": "shared",
30
+ };
31
+ export function reasoningCacheScope(modelId) {
32
+ return DEFAULT_REASONING_CACHE_SCOPES[canonicalModelId(modelId)] ?? "effort_keyed";
33
+ }
34
+ // The provider cache partition a prefix entry belongs to. An effort-keyed
35
+ // provider warms one partition per effective reasoning effort, so the pair
36
+ // identifies it; a shared-scope provider holds a single partition per model,
37
+ // and keying that by effort would remember several entries describing one
38
+ // cache. Hosts that store warmth per partition derive their key here so a
39
+ // reclassified provider moves the estimator and their storage together.
40
+ // modelId is used verbatim, matching how ChainHits keys warm prefixes: ids that
41
+ // differ only in spelling stay separate rather than cross-contaminating. The
42
+ // separator cannot appear in a model id or a bucket, so the pair is injective
43
+ // and can never collide with a shared-scope key.
44
+ export function cachePartitionKey(modelId, reasoningBucket, cacheScope = reasoningCacheScope) {
45
+ if (cacheScope(modelId) === "shared")
46
+ return modelId;
47
+ return `${modelId}\u0000${reasoningBucket ?? ""}`;
48
+ }
49
+ //# sourceMappingURL=cache_scope.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache_scope.js","sourceRoot":"","sources":["../src/cache_scope.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAIlD,yEAAyE;AACzE,uEAAuE;AACvE,0BAA0B;AAC1B,uEAAuE;AACvE,+EAA+E;AAC/E,8DAA8D;AAC9D,uEAAuE;AACvE,yEAAyE;AACzE,+EAA+E;AAC/E,wEAAwE;AACxE,6EAA6E;AAC7E,8EAA8E;AAC9E,mEAAmE;AACnE,oEAAoE;AACpE,oEAAoE;AACpE,MAAM,CAAC,MAAM,8BAA8B,GAAkD;IAC3F,oBAAoB,EAAE,cAAc;IACpC,sBAAsB,EAAE,cAAc;IACtC,qBAAqB,EAAE,cAAc;IACrC,gBAAgB,EAAE,cAAc;IAChC,qBAAqB,EAAE,QAAQ,EAAE,2CAA2C;IAC5E,2BAA2B,EAAE,cAAc;IAC3C,6BAA6B,EAAE,cAAc;IAC7C,4BAA4B,EAAE,cAAc,EAAE,wDAAwD;IACtG,uCAAuC,EAAE,QAAQ;IACjD,yCAAyC,EAAE,QAAQ;IACnD,8CAA8C,EAAE,QAAQ;IACxD,2BAA2B,EAAE,QAAQ;CACtC,CAAC;AAIF,MAAM,UAAU,mBAAmB,CAAC,OAAe;IACjD,OAAO,8BAA8B,CAAC,gBAAgB,CAAC,OAAO,CAAC,CAAC,IAAI,cAAc,CAAC;AACrF,CAAC;AAED,0EAA0E;AAC1E,2EAA2E;AAC3E,6EAA6E;AAC7E,0EAA0E;AAC1E,0EAA0E;AAC1E,wEAAwE;AACxE,gFAAgF;AAChF,6EAA6E;AAC7E,8EAA8E;AAC9E,iDAAiD;AACjD,MAAM,UAAU,iBAAiB,CAC/B,OAAe,EACf,eAA0C,EAC1C,aAAwC,mBAAmB;IAE3D,IAAI,UAAU,CAAC,OAAO,CAAC,KAAK,QAAQ;QAAE,OAAO,OAAO,CAAC;IACrD,OAAO,GAAG,OAAO,SAAS,eAAe,IAAI,EAAE,EAAE,CAAC;AACpD,CAAC"}
@@ -0,0 +1,24 @@
1
+ export type WarmIncumbentCost = {
2
+ fixedTurnCostUsd: number;
3
+ outputCostPerMtok: number;
4
+ };
5
+ export type SwitchCandidateCost = {
6
+ fixedTurnCostUsd: number | null;
7
+ outputCostPerMtok: number | null;
8
+ };
9
+ export type CacheSwitchDecision = {
10
+ action: "keep";
11
+ reason: "unknown_switch_cost" | "unknown_switch_output_price" | "not_cheaper_output_tier" | "saves_more_than_threshold";
12
+ fixedTurnCostUsd?: number;
13
+ savingsRatio?: number;
14
+ } | {
15
+ action: "prune";
16
+ reason: "insufficient_savings";
17
+ fixedTurnCostUsd: number;
18
+ savingsRatio: number;
19
+ };
20
+ export declare function decideCacheSwitch(args: {
21
+ warmIncumbent: WarmIncumbentCost;
22
+ switchCandidate: SwitchCandidateCost;
23
+ minSwitchSavingsRatio: number;
24
+ }): CacheSwitchDecision;
@@ -0,0 +1,21 @@
1
+ export function decideCacheSwitch(args) {
2
+ const fixedTurnCostUsd = args.switchCandidate.fixedTurnCostUsd;
3
+ if (fixedTurnCostUsd === null) {
4
+ return { action: "keep", reason: "unknown_switch_cost" };
5
+ }
6
+ const outputCostPerMtok = args.switchCandidate.outputCostPerMtok;
7
+ if (outputCostPerMtok === null) {
8
+ return { action: "keep", reason: "unknown_switch_output_price" };
9
+ }
10
+ // Higher output price is our capability-upgrade guard. The pruning rule is
11
+ // only for cheaper-tier switches that look cheap before cache effects.
12
+ if (outputCostPerMtok >= args.warmIncumbent.outputCostPerMtok) {
13
+ return { action: "keep", reason: "not_cheaper_output_tier", fixedTurnCostUsd };
14
+ }
15
+ const savingsRatio = (args.warmIncumbent.fixedTurnCostUsd - fixedTurnCostUsd) / args.warmIncumbent.fixedTurnCostUsd;
16
+ if (savingsRatio > args.minSwitchSavingsRatio) {
17
+ return { action: "keep", reason: "saves_more_than_threshold", fixedTurnCostUsd, savingsRatio };
18
+ }
19
+ return { action: "prune", reason: "insufficient_savings", fixedTurnCostUsd, savingsRatio };
20
+ }
21
+ //# sourceMappingURL=cache_switch_decision.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache_switch_decision.js","sourceRoot":"","sources":["../src/cache_switch_decision.ts"],"names":[],"mappings":"AAwBA,MAAM,UAAU,iBAAiB,CAAC,IAIjC;IACC,MAAM,gBAAgB,GAAG,IAAI,CAAC,eAAe,CAAC,gBAAgB,CAAC;IAC/D,IAAI,gBAAgB,KAAK,IAAI,EAAE,CAAC;QAC9B,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,qBAAqB,EAAE,CAAC;IAC3D,CAAC;IAED,MAAM,iBAAiB,GAAG,IAAI,CAAC,eAAe,CAAC,iBAAiB,CAAC;IACjE,IAAI,iBAAiB,KAAK,IAAI,EAAE,CAAC;QAC/B,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,6BAA6B,EAAE,CAAC;IACnE,CAAC;IAED,2EAA2E;IAC3E,uEAAuE;IACvE,IAAI,iBAAiB,IAAI,IAAI,CAAC,aAAa,CAAC,iBAAiB,EAAE,CAAC;QAC9D,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,yBAAyB,EAAE,gBAAgB,EAAE,CAAC;IACjF,CAAC;IAED,MAAM,YAAY,GAChB,CAAC,IAAI,CAAC,aAAa,CAAC,gBAAgB,GAAG,gBAAgB,CAAC,GAAG,IAAI,CAAC,aAAa,CAAC,gBAAgB,CAAC;IACjG,IAAI,YAAY,GAAG,IAAI,CAAC,qBAAqB,EAAE,CAAC;QAC9C,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,2BAA2B,EAAE,gBAAgB,EAAE,YAAY,EAAE,CAAC;IACjG,CAAC;IAED,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,YAAY,EAAE,CAAC;AAC7F,CAAC"}
@@ -0,0 +1,45 @@
1
+ import { type CacheSwitchDecision } from "./cache_switch_decision.js";
2
+ import { type CandidateCostEstimate, type PreviousDecision, type ReasoningEffort, type RoutingCandidate } from "./types.js";
3
+ export declare const MIN_SWITCH_SAVINGS_RATIO = 0.1;
4
+ type PrunedCandidate = {
5
+ model: string;
6
+ reasoning_effort: ReasoningEffort;
7
+ fixed_turn_cost_usd: number;
8
+ savings_ratio: number;
9
+ };
10
+ type CandidateSwitchKeepDecision = {
11
+ action: "keep";
12
+ reason: Extract<CacheSwitchDecision, {
13
+ action: "keep";
14
+ }>["reason"] | "same_model_upgrade";
15
+ fixedTurnCostUsd?: number;
16
+ savingsRatio?: number;
17
+ };
18
+ type KeptReason = "incumbent" | CandidateSwitchKeepDecision["reason"];
19
+ type KeptCandidate = {
20
+ model: string;
21
+ reasoning_effort: ReasoningEffort;
22
+ reason: KeptReason;
23
+ savings_ratio?: number;
24
+ };
25
+ export type CandidatePruningAudit = {
26
+ min_switch_savings_ratio: number;
27
+ incumbent_model: string | null;
28
+ incumbent_reasoning_effort: ReasoningEffort | null;
29
+ incumbent_fixed_turn_cost_usd: number | null;
30
+ skip_reason: "no_previous_decision" | "incumbent_not_warm" | "incumbent_unpriced" | null;
31
+ pruned_candidates: PrunedCandidate[];
32
+ kept_candidates: KeptCandidate[];
33
+ };
34
+ export type CandidatePruning = {
35
+ candidates: RoutingCandidate[];
36
+ costEstimates: CandidateCostEstimate[];
37
+ audit: CandidatePruningAudit;
38
+ };
39
+ export declare function pruneSwitchCandidates(args: {
40
+ candidates: RoutingCandidate[];
41
+ costEstimates: CandidateCostEstimate[];
42
+ previousDecision?: PreviousDecision;
43
+ minSwitchSavingsRatio: number;
44
+ }): CandidatePruning;
45
+ export {};
@@ -0,0 +1,154 @@
1
+ // Cache-aware pruning for selector-based routing strategies. When the previous
2
+ // model/thinking-level pair is still warm, a nominally cheaper pair can be a
3
+ // fake-cheap switch: the cold start erases most of the sticker-price saving.
4
+ import { decideCacheSwitch, } from "./cache_switch_decision.js";
5
+ import { turnCostUsdAt } from "./cost.js";
6
+ import { FIXED_TURN_COST_COMPARISON_TURNS } from "./fixed_turn_cost_config.js";
7
+ import { REASONING_EFFORTS, routingCandidateKey, } from "./types.js";
8
+ export const MIN_SWITCH_SAVINGS_RATIO = 0.1;
9
+ export function pruneSwitchCandidates(args) {
10
+ const incumbentCandidate = args.previousDecision
11
+ ? {
12
+ model: args.previousDecision.model,
13
+ reasoningEffort: args.previousDecision.reasoningEffort,
14
+ }
15
+ : null;
16
+ const candidateKeys = new Set(args.candidates.map(routingCandidateKey));
17
+ const incumbentKey = incumbentCandidate
18
+ ? routingCandidateKey(incumbentCandidate)
19
+ : null;
20
+ const estimateByKey = new Map(args.costEstimates.map((estimate) => [
21
+ routingCandidateKey({
22
+ model: estimate.model,
23
+ reasoningEffort: estimate.reasoning_effort,
24
+ }),
25
+ estimate,
26
+ ]));
27
+ const incumbent = incumbentKey && candidateKeys.has(incumbentKey)
28
+ ? estimateByKey.get(incumbentKey)
29
+ : undefined;
30
+ if (!incumbent || !incumbentCandidate) {
31
+ return keepAll(args, incumbentCandidate, "no_previous_decision");
32
+ }
33
+ if (incumbent.warm_tokens <= 0) {
34
+ return keepAll(args, incumbentCandidate, "incumbent_not_warm");
35
+ }
36
+ const incumbentCost = turnCostUsdAt(incumbent.fixed_turn_cost_estimate, FIXED_TURN_COST_COMPARISON_TURNS);
37
+ const incumbentOutputPrice = incumbent.output_cost_per_mtok;
38
+ if (incumbentCost === null ||
39
+ incumbentCost <= 0 ||
40
+ incumbentOutputPrice === null) {
41
+ return keepAll(args, incumbentCandidate, "incumbent_unpriced");
42
+ }
43
+ const prunedCandidates = [];
44
+ const keptCandidates = [];
45
+ const survivors = args.candidates.filter((candidate) => {
46
+ const key = routingCandidateKey(candidate);
47
+ if (key === incumbentKey) {
48
+ keptCandidates.push({
49
+ model: candidate.model,
50
+ reasoning_effort: candidate.reasoningEffort,
51
+ reason: "incumbent",
52
+ });
53
+ return true;
54
+ }
55
+ const decision = candidateSwitchDecision({
56
+ candidate,
57
+ estimate: estimateByKey.get(key),
58
+ incumbent: incumbentCandidate,
59
+ incumbentCost,
60
+ incumbentOutputPrice,
61
+ minSwitchSavingsRatio: args.minSwitchSavingsRatio,
62
+ });
63
+ if (decision.action === "keep") {
64
+ keptCandidates.push({
65
+ model: candidate.model,
66
+ reasoning_effort: candidate.reasoningEffort,
67
+ reason: decision.reason,
68
+ ...(decision.savingsRatio === undefined
69
+ ? {}
70
+ : { savings_ratio: decision.savingsRatio }),
71
+ });
72
+ return true;
73
+ }
74
+ prunedCandidates.push({
75
+ model: candidate.model,
76
+ reasoning_effort: candidate.reasoningEffort,
77
+ fixed_turn_cost_usd: decision.fixedTurnCostUsd,
78
+ savings_ratio: decision.savingsRatio,
79
+ });
80
+ return false;
81
+ });
82
+ const survivorKeys = new Set(survivors.map(routingCandidateKey));
83
+ return {
84
+ candidates: survivors,
85
+ costEstimates: args.costEstimates.filter((estimate) => survivorKeys.has(routingCandidateKey({
86
+ model: estimate.model,
87
+ reasoningEffort: estimate.reasoning_effort,
88
+ }))),
89
+ audit: {
90
+ min_switch_savings_ratio: args.minSwitchSavingsRatio,
91
+ incumbent_model: incumbentCandidate.model,
92
+ incumbent_reasoning_effort: incumbentCandidate.reasoningEffort,
93
+ incumbent_fixed_turn_cost_usd: incumbentCost,
94
+ skip_reason: null,
95
+ pruned_candidates: prunedCandidates,
96
+ kept_candidates: keptCandidates,
97
+ },
98
+ };
99
+ }
100
+ function candidateSwitchDecision(args) {
101
+ const candidateCost = turnCostUsdAt(args.estimate?.fixed_turn_cost_estimate, FIXED_TURN_COST_COMPARISON_TURNS);
102
+ if (args.candidate.model !== args.incumbent.model) {
103
+ return decideCacheSwitch({
104
+ warmIncumbent: {
105
+ fixedTurnCostUsd: args.incumbentCost,
106
+ outputCostPerMtok: args.incumbentOutputPrice,
107
+ },
108
+ switchCandidate: {
109
+ fixedTurnCostUsd: candidateCost,
110
+ outputCostPerMtok: args.estimate?.output_cost_per_mtok ?? null,
111
+ },
112
+ minSwitchSavingsRatio: args.minSwitchSavingsRatio,
113
+ });
114
+ }
115
+ // Output price cannot distinguish thinking levels on one model, so preserve
116
+ // capability upgrades and judge lower-effort switches by projected cost.
117
+ if (REASONING_EFFORTS.indexOf(args.candidate.reasoningEffort) >=
118
+ REASONING_EFFORTS.indexOf(args.incumbent.reasoningEffort)) {
119
+ return { action: "keep", reason: "same_model_upgrade" };
120
+ }
121
+ if (candidateCost === null) {
122
+ return { action: "keep", reason: "unknown_switch_cost" };
123
+ }
124
+ const savingsRatio = (args.incumbentCost - candidateCost) / args.incumbentCost;
125
+ return savingsRatio > args.minSwitchSavingsRatio
126
+ ? {
127
+ action: "keep",
128
+ reason: "saves_more_than_threshold",
129
+ fixedTurnCostUsd: candidateCost,
130
+ savingsRatio,
131
+ }
132
+ : {
133
+ action: "prune",
134
+ reason: "insufficient_savings",
135
+ fixedTurnCostUsd: candidateCost,
136
+ savingsRatio,
137
+ };
138
+ }
139
+ function keepAll(args, incumbent, skipReason) {
140
+ return {
141
+ candidates: args.candidates,
142
+ costEstimates: args.costEstimates,
143
+ audit: {
144
+ min_switch_savings_ratio: args.minSwitchSavingsRatio,
145
+ incumbent_model: incumbent?.model ?? null,
146
+ incumbent_reasoning_effort: incumbent?.reasoningEffort ?? null,
147
+ incumbent_fixed_turn_cost_usd: null,
148
+ skip_reason: skipReason,
149
+ pruned_candidates: [],
150
+ kept_candidates: [],
151
+ },
152
+ };
153
+ }
154
+ //# sourceMappingURL=candidate_pruning.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"candidate_pruning.js","sourceRoot":"","sources":["../src/candidate_pruning.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,41 @@
1
+ import { type CandidatePruningAudit } from "./candidate_pruning.js";
2
+ import { type CandidateCostEstimate, type CustomRouterConfig, type CustomRouterRule, type PreviousDecision, type ReasoningEffort, type RouterModelPrice, type RoutingCandidate, type RoutingStrategy } from "./types.js";
3
+ export type CustomDefaultTarget = {
4
+ model: string;
5
+ thinkingLevel: ReasoningEffort | null;
6
+ };
7
+ type CandidateSet = {
8
+ candidates: RoutingCandidate[];
9
+ costEstimates: CandidateCostEstimate[];
10
+ };
11
+ type CustomDetails = {
12
+ rules: CustomRouterRule[];
13
+ defaultTarget: CustomDefaultTarget | null;
14
+ audit: {
15
+ dropped_rules: CustomRouterRule[];
16
+ fallback_all_candidates: boolean;
17
+ no_routable_rule_models: boolean;
18
+ };
19
+ };
20
+ export type StrategyCandidateResolution = CandidateSet & ({
21
+ strategy: "slm";
22
+ pruning: CandidatePruningAudit;
23
+ } | {
24
+ strategy: "custom";
25
+ pruning: null;
26
+ custom: null;
27
+ } | {
28
+ strategy: "custom";
29
+ pruning: CandidatePruningAudit;
30
+ custom: CustomDetails;
31
+ });
32
+ type CandidateResolutionArgs = {
33
+ strategy: RoutingStrategy;
34
+ candidates: RoutingCandidate[];
35
+ costEstimates: CandidateCostEstimate[];
36
+ previousDecision?: PreviousDecision;
37
+ customConfig?: CustomRouterConfig | null;
38
+ modelPrices: Record<string, RouterModelPrice>;
39
+ };
40
+ export declare function resolveStrategyCandidates(args: CandidateResolutionArgs): StrategyCandidateResolution;
41
+ export {};