@mupt-ai/dari-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.
- package/README.md +89 -60
- package/dist/anonymous_actions.js +32 -10
- package/dist/anonymous_actions.js.map +1 -1
- package/dist/auto_router.d.ts +4 -1
- package/dist/auto_router.js +3 -3
- package/dist/auto_router.js.map +1 -1
- package/dist/cache_behavior.d.ts +8 -6
- package/dist/cache_behavior.js +56 -30
- package/dist/cache_behavior.js.map +1 -1
- package/dist/cache_scope.d.ts +2 -2
- package/dist/cache_scope.js +23 -18
- package/dist/cache_scope.js.map +1 -1
- package/dist/cost.d.ts +3 -2
- package/dist/cost.js +32 -23
- package/dist/cost.js.map +1 -1
- package/dist/dari_policy.d.ts +8 -2
- package/dist/dari_policy.js +39 -1
- package/dist/dari_policy.js.map +1 -1
- package/dist/eval_score_imputation.d.ts +26 -0
- package/dist/eval_score_imputation.js +83 -0
- package/dist/eval_score_imputation.js.map +1 -0
- package/dist/framework_lifecycle.d.ts +1 -1
- package/dist/framework_lifecycle.js +4 -1
- package/dist/framework_lifecycle.js.map +1 -1
- package/dist/framework_models.js +6 -0
- package/dist/framework_models.js.map +1 -1
- package/dist/framework_router.js +22 -11
- package/dist/framework_router.js.map +1 -1
- package/dist/framework_types.d.ts +9 -5
- package/dist/index.d.ts +9 -35
- package/dist/index.js +6 -37
- package/dist/index.js.map +1 -1
- package/dist/model_ids.js +4 -0
- package/dist/model_ids.js.map +1 -1
- package/dist/pi_runtime.js +19 -11
- package/dist/pi_runtime.js.map +1 -1
- package/dist/pi_types.d.ts +2 -2
- package/{src/index.ts → dist/policy-engine.d.ts} +2 -14
- package/dist/policy-engine.js +25 -0
- package/dist/policy-engine.js.map +1 -0
- package/dist/prompts.d.ts +1 -1
- package/dist/prompts.js +3 -3
- package/dist/prompts.js.map +1 -1
- package/dist/protocols.d.ts +5 -0
- package/dist/protocols.js +6 -0
- package/dist/protocols.js.map +1 -0
- package/dist/route.d.ts +3 -4
- package/dist/route.js +17 -10
- package/dist/route.js.map +1 -1
- package/dist/selector_input.d.ts +1 -0
- package/dist/selector_input.js +66 -78
- package/dist/selector_input.js.map +1 -1
- package/dist/selector_parse.d.ts +1 -0
- package/dist/selector_parse.js +2 -1
- package/dist/selector_parse.js.map +1 -1
- package/dist/selector_request.d.ts +1 -0
- package/dist/selector_request.js +1 -0
- package/dist/selector_request.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/examples/auto_router.ts +40 -0
- package/examples/basic_route.ts +130 -0
- package/examples/create_router.ts +114 -0
- package/examples/dari_policy.ts +86 -0
- package/examples/lease_stores.ts +159 -0
- package/examples/managed/README.md +50 -0
- package/examples/managed/byok-router/README.md +13 -0
- package/examples/managed/byok-router/router.yml +13 -0
- package/examples/managed/custom-rules-router/README.md +12 -0
- package/examples/managed/custom-rules-router/router.yml +24 -0
- package/examples/managed/eval-router/README.md +18 -0
- package/examples/managed/eval-router/router.yml +11 -0
- package/examples/managed/slm-router/README.md +12 -0
- package/examples/managed/slm-router/router.yml +8 -0
- package/examples/pi_runtime.ts +55 -0
- package/package.json +26 -7
- package/src/anonymous_actions.ts +0 -434
- package/src/auto_router.ts +0 -95
- package/src/cache_behavior.ts +0 -130
- package/src/cache_scope.ts +0 -58
- package/src/cache_switch_decision.ts +0 -53
- package/src/candidate_pruning.ts +0 -257
- package/src/candidate_resolution.ts +0 -208
- package/src/compatibility.ts +0 -162
- package/src/continuation_state.ts +0 -368
- package/src/cost.ts +0 -455
- package/src/dari_policy.ts +0 -360
- package/src/errors.ts +0 -27
- package/src/fingerprint.ts +0 -148
- package/src/fixed_turn_cost_config.ts +0 -24
- package/src/framework_error.ts +0 -38
- package/src/framework_lifecycle.ts +0 -74
- package/src/framework_models.ts +0 -186
- package/src/framework_router.ts +0 -490
- package/src/framework_streaming.ts +0 -613
- package/src/framework_types.ts +0 -372
- package/src/framework_validation.ts +0 -235
- package/src/json.ts +0 -43
- package/src/model_ids.ts +0 -29
- package/src/pi_context.ts +0 -251
- package/src/pi_options.ts +0 -295
- package/src/pi_output.ts +0 -387
- package/src/pi_reasoning_signature.ts +0 -41
- package/src/pi_runtime.ts +0 -238
- package/src/pi_types.ts +0 -71
- package/src/portable_reasoning_state.ts +0 -129
- package/src/previous_decision.ts +0 -109
- package/src/prompts.ts +0 -69
- package/src/protocol_anthropic.ts +0 -571
- package/src/protocol_openai_chat.ts +0 -645
- package/src/protocol_validation.ts +0 -202
- package/src/route.ts +0 -490
- package/src/routing_history.ts +0 -157
- package/src/selector_input.ts +0 -246
- package/src/selector_parse.ts +0 -138
- package/src/selector_request.ts +0 -130
- package/src/selector_truncation.ts +0 -48
- package/src/selector_wire.ts +0 -167
- package/src/types.ts +0 -231
package/src/cache_scope.ts
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import { canonicalModelId } from "./model_ids.js";
|
|
2
|
-
|
|
3
|
-
export type ReasoningCacheScope = "effort_keyed" | "shared";
|
|
4
|
-
|
|
5
|
-
// How a model's provider prompt cache behaves when the reasoning payload
|
|
6
|
-
// changes between otherwise identical requests, from live-provider and
|
|
7
|
-
// router-path validation:
|
|
8
|
-
// effort_keyed - each effective reasoning effort warms its own cache
|
|
9
|
-
// partition; switching effort misses/rewrites, switching back
|
|
10
|
-
// reuses that effort's still-warm partition.
|
|
11
|
-
// shared - one cache partition regardless of reasoning effort.
|
|
12
|
-
// OpenAI reasoning models are effort_keyed (a first-ever effort missed a
|
|
13
|
-
// steady-warm prefix on every model tested). Anthropic models are effort_keyed
|
|
14
|
-
// because pi-ai places cache_control on message blocks, which Anthropic
|
|
15
|
-
// invalidates on thinking changes; budget-thinking Haiku would share under a
|
|
16
|
-
// system-block breakpoint, so revalidate its entry if pi-ai's Anthropic cache
|
|
17
|
-
// placement changes. Fireworks caching ignored reasoning entirely.
|
|
18
|
-
// Unlisted models default to effort_keyed: the safe direction is to
|
|
19
|
-
// under-claim warmth, never to price a cold prefix as a cache read.
|
|
20
|
-
export const DEFAULT_REASONING_CACHE_SCOPES: Readonly<Record<string, ReasoningCacheScope>> = {
|
|
21
|
-
"openai/gpt-5.6-sol": "effort_keyed",
|
|
22
|
-
"openai/gpt-5.6-terra": "effort_keyed",
|
|
23
|
-
"openai/gpt-5.6-luna": "effort_keyed",
|
|
24
|
-
"openai/gpt-5.5": "effort_keyed",
|
|
25
|
-
"openai/gpt-4.1-mini": "shared", // single reasoning mode; nothing to key on
|
|
26
|
-
"anthropic/claude-opus-4-8": "effort_keyed",
|
|
27
|
-
"anthropic/claude-sonnet-4-6": "effort_keyed",
|
|
28
|
-
"anthropic/claude-haiku-4-5": "effort_keyed", // via pi-ai message-block cache_control; see note above
|
|
29
|
-
"fireworks/deepseek-ai/DeepSeek-V4-Pro": "shared",
|
|
30
|
-
"fireworks/deepseek-ai/DeepSeek-V4-Flash": "shared",
|
|
31
|
-
"fireworks/deepseek-ai/DeepSeek-V4-Flash-0731": "shared",
|
|
32
|
-
"fireworks/zai-org/GLM-5.2": "shared",
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
export type ReasoningCacheScopeLookup = (modelId: string) => ReasoningCacheScope;
|
|
36
|
-
|
|
37
|
-
export function reasoningCacheScope(modelId: string): ReasoningCacheScope {
|
|
38
|
-
return DEFAULT_REASONING_CACHE_SCOPES[canonicalModelId(modelId)] ?? "effort_keyed";
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// The provider cache partition a prefix entry belongs to. An effort-keyed
|
|
42
|
-
// provider warms one partition per effective reasoning effort, so the pair
|
|
43
|
-
// identifies it; a shared-scope provider holds a single partition per model,
|
|
44
|
-
// and keying that by effort would remember several entries describing one
|
|
45
|
-
// cache. Hosts that store warmth per partition derive their key here so a
|
|
46
|
-
// reclassified provider moves the estimator and their storage together.
|
|
47
|
-
// modelId is used verbatim, matching how ChainHits keys warm prefixes: ids that
|
|
48
|
-
// differ only in spelling stay separate rather than cross-contaminating. The
|
|
49
|
-
// separator cannot appear in a model id or a bucket, so the pair is injective
|
|
50
|
-
// and can never collide with a shared-scope key.
|
|
51
|
-
export function cachePartitionKey(
|
|
52
|
-
modelId: string,
|
|
53
|
-
reasoningBucket: string | null | undefined,
|
|
54
|
-
cacheScope: ReasoningCacheScopeLookup = reasoningCacheScope
|
|
55
|
-
): string {
|
|
56
|
-
if (cacheScope(modelId) === "shared") return modelId;
|
|
57
|
-
return `${modelId}\u0000${reasoningBucket ?? ""}`;
|
|
58
|
-
}
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
export type WarmIncumbentCost = {
|
|
2
|
-
fixedTurnCostUsd: number;
|
|
3
|
-
outputCostPerMtok: number;
|
|
4
|
-
};
|
|
5
|
-
|
|
6
|
-
export type SwitchCandidateCost = {
|
|
7
|
-
fixedTurnCostUsd: number | null;
|
|
8
|
-
outputCostPerMtok: number | null;
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
export type CacheSwitchDecision =
|
|
12
|
-
| {
|
|
13
|
-
action: "keep";
|
|
14
|
-
reason: "unknown_switch_cost" | "unknown_switch_output_price" | "not_cheaper_output_tier" | "saves_more_than_threshold";
|
|
15
|
-
fixedTurnCostUsd?: number;
|
|
16
|
-
savingsRatio?: number;
|
|
17
|
-
}
|
|
18
|
-
| {
|
|
19
|
-
action: "prune";
|
|
20
|
-
reason: "insufficient_savings";
|
|
21
|
-
fixedTurnCostUsd: number;
|
|
22
|
-
savingsRatio: number;
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
export function decideCacheSwitch(args: {
|
|
26
|
-
warmIncumbent: WarmIncumbentCost;
|
|
27
|
-
switchCandidate: SwitchCandidateCost;
|
|
28
|
-
minSwitchSavingsRatio: number;
|
|
29
|
-
}): CacheSwitchDecision {
|
|
30
|
-
const fixedTurnCostUsd = args.switchCandidate.fixedTurnCostUsd;
|
|
31
|
-
if (fixedTurnCostUsd === null) {
|
|
32
|
-
return { action: "keep", reason: "unknown_switch_cost" };
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const outputCostPerMtok = args.switchCandidate.outputCostPerMtok;
|
|
36
|
-
if (outputCostPerMtok === null) {
|
|
37
|
-
return { action: "keep", reason: "unknown_switch_output_price" };
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Higher output price is our capability-upgrade guard. The pruning rule is
|
|
41
|
-
// only for cheaper-tier switches that look cheap before cache effects.
|
|
42
|
-
if (outputCostPerMtok >= args.warmIncumbent.outputCostPerMtok) {
|
|
43
|
-
return { action: "keep", reason: "not_cheaper_output_tier", fixedTurnCostUsd };
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const savingsRatio =
|
|
47
|
-
(args.warmIncumbent.fixedTurnCostUsd - fixedTurnCostUsd) / args.warmIncumbent.fixedTurnCostUsd;
|
|
48
|
-
if (savingsRatio > args.minSwitchSavingsRatio) {
|
|
49
|
-
return { action: "keep", reason: "saves_more_than_threshold", fixedTurnCostUsd, savingsRatio };
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
return { action: "prune", reason: "insufficient_savings", fixedTurnCostUsd, savingsRatio };
|
|
53
|
-
}
|
package/src/candidate_pruning.ts
DELETED
|
@@ -1,257 +0,0 @@
|
|
|
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
|
-
|
|
5
|
-
import {
|
|
6
|
-
decideCacheSwitch,
|
|
7
|
-
type CacheSwitchDecision,
|
|
8
|
-
} from "./cache_switch_decision.js";
|
|
9
|
-
import { turnCostUsdAt } from "./cost.js";
|
|
10
|
-
import { FIXED_TURN_COST_COMPARISON_TURNS } from "./fixed_turn_cost_config.js";
|
|
11
|
-
import {
|
|
12
|
-
REASONING_EFFORTS,
|
|
13
|
-
routingCandidateKey,
|
|
14
|
-
type CandidateCostEstimate,
|
|
15
|
-
type PreviousDecision,
|
|
16
|
-
type ReasoningEffort,
|
|
17
|
-
type RoutingCandidate,
|
|
18
|
-
} from "./types.js";
|
|
19
|
-
|
|
20
|
-
export const MIN_SWITCH_SAVINGS_RATIO = 0.1;
|
|
21
|
-
|
|
22
|
-
type PrunedCandidate = {
|
|
23
|
-
model: string;
|
|
24
|
-
reasoning_effort: ReasoningEffort;
|
|
25
|
-
fixed_turn_cost_usd: number;
|
|
26
|
-
savings_ratio: number;
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
type CandidateSwitchKeepDecision = {
|
|
30
|
-
action: "keep";
|
|
31
|
-
reason:
|
|
32
|
-
| Extract<CacheSwitchDecision, { action: "keep" }>["reason"]
|
|
33
|
-
| "same_model_upgrade";
|
|
34
|
-
fixedTurnCostUsd?: number;
|
|
35
|
-
savingsRatio?: number;
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
type CandidateSwitchDecision =
|
|
39
|
-
| CandidateSwitchKeepDecision
|
|
40
|
-
| Extract<CacheSwitchDecision, { action: "prune" }>;
|
|
41
|
-
|
|
42
|
-
type KeptReason = "incumbent" | CandidateSwitchKeepDecision["reason"];
|
|
43
|
-
|
|
44
|
-
type KeptCandidate = {
|
|
45
|
-
model: string;
|
|
46
|
-
reasoning_effort: ReasoningEffort;
|
|
47
|
-
reason: KeptReason;
|
|
48
|
-
savings_ratio?: number;
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
export type CandidatePruningAudit = {
|
|
52
|
-
min_switch_savings_ratio: number;
|
|
53
|
-
incumbent_model: string | null;
|
|
54
|
-
incumbent_reasoning_effort: ReasoningEffort | null;
|
|
55
|
-
incumbent_fixed_turn_cost_usd: number | null;
|
|
56
|
-
skip_reason:
|
|
57
|
-
| "no_previous_decision"
|
|
58
|
-
| "incumbent_not_warm"
|
|
59
|
-
| "incumbent_unpriced"
|
|
60
|
-
| null;
|
|
61
|
-
pruned_candidates: PrunedCandidate[];
|
|
62
|
-
kept_candidates: KeptCandidate[];
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
export type CandidatePruning = {
|
|
66
|
-
candidates: RoutingCandidate[];
|
|
67
|
-
costEstimates: CandidateCostEstimate[];
|
|
68
|
-
audit: CandidatePruningAudit;
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
export function pruneSwitchCandidates(args: {
|
|
72
|
-
candidates: RoutingCandidate[];
|
|
73
|
-
costEstimates: CandidateCostEstimate[];
|
|
74
|
-
previousDecision?: PreviousDecision;
|
|
75
|
-
minSwitchSavingsRatio: number;
|
|
76
|
-
}): CandidatePruning {
|
|
77
|
-
const incumbentCandidate = args.previousDecision
|
|
78
|
-
? {
|
|
79
|
-
model: args.previousDecision.model,
|
|
80
|
-
reasoningEffort: args.previousDecision.reasoningEffort,
|
|
81
|
-
}
|
|
82
|
-
: null;
|
|
83
|
-
const candidateKeys = new Set(args.candidates.map(routingCandidateKey));
|
|
84
|
-
const incumbentKey = incumbentCandidate
|
|
85
|
-
? routingCandidateKey(incumbentCandidate)
|
|
86
|
-
: null;
|
|
87
|
-
const estimateByKey = new Map(
|
|
88
|
-
args.costEstimates.map((estimate) => [
|
|
89
|
-
routingCandidateKey({
|
|
90
|
-
model: estimate.model,
|
|
91
|
-
reasoningEffort: estimate.reasoning_effort,
|
|
92
|
-
}),
|
|
93
|
-
estimate,
|
|
94
|
-
]),
|
|
95
|
-
);
|
|
96
|
-
const incumbent = incumbentKey && candidateKeys.has(incumbentKey)
|
|
97
|
-
? estimateByKey.get(incumbentKey)
|
|
98
|
-
: undefined;
|
|
99
|
-
if (!incumbent || !incumbentCandidate) {
|
|
100
|
-
return keepAll(args, incumbentCandidate, "no_previous_decision");
|
|
101
|
-
}
|
|
102
|
-
if (incumbent.warm_tokens <= 0) {
|
|
103
|
-
return keepAll(args, incumbentCandidate, "incumbent_not_warm");
|
|
104
|
-
}
|
|
105
|
-
const incumbentCost = turnCostUsdAt(
|
|
106
|
-
incumbent.fixed_turn_cost_estimate,
|
|
107
|
-
FIXED_TURN_COST_COMPARISON_TURNS,
|
|
108
|
-
);
|
|
109
|
-
const incumbentOutputPrice = incumbent.output_cost_per_mtok;
|
|
110
|
-
if (
|
|
111
|
-
incumbentCost === null ||
|
|
112
|
-
incumbentCost <= 0 ||
|
|
113
|
-
incumbentOutputPrice === null
|
|
114
|
-
) {
|
|
115
|
-
return keepAll(args, incumbentCandidate, "incumbent_unpriced");
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const prunedCandidates: PrunedCandidate[] = [];
|
|
119
|
-
const keptCandidates: KeptCandidate[] = [];
|
|
120
|
-
const survivors = args.candidates.filter((candidate) => {
|
|
121
|
-
const key = routingCandidateKey(candidate);
|
|
122
|
-
if (key === incumbentKey) {
|
|
123
|
-
keptCandidates.push({
|
|
124
|
-
model: candidate.model,
|
|
125
|
-
reasoning_effort: candidate.reasoningEffort,
|
|
126
|
-
reason: "incumbent",
|
|
127
|
-
});
|
|
128
|
-
return true;
|
|
129
|
-
}
|
|
130
|
-
const decision = candidateSwitchDecision({
|
|
131
|
-
candidate,
|
|
132
|
-
estimate: estimateByKey.get(key),
|
|
133
|
-
incumbent: incumbentCandidate,
|
|
134
|
-
incumbentCost,
|
|
135
|
-
incumbentOutputPrice,
|
|
136
|
-
minSwitchSavingsRatio: args.minSwitchSavingsRatio,
|
|
137
|
-
});
|
|
138
|
-
if (decision.action === "keep") {
|
|
139
|
-
keptCandidates.push({
|
|
140
|
-
model: candidate.model,
|
|
141
|
-
reasoning_effort: candidate.reasoningEffort,
|
|
142
|
-
reason: decision.reason,
|
|
143
|
-
...(decision.savingsRatio === undefined
|
|
144
|
-
? {}
|
|
145
|
-
: { savings_ratio: decision.savingsRatio }),
|
|
146
|
-
});
|
|
147
|
-
return true;
|
|
148
|
-
}
|
|
149
|
-
prunedCandidates.push({
|
|
150
|
-
model: candidate.model,
|
|
151
|
-
reasoning_effort: candidate.reasoningEffort,
|
|
152
|
-
fixed_turn_cost_usd: decision.fixedTurnCostUsd,
|
|
153
|
-
savings_ratio: decision.savingsRatio,
|
|
154
|
-
});
|
|
155
|
-
return false;
|
|
156
|
-
});
|
|
157
|
-
const survivorKeys = new Set(survivors.map(routingCandidateKey));
|
|
158
|
-
|
|
159
|
-
return {
|
|
160
|
-
candidates: survivors,
|
|
161
|
-
costEstimates: args.costEstimates.filter((estimate) =>
|
|
162
|
-
survivorKeys.has(
|
|
163
|
-
routingCandidateKey({
|
|
164
|
-
model: estimate.model,
|
|
165
|
-
reasoningEffort: estimate.reasoning_effort,
|
|
166
|
-
}),
|
|
167
|
-
)
|
|
168
|
-
),
|
|
169
|
-
audit: {
|
|
170
|
-
min_switch_savings_ratio: args.minSwitchSavingsRatio,
|
|
171
|
-
incumbent_model: incumbentCandidate.model,
|
|
172
|
-
incumbent_reasoning_effort: incumbentCandidate.reasoningEffort,
|
|
173
|
-
incumbent_fixed_turn_cost_usd: incumbentCost,
|
|
174
|
-
skip_reason: null,
|
|
175
|
-
pruned_candidates: prunedCandidates,
|
|
176
|
-
kept_candidates: keptCandidates,
|
|
177
|
-
},
|
|
178
|
-
};
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function candidateSwitchDecision(args: {
|
|
182
|
-
candidate: RoutingCandidate;
|
|
183
|
-
estimate?: CandidateCostEstimate;
|
|
184
|
-
incumbent: RoutingCandidate;
|
|
185
|
-
incumbentCost: number;
|
|
186
|
-
incumbentOutputPrice: number;
|
|
187
|
-
minSwitchSavingsRatio: number;
|
|
188
|
-
}): CandidateSwitchDecision {
|
|
189
|
-
const candidateCost = turnCostUsdAt(
|
|
190
|
-
args.estimate?.fixed_turn_cost_estimate,
|
|
191
|
-
FIXED_TURN_COST_COMPARISON_TURNS,
|
|
192
|
-
);
|
|
193
|
-
if (args.candidate.model !== args.incumbent.model) {
|
|
194
|
-
return decideCacheSwitch({
|
|
195
|
-
warmIncumbent: {
|
|
196
|
-
fixedTurnCostUsd: args.incumbentCost,
|
|
197
|
-
outputCostPerMtok: args.incumbentOutputPrice,
|
|
198
|
-
},
|
|
199
|
-
switchCandidate: {
|
|
200
|
-
fixedTurnCostUsd: candidateCost,
|
|
201
|
-
outputCostPerMtok: args.estimate?.output_cost_per_mtok ?? null,
|
|
202
|
-
},
|
|
203
|
-
minSwitchSavingsRatio: args.minSwitchSavingsRatio,
|
|
204
|
-
});
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
// Output price cannot distinguish thinking levels on one model, so preserve
|
|
208
|
-
// capability upgrades and judge lower-effort switches by projected cost.
|
|
209
|
-
if (
|
|
210
|
-
REASONING_EFFORTS.indexOf(args.candidate.reasoningEffort) >=
|
|
211
|
-
REASONING_EFFORTS.indexOf(args.incumbent.reasoningEffort)
|
|
212
|
-
) {
|
|
213
|
-
return { action: "keep", reason: "same_model_upgrade" };
|
|
214
|
-
}
|
|
215
|
-
if (candidateCost === null) {
|
|
216
|
-
return { action: "keep", reason: "unknown_switch_cost" };
|
|
217
|
-
}
|
|
218
|
-
const savingsRatio =
|
|
219
|
-
(args.incumbentCost - candidateCost) / args.incumbentCost;
|
|
220
|
-
return savingsRatio > args.minSwitchSavingsRatio
|
|
221
|
-
? {
|
|
222
|
-
action: "keep",
|
|
223
|
-
reason: "saves_more_than_threshold",
|
|
224
|
-
fixedTurnCostUsd: candidateCost,
|
|
225
|
-
savingsRatio,
|
|
226
|
-
}
|
|
227
|
-
: {
|
|
228
|
-
action: "prune",
|
|
229
|
-
reason: "insufficient_savings",
|
|
230
|
-
fixedTurnCostUsd: candidateCost,
|
|
231
|
-
savingsRatio,
|
|
232
|
-
};
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
function keepAll(
|
|
236
|
-
args: {
|
|
237
|
-
candidates: RoutingCandidate[];
|
|
238
|
-
costEstimates: CandidateCostEstimate[];
|
|
239
|
-
minSwitchSavingsRatio: number;
|
|
240
|
-
},
|
|
241
|
-
incumbent: RoutingCandidate | null,
|
|
242
|
-
skipReason: NonNullable<CandidatePruningAudit["skip_reason"]>,
|
|
243
|
-
): CandidatePruning {
|
|
244
|
-
return {
|
|
245
|
-
candidates: args.candidates,
|
|
246
|
-
costEstimates: args.costEstimates,
|
|
247
|
-
audit: {
|
|
248
|
-
min_switch_savings_ratio: args.minSwitchSavingsRatio,
|
|
249
|
-
incumbent_model: incumbent?.model ?? null,
|
|
250
|
-
incumbent_reasoning_effort: incumbent?.reasoningEffort ?? null,
|
|
251
|
-
incumbent_fixed_turn_cost_usd: null,
|
|
252
|
-
skip_reason: skipReason,
|
|
253
|
-
pruned_candidates: [],
|
|
254
|
-
kept_candidates: [],
|
|
255
|
-
},
|
|
256
|
-
};
|
|
257
|
-
}
|
|
@@ -1,208 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
MIN_SWITCH_SAVINGS_RATIO,
|
|
3
|
-
pruneSwitchCandidates,
|
|
4
|
-
type CandidatePruningAudit,
|
|
5
|
-
} from "./candidate_pruning.js";
|
|
6
|
-
import {
|
|
7
|
-
routingCandidateKey,
|
|
8
|
-
type CandidateCostEstimate,
|
|
9
|
-
type CustomRouterConfig,
|
|
10
|
-
type CustomRouterRule,
|
|
11
|
-
type PreviousDecision,
|
|
12
|
-
type ReasoningEffort,
|
|
13
|
-
type RouterModelPrice,
|
|
14
|
-
type RoutingCandidate,
|
|
15
|
-
type RoutingStrategy,
|
|
16
|
-
} from "./types.js";
|
|
17
|
-
|
|
18
|
-
export type CustomDefaultTarget = {
|
|
19
|
-
model: string;
|
|
20
|
-
thinkingLevel: ReasoningEffort | null;
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
type CandidateSet = {
|
|
24
|
-
candidates: RoutingCandidate[];
|
|
25
|
-
costEstimates: CandidateCostEstimate[];
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
type CustomDetails = {
|
|
29
|
-
rules: CustomRouterRule[];
|
|
30
|
-
defaultTarget: CustomDefaultTarget | null;
|
|
31
|
-
audit: {
|
|
32
|
-
dropped_rules: CustomRouterRule[];
|
|
33
|
-
fallback_all_candidates: boolean;
|
|
34
|
-
no_routable_rule_models: boolean;
|
|
35
|
-
};
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
export type StrategyCandidateResolution = CandidateSet & (
|
|
39
|
-
| { strategy: "slm"; pruning: CandidatePruningAudit }
|
|
40
|
-
| { strategy: "custom"; pruning: null; custom: null }
|
|
41
|
-
| {
|
|
42
|
-
strategy: "custom";
|
|
43
|
-
pruning: CandidatePruningAudit;
|
|
44
|
-
custom: CustomDetails;
|
|
45
|
-
}
|
|
46
|
-
);
|
|
47
|
-
|
|
48
|
-
type CustomCandidateResolution = Extract<
|
|
49
|
-
StrategyCandidateResolution,
|
|
50
|
-
{ strategy: "custom"; custom: CustomDetails }
|
|
51
|
-
>;
|
|
52
|
-
|
|
53
|
-
type CandidateResolutionArgs = {
|
|
54
|
-
strategy: RoutingStrategy;
|
|
55
|
-
candidates: RoutingCandidate[];
|
|
56
|
-
costEstimates: CandidateCostEstimate[];
|
|
57
|
-
previousDecision?: PreviousDecision;
|
|
58
|
-
customConfig?: CustomRouterConfig | null;
|
|
59
|
-
modelPrices: Record<string, RouterModelPrice>;
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
export function resolveStrategyCandidates(
|
|
63
|
-
args: CandidateResolutionArgs,
|
|
64
|
-
): StrategyCandidateResolution {
|
|
65
|
-
if (args.strategy === "slm") {
|
|
66
|
-
const pruned = pruneSwitchCandidates({
|
|
67
|
-
candidates: args.candidates,
|
|
68
|
-
costEstimates: args.costEstimates,
|
|
69
|
-
previousDecision: args.previousDecision,
|
|
70
|
-
minSwitchSavingsRatio: MIN_SWITCH_SAVINGS_RATIO,
|
|
71
|
-
});
|
|
72
|
-
return {
|
|
73
|
-
strategy: "slm",
|
|
74
|
-
candidates: pruned.candidates,
|
|
75
|
-
costEstimates: pruned.costEstimates,
|
|
76
|
-
pruning: pruned.audit,
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const config = args.customConfig;
|
|
81
|
-
if (!config || !Array.isArray(config.rules) || config.rules.length === 0) {
|
|
82
|
-
return {
|
|
83
|
-
strategy: "custom",
|
|
84
|
-
candidates: [],
|
|
85
|
-
costEstimates: [],
|
|
86
|
-
pruning: null,
|
|
87
|
-
custom: null,
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
return resolveCustomCandidates(args, config);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function resolveCustomCandidates(
|
|
95
|
-
args: CandidateResolutionArgs,
|
|
96
|
-
config: CustomRouterConfig,
|
|
97
|
-
): CustomCandidateResolution {
|
|
98
|
-
const usableRules = config.rules.filter((rule) =>
|
|
99
|
-
args.candidates.some((candidate) => matchesRule(candidate, rule)),
|
|
100
|
-
);
|
|
101
|
-
const defaultModel = typeof config.default === "string" ? config.default : null;
|
|
102
|
-
const defaultTarget =
|
|
103
|
-
defaultModel !== null &&
|
|
104
|
-
args.candidates.some((candidate) =>
|
|
105
|
-
matchesTarget(candidate, defaultModel, config.default_thinking_level),
|
|
106
|
-
)
|
|
107
|
-
? {
|
|
108
|
-
model: defaultModel,
|
|
109
|
-
thinkingLevel: config.default_thinking_level ?? null,
|
|
110
|
-
}
|
|
111
|
-
: null;
|
|
112
|
-
const matchesDefault = (candidate: RoutingCandidate) =>
|
|
113
|
-
defaultTarget !== null &&
|
|
114
|
-
matchesTarget(candidate, defaultTarget.model, defaultTarget.thinkingLevel);
|
|
115
|
-
const configuredCandidates = orderCandidatesByCostDesc(
|
|
116
|
-
args.candidates.filter(
|
|
117
|
-
(candidate) =>
|
|
118
|
-
usableRules.some((rule) => matchesRule(candidate, rule)) ||
|
|
119
|
-
matchesDefault(candidate),
|
|
120
|
-
),
|
|
121
|
-
args.modelPrices,
|
|
122
|
-
);
|
|
123
|
-
const pruned = pruneSwitchCandidates({
|
|
124
|
-
candidates: configuredCandidates,
|
|
125
|
-
costEstimates: reorderEstimates(args.costEstimates, configuredCandidates),
|
|
126
|
-
previousDecision: args.previousDecision,
|
|
127
|
-
minSwitchSavingsRatio: MIN_SWITCH_SAVINGS_RATIO,
|
|
128
|
-
});
|
|
129
|
-
const ruleOrder = (rule: CustomRouterRule) =>
|
|
130
|
-
pruned.candidates.findIndex((candidate) => matchesRule(candidate, rule));
|
|
131
|
-
|
|
132
|
-
return {
|
|
133
|
-
strategy: "custom",
|
|
134
|
-
candidates: pruned.candidates,
|
|
135
|
-
costEstimates: pruned.costEstimates,
|
|
136
|
-
pruning: pruned.audit,
|
|
137
|
-
custom: {
|
|
138
|
-
rules: usableRules
|
|
139
|
-
.filter((rule) => ruleOrder(rule) >= 0)
|
|
140
|
-
.sort((a, b) => ruleOrder(a) - ruleOrder(b)),
|
|
141
|
-
defaultTarget: pruned.candidates.some(matchesDefault)
|
|
142
|
-
? defaultTarget
|
|
143
|
-
: null,
|
|
144
|
-
audit: {
|
|
145
|
-
dropped_rules: config.rules.filter((rule) => !usableRules.includes(rule)),
|
|
146
|
-
fallback_all_candidates: false,
|
|
147
|
-
no_routable_rule_models: configuredCandidates.length === 0,
|
|
148
|
-
},
|
|
149
|
-
},
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
function matchesRule(
|
|
154
|
-
candidate: RoutingCandidate,
|
|
155
|
-
rule: CustomRouterRule,
|
|
156
|
-
): boolean {
|
|
157
|
-
return matchesTarget(candidate, rule.use, rule.thinking_level);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
function matchesTarget(
|
|
161
|
-
candidate: RoutingCandidate,
|
|
162
|
-
model: string,
|
|
163
|
-
thinkingLevel: ReasoningEffort | null | undefined,
|
|
164
|
-
): boolean {
|
|
165
|
-
return candidate.model === model &&
|
|
166
|
-
(thinkingLevel == null || candidate.reasoningEffort === thinkingLevel);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// Catalog rates are model-scoped, so sorting remains stable across a model's
|
|
170
|
-
// thinking-level variants.
|
|
171
|
-
function orderCandidatesByCostDesc(
|
|
172
|
-
candidates: RoutingCandidate[],
|
|
173
|
-
modelPrices: Record<string, RouterModelPrice>,
|
|
174
|
-
): RoutingCandidate[] {
|
|
175
|
-
return [...candidates].sort((a, b) => {
|
|
176
|
-
const priceA = usablePrice(modelPrices[a.model]);
|
|
177
|
-
const priceB = usablePrice(modelPrices[b.model]);
|
|
178
|
-
if (priceA === null && priceB === null) return 0;
|
|
179
|
-
if (priceA === null) return 1;
|
|
180
|
-
if (priceB === null) return -1;
|
|
181
|
-
return priceB.output - priceA.output || priceB.input - priceA.input;
|
|
182
|
-
});
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
function usablePrice(price: RouterModelPrice | undefined): RouterModelPrice | null {
|
|
186
|
-
return price && Number.isFinite(price.input) && Number.isFinite(price.output)
|
|
187
|
-
? price
|
|
188
|
-
: null;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
function reorderEstimates(
|
|
192
|
-
estimates: CandidateCostEstimate[],
|
|
193
|
-
candidates: RoutingCandidate[],
|
|
194
|
-
): CandidateCostEstimate[] {
|
|
195
|
-
const byCandidate = new Map(
|
|
196
|
-
estimates.map((estimate) => [
|
|
197
|
-
routingCandidateKey({
|
|
198
|
-
model: estimate.model,
|
|
199
|
-
reasoningEffort: estimate.reasoning_effort,
|
|
200
|
-
}),
|
|
201
|
-
estimate,
|
|
202
|
-
]),
|
|
203
|
-
);
|
|
204
|
-
return candidates.flatMap((candidate) => {
|
|
205
|
-
const estimate = byCandidate.get(routingCandidateKey(candidate));
|
|
206
|
-
return estimate ? [estimate] : [];
|
|
207
|
-
});
|
|
208
|
-
}
|