@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.
- package/LICENSE +202 -0
- package/README.md +100 -0
- package/dist/anonymous_actions.d.ts +55 -0
- package/dist/anonymous_actions.js +277 -0
- package/dist/anonymous_actions.js.map +1 -0
- package/dist/auto_router.d.ts +10 -0
- package/dist/auto_router.js +56 -0
- package/dist/auto_router.js.map +1 -0
- package/dist/cache_behavior.d.ts +14 -0
- package/dist/cache_behavior.js +112 -0
- package/dist/cache_behavior.js.map +1 -0
- package/dist/cache_scope.d.ts +5 -0
- package/dist/cache_scope.js +49 -0
- package/dist/cache_scope.js.map +1 -0
- package/dist/cache_switch_decision.d.ts +24 -0
- package/dist/cache_switch_decision.js +21 -0
- package/dist/cache_switch_decision.js.map +1 -0
- package/dist/candidate_pruning.d.ts +45 -0
- package/dist/candidate_pruning.js +154 -0
- package/dist/candidate_pruning.js.map +1 -0
- package/dist/candidate_resolution.d.ts +41 -0
- package/dist/candidate_resolution.js +111 -0
- package/dist/candidate_resolution.js.map +1 -0
- package/dist/compatibility.d.ts +22 -0
- package/dist/compatibility.js +76 -0
- package/dist/compatibility.js.map +1 -0
- package/dist/continuation_state.d.ts +36 -0
- package/dist/continuation_state.js +249 -0
- package/dist/continuation_state.js.map +1 -0
- package/dist/cost.d.ts +31 -0
- package/dist/cost.js +314 -0
- package/dist/cost.js.map +1 -0
- package/dist/dari_policy.d.ts +34 -0
- package/dist/dari_policy.js +267 -0
- package/dist/dari_policy.js.map +1 -0
- package/dist/errors.d.ts +7 -0
- package/dist/errors.js +14 -0
- package/dist/errors.js.map +1 -0
- package/dist/fingerprint.d.ts +13 -0
- package/dist/fingerprint.js +130 -0
- package/dist/fingerprint.js.map +1 -0
- package/dist/fixed_turn_cost_config.d.ts +9 -0
- package/dist/fixed_turn_cost_config.js +22 -0
- package/dist/fixed_turn_cost_config.js.map +1 -0
- package/dist/framework_error.d.ts +8 -0
- package/dist/framework_error.js +24 -0
- package/dist/framework_error.js.map +1 -0
- package/dist/framework_lifecycle.d.ts +11 -0
- package/dist/framework_lifecycle.js +57 -0
- package/dist/framework_lifecycle.js.map +1 -0
- package/dist/framework_models.d.ts +3 -0
- package/dist/framework_models.js +120 -0
- package/dist/framework_models.js.map +1 -0
- package/dist/framework_router.d.ts +2 -0
- package/dist/framework_router.js +384 -0
- package/dist/framework_router.js.map +1 -0
- package/dist/framework_streaming.d.ts +26 -0
- package/dist/framework_streaming.js +486 -0
- package/dist/framework_streaming.js.map +1 -0
- package/dist/framework_types.d.ts +272 -0
- package/dist/framework_types.js +5 -0
- package/dist/framework_types.js.map +1 -0
- package/dist/framework_validation.d.ts +7 -0
- package/dist/framework_validation.js +169 -0
- package/dist/framework_validation.js.map +1 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +38 -0
- package/dist/index.js.map +1 -0
- package/dist/json.d.ts +10 -0
- package/dist/json.js +29 -0
- package/dist/json.js.map +1 -0
- package/dist/model_ids.d.ts +4 -0
- package/dist/model_ids.js +26 -0
- package/dist/model_ids.js.map +1 -0
- package/dist/pi_context.d.ts +4 -0
- package/dist/pi_context.js +206 -0
- package/dist/pi_context.js.map +1 -0
- package/dist/pi_options.d.ts +3 -0
- package/dist/pi_options.js +240 -0
- package/dist/pi_options.js.map +1 -0
- package/dist/pi_output.d.ts +4 -0
- package/dist/pi_output.js +328 -0
- package/dist/pi_output.js.map +1 -0
- package/dist/pi_reasoning_signature.d.ts +10 -0
- package/dist/pi_reasoning_signature.js +33 -0
- package/dist/pi_reasoning_signature.js.map +1 -0
- package/dist/pi_runtime.d.ts +3 -0
- package/dist/pi_runtime.js +155 -0
- package/dist/pi_runtime.js.map +1 -0
- package/dist/pi_types.d.ts +35 -0
- package/dist/pi_types.js +2 -0
- package/dist/pi_types.js.map +1 -0
- package/dist/portable_reasoning_state.d.ts +7 -0
- package/dist/portable_reasoning_state.js +81 -0
- package/dist/portable_reasoning_state.js.map +1 -0
- package/dist/previous_decision.d.ts +13 -0
- package/dist/previous_decision.js +82 -0
- package/dist/previous_decision.js.map +1 -0
- package/dist/prompts.d.ts +3 -0
- package/dist/prompts.js +65 -0
- package/dist/prompts.js.map +1 -0
- package/dist/protocol_anthropic.d.ts +18 -0
- package/dist/protocol_anthropic.js +443 -0
- package/dist/protocol_anthropic.js.map +1 -0
- package/dist/protocol_openai_chat.d.ts +25 -0
- package/dist/protocol_openai_chat.js +503 -0
- package/dist/protocol_openai_chat.js.map +1 -0
- package/dist/protocol_validation.d.ts +23 -0
- package/dist/protocol_validation.js +134 -0
- package/dist/protocol_validation.js.map +1 -0
- package/dist/route.d.ts +105 -0
- package/dist/route.js +226 -0
- package/dist/route.js.map +1 -0
- package/dist/routing_history.d.ts +7 -0
- package/dist/routing_history.js +132 -0
- package/dist/routing_history.js.map +1 -0
- package/dist/selector_input.d.ts +42 -0
- package/dist/selector_input.js +153 -0
- package/dist/selector_input.js.map +1 -0
- package/dist/selector_parse.d.ts +13 -0
- package/dist/selector_parse.js +95 -0
- package/dist/selector_parse.js.map +1 -0
- package/dist/selector_request.d.ts +26 -0
- package/dist/selector_request.js +85 -0
- package/dist/selector_request.js.map +1 -0
- package/dist/selector_truncation.d.ts +2 -0
- package/dist/selector_truncation.js +48 -0
- package/dist/selector_truncation.js.map +1 -0
- package/dist/selector_wire.d.ts +10 -0
- package/dist/selector_wire.js +113 -0
- package/dist/selector_wire.js.map +1 -0
- package/dist/types.d.ts +152 -0
- package/dist/types.js +14 -0
- package/dist/types.js.map +1 -0
- package/package.json +64 -0
- package/src/anonymous_actions.ts +434 -0
- package/src/auto_router.ts +95 -0
- package/src/cache_behavior.ts +130 -0
- package/src/cache_scope.ts +58 -0
- package/src/cache_switch_decision.ts +53 -0
- package/src/candidate_pruning.ts +257 -0
- package/src/candidate_resolution.ts +208 -0
- package/src/compatibility.ts +162 -0
- package/src/continuation_state.ts +368 -0
- package/src/cost.ts +455 -0
- package/src/dari_policy.ts +360 -0
- package/src/errors.ts +27 -0
- package/src/fingerprint.ts +148 -0
- package/src/fixed_turn_cost_config.ts +24 -0
- package/src/framework_error.ts +38 -0
- package/src/framework_lifecycle.ts +74 -0
- package/src/framework_models.ts +186 -0
- package/src/framework_router.ts +490 -0
- package/src/framework_streaming.ts +613 -0
- package/src/framework_types.ts +372 -0
- package/src/framework_validation.ts +235 -0
- package/src/index.ts +37 -0
- package/src/json.ts +43 -0
- package/src/model_ids.ts +29 -0
- package/src/pi_context.ts +251 -0
- package/src/pi_options.ts +295 -0
- package/src/pi_output.ts +387 -0
- package/src/pi_reasoning_signature.ts +41 -0
- package/src/pi_runtime.ts +238 -0
- package/src/pi_types.ts +71 -0
- package/src/portable_reasoning_state.ts +129 -0
- package/src/previous_decision.ts +109 -0
- package/src/prompts.ts +69 -0
- package/src/protocol_anthropic.ts +571 -0
- package/src/protocol_openai_chat.ts +645 -0
- package/src/protocol_validation.ts +202 -0
- package/src/route.ts +490 -0
- package/src/routing_history.ts +157 -0
- package/src/selector_input.ts +246 -0
- package/src/selector_parse.ts +138 -0
- package/src/selector_request.ts +130 -0
- package/src/selector_truncation.ts +48 -0
- package/src/selector_wire.ts +167 -0
- package/src/types.ts +231 -0
|
@@ -0,0 +1,208 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { RouterCoreError } from "./errors.js";
|
|
2
|
+
import {
|
|
3
|
+
isReasoningEffort,
|
|
4
|
+
type HardCapability,
|
|
5
|
+
type ReasoningEffort,
|
|
6
|
+
type RoutingCandidate,
|
|
7
|
+
} from "./types.js";
|
|
8
|
+
|
|
9
|
+
// Host-supplied capability facts for one candidate model. Hosts derive this
|
|
10
|
+
// from their model registry/catalog; the core only applies the eligibility
|
|
11
|
+
// rules and never inspects provider metadata records directly.
|
|
12
|
+
export type CandidateModelMetadata = {
|
|
13
|
+
// Provider identifier, e.g. "meta" or "anthropic".
|
|
14
|
+
provider: string;
|
|
15
|
+
// Provider API identifier, e.g. "anthropic-messages" or "openai-responses".
|
|
16
|
+
api: string;
|
|
17
|
+
supportsImageInput: boolean;
|
|
18
|
+
supportsHostedWebSearch: boolean;
|
|
19
|
+
// Whether the provider API can serialize response_format for structured
|
|
20
|
+
// output (JSON schema) requests.
|
|
21
|
+
supportsStructuredOutput: boolean;
|
|
22
|
+
supportedThinkingLevels: string[];
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type CandidateMetadataLookup = (modelId: string) => CandidateModelMetadata;
|
|
26
|
+
|
|
27
|
+
export type CompatibleCandidates = {
|
|
28
|
+
candidates: RoutingCandidate[];
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export function resolveCompatibleCandidates(args: {
|
|
32
|
+
requiredCapabilities: readonly HardCapability[];
|
|
33
|
+
// The client's explicit reasoning_effort request, when present. Pins every
|
|
34
|
+
// candidate to that level.
|
|
35
|
+
requestedReasoningEffort?: ReasoningEffort;
|
|
36
|
+
// Some protocols carry an independent hard thinking-enabled constraint.
|
|
37
|
+
thinkingEnabled?: boolean;
|
|
38
|
+
// The request's tool_choice value in OpenAI wire shape ("none" | "auto" |
|
|
39
|
+
// "required" | "any" | {..forced tool..}). Only forced-tool detection is
|
|
40
|
+
// applied here.
|
|
41
|
+
toolChoice?: unknown;
|
|
42
|
+
candidateModels: string[];
|
|
43
|
+
metadataLookup: CandidateMetadataLookup;
|
|
44
|
+
modelThinkingLevels?: Readonly<Record<string, ReasoningEffort[]>>;
|
|
45
|
+
}): CompatibleCandidates {
|
|
46
|
+
const compatible: RoutingCandidate[] = [];
|
|
47
|
+
let anthropicToolChoiceRejected = false;
|
|
48
|
+
let metaToolChoiceRejected = false;
|
|
49
|
+
let candidates = args.candidateModels.map((model) => ({
|
|
50
|
+
model,
|
|
51
|
+
metadata: args.metadataLookup(model),
|
|
52
|
+
}));
|
|
53
|
+
|
|
54
|
+
if (args.requiredCapabilities.includes("image_input")) {
|
|
55
|
+
candidates = candidates.filter(({ metadata }) => metadata.supportsImageInput);
|
|
56
|
+
if (candidates.length === 0) {
|
|
57
|
+
throw new RouterCoreError(
|
|
58
|
+
"invalid_request",
|
|
59
|
+
"Image input is not supported by any enabled model for this router.",
|
|
60
|
+
"unsupported_input_modality",
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (args.requiredCapabilities.includes("structured_output")) {
|
|
66
|
+
candidates = candidates.filter(({ metadata }) => metadata.supportsStructuredOutput);
|
|
67
|
+
if (candidates.length === 0) {
|
|
68
|
+
throw new RouterCoreError(
|
|
69
|
+
"invalid_request",
|
|
70
|
+
"Structured output is not supported by any enabled model for this router.",
|
|
71
|
+
"unsupported_response_format",
|
|
72
|
+
"response_format",
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (args.requiredCapabilities.includes("openai_hosted_web_search")) {
|
|
78
|
+
candidates = candidates.filter(({ metadata }) => metadata.supportsHostedWebSearch);
|
|
79
|
+
if (candidates.length === 0) {
|
|
80
|
+
throw new RouterCoreError(
|
|
81
|
+
"invalid_request",
|
|
82
|
+
"Hosted web search requires an enabled OpenAI or Azure Responses model for this router.",
|
|
83
|
+
"unsupported_tool",
|
|
84
|
+
"tools",
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const requested = args.requestedReasoningEffort;
|
|
90
|
+
for (const { model, metadata } of candidates) {
|
|
91
|
+
if (
|
|
92
|
+
metadata.provider === "meta" &&
|
|
93
|
+
args.toolChoice !== undefined &&
|
|
94
|
+
args.toolChoice !== "auto"
|
|
95
|
+
) {
|
|
96
|
+
metaToolChoiceRejected = true;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
const supportedLevels = metadata.supportedThinkingLevels.filter(
|
|
100
|
+
isReasoningEffort,
|
|
101
|
+
);
|
|
102
|
+
const configuredLevels = args.modelThinkingLevels?.[model] ?? supportedLevels;
|
|
103
|
+
const enabledLevels = configuredLevels.filter(
|
|
104
|
+
(level) => isReasoningEffort(level) && supportedLevels.includes(level),
|
|
105
|
+
);
|
|
106
|
+
const levels = requested === undefined
|
|
107
|
+
? args.thinkingEnabled
|
|
108
|
+
? enabledLevels.filter((level) => level !== "off")
|
|
109
|
+
: enabledLevels
|
|
110
|
+
: enabledLevels.includes(requested)
|
|
111
|
+
? [requested]
|
|
112
|
+
: [];
|
|
113
|
+
for (const reasoningEffort of levels) {
|
|
114
|
+
if (
|
|
115
|
+
reasoningEffort !== "off" &&
|
|
116
|
+
metadata.api === "anthropic-messages" &&
|
|
117
|
+
forcesToolUse(args.toolChoice)
|
|
118
|
+
) {
|
|
119
|
+
anthropicToolChoiceRejected = true;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
compatible.push({ model, reasoningEffort });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (compatible.length > 0) return { candidates: compatible };
|
|
127
|
+
|
|
128
|
+
if (anthropicToolChoiceRejected) {
|
|
129
|
+
throw new RouterCoreError(
|
|
130
|
+
"invalid_request",
|
|
131
|
+
"Forced tool choice is not supported by any enabled model at the requested reasoning setting.",
|
|
132
|
+
"unsupported_tool_choice",
|
|
133
|
+
"tool_choice",
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (metaToolChoiceRejected) {
|
|
138
|
+
throw new RouterCoreError(
|
|
139
|
+
"invalid_request",
|
|
140
|
+
"Meta models currently support only omitted or 'auto' tool_choice.",
|
|
141
|
+
"unsupported_tool_choice",
|
|
142
|
+
"tool_choice",
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
throw new RouterCoreError(
|
|
147
|
+
"invalid_request",
|
|
148
|
+
requested === undefined
|
|
149
|
+
? args.thinkingEnabled
|
|
150
|
+
? "Thinking is enabled, but no enabled non-off model/thinking-level pair is compatible with this request."
|
|
151
|
+
: "No enabled model/thinking-level pair is compatible with this request."
|
|
152
|
+
: `reasoning_effort '${requested}' is not supported by any enabled model for this router.`,
|
|
153
|
+
"unsupported_reasoning_effort",
|
|
154
|
+
requested === undefined ? undefined : "reasoning_effort",
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function forcesToolUse(choice: unknown): boolean {
|
|
159
|
+
return choice === "required" ||
|
|
160
|
+
choice === "any" ||
|
|
161
|
+
(typeof choice === "object" && choice !== null);
|
|
162
|
+
}
|