cascade-ai 0.72.0 → 0.73.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 +734 -220
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +734 -220
- package/dist/cli.js.map +1 -1
- package/dist/desktop-core.cjs +722 -220
- package/dist/index.cjs +721 -219
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +89 -0
- package/dist/index.d.ts +89 -0
- package/dist/index.js +721 -219
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -1792,6 +1792,15 @@ function findPricing(modelId, provider, opts = {}) {
|
|
|
1792
1792
|
}
|
|
1793
1793
|
return null;
|
|
1794
1794
|
}
|
|
1795
|
+
function hasContextBands(model) {
|
|
1796
|
+
if (model.isLocal) return false;
|
|
1797
|
+
const fallbackProviders = model.provider === "azure" ? ["openai"] : model.provider === "gemini" ? ["vertex"] : [];
|
|
1798
|
+
const match = findPricing(model.id, model.provider, {
|
|
1799
|
+
baseModelId: model.baseModelId,
|
|
1800
|
+
fallbackProviders
|
|
1801
|
+
});
|
|
1802
|
+
return (match?.entry.tiers?.length ?? 0) > 1;
|
|
1803
|
+
}
|
|
1795
1804
|
function tierFor(entry, inputTokens = 0) {
|
|
1796
1805
|
const tiers = entry.tiers;
|
|
1797
1806
|
if (!tiers?.length) return null;
|
|
@@ -1908,7 +1917,7 @@ function reconcilePrice(model, live) {
|
|
|
1908
1917
|
}
|
|
1909
1918
|
|
|
1910
1919
|
// src/constants.ts
|
|
1911
|
-
var CASCADE_VERSION = "0.
|
|
1920
|
+
var CASCADE_VERSION = "0.73.0";
|
|
1912
1921
|
var CASCADE_CONFIG_DIR = ".cascade";
|
|
1913
1922
|
var CASCADE_MD_FILE = "CASCADE.md";
|
|
1914
1923
|
var CASCADE_IGNORE_FILE = ".cascadeignore";
|
|
@@ -2353,8 +2362,23 @@ var PROVIDER_DISPLAY_NAMES = {
|
|
|
2353
2362
|
};
|
|
2354
2363
|
|
|
2355
2364
|
// src/utils/cost.ts
|
|
2356
|
-
function resolveModelPricing(model) {
|
|
2365
|
+
function resolveModelPricing(model, opts = {}) {
|
|
2357
2366
|
if (model.isLocal) return { input: 0, output: 0, unknown: false };
|
|
2367
|
+
if (opts.inputTokens != null && hasContextBands(model)) {
|
|
2368
|
+
const banded = resolvePricing(model, { inputTokens: opts.inputTokens });
|
|
2369
|
+
const base = resolvePricing(model, { inputTokens: 0 });
|
|
2370
|
+
if (!banded.unknown && !base.unknown) {
|
|
2371
|
+
const scale = (stamped, baseRate) => stamped > 0 && baseRate > 0 ? stamped / baseRate : 1;
|
|
2372
|
+
const inScale = scale(model.inputCostPer1kTokens, base.input);
|
|
2373
|
+
const outScale = scale(model.outputCostPer1kTokens, base.output);
|
|
2374
|
+
return {
|
|
2375
|
+
input: banded.input * inScale,
|
|
2376
|
+
output: banded.output * outScale,
|
|
2377
|
+
unknown: false,
|
|
2378
|
+
...banded.estimatedFromProvider ? { estimatedFromProvider: banded.estimatedFromProvider } : {}
|
|
2379
|
+
};
|
|
2380
|
+
}
|
|
2381
|
+
}
|
|
2358
2382
|
if (model.inputCostPer1kTokens > 0 || model.outputCostPer1kTokens > 0) {
|
|
2359
2383
|
return {
|
|
2360
2384
|
input: model.inputCostPer1kTokens,
|
|
@@ -2362,7 +2386,7 @@ function resolveModelPricing(model) {
|
|
|
2362
2386
|
unknown: false
|
|
2363
2387
|
};
|
|
2364
2388
|
}
|
|
2365
|
-
const fromDataset = resolvePricing(model);
|
|
2389
|
+
const fromDataset = resolvePricing(model, { inputTokens: opts.inputTokens });
|
|
2366
2390
|
if (!fromDataset.unknown) {
|
|
2367
2391
|
return {
|
|
2368
2392
|
input: fromDataset.input,
|
|
@@ -2384,11 +2408,11 @@ function resolveModelPricing(model) {
|
|
|
2384
2408
|
return { input: 0, output: 0, unknown: true };
|
|
2385
2409
|
}
|
|
2386
2410
|
function calculateCost(inputTokens, outputTokens, model) {
|
|
2387
|
-
const { input, output } = resolveModelPricing(model);
|
|
2411
|
+
const { input, output } = resolveModelPricing(model, { inputTokens });
|
|
2388
2412
|
return inputTokens / 1e3 * input + outputTokens / 1e3 * output;
|
|
2389
2413
|
}
|
|
2390
2414
|
function buildTokenUsage(inputTokens, outputTokens, model) {
|
|
2391
|
-
const { input, output, unknown } = resolveModelPricing(model);
|
|
2415
|
+
const { input, output, unknown } = resolveModelPricing(model, { inputTokens });
|
|
2392
2416
|
return {
|
|
2393
2417
|
inputTokens,
|
|
2394
2418
|
outputTokens,
|
|
@@ -2467,6 +2491,21 @@ function anthropicThinkingParam(modelId, maxTokens) {
|
|
|
2467
2491
|
if (budget < 1024) return {};
|
|
2468
2492
|
return { thinking: { type: "enabled", budget_tokens: budget } };
|
|
2469
2493
|
}
|
|
2494
|
+
function toAnthropicTools(tools) {
|
|
2495
|
+
return tools.map((t) => ({
|
|
2496
|
+
name: t.name,
|
|
2497
|
+
description: t.description,
|
|
2498
|
+
input_schema: t.inputSchema
|
|
2499
|
+
}));
|
|
2500
|
+
}
|
|
2501
|
+
function toAnthropicToolUse(toolCalls) {
|
|
2502
|
+
return toolCalls.map((tc) => ({
|
|
2503
|
+
type: "tool_use",
|
|
2504
|
+
id: tc.id,
|
|
2505
|
+
name: tc.name,
|
|
2506
|
+
input: tc.input
|
|
2507
|
+
}));
|
|
2508
|
+
}
|
|
2470
2509
|
var AnthropicProvider = class extends BaseProvider {
|
|
2471
2510
|
client;
|
|
2472
2511
|
constructor(config, model) {
|
|
@@ -2488,11 +2527,7 @@ var AnthropicProvider = class extends BaseProvider {
|
|
|
2488
2527
|
}
|
|
2489
2528
|
async generateStream(options, onChunk) {
|
|
2490
2529
|
const messages = this.convertMessages(options.messages);
|
|
2491
|
-
const tools = options.tools
|
|
2492
|
-
name: t.name,
|
|
2493
|
-
description: t.description,
|
|
2494
|
-
input_schema: t.inputSchema
|
|
2495
|
-
}));
|
|
2530
|
+
const tools = options.tools && toAnthropicTools(options.tools);
|
|
2496
2531
|
let fullContent = "";
|
|
2497
2532
|
let inputTokens = 0;
|
|
2498
2533
|
let outputTokens = 0;
|
|
@@ -2620,14 +2655,7 @@ var AnthropicProvider = class extends BaseProvider {
|
|
|
2620
2655
|
const content = [];
|
|
2621
2656
|
const text = typeof m.content === "string" ? m.content : "";
|
|
2622
2657
|
if (text) content.push({ type: "text", text });
|
|
2623
|
-
|
|
2624
|
-
content.push({
|
|
2625
|
-
type: "tool_use",
|
|
2626
|
-
id: tc.id,
|
|
2627
|
-
name: tc.name,
|
|
2628
|
-
input: tc.input
|
|
2629
|
-
});
|
|
2630
|
-
}
|
|
2658
|
+
content.push(...toAnthropicToolUse(m.toolCalls ?? []));
|
|
2631
2659
|
if (content.length > 0) {
|
|
2632
2660
|
result.push({ role: "assistant", content });
|
|
2633
2661
|
}
|
|
@@ -2667,6 +2695,26 @@ function isParamShapeError(err) {
|
|
|
2667
2695
|
const msg = String(e?.message ?? e?.error?.message ?? "").toLowerCase();
|
|
2668
2696
|
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"));
|
|
2669
2697
|
}
|
|
2698
|
+
function toOpenAITools(tools) {
|
|
2699
|
+
return tools.map((t) => ({
|
|
2700
|
+
type: "function",
|
|
2701
|
+
function: {
|
|
2702
|
+
name: t.name,
|
|
2703
|
+
description: t.description,
|
|
2704
|
+
parameters: t.inputSchema
|
|
2705
|
+
}
|
|
2706
|
+
}));
|
|
2707
|
+
}
|
|
2708
|
+
function toOpenAIToolCalls(toolCalls) {
|
|
2709
|
+
return toolCalls.map((tc) => ({
|
|
2710
|
+
id: tc.id,
|
|
2711
|
+
type: "function",
|
|
2712
|
+
function: {
|
|
2713
|
+
name: tc.name,
|
|
2714
|
+
arguments: JSON.stringify(tc.input)
|
|
2715
|
+
}
|
|
2716
|
+
}));
|
|
2717
|
+
}
|
|
2670
2718
|
var OpenAIProvider = class extends BaseProvider {
|
|
2671
2719
|
client;
|
|
2672
2720
|
/** Once we learn (from the model id or an API error) that this deployment
|
|
@@ -2686,14 +2734,7 @@ var OpenAIProvider = class extends BaseProvider {
|
|
|
2686
2734
|
}
|
|
2687
2735
|
async generateStream(options, onChunk) {
|
|
2688
2736
|
const messages = this.convertMessages(options.messages, options.systemPrompt);
|
|
2689
|
-
const tools = options.tools
|
|
2690
|
-
type: "function",
|
|
2691
|
-
function: {
|
|
2692
|
-
name: t.name,
|
|
2693
|
-
description: t.description,
|
|
2694
|
-
parameters: t.inputSchema
|
|
2695
|
-
}
|
|
2696
|
-
}));
|
|
2737
|
+
const tools = options.tools && toOpenAITools(options.tools);
|
|
2697
2738
|
let fullContent = "";
|
|
2698
2739
|
let inputTokens = 0;
|
|
2699
2740
|
let outputTokens = 0;
|
|
@@ -2839,14 +2880,7 @@ var OpenAIProvider = class extends BaseProvider {
|
|
|
2839
2880
|
result.push({
|
|
2840
2881
|
role: "assistant",
|
|
2841
2882
|
content: m.content || "",
|
|
2842
|
-
tool_calls: m.toolCalls
|
|
2843
|
-
id: toolCall.id,
|
|
2844
|
-
type: "function",
|
|
2845
|
-
function: {
|
|
2846
|
-
name: toolCall.name,
|
|
2847
|
-
arguments: JSON.stringify(toolCall.input)
|
|
2848
|
-
}
|
|
2849
|
-
}))
|
|
2883
|
+
tool_calls: toOpenAIToolCalls(m.toolCalls)
|
|
2850
2884
|
});
|
|
2851
2885
|
} else {
|
|
2852
2886
|
result.push({ role: m.role, content: m.content });
|
|
@@ -3089,6 +3123,26 @@ function toGeminiParameters(inputSchema) {
|
|
|
3089
3123
|
}
|
|
3090
3124
|
|
|
3091
3125
|
// src/providers/gemini.ts
|
|
3126
|
+
function toGeminiTools(tools) {
|
|
3127
|
+
return tools.map((tool) => ({
|
|
3128
|
+
name: tool.name,
|
|
3129
|
+
description: tool.description,
|
|
3130
|
+
// Convert, never cast. Gemini's parameters field is an OpenAPI subset
|
|
3131
|
+
// that 400s on unknown keys, and MCP servers ship schemas carrying their
|
|
3132
|
+
// own extensions (GitHub's adds `x-mcp-header` to every header-bound
|
|
3133
|
+
// property). Casting sent those through verbatim and every request failed
|
|
3134
|
+
// before the model saw it. See providers/gemini-schema.ts.
|
|
3135
|
+
parameters: toGeminiParameters(tool.inputSchema)
|
|
3136
|
+
}));
|
|
3137
|
+
}
|
|
3138
|
+
function toGeminiFunctionCalls(toolCalls) {
|
|
3139
|
+
return toolCalls.map((tc) => ({
|
|
3140
|
+
functionCall: {
|
|
3141
|
+
name: tc.name,
|
|
3142
|
+
args: tc.input
|
|
3143
|
+
}
|
|
3144
|
+
}));
|
|
3145
|
+
}
|
|
3092
3146
|
var GeminiProvider = class extends BaseProvider {
|
|
3093
3147
|
client;
|
|
3094
3148
|
constructor(config, model) {
|
|
@@ -3110,7 +3164,7 @@ var GeminiProvider = class extends BaseProvider {
|
|
|
3110
3164
|
{ category: genai.HarmCategory.HARM_CATEGORY_HARASSMENT, threshold: genai.HarmBlockThreshold.BLOCK_NONE },
|
|
3111
3165
|
{ category: genai.HarmCategory.HARM_CATEGORY_HATE_SPEECH, threshold: genai.HarmBlockThreshold.BLOCK_NONE }
|
|
3112
3166
|
],
|
|
3113
|
-
tools: options.tools?.length ? [{ functionDeclarations: options.tools
|
|
3167
|
+
tools: options.tools?.length ? [{ functionDeclarations: toGeminiTools(options.tools) }] : void 0,
|
|
3114
3168
|
abortSignal: options.signal
|
|
3115
3169
|
}
|
|
3116
3170
|
});
|
|
@@ -3251,14 +3305,7 @@ var GeminiProvider = class extends BaseProvider {
|
|
|
3251
3305
|
const parts = [];
|
|
3252
3306
|
const textContent = typeof m.content === "string" ? m.content : "";
|
|
3253
3307
|
if (textContent) parts.push({ text: textContent });
|
|
3254
|
-
|
|
3255
|
-
parts.push({
|
|
3256
|
-
functionCall: {
|
|
3257
|
-
name: tc.name,
|
|
3258
|
-
args: tc.input
|
|
3259
|
-
}
|
|
3260
|
-
});
|
|
3261
|
-
}
|
|
3308
|
+
parts.push(...toGeminiFunctionCalls(m.toolCalls ?? []));
|
|
3262
3309
|
if (parts.length > 0) {
|
|
3263
3310
|
contents.push({ role: "model", parts });
|
|
3264
3311
|
}
|
|
@@ -3305,18 +3352,6 @@ var GeminiProvider = class extends BaseProvider {
|
|
|
3305
3352
|
}
|
|
3306
3353
|
return parts;
|
|
3307
3354
|
}
|
|
3308
|
-
convertTool(tool) {
|
|
3309
|
-
return {
|
|
3310
|
-
name: tool.name,
|
|
3311
|
-
description: tool.description,
|
|
3312
|
-
// Convert, never cast. Gemini's parameters field is an OpenAPI subset
|
|
3313
|
-
// that 400s on unknown keys, and MCP servers ship schemas carrying their
|
|
3314
|
-
// own extensions (GitHub's adds `x-mcp-header` to every header-bound
|
|
3315
|
-
// property). Casting sent those through verbatim and every request failed
|
|
3316
|
-
// before the model saw it. See providers/gemini-schema.ts.
|
|
3317
|
-
parameters: toGeminiParameters(tool.inputSchema)
|
|
3318
|
-
};
|
|
3319
|
-
}
|
|
3320
3355
|
};
|
|
3321
3356
|
try {
|
|
3322
3357
|
dns__default.default.setDefaultResultOrder("ipv4first");
|
|
@@ -3409,6 +3444,24 @@ function isToolCapable(modelName) {
|
|
|
3409
3444
|
const name = modelName.toLowerCase();
|
|
3410
3445
|
return TOOL_CAPABLE_FAMILIES.some((family) => name.includes(family));
|
|
3411
3446
|
}
|
|
3447
|
+
function toOllamaTools(tools) {
|
|
3448
|
+
return tools.map((t) => ({
|
|
3449
|
+
type: "function",
|
|
3450
|
+
function: {
|
|
3451
|
+
name: t.name,
|
|
3452
|
+
description: t.description,
|
|
3453
|
+
parameters: t.inputSchema
|
|
3454
|
+
}
|
|
3455
|
+
}));
|
|
3456
|
+
}
|
|
3457
|
+
function toOllamaToolCalls(toolCalls) {
|
|
3458
|
+
return toolCalls.map((tc) => ({
|
|
3459
|
+
function: {
|
|
3460
|
+
name: tc.name,
|
|
3461
|
+
arguments: tc.input
|
|
3462
|
+
}
|
|
3463
|
+
}));
|
|
3464
|
+
}
|
|
3412
3465
|
var OllamaProvider = class extends BaseProvider {
|
|
3413
3466
|
baseUrl;
|
|
3414
3467
|
constructor(config, model) {
|
|
@@ -3421,14 +3474,7 @@ var OllamaProvider = class extends BaseProvider {
|
|
|
3421
3474
|
}
|
|
3422
3475
|
async generateStream(options, onChunk) {
|
|
3423
3476
|
const messages = this.convertMessages(options.messages, options.systemPrompt);
|
|
3424
|
-
const ollamaTools = options.tools
|
|
3425
|
-
type: "function",
|
|
3426
|
-
function: {
|
|
3427
|
-
name: t.name,
|
|
3428
|
-
description: t.description,
|
|
3429
|
-
parameters: t.inputSchema
|
|
3430
|
-
}
|
|
3431
|
-
}));
|
|
3477
|
+
const ollamaTools = options.tools && toOllamaTools(options.tools);
|
|
3432
3478
|
const response = await fetch(`${this.baseUrl}/api/chat`, {
|
|
3433
3479
|
method: "POST",
|
|
3434
3480
|
headers: { "Content-Type": "application/json" },
|
|
@@ -3608,12 +3654,7 @@ var OllamaProvider = class extends BaseProvider {
|
|
|
3608
3654
|
result.push({
|
|
3609
3655
|
role: "assistant",
|
|
3610
3656
|
content: typeof m.content === "string" ? m.content : "",
|
|
3611
|
-
tool_calls: m.toolCalls
|
|
3612
|
-
function: {
|
|
3613
|
-
name: tc.name,
|
|
3614
|
-
arguments: tc.input
|
|
3615
|
-
}
|
|
3616
|
-
}))
|
|
3657
|
+
tool_calls: toOllamaToolCalls(m.toolCalls)
|
|
3617
3658
|
});
|
|
3618
3659
|
continue;
|
|
3619
3660
|
}
|
|
@@ -4014,6 +4055,21 @@ var DEFAULT_PROVIDER_TPM = {
|
|
|
4014
4055
|
"openai-compatible": 3e4,
|
|
4015
4056
|
ollama: Number.POSITIVE_INFINITY
|
|
4016
4057
|
};
|
|
4058
|
+
function abortReason(signal) {
|
|
4059
|
+
return signal.reason instanceof Error ? signal.reason : new Error("Aborted while waiting for rate-limit capacity");
|
|
4060
|
+
}
|
|
4061
|
+
function sleepUnlessAborted(ms, signal) {
|
|
4062
|
+
if (!signal) return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
4063
|
+
return new Promise((resolve2) => {
|
|
4064
|
+
const done = () => {
|
|
4065
|
+
clearTimeout(timer);
|
|
4066
|
+
signal.removeEventListener("abort", done);
|
|
4067
|
+
resolve2();
|
|
4068
|
+
};
|
|
4069
|
+
const timer = setTimeout(done, ms);
|
|
4070
|
+
signal.addEventListener("abort", done, { once: true });
|
|
4071
|
+
});
|
|
4072
|
+
}
|
|
4017
4073
|
var TpmLimiter = class {
|
|
4018
4074
|
buckets = /* @__PURE__ */ new Map();
|
|
4019
4075
|
constructor(overrides = {}) {
|
|
@@ -4032,9 +4088,10 @@ var TpmLimiter = class {
|
|
|
4032
4088
|
* are reported back via `refund` when short, or simply settled at the next
|
|
4033
4089
|
* refill.
|
|
4034
4090
|
*/
|
|
4035
|
-
async acquire(provider, estimatedTokens) {
|
|
4091
|
+
async acquire(provider, estimatedTokens, signal) {
|
|
4036
4092
|
const bucket = this.buckets.get(provider);
|
|
4037
4093
|
if (!bucket || bucket.tokensPerMinute === Number.POSITIVE_INFINITY) return;
|
|
4094
|
+
if (signal?.aborted) throw abortReason(signal);
|
|
4038
4095
|
const want = Math.min(estimatedTokens, bucket.tokensPerMinute);
|
|
4039
4096
|
for (; ; ) {
|
|
4040
4097
|
this.refill(bucket);
|
|
@@ -4044,7 +4101,8 @@ var TpmLimiter = class {
|
|
|
4044
4101
|
}
|
|
4045
4102
|
const deficit = want - bucket.available;
|
|
4046
4103
|
const waitMs = Math.max(50, Math.ceil(deficit / bucket.tokensPerMinute * 6e4));
|
|
4047
|
-
await
|
|
4104
|
+
await sleepUnlessAborted(waitMs, signal);
|
|
4105
|
+
if (signal?.aborted) throw abortReason(signal);
|
|
4048
4106
|
}
|
|
4049
4107
|
}
|
|
4050
4108
|
/**
|
|
@@ -4090,6 +4148,9 @@ var TpmLimiter = class {
|
|
|
4090
4148
|
};
|
|
4091
4149
|
|
|
4092
4150
|
// src/core/router/local-queue.ts
|
|
4151
|
+
function abortReason2(signal) {
|
|
4152
|
+
return signal.reason instanceof Error ? signal.reason : new Error("Aborted while waiting for a local inference slot");
|
|
4153
|
+
}
|
|
4093
4154
|
var LocalRequestQueue = class {
|
|
4094
4155
|
maxConcurrent;
|
|
4095
4156
|
active = 0;
|
|
@@ -4100,9 +4161,16 @@ var LocalRequestQueue = class {
|
|
|
4100
4161
|
/**
|
|
4101
4162
|
* Acquire a queue slot. Returns a `release` function that MUST be called
|
|
4102
4163
|
* when the inference call is done (even on error). Rejects if the slot
|
|
4103
|
-
* cannot be acquired within `timeoutMs
|
|
4164
|
+
* cannot be acquired within `timeoutMs`, or as soon as `signal` fires.
|
|
4165
|
+
*
|
|
4166
|
+
* The signal matters as much as the timeout: with concurrency of 1 a caller
|
|
4167
|
+
* can sit here for half the inference timeout — 150 seconds by default —
|
|
4168
|
+
* and until it was wired up, neither cancelling the run nor spending its
|
|
4169
|
+
* budget got it out. It waited out the window for a slot it would drop the
|
|
4170
|
+
* moment it received one.
|
|
4104
4171
|
*/
|
|
4105
|
-
async acquire(timeoutMs) {
|
|
4172
|
+
async acquire(timeoutMs, signal) {
|
|
4173
|
+
if (signal?.aborted) throw abortReason2(signal);
|
|
4106
4174
|
if (this.active < this.maxConcurrent) {
|
|
4107
4175
|
this.active++;
|
|
4108
4176
|
return this.makeRelease();
|
|
@@ -4110,23 +4178,36 @@ var LocalRequestQueue = class {
|
|
|
4110
4178
|
return new Promise((resolve2, reject) => {
|
|
4111
4179
|
let settled = false;
|
|
4112
4180
|
let timer;
|
|
4181
|
+
const cleanup = () => {
|
|
4182
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
4183
|
+
signal?.removeEventListener("abort", onAbort);
|
|
4184
|
+
};
|
|
4113
4185
|
const resolver = (release) => {
|
|
4114
4186
|
if (settled) return;
|
|
4115
4187
|
settled = true;
|
|
4116
|
-
|
|
4188
|
+
cleanup();
|
|
4117
4189
|
resolve2(release);
|
|
4118
4190
|
};
|
|
4191
|
+
const abandon = () => {
|
|
4192
|
+
if (settled) return false;
|
|
4193
|
+
settled = true;
|
|
4194
|
+
cleanup();
|
|
4195
|
+
const idx = this.queue.indexOf(resolver);
|
|
4196
|
+
if (idx !== -1) this.queue.splice(idx, 1);
|
|
4197
|
+
return true;
|
|
4198
|
+
};
|
|
4199
|
+
function onAbort() {
|
|
4200
|
+
if (abandon()) reject(abortReason2(signal));
|
|
4201
|
+
}
|
|
4119
4202
|
if (timeoutMs !== void 0 && timeoutMs > 0) {
|
|
4120
4203
|
timer = setTimeout(() => {
|
|
4121
|
-
if (
|
|
4122
|
-
settled = true;
|
|
4123
|
-
const idx = this.queue.indexOf(resolver);
|
|
4124
|
-
if (idx !== -1) this.queue.splice(idx, 1);
|
|
4204
|
+
if (!abandon()) return;
|
|
4125
4205
|
reject(new Error(
|
|
4126
4206
|
`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.`
|
|
4127
4207
|
));
|
|
4128
4208
|
}, timeoutMs);
|
|
4129
4209
|
}
|
|
4210
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
4130
4211
|
this.queue.push(resolver);
|
|
4131
4212
|
});
|
|
4132
4213
|
}
|
|
@@ -4263,6 +4344,116 @@ function fileDeadModelPersistence(filePath) {
|
|
|
4263
4344
|
};
|
|
4264
4345
|
}
|
|
4265
4346
|
|
|
4347
|
+
// src/core/context/compaction.ts
|
|
4348
|
+
var CHARS_PER_TOKEN = 4;
|
|
4349
|
+
function estimateTokens(text) {
|
|
4350
|
+
if (!text) return 0;
|
|
4351
|
+
return Math.max(1, Math.ceil(text.length / CHARS_PER_TOKEN));
|
|
4352
|
+
}
|
|
4353
|
+
function contentToText(content) {
|
|
4354
|
+
if (typeof content === "string") return content;
|
|
4355
|
+
return content.map((c) => c.type === "text" ? c.text : c.type === "tool_result" ? c.content : `[${c.type}]`).join(" ");
|
|
4356
|
+
}
|
|
4357
|
+
function messagesTokens(messages) {
|
|
4358
|
+
return messages.reduce((n, m) => n + estimateTokens(contentToText(m.content)), 0);
|
|
4359
|
+
}
|
|
4360
|
+
function atomize(text, maxChars) {
|
|
4361
|
+
const seps = ["\n\n", "\n", ". ", " "];
|
|
4362
|
+
let units = [text];
|
|
4363
|
+
for (const sep of seps) {
|
|
4364
|
+
if (units.every((u) => u.length <= maxChars)) break;
|
|
4365
|
+
const next = [];
|
|
4366
|
+
for (const u of units) {
|
|
4367
|
+
if (u.length <= maxChars) {
|
|
4368
|
+
next.push(u);
|
|
4369
|
+
continue;
|
|
4370
|
+
}
|
|
4371
|
+
const parts = u.split(sep);
|
|
4372
|
+
parts.forEach((p, i) => next.push(i < parts.length - 1 ? p + sep : p));
|
|
4373
|
+
}
|
|
4374
|
+
units = next;
|
|
4375
|
+
}
|
|
4376
|
+
const out = [];
|
|
4377
|
+
for (const u of units) {
|
|
4378
|
+
if (u.length <= maxChars) out.push(u);
|
|
4379
|
+
else for (let i = 0; i < u.length; i += maxChars) out.push(u.slice(i, i + maxChars));
|
|
4380
|
+
}
|
|
4381
|
+
return out;
|
|
4382
|
+
}
|
|
4383
|
+
function chunkText(text, opts) {
|
|
4384
|
+
const maxChars = Math.max(1, Math.floor(opts.maxTokens * 4));
|
|
4385
|
+
const overlap = Math.min(maxChars - 1, Math.max(0, Math.floor(maxChars * (opts.overlapRatio ?? 0.1))));
|
|
4386
|
+
const trimmed = text.trim();
|
|
4387
|
+
if (!trimmed) return [];
|
|
4388
|
+
if (trimmed.length <= maxChars) return [trimmed];
|
|
4389
|
+
const units = atomize(trimmed, maxChars);
|
|
4390
|
+
const chunks = [];
|
|
4391
|
+
let cur = "";
|
|
4392
|
+
for (const u of units) {
|
|
4393
|
+
if (cur && cur.length + u.length > maxChars) {
|
|
4394
|
+
chunks.push(cur.trim());
|
|
4395
|
+
cur = overlap > 0 ? cur.slice(-overlap) : "";
|
|
4396
|
+
}
|
|
4397
|
+
cur += u;
|
|
4398
|
+
}
|
|
4399
|
+
if (cur.trim()) chunks.push(cur.trim());
|
|
4400
|
+
return chunks;
|
|
4401
|
+
}
|
|
4402
|
+
function needsCompaction(totalTokens, contextWindow, reserve = 0.2) {
|
|
4403
|
+
if (!contextWindow || contextWindow <= 0) return false;
|
|
4404
|
+
return totalTokens > contextWindow * (1 - reserve);
|
|
4405
|
+
}
|
|
4406
|
+
async function mapReduceCompact(text, opts) {
|
|
4407
|
+
const capChars = opts.capTokens * 4;
|
|
4408
|
+
let working = text;
|
|
4409
|
+
let truncated = false;
|
|
4410
|
+
if (working.length > capChars) {
|
|
4411
|
+
working = working.slice(0, capChars);
|
|
4412
|
+
truncated = true;
|
|
4413
|
+
}
|
|
4414
|
+
const instruction = opts.instruction ?? "Summarize this content, preserving key facts, names, numbers, and decisions.";
|
|
4415
|
+
let chunks = chunkText(working, { maxTokens: opts.chunkTokens });
|
|
4416
|
+
if (chunks.length <= 1) return { text: working, chunks: chunks.length, calls: 0, truncated };
|
|
4417
|
+
let calls = 0;
|
|
4418
|
+
let parts = await Promise.all(
|
|
4419
|
+
chunks.map((c, i) => {
|
|
4420
|
+
calls++;
|
|
4421
|
+
return opts.summarize(c, `${instruction} (part ${i + 1} of ${chunks.length})`);
|
|
4422
|
+
})
|
|
4423
|
+
);
|
|
4424
|
+
let combined = parts.join("\n\n");
|
|
4425
|
+
const maxPasses = opts.maxReducePasses ?? 3;
|
|
4426
|
+
for (let pass = 0; pass < maxPasses && estimateTokens(combined) > opts.targetTokens; pass++) {
|
|
4427
|
+
const groups = chunkText(combined, { maxTokens: opts.chunkTokens });
|
|
4428
|
+
if (groups.length <= 1) break;
|
|
4429
|
+
parts = await Promise.all(
|
|
4430
|
+
groups.map((g) => {
|
|
4431
|
+
calls++;
|
|
4432
|
+
return opts.summarize(g, "Merge these partial summaries into one cohesive summary; keep all distinct facts.");
|
|
4433
|
+
})
|
|
4434
|
+
);
|
|
4435
|
+
combined = parts.join("\n\n");
|
|
4436
|
+
}
|
|
4437
|
+
const note = truncated ? "\n\n[Note: input exceeded the extended-context cap and was truncated.]" : "";
|
|
4438
|
+
return { text: combined + note, chunks: chunks.length, calls, truncated };
|
|
4439
|
+
}
|
|
4440
|
+
async function rollingSummary(messages, opts) {
|
|
4441
|
+
if (messages.length <= opts.keepRecent) return messages;
|
|
4442
|
+
const older = messages.slice(0, messages.length - opts.keepRecent);
|
|
4443
|
+
const recent = messages.slice(messages.length - opts.keepRecent);
|
|
4444
|
+
const transcript = older.map((m) => `${m.role}: ${contentToText(m.content)}`).join("\n");
|
|
4445
|
+
const summary = await opts.summarize(
|
|
4446
|
+
transcript,
|
|
4447
|
+
"Summarize this earlier conversation. Preserve facts, decisions, names, numbers, and any open threads or todos."
|
|
4448
|
+
);
|
|
4449
|
+
const summaryMessage = {
|
|
4450
|
+
role: "system",
|
|
4451
|
+
content: `Summary of earlier conversation (folded to fit the context window):
|
|
4452
|
+
${summary}`
|
|
4453
|
+
};
|
|
4454
|
+
return [summaryMessage, ...recent];
|
|
4455
|
+
}
|
|
4456
|
+
|
|
4266
4457
|
// src/utils/retry.ts
|
|
4267
4458
|
var CascadeCancelledError = class extends Error {
|
|
4268
4459
|
constructor(reason) {
|
|
@@ -4297,6 +4488,158 @@ async function withTimeout(promise, timeoutMs, errorMessage = "Operation timed o
|
|
|
4297
4488
|
if (timer !== void 0) clearTimeout(timer);
|
|
4298
4489
|
}
|
|
4299
4490
|
}
|
|
4491
|
+
async function withTimeoutAbort(run, timeoutMs, errorMessage = "Operation timed out", outer) {
|
|
4492
|
+
const outers = (Array.isArray(outer) ? outer : [outer]).filter((sig) => sig !== void 0);
|
|
4493
|
+
const controller = new AbortController();
|
|
4494
|
+
let timer;
|
|
4495
|
+
let rejectRace;
|
|
4496
|
+
const settle = (reason) => {
|
|
4497
|
+
rejectRace?.(reason);
|
|
4498
|
+
controller.abort(reason);
|
|
4499
|
+
};
|
|
4500
|
+
const abortOuter = () => {
|
|
4501
|
+
const fired = outers.find((sig) => sig.aborted);
|
|
4502
|
+
settle(fired?.reason instanceof Error ? fired.reason : new CascadeCancelledError("Run cancelled"));
|
|
4503
|
+
};
|
|
4504
|
+
const failPromise = new Promise((_, reject) => {
|
|
4505
|
+
rejectRace = reject;
|
|
4506
|
+
timer = setTimeout(() => settle(new Error(errorMessage)), timeoutMs);
|
|
4507
|
+
});
|
|
4508
|
+
try {
|
|
4509
|
+
if (outers.some((sig) => sig.aborted)) {
|
|
4510
|
+
abortOuter();
|
|
4511
|
+
return await failPromise;
|
|
4512
|
+
}
|
|
4513
|
+
for (const sig of outers) sig.addEventListener("abort", abortOuter, { once: true });
|
|
4514
|
+
return await Promise.race([run(controller.signal), failPromise]);
|
|
4515
|
+
} finally {
|
|
4516
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
4517
|
+
for (const sig of outers) sig.removeEventListener("abort", abortOuter);
|
|
4518
|
+
}
|
|
4519
|
+
}
|
|
4520
|
+
function anySignal(sources) {
|
|
4521
|
+
const live = sources.filter((s) => s !== void 0);
|
|
4522
|
+
if (live.length === 0) return { release: () => {
|
|
4523
|
+
} };
|
|
4524
|
+
if (live.length === 1) return { signal: live[0], release: () => {
|
|
4525
|
+
} };
|
|
4526
|
+
const controller = new AbortController();
|
|
4527
|
+
function detach() {
|
|
4528
|
+
for (const s of live) s.removeEventListener("abort", onAbort);
|
|
4529
|
+
}
|
|
4530
|
+
function onAbort() {
|
|
4531
|
+
const fired = live.find((s) => s.aborted);
|
|
4532
|
+
controller.abort(fired?.reason instanceof Error ? fired.reason : new CascadeCancelledError("Run cancelled"));
|
|
4533
|
+
detach();
|
|
4534
|
+
}
|
|
4535
|
+
if (live.some((s) => s.aborted)) {
|
|
4536
|
+
onAbort();
|
|
4537
|
+
return { signal: controller.signal, release: detach };
|
|
4538
|
+
}
|
|
4539
|
+
for (const s of live) s.addEventListener("abort", onAbort, { once: true });
|
|
4540
|
+
return { signal: controller.signal, release: detach };
|
|
4541
|
+
}
|
|
4542
|
+
|
|
4543
|
+
// src/core/router/wire-profile.ts
|
|
4544
|
+
var textAndImageOnly = (block) => block.type === "text" || block.type === "image";
|
|
4545
|
+
var ANTHROPIC = {
|
|
4546
|
+
// convertMessages: `if (m.role === 'system') continue` — history system
|
|
4547
|
+
// messages are skipped outright; only options.systemPrompt goes as system
|
|
4548
|
+
// input. Compaction emits exactly such a message, holding a summary of the
|
|
4549
|
+
// whole conversation, so charging it refused runs over input never sent.
|
|
4550
|
+
dropsMessage: (m) => m.role === "system",
|
|
4551
|
+
blockHandling: (m) => {
|
|
4552
|
+
if (m.role === "tool") return "stringified";
|
|
4553
|
+
if (m.role === "assistant") return "dropped";
|
|
4554
|
+
return "blocks";
|
|
4555
|
+
},
|
|
4556
|
+
sendsToolCalls: (m) => m.role === "assistant",
|
|
4557
|
+
sendsBlock: textAndImageOnly,
|
|
4558
|
+
sendsToolCallId: true,
|
|
4559
|
+
sizeToolCalls: toAnthropicToolUse,
|
|
4560
|
+
sendsUrlImages: true,
|
|
4561
|
+
readsTopLevelImages: false,
|
|
4562
|
+
sizeTools: toAnthropicTools
|
|
4563
|
+
};
|
|
4564
|
+
var GEMINI = {
|
|
4565
|
+
// buildContents folds a system message into the next user turn, but only
|
|
4566
|
+
// when it has text: array content collapses to '' and the message is
|
|
4567
|
+
// skipped, as is a whitespace-only string.
|
|
4568
|
+
dropsMessage: (m) => m.role === "system" && !(typeof m.content === "string" ? m.content : "").trim(),
|
|
4569
|
+
blockHandling: (m) => {
|
|
4570
|
+
if (m.role === "tool") return "stringified";
|
|
4571
|
+
if (m.role === "assistant" || m.role === "system") return "dropped";
|
|
4572
|
+
return "blocks";
|
|
4573
|
+
},
|
|
4574
|
+
sendsToolCalls: (m) => m.role === "assistant",
|
|
4575
|
+
sendsBlock: textAndImageOnly,
|
|
4576
|
+
sendsToolCallId: true,
|
|
4577
|
+
sizeToolCalls: toGeminiFunctionCalls,
|
|
4578
|
+
sendsUrlImages: false,
|
|
4579
|
+
readsTopLevelImages: true,
|
|
4580
|
+
sizeTools: toGeminiTools
|
|
4581
|
+
};
|
|
4582
|
+
var OPENAI = {
|
|
4583
|
+
dropsMessage: () => false,
|
|
4584
|
+
blockHandling: (m) => {
|
|
4585
|
+
if (m.role === "system" || m.role === "tool") return "dropped";
|
|
4586
|
+
return "blocks";
|
|
4587
|
+
},
|
|
4588
|
+
// The tool_calls branch is inside `if (typeof m.content === 'string')`; an
|
|
4589
|
+
// assistant turn with array content falls through to the parts branch and
|
|
4590
|
+
// its tool calls are never attached.
|
|
4591
|
+
sendsToolCalls: (m) => m.role === "assistant" && typeof m.content === "string",
|
|
4592
|
+
sendsBlock: textAndImageOnly,
|
|
4593
|
+
sendsToolCallId: true,
|
|
4594
|
+
sizeToolCalls: toOpenAIToolCalls,
|
|
4595
|
+
sendsUrlImages: true,
|
|
4596
|
+
readsTopLevelImages: false,
|
|
4597
|
+
sizeTools: toOpenAITools
|
|
4598
|
+
};
|
|
4599
|
+
var OLLAMA = {
|
|
4600
|
+
dropsMessage: () => false,
|
|
4601
|
+
blockHandling: (m) => {
|
|
4602
|
+
if (m.role === "system") return "dropped";
|
|
4603
|
+
if (m.role === "tool") return "stringified";
|
|
4604
|
+
if (m.role === "assistant" && m.toolCalls?.length) return "dropped";
|
|
4605
|
+
return "blocks";
|
|
4606
|
+
},
|
|
4607
|
+
sendsToolCalls: (m) => m.role === "assistant",
|
|
4608
|
+
// The image split maps `b.image.data` with no check on `type`, so a URL is
|
|
4609
|
+
// submitted too (as a URL string in the `images` array).
|
|
4610
|
+
sendsBlock: textAndImageOnly,
|
|
4611
|
+
sendsToolCallId: false,
|
|
4612
|
+
sizeToolCalls: toOllamaToolCalls,
|
|
4613
|
+
sendsUrlImages: true,
|
|
4614
|
+
readsTopLevelImages: false,
|
|
4615
|
+
sizeTools: toOllamaTools
|
|
4616
|
+
};
|
|
4617
|
+
var PROFILES = {
|
|
4618
|
+
anthropic: ANTHROPIC,
|
|
4619
|
+
gemini: GEMINI,
|
|
4620
|
+
openai: OPENAI,
|
|
4621
|
+
// AzureOpenAIProvider and OpenAICompatibleProvider both extend OpenAIProvider
|
|
4622
|
+
// and neither overrides convertMessages.
|
|
4623
|
+
azure: OPENAI,
|
|
4624
|
+
"openai-compatible": OPENAI,
|
|
4625
|
+
ollama: OLLAMA
|
|
4626
|
+
};
|
|
4627
|
+
function wireProfile(provider) {
|
|
4628
|
+
return PROFILES[provider] ?? OPENAI;
|
|
4629
|
+
}
|
|
4630
|
+
function geminiImageCopies(messages) {
|
|
4631
|
+
for (const m of messages) {
|
|
4632
|
+
if (m.role === "user") {
|
|
4633
|
+
const laterUserTurn = messages.slice(messages.indexOf(m) + 1).some((x) => x.role === "user");
|
|
4634
|
+
return laterUserTurn ? 2 : 1;
|
|
4635
|
+
}
|
|
4636
|
+
const text = typeof m.content === "string" ? m.content : "";
|
|
4637
|
+
if (m.role === "system" && text.trim()) return 1;
|
|
4638
|
+
if (m.role === "tool") return 1;
|
|
4639
|
+
if (m.role === "assistant" && (text || m.toolCalls?.length)) return 1;
|
|
4640
|
+
}
|
|
4641
|
+
return 1;
|
|
4642
|
+
}
|
|
4300
4643
|
|
|
4301
4644
|
// src/core/router/model-profiler.ts
|
|
4302
4645
|
var SKIP_PATTERN = /embed|dall-e|whisper|tts|vision|instruct-vision|rerank/i;
|
|
@@ -4801,6 +5144,36 @@ function azureModelForTier(tier, ranked) {
|
|
|
4801
5144
|
if (tier === "T3") return ranked[ranked.length - 1];
|
|
4802
5145
|
return ranked[Math.min(1, ranked.length - 1)];
|
|
4803
5146
|
}
|
|
5147
|
+
var IMAGE_TOKENS_MAX = 2e3;
|
|
5148
|
+
var IMAGE_TOKENS_EACH = IMAGE_TOKENS_MAX;
|
|
5149
|
+
function safeJson(value) {
|
|
5150
|
+
try {
|
|
5151
|
+
return JSON.stringify(value) ?? "";
|
|
5152
|
+
} catch {
|
|
5153
|
+
return "";
|
|
5154
|
+
}
|
|
5155
|
+
}
|
|
5156
|
+
function newRunAbort() {
|
|
5157
|
+
const controller = new AbortController();
|
|
5158
|
+
EventEmitter.setMaxListeners(64, controller.signal);
|
|
5159
|
+
return controller;
|
|
5160
|
+
}
|
|
5161
|
+
var TOKENS_PER_MESSAGE_FRAMING = 4;
|
|
5162
|
+
function guardTokens(text) {
|
|
5163
|
+
if (!text) return 0;
|
|
5164
|
+
let asciiChars = 0;
|
|
5165
|
+
let denseBytes = 0;
|
|
5166
|
+
for (const ch of text) {
|
|
5167
|
+
const cp = ch.codePointAt(0);
|
|
5168
|
+
if (cp <= 127) {
|
|
5169
|
+
asciiChars++;
|
|
5170
|
+
continue;
|
|
5171
|
+
}
|
|
5172
|
+
denseBytes += cp <= 2047 ? 2 : cp <= 65535 ? 3 : 4;
|
|
5173
|
+
}
|
|
5174
|
+
if (denseBytes === 0) return estimateTokens(text);
|
|
5175
|
+
return Math.ceil(asciiChars / CHARS_PER_TOKEN) + denseBytes;
|
|
5176
|
+
}
|
|
4804
5177
|
var DISCOVERY_TTL_MS = 15 * 60 * 1e3;
|
|
4805
5178
|
var DISCOVERY_TIMEOUT_MS = 4e3;
|
|
4806
5179
|
var cloudDiscoveryCache = /* @__PURE__ */ new Map();
|
|
@@ -4853,6 +5226,33 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
4853
5226
|
// start of every `cascade run`, independent of the session-wide budget.
|
|
4854
5227
|
runTokens = 0;
|
|
4855
5228
|
runCostUsd = 0;
|
|
5229
|
+
/**
|
|
5230
|
+
* Budget held for calls that are IN FLIGHT — admitted by the preflight check
|
|
5231
|
+
* but not yet counted by recordStats. Without these, concurrent calls all
|
|
5232
|
+
* measure themselves against the same unspent allowance and are all admitted.
|
|
5233
|
+
* Released when each call settles; reset by beginRun() alongside the totals.
|
|
5234
|
+
*/
|
|
5235
|
+
reservedTokens = 0;
|
|
5236
|
+
reservedCostUsd = 0;
|
|
5237
|
+
/**
|
|
5238
|
+
* Fires when this run's budget is spent, cancelling work already in flight.
|
|
5239
|
+
*
|
|
5240
|
+
* The kill switch was previously only a flag, which stops calls that have
|
|
5241
|
+
* not yet submitted. A parallel wave's earlier members are already at the
|
|
5242
|
+
* provider by then, and they carried on generating for a run whose output
|
|
5243
|
+
* would be discarded. Every provider call chains to this, so tripping the
|
|
5244
|
+
* ceiling reaches the requests that are running, not just the ones queued.
|
|
5245
|
+
*/
|
|
5246
|
+
runAbort = newRunAbort();
|
|
5247
|
+
/**
|
|
5248
|
+
* Bumped by every beginRun(). A release handle remembers the generation it
|
|
5249
|
+
* was issued in and does nothing if that has moved on: beginRun() zeroes the
|
|
5250
|
+
* counters, so a call still in flight from the previous run would otherwise
|
|
5251
|
+
* subtract its estimate from the NEW run's totals and drive them negative —
|
|
5252
|
+
* leaving later preflights believing there is more allowance than the cap
|
|
5253
|
+
* actually holds, which is worse than no gate at all.
|
|
5254
|
+
*/
|
|
5255
|
+
runGeneration = 0;
|
|
4856
5256
|
runBudgetExceeded = false;
|
|
4857
5257
|
runBudgetExceededReason;
|
|
4858
5258
|
/**
|
|
@@ -5022,7 +5422,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
5022
5422
|
try {
|
|
5023
5423
|
const cfg = this.config.providers.find((p) => p.type === model.provider) ?? { type: model.provider };
|
|
5024
5424
|
const provider = this.createProvider(cfg, model);
|
|
5025
|
-
const result = await
|
|
5425
|
+
const result = await withTimeoutAbort((signal) => provider.generate({
|
|
5026
5426
|
messages: [{ role: "user", content: "Call the echo tool with text set to 'ping'. Use the tool; do not answer in prose." }],
|
|
5027
5427
|
tools: [{
|
|
5028
5428
|
name: "echo",
|
|
@@ -5030,7 +5430,8 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
5030
5430
|
inputSchema: { type: "object", properties: { text: { type: "string", description: "Text to echo" } }, required: ["text"] }
|
|
5031
5431
|
}],
|
|
5032
5432
|
maxTokens: 80,
|
|
5033
|
-
temperature: 0
|
|
5433
|
+
temperature: 0,
|
|
5434
|
+
signal
|
|
5034
5435
|
}), 3e4, "tool-support probe timed out");
|
|
5035
5436
|
return (result.toolCalls?.length ?? 0) > 0;
|
|
5036
5437
|
} catch {
|
|
@@ -5151,9 +5552,12 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
5151
5552
|
this.runBudgetExceededReason ?? "Per-task budget exceeded."
|
|
5152
5553
|
);
|
|
5153
5554
|
}
|
|
5555
|
+
const runGeneration = this.runGeneration;
|
|
5556
|
+
const runAbort = this.runAbort;
|
|
5557
|
+
const runSignal = this.runSignal;
|
|
5154
5558
|
options = applyTierLimits(options, tier, this.config?.tierLimits);
|
|
5155
|
-
if (
|
|
5156
|
-
options = { ...options, signal:
|
|
5559
|
+
if (runSignal && !options.signal) {
|
|
5560
|
+
options = { ...options, signal: runSignal };
|
|
5157
5561
|
}
|
|
5158
5562
|
if (options.model && !requireVision) {
|
|
5159
5563
|
this.ensureProvider(options.model, this.config.providers);
|
|
@@ -5175,58 +5579,84 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
5175
5579
|
if (!model) throw new Error(`No model available for tier ${tier}`);
|
|
5176
5580
|
const provider = this.getProvider(model);
|
|
5177
5581
|
if (!provider) throw new Error(`No provider for model ${model.id}`);
|
|
5582
|
+
let releaseReservation = this.enforcePreflightBudget(model, options);
|
|
5178
5583
|
const requestedTokens = options.maxTokens ?? model.maxOutputTokens ?? 1024;
|
|
5179
5584
|
const estimatedTokens = Math.min(requestedTokens, model.maxOutputTokens ?? requestedTokens) + 512;
|
|
5180
5585
|
if (this.tpmLimiter) {
|
|
5181
|
-
|
|
5586
|
+
const wait = anySignal([options.signal, runAbort.signal]);
|
|
5587
|
+
try {
|
|
5588
|
+
await this.tpmLimiter.acquire(model.provider, estimatedTokens, wait.signal);
|
|
5589
|
+
} catch (err) {
|
|
5590
|
+
releaseReservation?.();
|
|
5591
|
+
throw this.asAbortFailure(err, options.signal, runSignal);
|
|
5592
|
+
} finally {
|
|
5593
|
+
wait.release();
|
|
5594
|
+
}
|
|
5182
5595
|
}
|
|
5183
5596
|
const useStream = Boolean(onChunk) && model.supportsStreaming && typeof provider.generateStream === "function";
|
|
5184
5597
|
let releaseLocalSlot;
|
|
5185
5598
|
if (model.isLocal) {
|
|
5186
5599
|
const inferenceTimeoutMs = this.config.localInferenceTimeoutMs ?? 3e5;
|
|
5187
5600
|
const queueWaitMs = Math.round(inferenceTimeoutMs / 2);
|
|
5188
|
-
|
|
5601
|
+
const wait = anySignal([options.signal, runAbort.signal]);
|
|
5602
|
+
try {
|
|
5603
|
+
releaseLocalSlot = await this.localQueue.acquire(queueWaitMs, wait.signal);
|
|
5604
|
+
} catch (err) {
|
|
5605
|
+
releaseReservation?.();
|
|
5606
|
+
throw this.asAbortFailure(err, options.signal, runSignal);
|
|
5607
|
+
} finally {
|
|
5608
|
+
wait.release();
|
|
5609
|
+
}
|
|
5189
5610
|
}
|
|
5190
5611
|
try {
|
|
5612
|
+
if (runGeneration === this.runGeneration && this.runBudgetExceeded) {
|
|
5613
|
+
this.tpmLimiter?.refund(model.provider, estimatedTokens);
|
|
5614
|
+
throw new _CascadeRouter.BudgetExceededError(
|
|
5615
|
+
this.runBudgetExceededReason ?? "Per-task budget exceeded."
|
|
5616
|
+
);
|
|
5617
|
+
}
|
|
5191
5618
|
let result;
|
|
5192
5619
|
if (model.isLocal) {
|
|
5193
5620
|
const inferenceTimeoutMs = this.config.localInferenceTimeoutMs ?? 3e5;
|
|
5194
|
-
|
|
5195
|
-
|
|
5196
|
-
|
|
5197
|
-
|
|
5198
|
-
|
|
5199
|
-
inferencePromise,
|
|
5621
|
+
result = await withTimeoutAbort(
|
|
5622
|
+
(signal) => useStream && onChunk ? provider.generateStream({ ...options, signal }, (chunk) => {
|
|
5623
|
+
const text = typeof chunk?.text === "string" ? chunk.text : "";
|
|
5624
|
+
if (text) onChunk({ ...chunk, text });
|
|
5625
|
+
}) : provider.generate({ ...options, signal }),
|
|
5200
5626
|
inferenceTimeoutMs,
|
|
5201
|
-
`Local model ${model.id} inference timed out after ${inferenceTimeoutMs}ms
|
|
5627
|
+
`Local model ${model.id} inference timed out after ${inferenceTimeoutMs}ms`,
|
|
5628
|
+
[options.signal, runAbort.signal]
|
|
5202
5629
|
);
|
|
5203
5630
|
} else if (useStream && onChunk) {
|
|
5204
5631
|
const cloudTimeoutMs = this.config.cloudInferenceTimeoutMs ?? 12e4;
|
|
5205
5632
|
try {
|
|
5206
|
-
result = await
|
|
5207
|
-
provider.generateStream(options, (chunk) => {
|
|
5633
|
+
result = await withTimeoutAbort(
|
|
5634
|
+
(signal) => provider.generateStream({ ...options, signal }, (chunk) => {
|
|
5208
5635
|
const text = typeof chunk?.text === "string" ? chunk.text : "";
|
|
5209
5636
|
if (text) onChunk({ ...chunk, text });
|
|
5210
5637
|
}),
|
|
5211
5638
|
cloudTimeoutMs,
|
|
5212
|
-
`Model ${model.id} stream timed out after ${cloudTimeoutMs}ms
|
|
5639
|
+
`Model ${model.id} stream timed out after ${cloudTimeoutMs}ms`,
|
|
5640
|
+
[options.signal, runAbort.signal]
|
|
5213
5641
|
);
|
|
5214
5642
|
} catch (streamErr) {
|
|
5215
|
-
if (streamErr instanceof Error && streamErr.name === "AbortError" ||
|
|
5643
|
+
if (streamErr instanceof Error && streamErr.name === "AbortError" || runSignal?.aborted || options.signal?.aborted) {
|
|
5216
5644
|
throw streamErr;
|
|
5217
5645
|
}
|
|
5218
|
-
result = await
|
|
5219
|
-
provider.generate(options),
|
|
5646
|
+
result = await withTimeoutAbort(
|
|
5647
|
+
(signal) => provider.generate({ ...options, signal }),
|
|
5220
5648
|
cloudTimeoutMs,
|
|
5221
|
-
`Model ${model.id} inference timed out after ${cloudTimeoutMs}ms
|
|
5649
|
+
`Model ${model.id} inference timed out after ${cloudTimeoutMs}ms`,
|
|
5650
|
+
[options.signal, runAbort.signal]
|
|
5222
5651
|
);
|
|
5223
5652
|
}
|
|
5224
5653
|
} else {
|
|
5225
5654
|
const cloudTimeoutMs = this.config.cloudInferenceTimeoutMs ?? 12e4;
|
|
5226
|
-
result = await
|
|
5227
|
-
provider.generate(options),
|
|
5655
|
+
result = await withTimeoutAbort(
|
|
5656
|
+
(signal) => provider.generate({ ...options, signal }),
|
|
5228
5657
|
cloudTimeoutMs,
|
|
5229
|
-
`Model ${model.id} inference timed out after ${cloudTimeoutMs}ms
|
|
5658
|
+
`Model ${model.id} inference timed out after ${cloudTimeoutMs}ms`,
|
|
5659
|
+
[options.signal, runAbort.signal]
|
|
5230
5660
|
);
|
|
5231
5661
|
}
|
|
5232
5662
|
const corrected = buildTokenUsage(
|
|
@@ -5245,11 +5675,12 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
5245
5675
|
if (!result || typeof result.content !== "string" || !result.usage) {
|
|
5246
5676
|
throw new Error(`Provider ${model.provider}:${model.id} returned an invalid generation result.`);
|
|
5247
5677
|
}
|
|
5248
|
-
this.recordStats(tier, model, result.usage, options.featureTag);
|
|
5678
|
+
this.recordStats(tier, model, result.usage, options.featureTag, runGeneration);
|
|
5249
5679
|
this.failover.recordSuccess(model.provider);
|
|
5250
5680
|
return result;
|
|
5251
5681
|
} catch (err) {
|
|
5252
|
-
if (err instanceof
|
|
5682
|
+
if (err instanceof _CascadeRouter.BudgetExceededError) throw err;
|
|
5683
|
+
if (err instanceof Error && err.name === "AbortError" || runSignal?.aborted || options.signal?.aborted) {
|
|
5253
5684
|
throw new CascadeCancelledError("Run cancelled");
|
|
5254
5685
|
}
|
|
5255
5686
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
@@ -5267,6 +5698,8 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
5267
5698
|
});
|
|
5268
5699
|
releaseLocalSlot?.();
|
|
5269
5700
|
releaseLocalSlot = void 0;
|
|
5701
|
+
releaseReservation?.();
|
|
5702
|
+
releaseReservation = void 0;
|
|
5270
5703
|
const retryOpts = options.model && options.model.id === model.id ? { ...options, model: void 0 } : options;
|
|
5271
5704
|
return this.generate(tier, retryOpts, onChunk, requireVision);
|
|
5272
5705
|
}
|
|
@@ -5288,6 +5721,8 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
5288
5721
|
});
|
|
5289
5722
|
releaseLocalSlot?.();
|
|
5290
5723
|
releaseLocalSlot = void 0;
|
|
5724
|
+
releaseReservation?.();
|
|
5725
|
+
releaseReservation = void 0;
|
|
5291
5726
|
const base = options.model && options.model.id === model.id ? { ...options, model: void 0 } : options;
|
|
5292
5727
|
const retryOpts = { ...base, _notFoundDepth: depth };
|
|
5293
5728
|
return this.generate(tier, retryOpts, onChunk, requireVision);
|
|
@@ -5296,6 +5731,8 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
5296
5731
|
throw err;
|
|
5297
5732
|
} finally {
|
|
5298
5733
|
releaseLocalSlot?.();
|
|
5734
|
+
releaseReservation?.();
|
|
5735
|
+
releaseReservation = void 0;
|
|
5299
5736
|
}
|
|
5300
5737
|
}
|
|
5301
5738
|
getModelForTier(tier) {
|
|
@@ -5666,7 +6103,14 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
5666
6103
|
}
|
|
5667
6104
|
return void 0;
|
|
5668
6105
|
}
|
|
5669
|
-
|
|
6106
|
+
/**
|
|
6107
|
+
* `runGeneration` is the generation the call was ADMITTED in — see
|
|
6108
|
+
* `generate()`. Session-wide accounting always applies (the spend is real
|
|
6109
|
+
* whenever it lands), but the per-RUN ceiling is only meaningful for the run
|
|
6110
|
+
* the call belongs to, so a straggler from a finished run is left out of it.
|
|
6111
|
+
* Omitted by the direct callers in tests, which have no run to straddle.
|
|
6112
|
+
*/
|
|
6113
|
+
recordStats(tier, model, usage, featureTag, runGeneration) {
|
|
5670
6114
|
this.stats.totalTokens += usage.totalTokens;
|
|
5671
6115
|
this.stats.totalCostUsd += usage.estimatedCostUsd;
|
|
5672
6116
|
this.sessionCostUsd += usage.estimatedCostUsd;
|
|
@@ -5691,10 +6135,13 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
5691
6135
|
});
|
|
5692
6136
|
}
|
|
5693
6137
|
}
|
|
5694
|
-
|
|
5695
|
-
|
|
6138
|
+
const currentRun = runGeneration === void 0 || runGeneration === this.runGeneration;
|
|
6139
|
+
if (currentRun) {
|
|
6140
|
+
this.runTokens += usage.totalTokens;
|
|
6141
|
+
this.runCostUsd += usage.estimatedCostUsd;
|
|
6142
|
+
}
|
|
5696
6143
|
this.updateBudgetState();
|
|
5697
|
-
this.enforceRunBudget();
|
|
6144
|
+
if (currentRun) this.enforceRunBudget();
|
|
5698
6145
|
}
|
|
5699
6146
|
/**
|
|
5700
6147
|
* Resets per-run accounting at the start of each `cascade run`. Session
|
|
@@ -5704,9 +6151,158 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
5704
6151
|
beginRun() {
|
|
5705
6152
|
this.runTokens = 0;
|
|
5706
6153
|
this.runCostUsd = 0;
|
|
6154
|
+
this.reservedTokens = 0;
|
|
6155
|
+
this.reservedCostUsd = 0;
|
|
6156
|
+
this.runAbort = newRunAbort();
|
|
6157
|
+
this.runGeneration++;
|
|
5707
6158
|
this.runBudgetExceeded = false;
|
|
5708
6159
|
this.runBudgetExceededReason = void 0;
|
|
5709
6160
|
}
|
|
6161
|
+
/**
|
|
6162
|
+
* Refuse a call whose input alone cannot fit what is left of the run budget.
|
|
6163
|
+
*
|
|
6164
|
+
* `enforceRunBudget()` below runs AFTER a call returns, which makes the caps
|
|
6165
|
+
* a stop rather than a pre-authorisation: the tokens are already bought by
|
|
6166
|
+
* the time it looks. That was tolerable while a prompt could not exceed
|
|
6167
|
+
* 20,000 characters, and stopped being so when that cap was removed in
|
|
6168
|
+
* 0.72.0 — a multi-megabyte prompt is billed in full on the very first call
|
|
6169
|
+
* (the complexity classifier sends the whole thing), and no ceiling
|
|
6170
|
+
* configured downstream can give that money back.
|
|
6171
|
+
*
|
|
6172
|
+
* Deliberately narrow, because a false refusal is worse than a late stop:
|
|
6173
|
+
*
|
|
6174
|
+
* • INPUT ONLY. What a call returns is not knowable in advance, and
|
|
6175
|
+
* charging a worst-case `maxTokens` of output against the budget would
|
|
6176
|
+
* decline runs that would have finished comfortably. Input is the part
|
|
6177
|
+
* that is already determined, and it is the part the removed cap used to
|
|
6178
|
+
* bound.
|
|
6179
|
+
* • Compared against what REMAINS (`cap - spent`), not the whole cap, so a
|
|
6180
|
+
* call is judged on the budget it can actually draw from.
|
|
6181
|
+
* • Skipped when the model has no usable price. An estimate cannot be made,
|
|
6182
|
+
* and refusing on ignorance would break every self-hosted and local model
|
|
6183
|
+
* the moment a cost cap is set. The post-hoc stop still applies there.
|
|
6184
|
+
* • Skipped entirely when no cap is configured — there is nothing to
|
|
6185
|
+
* pre-authorise against.
|
|
6186
|
+
*
|
|
6187
|
+
* The message names the cap AND the estimate, because "too expensive" with
|
|
6188
|
+
* no numbers gives the user nothing to change.
|
|
6189
|
+
*/
|
|
6190
|
+
enforcePreflightBudget(model, options) {
|
|
6191
|
+
const budget = this.config?.budget;
|
|
6192
|
+
const maxCost = budget?.maxCostPerRunUsd;
|
|
6193
|
+
const maxTokens = budget?.maxTokensPerRun;
|
|
6194
|
+
if (maxCost == null && maxTokens == null) return void 0;
|
|
6195
|
+
const inputTokens = this.estimateInputTokens(options, model);
|
|
6196
|
+
if (maxTokens != null) {
|
|
6197
|
+
const remaining = maxTokens - this.runTokens - this.reservedTokens;
|
|
6198
|
+
if (inputTokens > remaining) {
|
|
6199
|
+
this.failPreflight(
|
|
6200
|
+
`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.`
|
|
6201
|
+
);
|
|
6202
|
+
}
|
|
6203
|
+
}
|
|
6204
|
+
let estimatedUsd = 0;
|
|
6205
|
+
if (maxCost != null) {
|
|
6206
|
+
const { input: inputPer1k, unknown } = resolveModelPricing(model, { inputTokens });
|
|
6207
|
+
if (!unknown && inputPer1k > 0) {
|
|
6208
|
+
estimatedUsd = inputTokens / 1e3 * inputPer1k;
|
|
6209
|
+
const remaining = maxCost - this.runCostUsd - this.reservedCostUsd;
|
|
6210
|
+
if (estimatedUsd > remaining) {
|
|
6211
|
+
this.failPreflight(
|
|
6212
|
+
`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.`
|
|
6213
|
+
);
|
|
6214
|
+
}
|
|
6215
|
+
}
|
|
6216
|
+
}
|
|
6217
|
+
this.reservedTokens += inputTokens;
|
|
6218
|
+
this.reservedCostUsd += estimatedUsd;
|
|
6219
|
+
const generation = this.runGeneration;
|
|
6220
|
+
let released = false;
|
|
6221
|
+
return () => {
|
|
6222
|
+
if (released || generation !== this.runGeneration) return;
|
|
6223
|
+
released = true;
|
|
6224
|
+
this.reservedTokens -= inputTokens;
|
|
6225
|
+
this.reservedCostUsd -= estimatedUsd;
|
|
6226
|
+
};
|
|
6227
|
+
}
|
|
6228
|
+
/**
|
|
6229
|
+
* What this call will send, in tokens.
|
|
6230
|
+
*
|
|
6231
|
+
* Text is the bulk of it, but not all of it: providers bill the serialized
|
|
6232
|
+
* TOOL DEFINITIONS on every native-tool call (Anthropic sends each name,
|
|
6233
|
+
* description and input schema), and an image is billed by its own size
|
|
6234
|
+
* rather than as the `[image]` placeholder `contentToText` reduces it to. A
|
|
6235
|
+
* tool-heavy or vision request could therefore pass either cap while its real
|
|
6236
|
+
* input already exceeded the allowance.
|
|
6237
|
+
*/
|
|
6238
|
+
estimateInputTokens(options, model) {
|
|
6239
|
+
let tokens = guardTokens(options.systemPrompt ?? "");
|
|
6240
|
+
let images = 0;
|
|
6241
|
+
const wire = wireProfile(model.provider);
|
|
6242
|
+
const countsImage = (img) => wire.sendsUrlImages || img?.type === "base64";
|
|
6243
|
+
for (const message of options.messages) {
|
|
6244
|
+
if (wire.dropsMessage(message)) continue;
|
|
6245
|
+
tokens += TOKENS_PER_MESSAGE_FRAMING;
|
|
6246
|
+
if (typeof message.content === "string") {
|
|
6247
|
+
tokens += guardTokens(message.content);
|
|
6248
|
+
} else {
|
|
6249
|
+
switch (wire.blockHandling(message)) {
|
|
6250
|
+
case "blocks":
|
|
6251
|
+
for (const block of message.content) {
|
|
6252
|
+
if (!wire.sendsBlock(block)) continue;
|
|
6253
|
+
if (block.type === "image") {
|
|
6254
|
+
if (countsImage(block.image)) images++;
|
|
6255
|
+
continue;
|
|
6256
|
+
}
|
|
6257
|
+
tokens += guardTokens(contentToText([block]));
|
|
6258
|
+
}
|
|
6259
|
+
break;
|
|
6260
|
+
case "stringified":
|
|
6261
|
+
tokens += guardTokens(safeJson(message.content));
|
|
6262
|
+
break;
|
|
6263
|
+
}
|
|
6264
|
+
}
|
|
6265
|
+
if (message.toolCalls?.length && wire.sendsToolCalls(message)) {
|
|
6266
|
+
tokens += guardTokens(safeJson(wire.sizeToolCalls(message.toolCalls)));
|
|
6267
|
+
}
|
|
6268
|
+
if (message.toolCallId && message.role === "tool" && wire.sendsToolCallId) {
|
|
6269
|
+
tokens += guardTokens(message.toolCallId);
|
|
6270
|
+
}
|
|
6271
|
+
}
|
|
6272
|
+
if (options.tools?.length) {
|
|
6273
|
+
tokens += guardTokens(safeJson(wire.sizeTools(options.tools)));
|
|
6274
|
+
}
|
|
6275
|
+
if (wire.readsTopLevelImages) {
|
|
6276
|
+
const topLevel = (options.images ?? []).filter(countsImage).length;
|
|
6277
|
+
images += topLevel * geminiImageCopies(options.messages);
|
|
6278
|
+
}
|
|
6279
|
+
tokens += images * IMAGE_TOKENS_EACH;
|
|
6280
|
+
return tokens;
|
|
6281
|
+
}
|
|
6282
|
+
/**
|
|
6283
|
+
* Report a wait that ended because something asked it to stop, the same way
|
|
6284
|
+
* the provider call below reports one.
|
|
6285
|
+
*
|
|
6286
|
+
* The queue waits sit OUTSIDE the try/catch that maps aborts, so without
|
|
6287
|
+
* this a cancellation surfaced as whatever the queue happened to throw and
|
|
6288
|
+
* was retried or failed over like an ordinary error. A budget abort keeps
|
|
6289
|
+
* saying it ran out of budget — otherwise the reason the run stopped is lost
|
|
6290
|
+
* on the way up — and anything else that was aborted becomes a cancellation.
|
|
6291
|
+
*/
|
|
6292
|
+
asAbortFailure(err, callSignal, runSignal) {
|
|
6293
|
+
if (err instanceof _CascadeRouter.BudgetExceededError) return err;
|
|
6294
|
+
if (err instanceof CascadeCancelledError) return err;
|
|
6295
|
+
if (callSignal?.aborted || runSignal?.aborted) return new CascadeCancelledError("Run cancelled");
|
|
6296
|
+
return err;
|
|
6297
|
+
}
|
|
6298
|
+
/** Trips the same run-level flag a post-hoc overrun does, and reports it the same way. */
|
|
6299
|
+
failPreflight(reason) {
|
|
6300
|
+
this.runBudgetExceeded = true;
|
|
6301
|
+
this.runBudgetExceededReason = reason;
|
|
6302
|
+
this.runAbort.abort(new _CascadeRouter.BudgetExceededError(reason));
|
|
6303
|
+
this.emit("budget:exceeded", { reason, spentUsd: this.sessionCostUsd });
|
|
6304
|
+
throw new _CascadeRouter.BudgetExceededError(reason);
|
|
6305
|
+
}
|
|
5710
6306
|
/**
|
|
5711
6307
|
* Enforce the hard per-task ceiling. Once tripped, the flag makes every
|
|
5712
6308
|
* subsequent (and concurrent) generate() call in this run fail fast.
|
|
@@ -5722,6 +6318,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
5722
6318
|
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.`;
|
|
5723
6319
|
this.runBudgetExceeded = true;
|
|
5724
6320
|
this.runBudgetExceededReason = reason;
|
|
6321
|
+
this.runAbort.abort(new _CascadeRouter.BudgetExceededError(reason));
|
|
5725
6322
|
this.emit("budget:exceeded", { reason, spentUsd: this.sessionCostUsd });
|
|
5726
6323
|
throw new _CascadeRouter.BudgetExceededError(reason);
|
|
5727
6324
|
}
|
|
@@ -6958,6 +7555,12 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
|
|
|
6958
7555
|
maxTokens: 4096,
|
|
6959
7556
|
...subtaskModel ? { model: subtaskModel } : {},
|
|
6960
7557
|
featureTag: this.assignment?.sectionTitle,
|
|
7558
|
+
// The wave signal, so cancel-and-respawn reaches the request that is
|
|
7559
|
+
// RUNNING rather than only stopping the worker at its next checkpoint.
|
|
7560
|
+
// It is a superset of the run signal the router injects when this is
|
|
7561
|
+
// absent (T2 composes it from that signal plus its own per-wave one),
|
|
7562
|
+
// so wiring it here loses no cancellation and adds the wave's.
|
|
7563
|
+
...this.signal ? { signal: this.signal } : {},
|
|
6961
7564
|
...this.localOnlyMatch ? { forceLocal: true } : {}
|
|
6962
7565
|
};
|
|
6963
7566
|
const result = await this.router.generate(
|
|
@@ -7436,6 +8039,7 @@ ${current}`
|
|
|
7436
8039
|
HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
7437
8040
|
maxTokens: 4096,
|
|
7438
8041
|
featureTag: assignment.sectionTitle,
|
|
8042
|
+
...this.signal ? { signal: this.signal } : {},
|
|
7439
8043
|
...this.localOnlyMatch ? { forceLocal: true } : {}
|
|
7440
8044
|
});
|
|
7441
8045
|
const next = (improved.content ?? "").trim();
|
|
@@ -7497,6 +8101,7 @@ Reply with JSON: { "completeness": "pass"|"fail", "correctness": "pass"|"fail",
|
|
|
7497
8101
|
|
|
7498
8102
|
HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
7499
8103
|
featureTag: assignment.sectionTitle,
|
|
8104
|
+
...this.signal ? { signal: this.signal } : {},
|
|
7500
8105
|
...this.localOnlyMatch ? { forceLocal: true } : {}
|
|
7501
8106
|
});
|
|
7502
8107
|
try {
|
|
@@ -7536,6 +8141,7 @@ ${output.slice(0, 4e3)}`;
|
|
|
7536
8141
|
messages: [{ role: "user", content: prompt }],
|
|
7537
8142
|
maxTokens: 300,
|
|
7538
8143
|
temperature: 0,
|
|
8144
|
+
...this.signal ? { signal: this.signal } : {},
|
|
7539
8145
|
...this.localOnlyMatch ? { forceLocal: true } : {}
|
|
7540
8146
|
});
|
|
7541
8147
|
const match = /\[[\s\S]*\]/.exec(result.content);
|
|
@@ -7647,6 +8253,10 @@ Begin execution now.`;
|
|
|
7647
8253
|
}
|
|
7648
8254
|
};
|
|
7649
8255
|
var PeerBus = class extends EventEmitter__default.default {
|
|
8256
|
+
constructor() {
|
|
8257
|
+
super();
|
|
8258
|
+
this.setMaxListeners(64);
|
|
8259
|
+
}
|
|
7650
8260
|
outputs = /* @__PURE__ */ new Map();
|
|
7651
8261
|
waiters = /* @__PURE__ */ new Map();
|
|
7652
8262
|
members = /* @__PURE__ */ new Set();
|
|
@@ -8894,6 +9504,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
8894
9504
|
const waveSignal = AbortSignal.any(
|
|
8895
9505
|
[this.signal, this.waveAbortController.signal].filter(Boolean)
|
|
8896
9506
|
);
|
|
9507
|
+
EventEmitter.setMaxListeners(64, waveSignal);
|
|
8897
9508
|
const runOne = async (id) => {
|
|
8898
9509
|
const assignment = sanitizedAssignments.find((a) => a.subtaskId === id);
|
|
8899
9510
|
const worker = workerMap.get(id);
|
|
@@ -16537,115 +17148,6 @@ var GraphSearchTool = class extends BaseTool {
|
|
|
16537
17148
|
}
|
|
16538
17149
|
};
|
|
16539
17150
|
|
|
16540
|
-
// src/core/context/compaction.ts
|
|
16541
|
-
function estimateTokens(text) {
|
|
16542
|
-
if (!text) return 0;
|
|
16543
|
-
return Math.max(1, Math.ceil(text.length / 4));
|
|
16544
|
-
}
|
|
16545
|
-
function contentToText(content) {
|
|
16546
|
-
if (typeof content === "string") return content;
|
|
16547
|
-
return content.map((c) => c.type === "text" ? c.text : c.type === "tool_result" ? c.content : `[${c.type}]`).join(" ");
|
|
16548
|
-
}
|
|
16549
|
-
function messagesTokens(messages) {
|
|
16550
|
-
return messages.reduce((n, m) => n + estimateTokens(contentToText(m.content)), 0);
|
|
16551
|
-
}
|
|
16552
|
-
function atomize(text, maxChars) {
|
|
16553
|
-
const seps = ["\n\n", "\n", ". ", " "];
|
|
16554
|
-
let units = [text];
|
|
16555
|
-
for (const sep of seps) {
|
|
16556
|
-
if (units.every((u) => u.length <= maxChars)) break;
|
|
16557
|
-
const next = [];
|
|
16558
|
-
for (const u of units) {
|
|
16559
|
-
if (u.length <= maxChars) {
|
|
16560
|
-
next.push(u);
|
|
16561
|
-
continue;
|
|
16562
|
-
}
|
|
16563
|
-
const parts = u.split(sep);
|
|
16564
|
-
parts.forEach((p, i) => next.push(i < parts.length - 1 ? p + sep : p));
|
|
16565
|
-
}
|
|
16566
|
-
units = next;
|
|
16567
|
-
}
|
|
16568
|
-
const out = [];
|
|
16569
|
-
for (const u of units) {
|
|
16570
|
-
if (u.length <= maxChars) out.push(u);
|
|
16571
|
-
else for (let i = 0; i < u.length; i += maxChars) out.push(u.slice(i, i + maxChars));
|
|
16572
|
-
}
|
|
16573
|
-
return out;
|
|
16574
|
-
}
|
|
16575
|
-
function chunkText(text, opts) {
|
|
16576
|
-
const maxChars = Math.max(1, Math.floor(opts.maxTokens * 4));
|
|
16577
|
-
const overlap = Math.min(maxChars - 1, Math.max(0, Math.floor(maxChars * (opts.overlapRatio ?? 0.1))));
|
|
16578
|
-
const trimmed = text.trim();
|
|
16579
|
-
if (!trimmed) return [];
|
|
16580
|
-
if (trimmed.length <= maxChars) return [trimmed];
|
|
16581
|
-
const units = atomize(trimmed, maxChars);
|
|
16582
|
-
const chunks = [];
|
|
16583
|
-
let cur = "";
|
|
16584
|
-
for (const u of units) {
|
|
16585
|
-
if (cur && cur.length + u.length > maxChars) {
|
|
16586
|
-
chunks.push(cur.trim());
|
|
16587
|
-
cur = overlap > 0 ? cur.slice(-overlap) : "";
|
|
16588
|
-
}
|
|
16589
|
-
cur += u;
|
|
16590
|
-
}
|
|
16591
|
-
if (cur.trim()) chunks.push(cur.trim());
|
|
16592
|
-
return chunks;
|
|
16593
|
-
}
|
|
16594
|
-
function needsCompaction(totalTokens, contextWindow, reserve = 0.2) {
|
|
16595
|
-
if (!contextWindow || contextWindow <= 0) return false;
|
|
16596
|
-
return totalTokens > contextWindow * (1 - reserve);
|
|
16597
|
-
}
|
|
16598
|
-
async function mapReduceCompact(text, opts) {
|
|
16599
|
-
const capChars = opts.capTokens * 4;
|
|
16600
|
-
let working = text;
|
|
16601
|
-
let truncated = false;
|
|
16602
|
-
if (working.length > capChars) {
|
|
16603
|
-
working = working.slice(0, capChars);
|
|
16604
|
-
truncated = true;
|
|
16605
|
-
}
|
|
16606
|
-
const instruction = opts.instruction ?? "Summarize this content, preserving key facts, names, numbers, and decisions.";
|
|
16607
|
-
let chunks = chunkText(working, { maxTokens: opts.chunkTokens });
|
|
16608
|
-
if (chunks.length <= 1) return { text: working, chunks: chunks.length, calls: 0, truncated };
|
|
16609
|
-
let calls = 0;
|
|
16610
|
-
let parts = await Promise.all(
|
|
16611
|
-
chunks.map((c, i) => {
|
|
16612
|
-
calls++;
|
|
16613
|
-
return opts.summarize(c, `${instruction} (part ${i + 1} of ${chunks.length})`);
|
|
16614
|
-
})
|
|
16615
|
-
);
|
|
16616
|
-
let combined = parts.join("\n\n");
|
|
16617
|
-
const maxPasses = opts.maxReducePasses ?? 3;
|
|
16618
|
-
for (let pass = 0; pass < maxPasses && estimateTokens(combined) > opts.targetTokens; pass++) {
|
|
16619
|
-
const groups = chunkText(combined, { maxTokens: opts.chunkTokens });
|
|
16620
|
-
if (groups.length <= 1) break;
|
|
16621
|
-
parts = await Promise.all(
|
|
16622
|
-
groups.map((g) => {
|
|
16623
|
-
calls++;
|
|
16624
|
-
return opts.summarize(g, "Merge these partial summaries into one cohesive summary; keep all distinct facts.");
|
|
16625
|
-
})
|
|
16626
|
-
);
|
|
16627
|
-
combined = parts.join("\n\n");
|
|
16628
|
-
}
|
|
16629
|
-
const note = truncated ? "\n\n[Note: input exceeded the extended-context cap and was truncated.]" : "";
|
|
16630
|
-
return { text: combined + note, chunks: chunks.length, calls, truncated };
|
|
16631
|
-
}
|
|
16632
|
-
async function rollingSummary(messages, opts) {
|
|
16633
|
-
if (messages.length <= opts.keepRecent) return messages;
|
|
16634
|
-
const older = messages.slice(0, messages.length - opts.keepRecent);
|
|
16635
|
-
const recent = messages.slice(messages.length - opts.keepRecent);
|
|
16636
|
-
const transcript = older.map((m) => `${m.role}: ${contentToText(m.content)}`).join("\n");
|
|
16637
|
-
const summary = await opts.summarize(
|
|
16638
|
-
transcript,
|
|
16639
|
-
"Summarize this earlier conversation. Preserve facts, decisions, names, numbers, and any open threads or todos."
|
|
16640
|
-
);
|
|
16641
|
-
const summaryMessage = {
|
|
16642
|
-
role: "system",
|
|
16643
|
-
content: `Summary of earlier conversation (folded to fit the context window):
|
|
16644
|
-
${summary}`
|
|
16645
|
-
};
|
|
16646
|
-
return [summaryMessage, ...recent];
|
|
16647
|
-
}
|
|
16648
|
-
|
|
16649
17151
|
// src/core/steering/guidance.ts
|
|
16650
17152
|
var GuidanceQueue = class {
|
|
16651
17153
|
entries = [];
|
|
@@ -21788,10 +22290,10 @@ ${cur}`;
|
|
|
21788
22290
|
}
|
|
21789
22291
|
|
|
21790
22292
|
// src/retrieval/plan.ts
|
|
21791
|
-
var
|
|
22293
|
+
var CHARS_PER_TOKEN2 = 4;
|
|
21792
22294
|
function cagCharBudget(contextWindowTokens, opts = {}) {
|
|
21793
22295
|
const docFraction = opts.docFraction ?? 0.5;
|
|
21794
|
-
const charsPerToken = opts.charsPerToken ??
|
|
22296
|
+
const charsPerToken = opts.charsPerToken ?? CHARS_PER_TOKEN2;
|
|
21795
22297
|
return Math.max(0, Math.floor(contextWindowTokens * docFraction * charsPerToken));
|
|
21796
22298
|
}
|
|
21797
22299
|
function planRetrieval(ctx) {
|
|
@@ -22370,7 +22872,7 @@ exports.CASCADE_IGNORE_FILE = CASCADE_IGNORE_FILE;
|
|
|
22370
22872
|
exports.CASCADE_KEYSTORE_FILE = CASCADE_KEYSTORE_FILE;
|
|
22371
22873
|
exports.CASCADE_MD_FILE = CASCADE_MD_FILE;
|
|
22372
22874
|
exports.CASCADE_VERSION = CASCADE_VERSION;
|
|
22373
|
-
exports.CHARS_PER_TOKEN =
|
|
22875
|
+
exports.CHARS_PER_TOKEN = CHARS_PER_TOKEN2;
|
|
22374
22876
|
exports.COMPLEXITY_T2_COUNT = COMPLEXITY_T2_COUNT;
|
|
22375
22877
|
exports.Cascade = Cascade;
|
|
22376
22878
|
exports.CascadeCancelledError = CascadeCancelledError;
|