cascade-ai 0.72.0 → 0.74.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/dist/cli.cjs +839 -233
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +839 -233
- package/dist/cli.js.map +1 -1
- package/dist/desktop-core.cjs +825 -232
- package/dist/index.cjs +824 -231
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +104 -0
- package/dist/index.d.ts +104 -0
- package/dist/index.js +824 -231
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.cjs
CHANGED
|
@@ -1667,6 +1667,15 @@ function findPricing(modelId, provider, opts = {}) {
|
|
|
1667
1667
|
}
|
|
1668
1668
|
return null;
|
|
1669
1669
|
}
|
|
1670
|
+
function hasContextBands(model) {
|
|
1671
|
+
if (model.isLocal) return false;
|
|
1672
|
+
const fallbackProviders = model.provider === "azure" ? ["openai"] : model.provider === "gemini" ? ["vertex"] : [];
|
|
1673
|
+
const match = findPricing(model.id, model.provider, {
|
|
1674
|
+
baseModelId: model.baseModelId,
|
|
1675
|
+
fallbackProviders
|
|
1676
|
+
});
|
|
1677
|
+
return (match?.entry.tiers?.length ?? 0) > 1;
|
|
1678
|
+
}
|
|
1670
1679
|
function pricingDatasetMeta() {
|
|
1671
1680
|
const { schemaVersion, generatedAt, defaultCurrency } = DATASET;
|
|
1672
1681
|
return { schemaVersion, generatedAt, defaultCurrency };
|
|
@@ -1810,7 +1819,7 @@ var CASCADE_VERSION, CASCADE_CONFIG_FILE, CASCADE_DB_FILE, CASCADE_DASHBOARD_SEC
|
|
|
1810
1819
|
var init_constants = __esm({
|
|
1811
1820
|
"src/constants.ts"() {
|
|
1812
1821
|
init_pricing();
|
|
1813
|
-
CASCADE_VERSION = "0.
|
|
1822
|
+
CASCADE_VERSION = "0.74.0";
|
|
1814
1823
|
CASCADE_CONFIG_FILE = ".cascade/config.json";
|
|
1815
1824
|
CASCADE_DB_FILE = ".cascade/memory.db";
|
|
1816
1825
|
CASCADE_DASHBOARD_SECRET_FILE = ".cascade/dashboard-secret";
|
|
@@ -2241,8 +2250,23 @@ var init_constants = __esm({
|
|
|
2241
2250
|
});
|
|
2242
2251
|
|
|
2243
2252
|
// src/utils/cost.ts
|
|
2244
|
-
function resolveModelPricing(model) {
|
|
2253
|
+
function resolveModelPricing(model, opts = {}) {
|
|
2245
2254
|
if (model.isLocal) return { input: 0, output: 0, unknown: false };
|
|
2255
|
+
if (opts.inputTokens != null && hasContextBands(model)) {
|
|
2256
|
+
const banded = resolvePricing(model, { inputTokens: opts.inputTokens });
|
|
2257
|
+
const base = resolvePricing(model, { inputTokens: 0 });
|
|
2258
|
+
if (!banded.unknown && !base.unknown) {
|
|
2259
|
+
const scale = (stamped, baseRate) => stamped > 0 && baseRate > 0 ? stamped / baseRate : 1;
|
|
2260
|
+
const inScale = scale(model.inputCostPer1kTokens, base.input);
|
|
2261
|
+
const outScale = scale(model.outputCostPer1kTokens, base.output);
|
|
2262
|
+
return {
|
|
2263
|
+
input: banded.input * inScale,
|
|
2264
|
+
output: banded.output * outScale,
|
|
2265
|
+
unknown: false,
|
|
2266
|
+
...banded.estimatedFromProvider ? { estimatedFromProvider: banded.estimatedFromProvider } : {}
|
|
2267
|
+
};
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2246
2270
|
if (model.inputCostPer1kTokens > 0 || model.outputCostPer1kTokens > 0) {
|
|
2247
2271
|
return {
|
|
2248
2272
|
input: model.inputCostPer1kTokens,
|
|
@@ -2250,7 +2274,7 @@ function resolveModelPricing(model) {
|
|
|
2250
2274
|
unknown: false
|
|
2251
2275
|
};
|
|
2252
2276
|
}
|
|
2253
|
-
const fromDataset = resolvePricing(model);
|
|
2277
|
+
const fromDataset = resolvePricing(model, { inputTokens: opts.inputTokens });
|
|
2254
2278
|
if (!fromDataset.unknown) {
|
|
2255
2279
|
return {
|
|
2256
2280
|
input: fromDataset.input,
|
|
@@ -2272,11 +2296,11 @@ function resolveModelPricing(model) {
|
|
|
2272
2296
|
return { input: 0, output: 0, unknown: true };
|
|
2273
2297
|
}
|
|
2274
2298
|
function calculateCost(inputTokens, outputTokens, model) {
|
|
2275
|
-
const { input, output } = resolveModelPricing(model);
|
|
2299
|
+
const { input, output } = resolveModelPricing(model, { inputTokens });
|
|
2276
2300
|
return inputTokens / 1e3 * input + outputTokens / 1e3 * output;
|
|
2277
2301
|
}
|
|
2278
2302
|
function buildTokenUsage(inputTokens, outputTokens, model) {
|
|
2279
|
-
const { input, output, unknown } = resolveModelPricing(model);
|
|
2303
|
+
const { input, output, unknown } = resolveModelPricing(model, { inputTokens });
|
|
2280
2304
|
return {
|
|
2281
2305
|
inputTokens,
|
|
2282
2306
|
outputTokens,
|
|
@@ -2293,10 +2317,17 @@ var init_cost = __esm({
|
|
|
2293
2317
|
});
|
|
2294
2318
|
|
|
2295
2319
|
// src/providers/base.ts
|
|
2296
|
-
var BaseProvider;
|
|
2320
|
+
var ProviderUnreachableError, BaseProvider;
|
|
2297
2321
|
var init_base = __esm({
|
|
2298
2322
|
"src/providers/base.ts"() {
|
|
2299
2323
|
init_cost();
|
|
2324
|
+
ProviderUnreachableError = class extends Error {
|
|
2325
|
+
transient = true;
|
|
2326
|
+
constructor(message) {
|
|
2327
|
+
super(message);
|
|
2328
|
+
this.name = "ProviderUnreachableError";
|
|
2329
|
+
}
|
|
2330
|
+
};
|
|
2300
2331
|
BaseProvider = class {
|
|
2301
2332
|
config;
|
|
2302
2333
|
model;
|
|
@@ -2368,7 +2399,9 @@ var init_model_filter = __esm({
|
|
|
2368
2399
|
// src/providers/anthropic.ts
|
|
2369
2400
|
var anthropic_exports = {};
|
|
2370
2401
|
__export(anthropic_exports, {
|
|
2371
|
-
AnthropicProvider: () => AnthropicProvider
|
|
2402
|
+
AnthropicProvider: () => AnthropicProvider,
|
|
2403
|
+
toAnthropicToolUse: () => toAnthropicToolUse,
|
|
2404
|
+
toAnthropicTools: () => toAnthropicTools
|
|
2372
2405
|
});
|
|
2373
2406
|
function anthropicThinkingParam(modelId, maxTokens) {
|
|
2374
2407
|
if (!/claude-(opus|sonnet)-4/i.test(modelId)) return {};
|
|
@@ -2376,6 +2409,21 @@ function anthropicThinkingParam(modelId, maxTokens) {
|
|
|
2376
2409
|
if (budget < 1024) return {};
|
|
2377
2410
|
return { thinking: { type: "enabled", budget_tokens: budget } };
|
|
2378
2411
|
}
|
|
2412
|
+
function toAnthropicTools(tools) {
|
|
2413
|
+
return tools.map((t) => ({
|
|
2414
|
+
name: t.name,
|
|
2415
|
+
description: t.description,
|
|
2416
|
+
input_schema: t.inputSchema
|
|
2417
|
+
}));
|
|
2418
|
+
}
|
|
2419
|
+
function toAnthropicToolUse(toolCalls) {
|
|
2420
|
+
return toolCalls.map((tc) => ({
|
|
2421
|
+
type: "tool_use",
|
|
2422
|
+
id: tc.id,
|
|
2423
|
+
name: tc.name,
|
|
2424
|
+
input: tc.input
|
|
2425
|
+
}));
|
|
2426
|
+
}
|
|
2379
2427
|
var AnthropicProvider;
|
|
2380
2428
|
var init_anthropic = __esm({
|
|
2381
2429
|
"src/providers/anthropic.ts"() {
|
|
@@ -2404,11 +2452,7 @@ var init_anthropic = __esm({
|
|
|
2404
2452
|
}
|
|
2405
2453
|
async generateStream(options, onChunk) {
|
|
2406
2454
|
const messages = this.convertMessages(options.messages);
|
|
2407
|
-
const tools = options.tools
|
|
2408
|
-
name: t.name,
|
|
2409
|
-
description: t.description,
|
|
2410
|
-
input_schema: t.inputSchema
|
|
2411
|
-
}));
|
|
2455
|
+
const tools = options.tools && toAnthropicTools(options.tools);
|
|
2412
2456
|
let fullContent = "";
|
|
2413
2457
|
let inputTokens = 0;
|
|
2414
2458
|
let outputTokens = 0;
|
|
@@ -2536,14 +2580,7 @@ var init_anthropic = __esm({
|
|
|
2536
2580
|
const content = [];
|
|
2537
2581
|
const text = typeof m.content === "string" ? m.content : "";
|
|
2538
2582
|
if (text) content.push({ type: "text", text });
|
|
2539
|
-
|
|
2540
|
-
content.push({
|
|
2541
|
-
type: "tool_use",
|
|
2542
|
-
id: tc.id,
|
|
2543
|
-
name: tc.name,
|
|
2544
|
-
input: tc.input
|
|
2545
|
-
});
|
|
2546
|
-
}
|
|
2583
|
+
content.push(...toAnthropicToolUse(m.toolCalls ?? []));
|
|
2547
2584
|
if (content.length > 0) {
|
|
2548
2585
|
result.push({ role: "assistant", content });
|
|
2549
2586
|
}
|
|
@@ -2582,7 +2619,9 @@ var openai_exports = {};
|
|
|
2582
2619
|
__export(openai_exports, {
|
|
2583
2620
|
OpenAIProvider: () => OpenAIProvider,
|
|
2584
2621
|
isParamShapeError: () => isParamShapeError,
|
|
2585
|
-
isReasoningModel: () => isReasoningModel
|
|
2622
|
+
isReasoningModel: () => isReasoningModel,
|
|
2623
|
+
toOpenAIToolCalls: () => toOpenAIToolCalls,
|
|
2624
|
+
toOpenAITools: () => toOpenAITools
|
|
2586
2625
|
});
|
|
2587
2626
|
function isReasoningModel(id) {
|
|
2588
2627
|
const m = (id || "").toLowerCase();
|
|
@@ -2593,6 +2632,26 @@ function isParamShapeError(err) {
|
|
|
2593
2632
|
const msg = String(e?.message ?? e?.error?.message ?? "").toLowerCase();
|
|
2594
2633
|
return msg.includes("max_completion_tokens") || msg.includes("max_tokens") && (msg.includes("unsupported") || msg.includes("not supported") || msg.includes("use")) || msg.includes("temperature") && (msg.includes("unsupported") || msg.includes("does not support") || msg.includes("only the default"));
|
|
2595
2634
|
}
|
|
2635
|
+
function toOpenAITools(tools) {
|
|
2636
|
+
return tools.map((t) => ({
|
|
2637
|
+
type: "function",
|
|
2638
|
+
function: {
|
|
2639
|
+
name: t.name,
|
|
2640
|
+
description: t.description,
|
|
2641
|
+
parameters: t.inputSchema
|
|
2642
|
+
}
|
|
2643
|
+
}));
|
|
2644
|
+
}
|
|
2645
|
+
function toOpenAIToolCalls(toolCalls) {
|
|
2646
|
+
return toolCalls.map((tc) => ({
|
|
2647
|
+
id: tc.id,
|
|
2648
|
+
type: "function",
|
|
2649
|
+
function: {
|
|
2650
|
+
name: tc.name,
|
|
2651
|
+
arguments: JSON.stringify(tc.input)
|
|
2652
|
+
}
|
|
2653
|
+
}));
|
|
2654
|
+
}
|
|
2596
2655
|
var OpenAIProvider;
|
|
2597
2656
|
var init_openai = __esm({
|
|
2598
2657
|
"src/providers/openai.ts"() {
|
|
@@ -2619,14 +2678,7 @@ var init_openai = __esm({
|
|
|
2619
2678
|
}
|
|
2620
2679
|
async generateStream(options, onChunk) {
|
|
2621
2680
|
const messages = this.convertMessages(options.messages, options.systemPrompt);
|
|
2622
|
-
const tools = options.tools
|
|
2623
|
-
type: "function",
|
|
2624
|
-
function: {
|
|
2625
|
-
name: t.name,
|
|
2626
|
-
description: t.description,
|
|
2627
|
-
parameters: t.inputSchema
|
|
2628
|
-
}
|
|
2629
|
-
}));
|
|
2681
|
+
const tools = options.tools && toOpenAITools(options.tools);
|
|
2630
2682
|
let fullContent = "";
|
|
2631
2683
|
let inputTokens = 0;
|
|
2632
2684
|
let outputTokens = 0;
|
|
@@ -2772,14 +2824,7 @@ var init_openai = __esm({
|
|
|
2772
2824
|
result.push({
|
|
2773
2825
|
role: "assistant",
|
|
2774
2826
|
content: m.content || "",
|
|
2775
|
-
tool_calls: m.toolCalls
|
|
2776
|
-
id: toolCall.id,
|
|
2777
|
-
type: "function",
|
|
2778
|
-
function: {
|
|
2779
|
-
name: toolCall.name,
|
|
2780
|
-
arguments: JSON.stringify(toolCall.input)
|
|
2781
|
-
}
|
|
2782
|
-
}))
|
|
2827
|
+
tool_calls: toOpenAIToolCalls(m.toolCalls)
|
|
2783
2828
|
});
|
|
2784
2829
|
} else {
|
|
2785
2830
|
result.push({ role: m.role, content: m.content });
|
|
@@ -3046,8 +3091,42 @@ var init_gemini_schema = __esm({
|
|
|
3046
3091
|
// src/providers/gemini.ts
|
|
3047
3092
|
var gemini_exports = {};
|
|
3048
3093
|
__export(gemini_exports, {
|
|
3049
|
-
GeminiProvider: () => GeminiProvider
|
|
3094
|
+
GeminiProvider: () => GeminiProvider,
|
|
3095
|
+
toGeminiFunctionCalls: () => toGeminiFunctionCalls,
|
|
3096
|
+
toGeminiTools: () => toGeminiTools
|
|
3050
3097
|
});
|
|
3098
|
+
function toGeminiTools(tools) {
|
|
3099
|
+
return tools.map((tool) => ({
|
|
3100
|
+
name: tool.name,
|
|
3101
|
+
description: tool.description,
|
|
3102
|
+
// Convert, never cast. Gemini's parameters field is an OpenAPI subset
|
|
3103
|
+
// that 400s on unknown keys, and MCP servers ship schemas carrying their
|
|
3104
|
+
// own extensions (GitHub's adds `x-mcp-header` to every header-bound
|
|
3105
|
+
// property). Casting sent those through verbatim and every request failed
|
|
3106
|
+
// before the model saw it. See providers/gemini-schema.ts.
|
|
3107
|
+
parameters: toGeminiParameters(tool.inputSchema)
|
|
3108
|
+
}));
|
|
3109
|
+
}
|
|
3110
|
+
function toGeminiFunctionCalls(toolCalls) {
|
|
3111
|
+
return toolCalls.map((tc) => ({
|
|
3112
|
+
functionCall: {
|
|
3113
|
+
name: tc.name,
|
|
3114
|
+
args: tc.input
|
|
3115
|
+
}
|
|
3116
|
+
}));
|
|
3117
|
+
}
|
|
3118
|
+
function isGeminiAuthStatus(status) {
|
|
3119
|
+
return status === 400 || status === 401 || status === 403;
|
|
3120
|
+
}
|
|
3121
|
+
async function describeGeminiError(resp) {
|
|
3122
|
+
try {
|
|
3123
|
+
const body = await resp.json();
|
|
3124
|
+
const detail = body?.error?.message ?? body?.error?.status;
|
|
3125
|
+
return detail ? ` \u2014 ${detail}` : "";
|
|
3126
|
+
} catch {
|
|
3127
|
+
return "";
|
|
3128
|
+
}
|
|
3129
|
+
}
|
|
3051
3130
|
var GeminiProvider;
|
|
3052
3131
|
var init_gemini = __esm({
|
|
3053
3132
|
"src/providers/gemini.ts"() {
|
|
@@ -3077,7 +3156,7 @@ var init_gemini = __esm({
|
|
|
3077
3156
|
{ category: genai.HarmCategory.HARM_CATEGORY_HARASSMENT, threshold: genai.HarmBlockThreshold.BLOCK_NONE },
|
|
3078
3157
|
{ category: genai.HarmCategory.HARM_CATEGORY_HATE_SPEECH, threshold: genai.HarmBlockThreshold.BLOCK_NONE }
|
|
3079
3158
|
],
|
|
3080
|
-
tools: options.tools?.length ? [{ functionDeclarations: options.tools
|
|
3159
|
+
tools: options.tools?.length ? [{ functionDeclarations: toGeminiTools(options.tools) }] : void 0,
|
|
3081
3160
|
abortSignal: options.signal
|
|
3082
3161
|
}
|
|
3083
3162
|
});
|
|
@@ -3139,9 +3218,7 @@ var init_gemini = __esm({
|
|
|
3139
3218
|
}
|
|
3140
3219
|
async listModels() {
|
|
3141
3220
|
try {
|
|
3142
|
-
const resp = await
|
|
3143
|
-
`https://generativelanguage.googleapis.com/v1beta/models?key=${this.config.apiKey}`
|
|
3144
|
-
);
|
|
3221
|
+
const resp = await this.fetchModelList();
|
|
3145
3222
|
if (!resp.ok) {
|
|
3146
3223
|
return Object.values(MODELS).filter((m) => m.provider === "gemini");
|
|
3147
3224
|
}
|
|
@@ -3172,18 +3249,61 @@ var init_gemini = __esm({
|
|
|
3172
3249
|
return Object.values(MODELS).filter((m) => m.provider === "gemini");
|
|
3173
3250
|
}
|
|
3174
3251
|
}
|
|
3252
|
+
/**
|
|
3253
|
+
* Is this KEY usable — not "does one particular model answer".
|
|
3254
|
+
*
|
|
3255
|
+
* This used to call countTokens() against `this.model.id`, which the router
|
|
3256
|
+
* fills from the first Gemini entry in the bundled catalogue. That made the
|
|
3257
|
+
* whole provider's fate depend on one hard-coded model id: a key that cannot
|
|
3258
|
+
* reach that one model — retired, not enabled for the key's project, served
|
|
3259
|
+
* on a different API version — failed the probe, and because every later step
|
|
3260
|
+
* is gated on the result (`validateCloudProviderModels` and
|
|
3261
|
+
* `discoverProviderModels` both return early for an unavailable provider) the
|
|
3262
|
+
* real model list was never fetched. Gemini vanished entirely, and the CLI
|
|
3263
|
+
* reported "add a provider API key first" about a key that was present and
|
|
3264
|
+
* working.
|
|
3265
|
+
*
|
|
3266
|
+
* Listing models asks the account-level question instead, the same one
|
|
3267
|
+
* OpenAI's probe asks, and it is the same request `listModels()` already
|
|
3268
|
+
* makes for discovery.
|
|
3269
|
+
*
|
|
3270
|
+
* Throws rather than returning false. The router logs the reason it catches,
|
|
3271
|
+
* and "bad key, wrong endpoint/deployment, or unreachable" — three guesses,
|
|
3272
|
+
* no answer — is what made this take a user report to find. A swallowed probe
|
|
3273
|
+
* error is the same defect that once hid a misconfigured Azure deployment.
|
|
3274
|
+
*/
|
|
3175
3275
|
async isAvailable() {
|
|
3276
|
+
if (!this.config.apiKey) throw new Error("no Gemini API key configured");
|
|
3277
|
+
let resp;
|
|
3176
3278
|
try {
|
|
3177
|
-
await this.
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
}
|
|
3183
|
-
|
|
3279
|
+
resp = await this.fetchModelList();
|
|
3280
|
+
} catch (err) {
|
|
3281
|
+
throw new ProviderUnreachableError(
|
|
3282
|
+
`could not reach the Gemini API: ${err instanceof Error ? err.message : String(err)}`
|
|
3283
|
+
);
|
|
3284
|
+
}
|
|
3285
|
+
if (resp.ok) return true;
|
|
3286
|
+
const status = `HTTP ${resp.status} ${resp.statusText}`.trim();
|
|
3287
|
+
const detail = await describeGeminiError(resp);
|
|
3288
|
+
if (isGeminiAuthStatus(resp.status)) {
|
|
3289
|
+
throw new Error(`Gemini rejected the API key: ${status}${detail}`);
|
|
3184
3290
|
}
|
|
3291
|
+
throw new ProviderUnreachableError(`Gemini model list failed: ${status}${detail}`);
|
|
3185
3292
|
}
|
|
3186
3293
|
// ── Private ──────────────────────────────────
|
|
3294
|
+
/**
|
|
3295
|
+
* The account's model list. One place so the availability probe and
|
|
3296
|
+
* discovery cannot ask different questions of the same endpoint.
|
|
3297
|
+
*
|
|
3298
|
+
* The key goes in a header, not the query string: a URL carries into proxy
|
|
3299
|
+
* logs, error reports and shell history, and this one would carry the key
|
|
3300
|
+
* with it.
|
|
3301
|
+
*/
|
|
3302
|
+
fetchModelList() {
|
|
3303
|
+
return fetch("https://generativelanguage.googleapis.com/v1beta/models", {
|
|
3304
|
+
headers: { "x-goog-api-key": this.config.apiKey ?? "" }
|
|
3305
|
+
});
|
|
3306
|
+
}
|
|
3187
3307
|
buildContents(messages, extraImages) {
|
|
3188
3308
|
const contents = [];
|
|
3189
3309
|
for (const m of messages) {
|
|
@@ -3218,14 +3338,7 @@ var init_gemini = __esm({
|
|
|
3218
3338
|
const parts = [];
|
|
3219
3339
|
const textContent = typeof m.content === "string" ? m.content : "";
|
|
3220
3340
|
if (textContent) parts.push({ text: textContent });
|
|
3221
|
-
|
|
3222
|
-
parts.push({
|
|
3223
|
-
functionCall: {
|
|
3224
|
-
name: tc.name,
|
|
3225
|
-
args: tc.input
|
|
3226
|
-
}
|
|
3227
|
-
});
|
|
3228
|
-
}
|
|
3341
|
+
parts.push(...toGeminiFunctionCalls(m.toolCalls ?? []));
|
|
3229
3342
|
if (parts.length > 0) {
|
|
3230
3343
|
contents.push({ role: "model", parts });
|
|
3231
3344
|
}
|
|
@@ -3272,18 +3385,6 @@ var init_gemini = __esm({
|
|
|
3272
3385
|
}
|
|
3273
3386
|
return parts;
|
|
3274
3387
|
}
|
|
3275
|
-
convertTool(tool) {
|
|
3276
|
-
return {
|
|
3277
|
-
name: tool.name,
|
|
3278
|
-
description: tool.description,
|
|
3279
|
-
// Convert, never cast. Gemini's parameters field is an OpenAPI subset
|
|
3280
|
-
// that 400s on unknown keys, and MCP servers ship schemas carrying their
|
|
3281
|
-
// own extensions (GitHub's adds `x-mcp-header` to every header-bound
|
|
3282
|
-
// property). Casting sent those through verbatim and every request failed
|
|
3283
|
-
// before the model saw it. See providers/gemini-schema.ts.
|
|
3284
|
-
parameters: toGeminiParameters(tool.inputSchema)
|
|
3285
|
-
};
|
|
3286
|
-
}
|
|
3287
3388
|
};
|
|
3288
3389
|
}
|
|
3289
3390
|
});
|
|
@@ -3369,12 +3470,32 @@ var init_net = __esm({
|
|
|
3369
3470
|
// src/providers/ollama.ts
|
|
3370
3471
|
var ollama_exports = {};
|
|
3371
3472
|
__export(ollama_exports, {
|
|
3372
|
-
OllamaProvider: () => OllamaProvider
|
|
3473
|
+
OllamaProvider: () => OllamaProvider,
|
|
3474
|
+
toOllamaToolCalls: () => toOllamaToolCalls,
|
|
3475
|
+
toOllamaTools: () => toOllamaTools
|
|
3373
3476
|
});
|
|
3374
3477
|
function isToolCapable(modelName) {
|
|
3375
3478
|
const name = modelName.toLowerCase();
|
|
3376
3479
|
return TOOL_CAPABLE_FAMILIES.some((family) => name.includes(family));
|
|
3377
3480
|
}
|
|
3481
|
+
function toOllamaTools(tools) {
|
|
3482
|
+
return tools.map((t) => ({
|
|
3483
|
+
type: "function",
|
|
3484
|
+
function: {
|
|
3485
|
+
name: t.name,
|
|
3486
|
+
description: t.description,
|
|
3487
|
+
parameters: t.inputSchema
|
|
3488
|
+
}
|
|
3489
|
+
}));
|
|
3490
|
+
}
|
|
3491
|
+
function toOllamaToolCalls(toolCalls) {
|
|
3492
|
+
return toolCalls.map((tc) => ({
|
|
3493
|
+
function: {
|
|
3494
|
+
name: tc.name,
|
|
3495
|
+
arguments: tc.input
|
|
3496
|
+
}
|
|
3497
|
+
}));
|
|
3498
|
+
}
|
|
3378
3499
|
var TOOL_CAPABLE_FAMILIES, OllamaProvider;
|
|
3379
3500
|
var init_ollama = __esm({
|
|
3380
3501
|
"src/providers/ollama.ts"() {
|
|
@@ -3407,14 +3528,7 @@ var init_ollama = __esm({
|
|
|
3407
3528
|
}
|
|
3408
3529
|
async generateStream(options, onChunk) {
|
|
3409
3530
|
const messages = this.convertMessages(options.messages, options.systemPrompt);
|
|
3410
|
-
const ollamaTools = options.tools
|
|
3411
|
-
type: "function",
|
|
3412
|
-
function: {
|
|
3413
|
-
name: t.name,
|
|
3414
|
-
description: t.description,
|
|
3415
|
-
parameters: t.inputSchema
|
|
3416
|
-
}
|
|
3417
|
-
}));
|
|
3531
|
+
const ollamaTools = options.tools && toOllamaTools(options.tools);
|
|
3418
3532
|
const response = await fetch(`${this.baseUrl}/api/chat`, {
|
|
3419
3533
|
method: "POST",
|
|
3420
3534
|
headers: { "Content-Type": "application/json" },
|
|
@@ -3594,12 +3708,7 @@ var init_ollama = __esm({
|
|
|
3594
3708
|
result.push({
|
|
3595
3709
|
role: "assistant",
|
|
3596
3710
|
content: typeof m.content === "string" ? m.content : "",
|
|
3597
|
-
tool_calls: m.toolCalls
|
|
3598
|
-
function: {
|
|
3599
|
-
name: tc.name,
|
|
3600
|
-
arguments: tc.input
|
|
3601
|
-
}
|
|
3602
|
-
}))
|
|
3711
|
+
tool_calls: toOllamaToolCalls(m.toolCalls)
|
|
3603
3712
|
});
|
|
3604
3713
|
continue;
|
|
3605
3714
|
}
|
|
@@ -6274,6 +6383,7 @@ init_gemini();
|
|
|
6274
6383
|
init_ollama();
|
|
6275
6384
|
init_openai_compatible();
|
|
6276
6385
|
init_openai();
|
|
6386
|
+
init_base();
|
|
6277
6387
|
|
|
6278
6388
|
// src/core/router/selector.ts
|
|
6279
6389
|
init_constants();
|
|
@@ -6593,6 +6703,21 @@ var DEFAULT_PROVIDER_TPM = {
|
|
|
6593
6703
|
"openai-compatible": 3e4,
|
|
6594
6704
|
ollama: Number.POSITIVE_INFINITY
|
|
6595
6705
|
};
|
|
6706
|
+
function abortReason(signal) {
|
|
6707
|
+
return signal.reason instanceof Error ? signal.reason : new Error("Aborted while waiting for rate-limit capacity");
|
|
6708
|
+
}
|
|
6709
|
+
function sleepUnlessAborted(ms, signal) {
|
|
6710
|
+
if (!signal) return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
6711
|
+
return new Promise((resolve2) => {
|
|
6712
|
+
const done = () => {
|
|
6713
|
+
clearTimeout(timer);
|
|
6714
|
+
signal.removeEventListener("abort", done);
|
|
6715
|
+
resolve2();
|
|
6716
|
+
};
|
|
6717
|
+
const timer = setTimeout(done, ms);
|
|
6718
|
+
signal.addEventListener("abort", done, { once: true });
|
|
6719
|
+
});
|
|
6720
|
+
}
|
|
6596
6721
|
var TpmLimiter = class {
|
|
6597
6722
|
buckets = /* @__PURE__ */ new Map();
|
|
6598
6723
|
constructor(overrides = {}) {
|
|
@@ -6611,9 +6736,10 @@ var TpmLimiter = class {
|
|
|
6611
6736
|
* are reported back via `refund` when short, or simply settled at the next
|
|
6612
6737
|
* refill.
|
|
6613
6738
|
*/
|
|
6614
|
-
async acquire(provider, estimatedTokens) {
|
|
6739
|
+
async acquire(provider, estimatedTokens, signal) {
|
|
6615
6740
|
const bucket = this.buckets.get(provider);
|
|
6616
6741
|
if (!bucket || bucket.tokensPerMinute === Number.POSITIVE_INFINITY) return;
|
|
6742
|
+
if (signal?.aborted) throw abortReason(signal);
|
|
6617
6743
|
const want = Math.min(estimatedTokens, bucket.tokensPerMinute);
|
|
6618
6744
|
for (; ; ) {
|
|
6619
6745
|
this.refill(bucket);
|
|
@@ -6623,7 +6749,8 @@ var TpmLimiter = class {
|
|
|
6623
6749
|
}
|
|
6624
6750
|
const deficit = want - bucket.available;
|
|
6625
6751
|
const waitMs = Math.max(50, Math.ceil(deficit / bucket.tokensPerMinute * 6e4));
|
|
6626
|
-
await
|
|
6752
|
+
await sleepUnlessAborted(waitMs, signal);
|
|
6753
|
+
if (signal?.aborted) throw abortReason(signal);
|
|
6627
6754
|
}
|
|
6628
6755
|
}
|
|
6629
6756
|
/**
|
|
@@ -6669,6 +6796,9 @@ var TpmLimiter = class {
|
|
|
6669
6796
|
};
|
|
6670
6797
|
|
|
6671
6798
|
// src/core/router/local-queue.ts
|
|
6799
|
+
function abortReason2(signal) {
|
|
6800
|
+
return signal.reason instanceof Error ? signal.reason : new Error("Aborted while waiting for a local inference slot");
|
|
6801
|
+
}
|
|
6672
6802
|
var LocalRequestQueue = class {
|
|
6673
6803
|
maxConcurrent;
|
|
6674
6804
|
active = 0;
|
|
@@ -6679,9 +6809,16 @@ var LocalRequestQueue = class {
|
|
|
6679
6809
|
/**
|
|
6680
6810
|
* Acquire a queue slot. Returns a `release` function that MUST be called
|
|
6681
6811
|
* when the inference call is done (even on error). Rejects if the slot
|
|
6682
|
-
* cannot be acquired within `timeoutMs
|
|
6812
|
+
* cannot be acquired within `timeoutMs`, or as soon as `signal` fires.
|
|
6813
|
+
*
|
|
6814
|
+
* The signal matters as much as the timeout: with concurrency of 1 a caller
|
|
6815
|
+
* can sit here for half the inference timeout — 150 seconds by default —
|
|
6816
|
+
* and until it was wired up, neither cancelling the run nor spending its
|
|
6817
|
+
* budget got it out. It waited out the window for a slot it would drop the
|
|
6818
|
+
* moment it received one.
|
|
6683
6819
|
*/
|
|
6684
|
-
async acquire(timeoutMs) {
|
|
6820
|
+
async acquire(timeoutMs, signal) {
|
|
6821
|
+
if (signal?.aborted) throw abortReason2(signal);
|
|
6685
6822
|
if (this.active < this.maxConcurrent) {
|
|
6686
6823
|
this.active++;
|
|
6687
6824
|
return this.makeRelease();
|
|
@@ -6689,23 +6826,36 @@ var LocalRequestQueue = class {
|
|
|
6689
6826
|
return new Promise((resolve2, reject) => {
|
|
6690
6827
|
let settled = false;
|
|
6691
6828
|
let timer;
|
|
6829
|
+
const cleanup = () => {
|
|
6830
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
6831
|
+
signal?.removeEventListener("abort", onAbort);
|
|
6832
|
+
};
|
|
6692
6833
|
const resolver = (release) => {
|
|
6693
6834
|
if (settled) return;
|
|
6694
6835
|
settled = true;
|
|
6695
|
-
|
|
6836
|
+
cleanup();
|
|
6696
6837
|
resolve2(release);
|
|
6697
6838
|
};
|
|
6839
|
+
const abandon = () => {
|
|
6840
|
+
if (settled) return false;
|
|
6841
|
+
settled = true;
|
|
6842
|
+
cleanup();
|
|
6843
|
+
const idx = this.queue.indexOf(resolver);
|
|
6844
|
+
if (idx !== -1) this.queue.splice(idx, 1);
|
|
6845
|
+
return true;
|
|
6846
|
+
};
|
|
6847
|
+
function onAbort() {
|
|
6848
|
+
if (abandon()) reject(abortReason2(signal));
|
|
6849
|
+
}
|
|
6698
6850
|
if (timeoutMs !== void 0 && timeoutMs > 0) {
|
|
6699
6851
|
timer = setTimeout(() => {
|
|
6700
|
-
if (
|
|
6701
|
-
settled = true;
|
|
6702
|
-
const idx = this.queue.indexOf(resolver);
|
|
6703
|
-
if (idx !== -1) this.queue.splice(idx, 1);
|
|
6852
|
+
if (!abandon()) return;
|
|
6704
6853
|
reject(new Error(
|
|
6705
6854
|
`Local model queue: timed out waiting for a free slot after ${timeoutMs}ms. Active: ${this.active}, Queued: ${this.queue.length}. Consider increasing localConcurrency or localInferenceTimeoutMs in your config.`
|
|
6706
6855
|
));
|
|
6707
6856
|
}, timeoutMs);
|
|
6708
6857
|
}
|
|
6858
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
6709
6859
|
this.queue.push(resolver);
|
|
6710
6860
|
});
|
|
6711
6861
|
}
|
|
@@ -6846,6 +6996,116 @@ function fileDeadModelPersistence(filePath) {
|
|
|
6846
6996
|
init_constants();
|
|
6847
6997
|
init_cost();
|
|
6848
6998
|
|
|
6999
|
+
// src/core/context/compaction.ts
|
|
7000
|
+
var CHARS_PER_TOKEN = 4;
|
|
7001
|
+
function estimateTokens(text) {
|
|
7002
|
+
if (!text) return 0;
|
|
7003
|
+
return Math.max(1, Math.ceil(text.length / CHARS_PER_TOKEN));
|
|
7004
|
+
}
|
|
7005
|
+
function contentToText(content) {
|
|
7006
|
+
if (typeof content === "string") return content;
|
|
7007
|
+
return content.map((c) => c.type === "text" ? c.text : c.type === "tool_result" ? c.content : `[${c.type}]`).join(" ");
|
|
7008
|
+
}
|
|
7009
|
+
function messagesTokens(messages) {
|
|
7010
|
+
return messages.reduce((n, m) => n + estimateTokens(contentToText(m.content)), 0);
|
|
7011
|
+
}
|
|
7012
|
+
function atomize(text, maxChars) {
|
|
7013
|
+
const seps = ["\n\n", "\n", ". ", " "];
|
|
7014
|
+
let units = [text];
|
|
7015
|
+
for (const sep of seps) {
|
|
7016
|
+
if (units.every((u) => u.length <= maxChars)) break;
|
|
7017
|
+
const next = [];
|
|
7018
|
+
for (const u of units) {
|
|
7019
|
+
if (u.length <= maxChars) {
|
|
7020
|
+
next.push(u);
|
|
7021
|
+
continue;
|
|
7022
|
+
}
|
|
7023
|
+
const parts = u.split(sep);
|
|
7024
|
+
parts.forEach((p, i) => next.push(i < parts.length - 1 ? p + sep : p));
|
|
7025
|
+
}
|
|
7026
|
+
units = next;
|
|
7027
|
+
}
|
|
7028
|
+
const out = [];
|
|
7029
|
+
for (const u of units) {
|
|
7030
|
+
if (u.length <= maxChars) out.push(u);
|
|
7031
|
+
else for (let i = 0; i < u.length; i += maxChars) out.push(u.slice(i, i + maxChars));
|
|
7032
|
+
}
|
|
7033
|
+
return out;
|
|
7034
|
+
}
|
|
7035
|
+
function chunkText(text, opts) {
|
|
7036
|
+
const maxChars = Math.max(1, Math.floor(opts.maxTokens * 4));
|
|
7037
|
+
const overlap = Math.min(maxChars - 1, Math.max(0, Math.floor(maxChars * (opts.overlapRatio ?? 0.1))));
|
|
7038
|
+
const trimmed = text.trim();
|
|
7039
|
+
if (!trimmed) return [];
|
|
7040
|
+
if (trimmed.length <= maxChars) return [trimmed];
|
|
7041
|
+
const units = atomize(trimmed, maxChars);
|
|
7042
|
+
const chunks = [];
|
|
7043
|
+
let cur = "";
|
|
7044
|
+
for (const u of units) {
|
|
7045
|
+
if (cur && cur.length + u.length > maxChars) {
|
|
7046
|
+
chunks.push(cur.trim());
|
|
7047
|
+
cur = overlap > 0 ? cur.slice(-overlap) : "";
|
|
7048
|
+
}
|
|
7049
|
+
cur += u;
|
|
7050
|
+
}
|
|
7051
|
+
if (cur.trim()) chunks.push(cur.trim());
|
|
7052
|
+
return chunks;
|
|
7053
|
+
}
|
|
7054
|
+
function needsCompaction(totalTokens, contextWindow, reserve = 0.2) {
|
|
7055
|
+
if (!contextWindow || contextWindow <= 0) return false;
|
|
7056
|
+
return totalTokens > contextWindow * (1 - reserve);
|
|
7057
|
+
}
|
|
7058
|
+
async function mapReduceCompact(text, opts) {
|
|
7059
|
+
const capChars = opts.capTokens * 4;
|
|
7060
|
+
let working = text;
|
|
7061
|
+
let truncated = false;
|
|
7062
|
+
if (working.length > capChars) {
|
|
7063
|
+
working = working.slice(0, capChars);
|
|
7064
|
+
truncated = true;
|
|
7065
|
+
}
|
|
7066
|
+
const instruction = opts.instruction ?? "Summarize this content, preserving key facts, names, numbers, and decisions.";
|
|
7067
|
+
let chunks = chunkText(working, { maxTokens: opts.chunkTokens });
|
|
7068
|
+
if (chunks.length <= 1) return { text: working, chunks: chunks.length, calls: 0, truncated };
|
|
7069
|
+
let calls = 0;
|
|
7070
|
+
let parts = await Promise.all(
|
|
7071
|
+
chunks.map((c, i) => {
|
|
7072
|
+
calls++;
|
|
7073
|
+
return opts.summarize(c, `${instruction} (part ${i + 1} of ${chunks.length})`);
|
|
7074
|
+
})
|
|
7075
|
+
);
|
|
7076
|
+
let combined = parts.join("\n\n");
|
|
7077
|
+
const maxPasses = opts.maxReducePasses ?? 3;
|
|
7078
|
+
for (let pass = 0; pass < maxPasses && estimateTokens(combined) > opts.targetTokens; pass++) {
|
|
7079
|
+
const groups = chunkText(combined, { maxTokens: opts.chunkTokens });
|
|
7080
|
+
if (groups.length <= 1) break;
|
|
7081
|
+
parts = await Promise.all(
|
|
7082
|
+
groups.map((g) => {
|
|
7083
|
+
calls++;
|
|
7084
|
+
return opts.summarize(g, "Merge these partial summaries into one cohesive summary; keep all distinct facts.");
|
|
7085
|
+
})
|
|
7086
|
+
);
|
|
7087
|
+
combined = parts.join("\n\n");
|
|
7088
|
+
}
|
|
7089
|
+
const note = truncated ? "\n\n[Note: input exceeded the extended-context cap and was truncated.]" : "";
|
|
7090
|
+
return { text: combined + note, chunks: chunks.length, calls, truncated };
|
|
7091
|
+
}
|
|
7092
|
+
async function rollingSummary(messages, opts) {
|
|
7093
|
+
if (messages.length <= opts.keepRecent) return messages;
|
|
7094
|
+
const older = messages.slice(0, messages.length - opts.keepRecent);
|
|
7095
|
+
const recent = messages.slice(messages.length - opts.keepRecent);
|
|
7096
|
+
const transcript = older.map((m) => `${m.role}: ${contentToText(m.content)}`).join("\n");
|
|
7097
|
+
const summary = await opts.summarize(
|
|
7098
|
+
transcript,
|
|
7099
|
+
"Summarize this earlier conversation. Preserve facts, decisions, names, numbers, and any open threads or todos."
|
|
7100
|
+
);
|
|
7101
|
+
const summaryMessage = {
|
|
7102
|
+
role: "system",
|
|
7103
|
+
content: `Summary of earlier conversation (folded to fit the context window):
|
|
7104
|
+
${summary}`
|
|
7105
|
+
};
|
|
7106
|
+
return [summaryMessage, ...recent];
|
|
7107
|
+
}
|
|
7108
|
+
|
|
6849
7109
|
// src/utils/retry.ts
|
|
6850
7110
|
var CascadeCancelledError = class extends Error {
|
|
6851
7111
|
constructor(reason) {
|
|
@@ -6867,6 +7127,162 @@ async function withTimeout(promise, timeoutMs, errorMessage = "Operation timed o
|
|
|
6867
7127
|
if (timer !== void 0) clearTimeout(timer);
|
|
6868
7128
|
}
|
|
6869
7129
|
}
|
|
7130
|
+
async function withTimeoutAbort(run, timeoutMs, errorMessage = "Operation timed out", outer) {
|
|
7131
|
+
const outers = (Array.isArray(outer) ? outer : [outer]).filter((sig) => sig !== void 0);
|
|
7132
|
+
const controller = new AbortController();
|
|
7133
|
+
let timer;
|
|
7134
|
+
let rejectRace;
|
|
7135
|
+
const settle = (reason) => {
|
|
7136
|
+
rejectRace?.(reason);
|
|
7137
|
+
controller.abort(reason);
|
|
7138
|
+
};
|
|
7139
|
+
const abortOuter = () => {
|
|
7140
|
+
const fired = outers.find((sig) => sig.aborted);
|
|
7141
|
+
settle(fired?.reason instanceof Error ? fired.reason : new CascadeCancelledError("Run cancelled"));
|
|
7142
|
+
};
|
|
7143
|
+
const failPromise = new Promise((_, reject) => {
|
|
7144
|
+
rejectRace = reject;
|
|
7145
|
+
timer = setTimeout(() => settle(new Error(errorMessage)), timeoutMs);
|
|
7146
|
+
});
|
|
7147
|
+
try {
|
|
7148
|
+
if (outers.some((sig) => sig.aborted)) {
|
|
7149
|
+
abortOuter();
|
|
7150
|
+
return await failPromise;
|
|
7151
|
+
}
|
|
7152
|
+
for (const sig of outers) sig.addEventListener("abort", abortOuter, { once: true });
|
|
7153
|
+
return await Promise.race([run(controller.signal), failPromise]);
|
|
7154
|
+
} finally {
|
|
7155
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
7156
|
+
for (const sig of outers) sig.removeEventListener("abort", abortOuter);
|
|
7157
|
+
}
|
|
7158
|
+
}
|
|
7159
|
+
function anySignal(sources) {
|
|
7160
|
+
const live = sources.filter((s) => s !== void 0);
|
|
7161
|
+
if (live.length === 0) return { release: () => {
|
|
7162
|
+
} };
|
|
7163
|
+
if (live.length === 1) return { signal: live[0], release: () => {
|
|
7164
|
+
} };
|
|
7165
|
+
const controller = new AbortController();
|
|
7166
|
+
function detach() {
|
|
7167
|
+
for (const s of live) s.removeEventListener("abort", onAbort);
|
|
7168
|
+
}
|
|
7169
|
+
function onAbort() {
|
|
7170
|
+
const fired = live.find((s) => s.aborted);
|
|
7171
|
+
controller.abort(fired?.reason instanceof Error ? fired.reason : new CascadeCancelledError("Run cancelled"));
|
|
7172
|
+
detach();
|
|
7173
|
+
}
|
|
7174
|
+
if (live.some((s) => s.aborted)) {
|
|
7175
|
+
onAbort();
|
|
7176
|
+
return { signal: controller.signal, release: detach };
|
|
7177
|
+
}
|
|
7178
|
+
for (const s of live) s.addEventListener("abort", onAbort, { once: true });
|
|
7179
|
+
return { signal: controller.signal, release: detach };
|
|
7180
|
+
}
|
|
7181
|
+
|
|
7182
|
+
// src/core/router/wire-profile.ts
|
|
7183
|
+
init_anthropic();
|
|
7184
|
+
init_gemini();
|
|
7185
|
+
init_ollama();
|
|
7186
|
+
init_openai();
|
|
7187
|
+
var textAndImageOnly = (block) => block.type === "text" || block.type === "image";
|
|
7188
|
+
var ANTHROPIC = {
|
|
7189
|
+
// convertMessages: `if (m.role === 'system') continue` — history system
|
|
7190
|
+
// messages are skipped outright; only options.systemPrompt goes as system
|
|
7191
|
+
// input. Compaction emits exactly such a message, holding a summary of the
|
|
7192
|
+
// whole conversation, so charging it refused runs over input never sent.
|
|
7193
|
+
dropsMessage: (m) => m.role === "system",
|
|
7194
|
+
blockHandling: (m) => {
|
|
7195
|
+
if (m.role === "tool") return "stringified";
|
|
7196
|
+
if (m.role === "assistant") return "dropped";
|
|
7197
|
+
return "blocks";
|
|
7198
|
+
},
|
|
7199
|
+
sendsToolCalls: (m) => m.role === "assistant",
|
|
7200
|
+
sendsBlock: textAndImageOnly,
|
|
7201
|
+
sendsToolCallId: true,
|
|
7202
|
+
sizeToolCalls: toAnthropicToolUse,
|
|
7203
|
+
sendsUrlImages: true,
|
|
7204
|
+
readsTopLevelImages: false,
|
|
7205
|
+
sizeTools: toAnthropicTools
|
|
7206
|
+
};
|
|
7207
|
+
var GEMINI = {
|
|
7208
|
+
// buildContents folds a system message into the next user turn, but only
|
|
7209
|
+
// when it has text: array content collapses to '' and the message is
|
|
7210
|
+
// skipped, as is a whitespace-only string.
|
|
7211
|
+
dropsMessage: (m) => m.role === "system" && !(typeof m.content === "string" ? m.content : "").trim(),
|
|
7212
|
+
blockHandling: (m) => {
|
|
7213
|
+
if (m.role === "tool") return "stringified";
|
|
7214
|
+
if (m.role === "assistant" || m.role === "system") return "dropped";
|
|
7215
|
+
return "blocks";
|
|
7216
|
+
},
|
|
7217
|
+
sendsToolCalls: (m) => m.role === "assistant",
|
|
7218
|
+
sendsBlock: textAndImageOnly,
|
|
7219
|
+
sendsToolCallId: true,
|
|
7220
|
+
sizeToolCalls: toGeminiFunctionCalls,
|
|
7221
|
+
sendsUrlImages: false,
|
|
7222
|
+
readsTopLevelImages: true,
|
|
7223
|
+
sizeTools: toGeminiTools
|
|
7224
|
+
};
|
|
7225
|
+
var OPENAI = {
|
|
7226
|
+
dropsMessage: () => false,
|
|
7227
|
+
blockHandling: (m) => {
|
|
7228
|
+
if (m.role === "system" || m.role === "tool") return "dropped";
|
|
7229
|
+
return "blocks";
|
|
7230
|
+
},
|
|
7231
|
+
// The tool_calls branch is inside `if (typeof m.content === 'string')`; an
|
|
7232
|
+
// assistant turn with array content falls through to the parts branch and
|
|
7233
|
+
// its tool calls are never attached.
|
|
7234
|
+
sendsToolCalls: (m) => m.role === "assistant" && typeof m.content === "string",
|
|
7235
|
+
sendsBlock: textAndImageOnly,
|
|
7236
|
+
sendsToolCallId: true,
|
|
7237
|
+
sizeToolCalls: toOpenAIToolCalls,
|
|
7238
|
+
sendsUrlImages: true,
|
|
7239
|
+
readsTopLevelImages: false,
|
|
7240
|
+
sizeTools: toOpenAITools
|
|
7241
|
+
};
|
|
7242
|
+
var OLLAMA = {
|
|
7243
|
+
dropsMessage: () => false,
|
|
7244
|
+
blockHandling: (m) => {
|
|
7245
|
+
if (m.role === "system") return "dropped";
|
|
7246
|
+
if (m.role === "tool") return "stringified";
|
|
7247
|
+
if (m.role === "assistant" && m.toolCalls?.length) return "dropped";
|
|
7248
|
+
return "blocks";
|
|
7249
|
+
},
|
|
7250
|
+
sendsToolCalls: (m) => m.role === "assistant",
|
|
7251
|
+
// The image split maps `b.image.data` with no check on `type`, so a URL is
|
|
7252
|
+
// submitted too (as a URL string in the `images` array).
|
|
7253
|
+
sendsBlock: textAndImageOnly,
|
|
7254
|
+
sendsToolCallId: false,
|
|
7255
|
+
sizeToolCalls: toOllamaToolCalls,
|
|
7256
|
+
sendsUrlImages: true,
|
|
7257
|
+
readsTopLevelImages: false,
|
|
7258
|
+
sizeTools: toOllamaTools
|
|
7259
|
+
};
|
|
7260
|
+
var PROFILES = {
|
|
7261
|
+
anthropic: ANTHROPIC,
|
|
7262
|
+
gemini: GEMINI,
|
|
7263
|
+
openai: OPENAI,
|
|
7264
|
+
// AzureOpenAIProvider and OpenAICompatibleProvider both extend OpenAIProvider
|
|
7265
|
+
// and neither overrides convertMessages.
|
|
7266
|
+
azure: OPENAI,
|
|
7267
|
+
"openai-compatible": OPENAI,
|
|
7268
|
+
ollama: OLLAMA
|
|
7269
|
+
};
|
|
7270
|
+
function wireProfile(provider) {
|
|
7271
|
+
return PROFILES[provider] ?? OPENAI;
|
|
7272
|
+
}
|
|
7273
|
+
function geminiImageCopies(messages) {
|
|
7274
|
+
for (const m of messages) {
|
|
7275
|
+
if (m.role === "user") {
|
|
7276
|
+
const laterUserTurn = messages.slice(messages.indexOf(m) + 1).some((x) => x.role === "user");
|
|
7277
|
+
return laterUserTurn ? 2 : 1;
|
|
7278
|
+
}
|
|
7279
|
+
const text = typeof m.content === "string" ? m.content : "";
|
|
7280
|
+
if (m.role === "system" && text.trim()) return 1;
|
|
7281
|
+
if (m.role === "tool") return 1;
|
|
7282
|
+
if (m.role === "assistant" && (text || m.toolCalls?.length)) return 1;
|
|
7283
|
+
}
|
|
7284
|
+
return 1;
|
|
7285
|
+
}
|
|
6870
7286
|
|
|
6871
7287
|
// src/core/router/model-profiler.ts
|
|
6872
7288
|
var SKIP_PATTERN = /embed|dall-e|whisper|tts|vision|instruct-vision|rerank/i;
|
|
@@ -7376,6 +7792,36 @@ function azureModelForTier(tier, ranked) {
|
|
|
7376
7792
|
if (tier === "T3") return ranked[ranked.length - 1];
|
|
7377
7793
|
return ranked[Math.min(1, ranked.length - 1)];
|
|
7378
7794
|
}
|
|
7795
|
+
var IMAGE_TOKENS_MAX = 2e3;
|
|
7796
|
+
var IMAGE_TOKENS_EACH = IMAGE_TOKENS_MAX;
|
|
7797
|
+
function safeJson(value) {
|
|
7798
|
+
try {
|
|
7799
|
+
return JSON.stringify(value) ?? "";
|
|
7800
|
+
} catch {
|
|
7801
|
+
return "";
|
|
7802
|
+
}
|
|
7803
|
+
}
|
|
7804
|
+
function newRunAbort() {
|
|
7805
|
+
const controller = new AbortController();
|
|
7806
|
+
EventEmitter.setMaxListeners(64, controller.signal);
|
|
7807
|
+
return controller;
|
|
7808
|
+
}
|
|
7809
|
+
var TOKENS_PER_MESSAGE_FRAMING = 4;
|
|
7810
|
+
function guardTokens(text) {
|
|
7811
|
+
if (!text) return 0;
|
|
7812
|
+
let asciiChars = 0;
|
|
7813
|
+
let denseBytes = 0;
|
|
7814
|
+
for (const ch of text) {
|
|
7815
|
+
const cp = ch.codePointAt(0);
|
|
7816
|
+
if (cp <= 127) {
|
|
7817
|
+
asciiChars++;
|
|
7818
|
+
continue;
|
|
7819
|
+
}
|
|
7820
|
+
denseBytes += cp <= 2047 ? 2 : cp <= 65535 ? 3 : 4;
|
|
7821
|
+
}
|
|
7822
|
+
if (denseBytes === 0) return estimateTokens(text);
|
|
7823
|
+
return Math.ceil(asciiChars / CHARS_PER_TOKEN) + denseBytes;
|
|
7824
|
+
}
|
|
7379
7825
|
var DISCOVERY_TTL_MS = 15 * 60 * 1e3;
|
|
7380
7826
|
var DISCOVERY_TIMEOUT_MS = 4e3;
|
|
7381
7827
|
var cloudDiscoveryCache = /* @__PURE__ */ new Map();
|
|
@@ -7428,6 +7874,33 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
7428
7874
|
// start of every `cascade run`, independent of the session-wide budget.
|
|
7429
7875
|
runTokens = 0;
|
|
7430
7876
|
runCostUsd = 0;
|
|
7877
|
+
/**
|
|
7878
|
+
* Budget held for calls that are IN FLIGHT — admitted by the preflight check
|
|
7879
|
+
* but not yet counted by recordStats. Without these, concurrent calls all
|
|
7880
|
+
* measure themselves against the same unspent allowance and are all admitted.
|
|
7881
|
+
* Released when each call settles; reset by beginRun() alongside the totals.
|
|
7882
|
+
*/
|
|
7883
|
+
reservedTokens = 0;
|
|
7884
|
+
reservedCostUsd = 0;
|
|
7885
|
+
/**
|
|
7886
|
+
* Fires when this run's budget is spent, cancelling work already in flight.
|
|
7887
|
+
*
|
|
7888
|
+
* The kill switch was previously only a flag, which stops calls that have
|
|
7889
|
+
* not yet submitted. A parallel wave's earlier members are already at the
|
|
7890
|
+
* provider by then, and they carried on generating for a run whose output
|
|
7891
|
+
* would be discarded. Every provider call chains to this, so tripping the
|
|
7892
|
+
* ceiling reaches the requests that are running, not just the ones queued.
|
|
7893
|
+
*/
|
|
7894
|
+
runAbort = newRunAbort();
|
|
7895
|
+
/**
|
|
7896
|
+
* Bumped by every beginRun(). A release handle remembers the generation it
|
|
7897
|
+
* was issued in and does nothing if that has moved on: beginRun() zeroes the
|
|
7898
|
+
* counters, so a call still in flight from the previous run would otherwise
|
|
7899
|
+
* subtract its estimate from the NEW run's totals and drive them negative —
|
|
7900
|
+
* leaving later preflights believing there is more allowance than the cap
|
|
7901
|
+
* actually holds, which is worse than no gate at all.
|
|
7902
|
+
*/
|
|
7903
|
+
runGeneration = 0;
|
|
7431
7904
|
runBudgetExceeded = false;
|
|
7432
7905
|
runBudgetExceededReason;
|
|
7433
7906
|
/**
|
|
@@ -7597,7 +8070,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
7597
8070
|
try {
|
|
7598
8071
|
const cfg = this.config.providers.find((p) => p.type === model.provider) ?? { type: model.provider };
|
|
7599
8072
|
const provider = this.createProvider(cfg, model);
|
|
7600
|
-
const result = await
|
|
8073
|
+
const result = await withTimeoutAbort((signal) => provider.generate({
|
|
7601
8074
|
messages: [{ role: "user", content: "Call the echo tool with text set to 'ping'. Use the tool; do not answer in prose." }],
|
|
7602
8075
|
tools: [{
|
|
7603
8076
|
name: "echo",
|
|
@@ -7605,7 +8078,8 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
7605
8078
|
inputSchema: { type: "object", properties: { text: { type: "string", description: "Text to echo" } }, required: ["text"] }
|
|
7606
8079
|
}],
|
|
7607
8080
|
maxTokens: 80,
|
|
7608
|
-
temperature: 0
|
|
8081
|
+
temperature: 0,
|
|
8082
|
+
signal
|
|
7609
8083
|
}), 3e4, "tool-support probe timed out");
|
|
7610
8084
|
return (result.toolCalls?.length ?? 0) > 0;
|
|
7611
8085
|
} catch {
|
|
@@ -7726,9 +8200,12 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
7726
8200
|
this.runBudgetExceededReason ?? "Per-task budget exceeded."
|
|
7727
8201
|
);
|
|
7728
8202
|
}
|
|
8203
|
+
const runGeneration = this.runGeneration;
|
|
8204
|
+
const runAbort = this.runAbort;
|
|
8205
|
+
const runSignal = this.runSignal;
|
|
7729
8206
|
options = applyTierLimits(options, tier, this.config?.tierLimits);
|
|
7730
|
-
if (
|
|
7731
|
-
options = { ...options, signal:
|
|
8207
|
+
if (runSignal && !options.signal) {
|
|
8208
|
+
options = { ...options, signal: runSignal };
|
|
7732
8209
|
}
|
|
7733
8210
|
if (options.model && !requireVision) {
|
|
7734
8211
|
this.ensureProvider(options.model, this.config.providers);
|
|
@@ -7750,58 +8227,84 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
7750
8227
|
if (!model) throw new Error(`No model available for tier ${tier}`);
|
|
7751
8228
|
const provider = this.getProvider(model);
|
|
7752
8229
|
if (!provider) throw new Error(`No provider for model ${model.id}`);
|
|
8230
|
+
let releaseReservation = this.enforcePreflightBudget(model, options);
|
|
7753
8231
|
const requestedTokens = options.maxTokens ?? model.maxOutputTokens ?? 1024;
|
|
7754
8232
|
const estimatedTokens = Math.min(requestedTokens, model.maxOutputTokens ?? requestedTokens) + 512;
|
|
7755
8233
|
if (this.tpmLimiter) {
|
|
7756
|
-
|
|
8234
|
+
const wait = anySignal([options.signal, runAbort.signal]);
|
|
8235
|
+
try {
|
|
8236
|
+
await this.tpmLimiter.acquire(model.provider, estimatedTokens, wait.signal);
|
|
8237
|
+
} catch (err) {
|
|
8238
|
+
releaseReservation?.();
|
|
8239
|
+
throw this.asAbortFailure(err, options.signal, runSignal);
|
|
8240
|
+
} finally {
|
|
8241
|
+
wait.release();
|
|
8242
|
+
}
|
|
7757
8243
|
}
|
|
7758
8244
|
const useStream = Boolean(onChunk) && model.supportsStreaming && typeof provider.generateStream === "function";
|
|
7759
8245
|
let releaseLocalSlot;
|
|
7760
8246
|
if (model.isLocal) {
|
|
7761
8247
|
const inferenceTimeoutMs = this.config.localInferenceTimeoutMs ?? 3e5;
|
|
7762
8248
|
const queueWaitMs = Math.round(inferenceTimeoutMs / 2);
|
|
7763
|
-
|
|
8249
|
+
const wait = anySignal([options.signal, runAbort.signal]);
|
|
8250
|
+
try {
|
|
8251
|
+
releaseLocalSlot = await this.localQueue.acquire(queueWaitMs, wait.signal);
|
|
8252
|
+
} catch (err) {
|
|
8253
|
+
releaseReservation?.();
|
|
8254
|
+
throw this.asAbortFailure(err, options.signal, runSignal);
|
|
8255
|
+
} finally {
|
|
8256
|
+
wait.release();
|
|
8257
|
+
}
|
|
7764
8258
|
}
|
|
7765
8259
|
try {
|
|
8260
|
+
if (runGeneration === this.runGeneration && this.runBudgetExceeded) {
|
|
8261
|
+
this.tpmLimiter?.refund(model.provider, estimatedTokens);
|
|
8262
|
+
throw new _CascadeRouter.BudgetExceededError(
|
|
8263
|
+
this.runBudgetExceededReason ?? "Per-task budget exceeded."
|
|
8264
|
+
);
|
|
8265
|
+
}
|
|
7766
8266
|
let result;
|
|
7767
8267
|
if (model.isLocal) {
|
|
7768
8268
|
const inferenceTimeoutMs = this.config.localInferenceTimeoutMs ?? 3e5;
|
|
7769
|
-
|
|
7770
|
-
|
|
7771
|
-
|
|
7772
|
-
|
|
7773
|
-
|
|
7774
|
-
inferencePromise,
|
|
8269
|
+
result = await withTimeoutAbort(
|
|
8270
|
+
(signal) => useStream && onChunk ? provider.generateStream({ ...options, signal }, (chunk) => {
|
|
8271
|
+
const text = typeof chunk?.text === "string" ? chunk.text : "";
|
|
8272
|
+
if (text) onChunk({ ...chunk, text });
|
|
8273
|
+
}) : provider.generate({ ...options, signal }),
|
|
7775
8274
|
inferenceTimeoutMs,
|
|
7776
|
-
`Local model ${model.id} inference timed out after ${inferenceTimeoutMs}ms
|
|
8275
|
+
`Local model ${model.id} inference timed out after ${inferenceTimeoutMs}ms`,
|
|
8276
|
+
[options.signal, runAbort.signal]
|
|
7777
8277
|
);
|
|
7778
8278
|
} else if (useStream && onChunk) {
|
|
7779
8279
|
const cloudTimeoutMs = this.config.cloudInferenceTimeoutMs ?? 12e4;
|
|
7780
8280
|
try {
|
|
7781
|
-
result = await
|
|
7782
|
-
provider.generateStream(options, (chunk) => {
|
|
8281
|
+
result = await withTimeoutAbort(
|
|
8282
|
+
(signal) => provider.generateStream({ ...options, signal }, (chunk) => {
|
|
7783
8283
|
const text = typeof chunk?.text === "string" ? chunk.text : "";
|
|
7784
8284
|
if (text) onChunk({ ...chunk, text });
|
|
7785
8285
|
}),
|
|
7786
8286
|
cloudTimeoutMs,
|
|
7787
|
-
`Model ${model.id} stream timed out after ${cloudTimeoutMs}ms
|
|
8287
|
+
`Model ${model.id} stream timed out after ${cloudTimeoutMs}ms`,
|
|
8288
|
+
[options.signal, runAbort.signal]
|
|
7788
8289
|
);
|
|
7789
8290
|
} catch (streamErr) {
|
|
7790
|
-
if (streamErr instanceof Error && streamErr.name === "AbortError" ||
|
|
8291
|
+
if (streamErr instanceof Error && streamErr.name === "AbortError" || runSignal?.aborted || options.signal?.aborted) {
|
|
7791
8292
|
throw streamErr;
|
|
7792
8293
|
}
|
|
7793
|
-
result = await
|
|
7794
|
-
provider.generate(options),
|
|
8294
|
+
result = await withTimeoutAbort(
|
|
8295
|
+
(signal) => provider.generate({ ...options, signal }),
|
|
7795
8296
|
cloudTimeoutMs,
|
|
7796
|
-
`Model ${model.id} inference timed out after ${cloudTimeoutMs}ms
|
|
8297
|
+
`Model ${model.id} inference timed out after ${cloudTimeoutMs}ms`,
|
|
8298
|
+
[options.signal, runAbort.signal]
|
|
7797
8299
|
);
|
|
7798
8300
|
}
|
|
7799
8301
|
} else {
|
|
7800
8302
|
const cloudTimeoutMs = this.config.cloudInferenceTimeoutMs ?? 12e4;
|
|
7801
|
-
result = await
|
|
7802
|
-
provider.generate(options),
|
|
8303
|
+
result = await withTimeoutAbort(
|
|
8304
|
+
(signal) => provider.generate({ ...options, signal }),
|
|
7803
8305
|
cloudTimeoutMs,
|
|
7804
|
-
`Model ${model.id} inference timed out after ${cloudTimeoutMs}ms
|
|
8306
|
+
`Model ${model.id} inference timed out after ${cloudTimeoutMs}ms`,
|
|
8307
|
+
[options.signal, runAbort.signal]
|
|
7805
8308
|
);
|
|
7806
8309
|
}
|
|
7807
8310
|
const corrected = buildTokenUsage(
|
|
@@ -7820,11 +8323,12 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
7820
8323
|
if (!result || typeof result.content !== "string" || !result.usage) {
|
|
7821
8324
|
throw new Error(`Provider ${model.provider}:${model.id} returned an invalid generation result.`);
|
|
7822
8325
|
}
|
|
7823
|
-
this.recordStats(tier, model, result.usage, options.featureTag);
|
|
8326
|
+
this.recordStats(tier, model, result.usage, options.featureTag, runGeneration);
|
|
7824
8327
|
this.failover.recordSuccess(model.provider);
|
|
7825
8328
|
return result;
|
|
7826
8329
|
} catch (err) {
|
|
7827
|
-
if (err instanceof
|
|
8330
|
+
if (err instanceof _CascadeRouter.BudgetExceededError) throw err;
|
|
8331
|
+
if (err instanceof Error && err.name === "AbortError" || runSignal?.aborted || options.signal?.aborted) {
|
|
7828
8332
|
throw new CascadeCancelledError("Run cancelled");
|
|
7829
8333
|
}
|
|
7830
8334
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
@@ -7842,6 +8346,8 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
7842
8346
|
});
|
|
7843
8347
|
releaseLocalSlot?.();
|
|
7844
8348
|
releaseLocalSlot = void 0;
|
|
8349
|
+
releaseReservation?.();
|
|
8350
|
+
releaseReservation = void 0;
|
|
7845
8351
|
const retryOpts = options.model && options.model.id === model.id ? { ...options, model: void 0 } : options;
|
|
7846
8352
|
return this.generate(tier, retryOpts, onChunk, requireVision);
|
|
7847
8353
|
}
|
|
@@ -7863,6 +8369,8 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
7863
8369
|
});
|
|
7864
8370
|
releaseLocalSlot?.();
|
|
7865
8371
|
releaseLocalSlot = void 0;
|
|
8372
|
+
releaseReservation?.();
|
|
8373
|
+
releaseReservation = void 0;
|
|
7866
8374
|
const base = options.model && options.model.id === model.id ? { ...options, model: void 0 } : options;
|
|
7867
8375
|
const retryOpts = { ...base, _notFoundDepth: depth };
|
|
7868
8376
|
return this.generate(tier, retryOpts, onChunk, requireVision);
|
|
@@ -7871,6 +8379,8 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
7871
8379
|
throw err;
|
|
7872
8380
|
} finally {
|
|
7873
8381
|
releaseLocalSlot?.();
|
|
8382
|
+
releaseReservation?.();
|
|
8383
|
+
releaseReservation = void 0;
|
|
7874
8384
|
}
|
|
7875
8385
|
}
|
|
7876
8386
|
getModelForTier(tier) {
|
|
@@ -8133,12 +8643,28 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
8133
8643
|
return this.selector?.getAllAvailableModels() ?? [];
|
|
8134
8644
|
}
|
|
8135
8645
|
// ── Private ──────────────────────────────────
|
|
8646
|
+
/**
|
|
8647
|
+
* Why each configured provider failed its availability probe, kept so the
|
|
8648
|
+
* surfaces can say something better than "add an API key" to a user who
|
|
8649
|
+
* already has one. Cleared and refilled by each detectAvailableProviders().
|
|
8650
|
+
*/
|
|
8651
|
+
probeFailures = /* @__PURE__ */ new Map();
|
|
8652
|
+
/**
|
|
8653
|
+
* What stopped the configured providers from being usable, if anything.
|
|
8654
|
+
* Empty when every configured provider passed — or when none was configured,
|
|
8655
|
+
* which is a different problem with a different answer.
|
|
8656
|
+
*/
|
|
8657
|
+
providerProbeFailures() {
|
|
8658
|
+
return [...this.probeFailures].map(([provider, reason]) => ({ provider, reason }));
|
|
8659
|
+
}
|
|
8136
8660
|
/** Logs why a configured provider failed its availability probe. */
|
|
8137
8661
|
emitProbeFailure(type, reason) {
|
|
8662
|
+
this.probeFailures.set(type, reason);
|
|
8138
8663
|
console.warn(`[router] provider "${type}" is not available: ${reason}`);
|
|
8139
8664
|
}
|
|
8140
8665
|
async detectAvailableProviders(configs) {
|
|
8141
8666
|
const available = /* @__PURE__ */ new Set();
|
|
8667
|
+
this.probeFailures.clear();
|
|
8142
8668
|
const checks = configs.map(async (cfg) => {
|
|
8143
8669
|
try {
|
|
8144
8670
|
const testModel = this.getAnyModelForProvider(cfg.type);
|
|
@@ -8148,7 +8674,13 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
8148
8674
|
if (ok) available.add(cfg.type);
|
|
8149
8675
|
else this.emitProbeFailure(cfg.type, "availability check returned false (bad key, wrong endpoint/deployment, or unreachable)");
|
|
8150
8676
|
} catch (err) {
|
|
8151
|
-
|
|
8677
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
8678
|
+
if (err instanceof ProviderUnreachableError) {
|
|
8679
|
+
available.add(cfg.type);
|
|
8680
|
+
console.warn(`[router] provider "${cfg.type}" probe did not complete: ${reason} \u2014 continuing with it enabled`);
|
|
8681
|
+
return;
|
|
8682
|
+
}
|
|
8683
|
+
this.emitProbeFailure(cfg.type, reason);
|
|
8152
8684
|
}
|
|
8153
8685
|
});
|
|
8154
8686
|
await Promise.allSettled(checks);
|
|
@@ -8241,7 +8773,14 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
8241
8773
|
}
|
|
8242
8774
|
return void 0;
|
|
8243
8775
|
}
|
|
8244
|
-
|
|
8776
|
+
/**
|
|
8777
|
+
* `runGeneration` is the generation the call was ADMITTED in — see
|
|
8778
|
+
* `generate()`. Session-wide accounting always applies (the spend is real
|
|
8779
|
+
* whenever it lands), but the per-RUN ceiling is only meaningful for the run
|
|
8780
|
+
* the call belongs to, so a straggler from a finished run is left out of it.
|
|
8781
|
+
* Omitted by the direct callers in tests, which have no run to straddle.
|
|
8782
|
+
*/
|
|
8783
|
+
recordStats(tier, model, usage, featureTag, runGeneration) {
|
|
8245
8784
|
this.stats.totalTokens += usage.totalTokens;
|
|
8246
8785
|
this.stats.totalCostUsd += usage.estimatedCostUsd;
|
|
8247
8786
|
this.sessionCostUsd += usage.estimatedCostUsd;
|
|
@@ -8266,10 +8805,13 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
8266
8805
|
});
|
|
8267
8806
|
}
|
|
8268
8807
|
}
|
|
8269
|
-
|
|
8270
|
-
|
|
8808
|
+
const currentRun = runGeneration === void 0 || runGeneration === this.runGeneration;
|
|
8809
|
+
if (currentRun) {
|
|
8810
|
+
this.runTokens += usage.totalTokens;
|
|
8811
|
+
this.runCostUsd += usage.estimatedCostUsd;
|
|
8812
|
+
}
|
|
8271
8813
|
this.updateBudgetState();
|
|
8272
|
-
this.enforceRunBudget();
|
|
8814
|
+
if (currentRun) this.enforceRunBudget();
|
|
8273
8815
|
}
|
|
8274
8816
|
/**
|
|
8275
8817
|
* Resets per-run accounting at the start of each `cascade run`. Session
|
|
@@ -8279,9 +8821,158 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
8279
8821
|
beginRun() {
|
|
8280
8822
|
this.runTokens = 0;
|
|
8281
8823
|
this.runCostUsd = 0;
|
|
8824
|
+
this.reservedTokens = 0;
|
|
8825
|
+
this.reservedCostUsd = 0;
|
|
8826
|
+
this.runAbort = newRunAbort();
|
|
8827
|
+
this.runGeneration++;
|
|
8282
8828
|
this.runBudgetExceeded = false;
|
|
8283
8829
|
this.runBudgetExceededReason = void 0;
|
|
8284
8830
|
}
|
|
8831
|
+
/**
|
|
8832
|
+
* Refuse a call whose input alone cannot fit what is left of the run budget.
|
|
8833
|
+
*
|
|
8834
|
+
* `enforceRunBudget()` below runs AFTER a call returns, which makes the caps
|
|
8835
|
+
* a stop rather than a pre-authorisation: the tokens are already bought by
|
|
8836
|
+
* the time it looks. That was tolerable while a prompt could not exceed
|
|
8837
|
+
* 20,000 characters, and stopped being so when that cap was removed in
|
|
8838
|
+
* 0.72.0 — a multi-megabyte prompt is billed in full on the very first call
|
|
8839
|
+
* (the complexity classifier sends the whole thing), and no ceiling
|
|
8840
|
+
* configured downstream can give that money back.
|
|
8841
|
+
*
|
|
8842
|
+
* Deliberately narrow, because a false refusal is worse than a late stop:
|
|
8843
|
+
*
|
|
8844
|
+
* • INPUT ONLY. What a call returns is not knowable in advance, and
|
|
8845
|
+
* charging a worst-case `maxTokens` of output against the budget would
|
|
8846
|
+
* decline runs that would have finished comfortably. Input is the part
|
|
8847
|
+
* that is already determined, and it is the part the removed cap used to
|
|
8848
|
+
* bound.
|
|
8849
|
+
* • Compared against what REMAINS (`cap - spent`), not the whole cap, so a
|
|
8850
|
+
* call is judged on the budget it can actually draw from.
|
|
8851
|
+
* • Skipped when the model has no usable price. An estimate cannot be made,
|
|
8852
|
+
* and refusing on ignorance would break every self-hosted and local model
|
|
8853
|
+
* the moment a cost cap is set. The post-hoc stop still applies there.
|
|
8854
|
+
* • Skipped entirely when no cap is configured — there is nothing to
|
|
8855
|
+
* pre-authorise against.
|
|
8856
|
+
*
|
|
8857
|
+
* The message names the cap AND the estimate, because "too expensive" with
|
|
8858
|
+
* no numbers gives the user nothing to change.
|
|
8859
|
+
*/
|
|
8860
|
+
enforcePreflightBudget(model, options) {
|
|
8861
|
+
const budget = this.config?.budget;
|
|
8862
|
+
const maxCost = budget?.maxCostPerRunUsd;
|
|
8863
|
+
const maxTokens = budget?.maxTokensPerRun;
|
|
8864
|
+
if (maxCost == null && maxTokens == null) return void 0;
|
|
8865
|
+
const inputTokens = this.estimateInputTokens(options, model);
|
|
8866
|
+
if (maxTokens != null) {
|
|
8867
|
+
const remaining = maxTokens - this.runTokens - this.reservedTokens;
|
|
8868
|
+
if (inputTokens > remaining) {
|
|
8869
|
+
this.failPreflight(
|
|
8870
|
+
`This request's input is about ${inputTokens.toLocaleString()} tokens, but only ${Math.max(0, remaining).toLocaleString()} of the per-task cap of ${maxTokens.toLocaleString()} remain. Shorten the input, or raise budget.maxTokensPerRun.`
|
|
8871
|
+
);
|
|
8872
|
+
}
|
|
8873
|
+
}
|
|
8874
|
+
let estimatedUsd = 0;
|
|
8875
|
+
if (maxCost != null) {
|
|
8876
|
+
const { input: inputPer1k, unknown } = resolveModelPricing(model, { inputTokens });
|
|
8877
|
+
if (!unknown && inputPer1k > 0) {
|
|
8878
|
+
estimatedUsd = inputTokens / 1e3 * inputPer1k;
|
|
8879
|
+
const remaining = maxCost - this.runCostUsd - this.reservedCostUsd;
|
|
8880
|
+
if (estimatedUsd > remaining) {
|
|
8881
|
+
this.failPreflight(
|
|
8882
|
+
`Sending this request to ${model.provider}:${model.id} would cost about $${estimatedUsd.toFixed(4)} in input alone (~${inputTokens.toLocaleString()} tokens), and only $${Math.max(0, remaining).toFixed(4)} of the per-task cap of $${maxCost.toFixed(4)} remains. Shorten the input, choose a cheaper model, or raise budget.maxCostPerRunUsd.`
|
|
8883
|
+
);
|
|
8884
|
+
}
|
|
8885
|
+
}
|
|
8886
|
+
}
|
|
8887
|
+
this.reservedTokens += inputTokens;
|
|
8888
|
+
this.reservedCostUsd += estimatedUsd;
|
|
8889
|
+
const generation = this.runGeneration;
|
|
8890
|
+
let released = false;
|
|
8891
|
+
return () => {
|
|
8892
|
+
if (released || generation !== this.runGeneration) return;
|
|
8893
|
+
released = true;
|
|
8894
|
+
this.reservedTokens -= inputTokens;
|
|
8895
|
+
this.reservedCostUsd -= estimatedUsd;
|
|
8896
|
+
};
|
|
8897
|
+
}
|
|
8898
|
+
/**
|
|
8899
|
+
* What this call will send, in tokens.
|
|
8900
|
+
*
|
|
8901
|
+
* Text is the bulk of it, but not all of it: providers bill the serialized
|
|
8902
|
+
* TOOL DEFINITIONS on every native-tool call (Anthropic sends each name,
|
|
8903
|
+
* description and input schema), and an image is billed by its own size
|
|
8904
|
+
* rather than as the `[image]` placeholder `contentToText` reduces it to. A
|
|
8905
|
+
* tool-heavy or vision request could therefore pass either cap while its real
|
|
8906
|
+
* input already exceeded the allowance.
|
|
8907
|
+
*/
|
|
8908
|
+
estimateInputTokens(options, model) {
|
|
8909
|
+
let tokens = guardTokens(options.systemPrompt ?? "");
|
|
8910
|
+
let images = 0;
|
|
8911
|
+
const wire = wireProfile(model.provider);
|
|
8912
|
+
const countsImage = (img) => wire.sendsUrlImages || img?.type === "base64";
|
|
8913
|
+
for (const message of options.messages) {
|
|
8914
|
+
if (wire.dropsMessage(message)) continue;
|
|
8915
|
+
tokens += TOKENS_PER_MESSAGE_FRAMING;
|
|
8916
|
+
if (typeof message.content === "string") {
|
|
8917
|
+
tokens += guardTokens(message.content);
|
|
8918
|
+
} else {
|
|
8919
|
+
switch (wire.blockHandling(message)) {
|
|
8920
|
+
case "blocks":
|
|
8921
|
+
for (const block of message.content) {
|
|
8922
|
+
if (!wire.sendsBlock(block)) continue;
|
|
8923
|
+
if (block.type === "image") {
|
|
8924
|
+
if (countsImage(block.image)) images++;
|
|
8925
|
+
continue;
|
|
8926
|
+
}
|
|
8927
|
+
tokens += guardTokens(contentToText([block]));
|
|
8928
|
+
}
|
|
8929
|
+
break;
|
|
8930
|
+
case "stringified":
|
|
8931
|
+
tokens += guardTokens(safeJson(message.content));
|
|
8932
|
+
break;
|
|
8933
|
+
}
|
|
8934
|
+
}
|
|
8935
|
+
if (message.toolCalls?.length && wire.sendsToolCalls(message)) {
|
|
8936
|
+
tokens += guardTokens(safeJson(wire.sizeToolCalls(message.toolCalls)));
|
|
8937
|
+
}
|
|
8938
|
+
if (message.toolCallId && message.role === "tool" && wire.sendsToolCallId) {
|
|
8939
|
+
tokens += guardTokens(message.toolCallId);
|
|
8940
|
+
}
|
|
8941
|
+
}
|
|
8942
|
+
if (options.tools?.length) {
|
|
8943
|
+
tokens += guardTokens(safeJson(wire.sizeTools(options.tools)));
|
|
8944
|
+
}
|
|
8945
|
+
if (wire.readsTopLevelImages) {
|
|
8946
|
+
const topLevel = (options.images ?? []).filter(countsImage).length;
|
|
8947
|
+
images += topLevel * geminiImageCopies(options.messages);
|
|
8948
|
+
}
|
|
8949
|
+
tokens += images * IMAGE_TOKENS_EACH;
|
|
8950
|
+
return tokens;
|
|
8951
|
+
}
|
|
8952
|
+
/**
|
|
8953
|
+
* Report a wait that ended because something asked it to stop, the same way
|
|
8954
|
+
* the provider call below reports one.
|
|
8955
|
+
*
|
|
8956
|
+
* The queue waits sit OUTSIDE the try/catch that maps aborts, so without
|
|
8957
|
+
* this a cancellation surfaced as whatever the queue happened to throw and
|
|
8958
|
+
* was retried or failed over like an ordinary error. A budget abort keeps
|
|
8959
|
+
* saying it ran out of budget — otherwise the reason the run stopped is lost
|
|
8960
|
+
* on the way up — and anything else that was aborted becomes a cancellation.
|
|
8961
|
+
*/
|
|
8962
|
+
asAbortFailure(err, callSignal, runSignal) {
|
|
8963
|
+
if (err instanceof _CascadeRouter.BudgetExceededError) return err;
|
|
8964
|
+
if (err instanceof CascadeCancelledError) return err;
|
|
8965
|
+
if (callSignal?.aborted || runSignal?.aborted) return new CascadeCancelledError("Run cancelled");
|
|
8966
|
+
return err;
|
|
8967
|
+
}
|
|
8968
|
+
/** Trips the same run-level flag a post-hoc overrun does, and reports it the same way. */
|
|
8969
|
+
failPreflight(reason) {
|
|
8970
|
+
this.runBudgetExceeded = true;
|
|
8971
|
+
this.runBudgetExceededReason = reason;
|
|
8972
|
+
this.runAbort.abort(new _CascadeRouter.BudgetExceededError(reason));
|
|
8973
|
+
this.emit("budget:exceeded", { reason, spentUsd: this.sessionCostUsd });
|
|
8974
|
+
throw new _CascadeRouter.BudgetExceededError(reason);
|
|
8975
|
+
}
|
|
8285
8976
|
/**
|
|
8286
8977
|
* Enforce the hard per-task ceiling. Once tripped, the flag makes every
|
|
8287
8978
|
* subsequent (and concurrent) generate() call in this run fail fast.
|
|
@@ -8297,6 +8988,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
8297
8988
|
const reason = overTokens ? `Per-task token cap of ${maxTokens.toLocaleString()} reached (used ${this.runTokens.toLocaleString()}). Stopping this run to avoid runaway cost \u2014 raise budget.maxTokensPerRun for larger jobs.` : `Per-task cost cap of $${maxCost.toFixed(4)} reached (spent $${this.runCostUsd.toFixed(4)}). Stopping this run to avoid runaway cost.`;
|
|
8298
8989
|
this.runBudgetExceeded = true;
|
|
8299
8990
|
this.runBudgetExceededReason = reason;
|
|
8991
|
+
this.runAbort.abort(new _CascadeRouter.BudgetExceededError(reason));
|
|
8300
8992
|
this.emit("budget:exceeded", { reason, spentUsd: this.sessionCostUsd });
|
|
8301
8993
|
throw new _CascadeRouter.BudgetExceededError(reason);
|
|
8302
8994
|
}
|
|
@@ -9534,6 +10226,12 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
|
|
|
9534
10226
|
maxTokens: 4096,
|
|
9535
10227
|
...subtaskModel ? { model: subtaskModel } : {},
|
|
9536
10228
|
featureTag: this.assignment?.sectionTitle,
|
|
10229
|
+
// The wave signal, so cancel-and-respawn reaches the request that is
|
|
10230
|
+
// RUNNING rather than only stopping the worker at its next checkpoint.
|
|
10231
|
+
// It is a superset of the run signal the router injects when this is
|
|
10232
|
+
// absent (T2 composes it from that signal plus its own per-wave one),
|
|
10233
|
+
// so wiring it here loses no cancellation and adds the wave's.
|
|
10234
|
+
...this.signal ? { signal: this.signal } : {},
|
|
9537
10235
|
...this.localOnlyMatch ? { forceLocal: true } : {}
|
|
9538
10236
|
};
|
|
9539
10237
|
const result = await this.router.generate(
|
|
@@ -10012,6 +10710,7 @@ ${current}`
|
|
|
10012
10710
|
HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
10013
10711
|
maxTokens: 4096,
|
|
10014
10712
|
featureTag: assignment.sectionTitle,
|
|
10713
|
+
...this.signal ? { signal: this.signal } : {},
|
|
10015
10714
|
...this.localOnlyMatch ? { forceLocal: true } : {}
|
|
10016
10715
|
});
|
|
10017
10716
|
const next = (improved.content ?? "").trim();
|
|
@@ -10073,6 +10772,7 @@ Reply with JSON: { "completeness": "pass"|"fail", "correctness": "pass"|"fail",
|
|
|
10073
10772
|
|
|
10074
10773
|
HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
10075
10774
|
featureTag: assignment.sectionTitle,
|
|
10775
|
+
...this.signal ? { signal: this.signal } : {},
|
|
10076
10776
|
...this.localOnlyMatch ? { forceLocal: true } : {}
|
|
10077
10777
|
});
|
|
10078
10778
|
try {
|
|
@@ -10112,6 +10812,7 @@ ${output.slice(0, 4e3)}`;
|
|
|
10112
10812
|
messages: [{ role: "user", content: prompt }],
|
|
10113
10813
|
maxTokens: 300,
|
|
10114
10814
|
temperature: 0,
|
|
10815
|
+
...this.signal ? { signal: this.signal } : {},
|
|
10115
10816
|
...this.localOnlyMatch ? { forceLocal: true } : {}
|
|
10116
10817
|
});
|
|
10117
10818
|
const match = /\[[\s\S]*\]/.exec(result.content);
|
|
@@ -10223,6 +10924,10 @@ Begin execution now.`;
|
|
|
10223
10924
|
}
|
|
10224
10925
|
};
|
|
10225
10926
|
var PeerBus = class extends EventEmitter__default.default {
|
|
10927
|
+
constructor() {
|
|
10928
|
+
super();
|
|
10929
|
+
this.setMaxListeners(64);
|
|
10930
|
+
}
|
|
10226
10931
|
outputs = /* @__PURE__ */ new Map();
|
|
10227
10932
|
waiters = /* @__PURE__ */ new Map();
|
|
10228
10933
|
members = /* @__PURE__ */ new Set();
|
|
@@ -11471,6 +12176,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
11471
12176
|
const waveSignal = AbortSignal.any(
|
|
11472
12177
|
[this.signal, this.waveAbortController.signal].filter(Boolean)
|
|
11473
12178
|
);
|
|
12179
|
+
EventEmitter.setMaxListeners(64, waveSignal);
|
|
11474
12180
|
const runOne = async (id) => {
|
|
11475
12181
|
const assignment = sanitizedAssignments.find((a) => a.subtaskId === id);
|
|
11476
12182
|
const worker = workerMap.get(id);
|
|
@@ -18471,115 +19177,6 @@ var GraphSearchTool = class extends BaseTool {
|
|
|
18471
19177
|
}
|
|
18472
19178
|
};
|
|
18473
19179
|
|
|
18474
|
-
// src/core/context/compaction.ts
|
|
18475
|
-
function estimateTokens(text) {
|
|
18476
|
-
if (!text) return 0;
|
|
18477
|
-
return Math.max(1, Math.ceil(text.length / 4));
|
|
18478
|
-
}
|
|
18479
|
-
function contentToText(content) {
|
|
18480
|
-
if (typeof content === "string") return content;
|
|
18481
|
-
return content.map((c) => c.type === "text" ? c.text : c.type === "tool_result" ? c.content : `[${c.type}]`).join(" ");
|
|
18482
|
-
}
|
|
18483
|
-
function messagesTokens(messages) {
|
|
18484
|
-
return messages.reduce((n, m) => n + estimateTokens(contentToText(m.content)), 0);
|
|
18485
|
-
}
|
|
18486
|
-
function atomize(text, maxChars) {
|
|
18487
|
-
const seps = ["\n\n", "\n", ". ", " "];
|
|
18488
|
-
let units = [text];
|
|
18489
|
-
for (const sep of seps) {
|
|
18490
|
-
if (units.every((u) => u.length <= maxChars)) break;
|
|
18491
|
-
const next = [];
|
|
18492
|
-
for (const u of units) {
|
|
18493
|
-
if (u.length <= maxChars) {
|
|
18494
|
-
next.push(u);
|
|
18495
|
-
continue;
|
|
18496
|
-
}
|
|
18497
|
-
const parts = u.split(sep);
|
|
18498
|
-
parts.forEach((p, i) => next.push(i < parts.length - 1 ? p + sep : p));
|
|
18499
|
-
}
|
|
18500
|
-
units = next;
|
|
18501
|
-
}
|
|
18502
|
-
const out = [];
|
|
18503
|
-
for (const u of units) {
|
|
18504
|
-
if (u.length <= maxChars) out.push(u);
|
|
18505
|
-
else for (let i = 0; i < u.length; i += maxChars) out.push(u.slice(i, i + maxChars));
|
|
18506
|
-
}
|
|
18507
|
-
return out;
|
|
18508
|
-
}
|
|
18509
|
-
function chunkText(text, opts) {
|
|
18510
|
-
const maxChars = Math.max(1, Math.floor(opts.maxTokens * 4));
|
|
18511
|
-
const overlap = Math.min(maxChars - 1, Math.max(0, Math.floor(maxChars * (opts.overlapRatio ?? 0.1))));
|
|
18512
|
-
const trimmed = text.trim();
|
|
18513
|
-
if (!trimmed) return [];
|
|
18514
|
-
if (trimmed.length <= maxChars) return [trimmed];
|
|
18515
|
-
const units = atomize(trimmed, maxChars);
|
|
18516
|
-
const chunks = [];
|
|
18517
|
-
let cur = "";
|
|
18518
|
-
for (const u of units) {
|
|
18519
|
-
if (cur && cur.length + u.length > maxChars) {
|
|
18520
|
-
chunks.push(cur.trim());
|
|
18521
|
-
cur = overlap > 0 ? cur.slice(-overlap) : "";
|
|
18522
|
-
}
|
|
18523
|
-
cur += u;
|
|
18524
|
-
}
|
|
18525
|
-
if (cur.trim()) chunks.push(cur.trim());
|
|
18526
|
-
return chunks;
|
|
18527
|
-
}
|
|
18528
|
-
function needsCompaction(totalTokens, contextWindow, reserve = 0.2) {
|
|
18529
|
-
if (!contextWindow || contextWindow <= 0) return false;
|
|
18530
|
-
return totalTokens > contextWindow * (1 - reserve);
|
|
18531
|
-
}
|
|
18532
|
-
async function mapReduceCompact(text, opts) {
|
|
18533
|
-
const capChars = opts.capTokens * 4;
|
|
18534
|
-
let working = text;
|
|
18535
|
-
let truncated = false;
|
|
18536
|
-
if (working.length > capChars) {
|
|
18537
|
-
working = working.slice(0, capChars);
|
|
18538
|
-
truncated = true;
|
|
18539
|
-
}
|
|
18540
|
-
const instruction = opts.instruction ?? "Summarize this content, preserving key facts, names, numbers, and decisions.";
|
|
18541
|
-
let chunks = chunkText(working, { maxTokens: opts.chunkTokens });
|
|
18542
|
-
if (chunks.length <= 1) return { text: working, chunks: chunks.length, calls: 0, truncated };
|
|
18543
|
-
let calls = 0;
|
|
18544
|
-
let parts = await Promise.all(
|
|
18545
|
-
chunks.map((c, i) => {
|
|
18546
|
-
calls++;
|
|
18547
|
-
return opts.summarize(c, `${instruction} (part ${i + 1} of ${chunks.length})`);
|
|
18548
|
-
})
|
|
18549
|
-
);
|
|
18550
|
-
let combined = parts.join("\n\n");
|
|
18551
|
-
const maxPasses = opts.maxReducePasses ?? 3;
|
|
18552
|
-
for (let pass = 0; pass < maxPasses && estimateTokens(combined) > opts.targetTokens; pass++) {
|
|
18553
|
-
const groups = chunkText(combined, { maxTokens: opts.chunkTokens });
|
|
18554
|
-
if (groups.length <= 1) break;
|
|
18555
|
-
parts = await Promise.all(
|
|
18556
|
-
groups.map((g) => {
|
|
18557
|
-
calls++;
|
|
18558
|
-
return opts.summarize(g, "Merge these partial summaries into one cohesive summary; keep all distinct facts.");
|
|
18559
|
-
})
|
|
18560
|
-
);
|
|
18561
|
-
combined = parts.join("\n\n");
|
|
18562
|
-
}
|
|
18563
|
-
const note = truncated ? "\n\n[Note: input exceeded the extended-context cap and was truncated.]" : "";
|
|
18564
|
-
return { text: combined + note, chunks: chunks.length, calls, truncated };
|
|
18565
|
-
}
|
|
18566
|
-
async function rollingSummary(messages, opts) {
|
|
18567
|
-
if (messages.length <= opts.keepRecent) return messages;
|
|
18568
|
-
const older = messages.slice(0, messages.length - opts.keepRecent);
|
|
18569
|
-
const recent = messages.slice(messages.length - opts.keepRecent);
|
|
18570
|
-
const transcript = older.map((m) => `${m.role}: ${contentToText(m.content)}`).join("\n");
|
|
18571
|
-
const summary = await opts.summarize(
|
|
18572
|
-
transcript,
|
|
18573
|
-
"Summarize this earlier conversation. Preserve facts, decisions, names, numbers, and any open threads or todos."
|
|
18574
|
-
);
|
|
18575
|
-
const summaryMessage = {
|
|
18576
|
-
role: "system",
|
|
18577
|
-
content: `Summary of earlier conversation (folded to fit the context window):
|
|
18578
|
-
${summary}`
|
|
18579
|
-
};
|
|
18580
|
-
return [summaryMessage, ...recent];
|
|
18581
|
-
}
|
|
18582
|
-
|
|
18583
19180
|
// src/core/steering/guidance.ts
|
|
18584
19181
|
var GuidanceQueue = class {
|
|
18585
19182
|
entries = [];
|
|
@@ -20131,7 +20728,16 @@ ${finalOutput}` : ""
|
|
|
20131
20728
|
const selector = this.router.getSelector();
|
|
20132
20729
|
const model = options.fastAnswerModel ? selector.selectForTier(tier, options.fastAnswerModel) : selector.getCandidatesForTier(tier)[0] ?? selector.selectForTier(tier);
|
|
20133
20730
|
if (!model) {
|
|
20134
|
-
|
|
20731
|
+
const failures2 = this.router.providerProbeFailures();
|
|
20732
|
+
const context = failures2.length ? ` Configured providers: ${failures2.map((f) => `${f.provider}: ${f.reason}`).join("; ")}` : "";
|
|
20733
|
+
if (options.fastAnswerModel) {
|
|
20734
|
+
throw new Error(
|
|
20735
|
+
`No model is available for a fast answer: the pinned model "${options.fastAnswerModel}" could not be resolved \u2014 check it is spelled correctly and served by a configured provider.${context}`
|
|
20736
|
+
);
|
|
20737
|
+
}
|
|
20738
|
+
throw new Error(
|
|
20739
|
+
failures2.length ? `No model is available for a fast answer.${context}` : "No model is available for a fast answer \u2014 add a provider API key first."
|
|
20740
|
+
);
|
|
20135
20741
|
}
|
|
20136
20742
|
this.recordDecision(
|
|
20137
20743
|
"model",
|