cascade-ai 0.71.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 +777 -237
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +777 -237
- package/dist/cli.js.map +1 -1
- package/dist/desktop-core.cjs +765 -237
- package/dist/index.cjs +764 -236
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +113 -0
- package/dist/index.d.ts +113 -0
- package/dist/index.js +764 -236
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1614,6 +1614,15 @@ function findPricing(modelId, provider, opts = {}) {
|
|
|
1614
1614
|
}
|
|
1615
1615
|
return null;
|
|
1616
1616
|
}
|
|
1617
|
+
function hasContextBands(model) {
|
|
1618
|
+
if (model.isLocal) return false;
|
|
1619
|
+
const fallbackProviders = model.provider === "azure" ? ["openai"] : model.provider === "gemini" ? ["vertex"] : [];
|
|
1620
|
+
const match = findPricing(model.id, model.provider, {
|
|
1621
|
+
baseModelId: model.baseModelId,
|
|
1622
|
+
fallbackProviders
|
|
1623
|
+
});
|
|
1624
|
+
return (match?.entry.tiers?.length ?? 0) > 1;
|
|
1625
|
+
}
|
|
1617
1626
|
function pricingDatasetMeta() {
|
|
1618
1627
|
const { schemaVersion, generatedAt, defaultCurrency } = DATASET;
|
|
1619
1628
|
return { schemaVersion, generatedAt, defaultCurrency };
|
|
@@ -1757,7 +1766,7 @@ var CASCADE_VERSION, CASCADE_CONFIG_FILE, CASCADE_DB_FILE, CASCADE_DASHBOARD_SEC
|
|
|
1757
1766
|
var init_constants = __esm({
|
|
1758
1767
|
"src/constants.ts"() {
|
|
1759
1768
|
init_pricing();
|
|
1760
|
-
CASCADE_VERSION = "0.
|
|
1769
|
+
CASCADE_VERSION = "0.73.0";
|
|
1761
1770
|
CASCADE_CONFIG_FILE = ".cascade/config.json";
|
|
1762
1771
|
CASCADE_DB_FILE = ".cascade/memory.db";
|
|
1763
1772
|
CASCADE_DASHBOARD_SECRET_FILE = ".cascade/dashboard-secret";
|
|
@@ -2188,8 +2197,23 @@ var init_constants = __esm({
|
|
|
2188
2197
|
});
|
|
2189
2198
|
|
|
2190
2199
|
// src/utils/cost.ts
|
|
2191
|
-
function resolveModelPricing(model) {
|
|
2200
|
+
function resolveModelPricing(model, opts = {}) {
|
|
2192
2201
|
if (model.isLocal) return { input: 0, output: 0, unknown: false };
|
|
2202
|
+
if (opts.inputTokens != null && hasContextBands(model)) {
|
|
2203
|
+
const banded = resolvePricing(model, { inputTokens: opts.inputTokens });
|
|
2204
|
+
const base = resolvePricing(model, { inputTokens: 0 });
|
|
2205
|
+
if (!banded.unknown && !base.unknown) {
|
|
2206
|
+
const scale = (stamped, baseRate) => stamped > 0 && baseRate > 0 ? stamped / baseRate : 1;
|
|
2207
|
+
const inScale = scale(model.inputCostPer1kTokens, base.input);
|
|
2208
|
+
const outScale = scale(model.outputCostPer1kTokens, base.output);
|
|
2209
|
+
return {
|
|
2210
|
+
input: banded.input * inScale,
|
|
2211
|
+
output: banded.output * outScale,
|
|
2212
|
+
unknown: false,
|
|
2213
|
+
...banded.estimatedFromProvider ? { estimatedFromProvider: banded.estimatedFromProvider } : {}
|
|
2214
|
+
};
|
|
2215
|
+
}
|
|
2216
|
+
}
|
|
2193
2217
|
if (model.inputCostPer1kTokens > 0 || model.outputCostPer1kTokens > 0) {
|
|
2194
2218
|
return {
|
|
2195
2219
|
input: model.inputCostPer1kTokens,
|
|
@@ -2197,7 +2221,7 @@ function resolveModelPricing(model) {
|
|
|
2197
2221
|
unknown: false
|
|
2198
2222
|
};
|
|
2199
2223
|
}
|
|
2200
|
-
const fromDataset = resolvePricing(model);
|
|
2224
|
+
const fromDataset = resolvePricing(model, { inputTokens: opts.inputTokens });
|
|
2201
2225
|
if (!fromDataset.unknown) {
|
|
2202
2226
|
return {
|
|
2203
2227
|
input: fromDataset.input,
|
|
@@ -2219,11 +2243,11 @@ function resolveModelPricing(model) {
|
|
|
2219
2243
|
return { input: 0, output: 0, unknown: true };
|
|
2220
2244
|
}
|
|
2221
2245
|
function calculateCost(inputTokens, outputTokens, model) {
|
|
2222
|
-
const { input, output } = resolveModelPricing(model);
|
|
2246
|
+
const { input, output } = resolveModelPricing(model, { inputTokens });
|
|
2223
2247
|
return inputTokens / 1e3 * input + outputTokens / 1e3 * output;
|
|
2224
2248
|
}
|
|
2225
2249
|
function buildTokenUsage(inputTokens, outputTokens, model) {
|
|
2226
|
-
const { input, output, unknown } = resolveModelPricing(model);
|
|
2250
|
+
const { input, output, unknown } = resolveModelPricing(model, { inputTokens });
|
|
2227
2251
|
return {
|
|
2228
2252
|
inputTokens,
|
|
2229
2253
|
outputTokens,
|
|
@@ -2315,7 +2339,9 @@ var init_model_filter = __esm({
|
|
|
2315
2339
|
// src/providers/anthropic.ts
|
|
2316
2340
|
var anthropic_exports = {};
|
|
2317
2341
|
__export(anthropic_exports, {
|
|
2318
|
-
AnthropicProvider: () => AnthropicProvider
|
|
2342
|
+
AnthropicProvider: () => AnthropicProvider,
|
|
2343
|
+
toAnthropicToolUse: () => toAnthropicToolUse,
|
|
2344
|
+
toAnthropicTools: () => toAnthropicTools
|
|
2319
2345
|
});
|
|
2320
2346
|
function anthropicThinkingParam(modelId, maxTokens) {
|
|
2321
2347
|
if (!/claude-(opus|sonnet)-4/i.test(modelId)) return {};
|
|
@@ -2323,6 +2349,21 @@ function anthropicThinkingParam(modelId, maxTokens) {
|
|
|
2323
2349
|
if (budget < 1024) return {};
|
|
2324
2350
|
return { thinking: { type: "enabled", budget_tokens: budget } };
|
|
2325
2351
|
}
|
|
2352
|
+
function toAnthropicTools(tools) {
|
|
2353
|
+
return tools.map((t) => ({
|
|
2354
|
+
name: t.name,
|
|
2355
|
+
description: t.description,
|
|
2356
|
+
input_schema: t.inputSchema
|
|
2357
|
+
}));
|
|
2358
|
+
}
|
|
2359
|
+
function toAnthropicToolUse(toolCalls) {
|
|
2360
|
+
return toolCalls.map((tc) => ({
|
|
2361
|
+
type: "tool_use",
|
|
2362
|
+
id: tc.id,
|
|
2363
|
+
name: tc.name,
|
|
2364
|
+
input: tc.input
|
|
2365
|
+
}));
|
|
2366
|
+
}
|
|
2326
2367
|
var AnthropicProvider;
|
|
2327
2368
|
var init_anthropic = __esm({
|
|
2328
2369
|
"src/providers/anthropic.ts"() {
|
|
@@ -2351,11 +2392,7 @@ var init_anthropic = __esm({
|
|
|
2351
2392
|
}
|
|
2352
2393
|
async generateStream(options, onChunk) {
|
|
2353
2394
|
const messages = this.convertMessages(options.messages);
|
|
2354
|
-
const tools = options.tools
|
|
2355
|
-
name: t.name,
|
|
2356
|
-
description: t.description,
|
|
2357
|
-
input_schema: t.inputSchema
|
|
2358
|
-
}));
|
|
2395
|
+
const tools = options.tools && toAnthropicTools(options.tools);
|
|
2359
2396
|
let fullContent = "";
|
|
2360
2397
|
let inputTokens = 0;
|
|
2361
2398
|
let outputTokens = 0;
|
|
@@ -2483,14 +2520,7 @@ var init_anthropic = __esm({
|
|
|
2483
2520
|
const content = [];
|
|
2484
2521
|
const text = typeof m.content === "string" ? m.content : "";
|
|
2485
2522
|
if (text) content.push({ type: "text", text });
|
|
2486
|
-
|
|
2487
|
-
content.push({
|
|
2488
|
-
type: "tool_use",
|
|
2489
|
-
id: tc.id,
|
|
2490
|
-
name: tc.name,
|
|
2491
|
-
input: tc.input
|
|
2492
|
-
});
|
|
2493
|
-
}
|
|
2523
|
+
content.push(...toAnthropicToolUse(m.toolCalls ?? []));
|
|
2494
2524
|
if (content.length > 0) {
|
|
2495
2525
|
result.push({ role: "assistant", content });
|
|
2496
2526
|
}
|
|
@@ -2529,7 +2559,9 @@ var openai_exports = {};
|
|
|
2529
2559
|
__export(openai_exports, {
|
|
2530
2560
|
OpenAIProvider: () => OpenAIProvider,
|
|
2531
2561
|
isParamShapeError: () => isParamShapeError,
|
|
2532
|
-
isReasoningModel: () => isReasoningModel
|
|
2562
|
+
isReasoningModel: () => isReasoningModel,
|
|
2563
|
+
toOpenAIToolCalls: () => toOpenAIToolCalls,
|
|
2564
|
+
toOpenAITools: () => toOpenAITools
|
|
2533
2565
|
});
|
|
2534
2566
|
function isReasoningModel(id) {
|
|
2535
2567
|
const m = (id || "").toLowerCase();
|
|
@@ -2540,6 +2572,26 @@ function isParamShapeError(err) {
|
|
|
2540
2572
|
const msg = String(e?.message ?? e?.error?.message ?? "").toLowerCase();
|
|
2541
2573
|
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"));
|
|
2542
2574
|
}
|
|
2575
|
+
function toOpenAITools(tools) {
|
|
2576
|
+
return tools.map((t) => ({
|
|
2577
|
+
type: "function",
|
|
2578
|
+
function: {
|
|
2579
|
+
name: t.name,
|
|
2580
|
+
description: t.description,
|
|
2581
|
+
parameters: t.inputSchema
|
|
2582
|
+
}
|
|
2583
|
+
}));
|
|
2584
|
+
}
|
|
2585
|
+
function toOpenAIToolCalls(toolCalls) {
|
|
2586
|
+
return toolCalls.map((tc) => ({
|
|
2587
|
+
id: tc.id,
|
|
2588
|
+
type: "function",
|
|
2589
|
+
function: {
|
|
2590
|
+
name: tc.name,
|
|
2591
|
+
arguments: JSON.stringify(tc.input)
|
|
2592
|
+
}
|
|
2593
|
+
}));
|
|
2594
|
+
}
|
|
2543
2595
|
var OpenAIProvider;
|
|
2544
2596
|
var init_openai = __esm({
|
|
2545
2597
|
"src/providers/openai.ts"() {
|
|
@@ -2566,14 +2618,7 @@ var init_openai = __esm({
|
|
|
2566
2618
|
}
|
|
2567
2619
|
async generateStream(options, onChunk) {
|
|
2568
2620
|
const messages = this.convertMessages(options.messages, options.systemPrompt);
|
|
2569
|
-
const tools = options.tools
|
|
2570
|
-
type: "function",
|
|
2571
|
-
function: {
|
|
2572
|
-
name: t.name,
|
|
2573
|
-
description: t.description,
|
|
2574
|
-
parameters: t.inputSchema
|
|
2575
|
-
}
|
|
2576
|
-
}));
|
|
2621
|
+
const tools = options.tools && toOpenAITools(options.tools);
|
|
2577
2622
|
let fullContent = "";
|
|
2578
2623
|
let inputTokens = 0;
|
|
2579
2624
|
let outputTokens = 0;
|
|
@@ -2719,14 +2764,7 @@ var init_openai = __esm({
|
|
|
2719
2764
|
result.push({
|
|
2720
2765
|
role: "assistant",
|
|
2721
2766
|
content: m.content || "",
|
|
2722
|
-
tool_calls: m.toolCalls
|
|
2723
|
-
id: toolCall.id,
|
|
2724
|
-
type: "function",
|
|
2725
|
-
function: {
|
|
2726
|
-
name: toolCall.name,
|
|
2727
|
-
arguments: JSON.stringify(toolCall.input)
|
|
2728
|
-
}
|
|
2729
|
-
}))
|
|
2767
|
+
tool_calls: toOpenAIToolCalls(m.toolCalls)
|
|
2730
2768
|
});
|
|
2731
2769
|
} else {
|
|
2732
2770
|
result.push({ role: m.role, content: m.content });
|
|
@@ -2993,8 +3031,30 @@ var init_gemini_schema = __esm({
|
|
|
2993
3031
|
// src/providers/gemini.ts
|
|
2994
3032
|
var gemini_exports = {};
|
|
2995
3033
|
__export(gemini_exports, {
|
|
2996
|
-
GeminiProvider: () => GeminiProvider
|
|
3034
|
+
GeminiProvider: () => GeminiProvider,
|
|
3035
|
+
toGeminiFunctionCalls: () => toGeminiFunctionCalls,
|
|
3036
|
+
toGeminiTools: () => toGeminiTools
|
|
2997
3037
|
});
|
|
3038
|
+
function toGeminiTools(tools) {
|
|
3039
|
+
return tools.map((tool) => ({
|
|
3040
|
+
name: tool.name,
|
|
3041
|
+
description: tool.description,
|
|
3042
|
+
// Convert, never cast. Gemini's parameters field is an OpenAPI subset
|
|
3043
|
+
// that 400s on unknown keys, and MCP servers ship schemas carrying their
|
|
3044
|
+
// own extensions (GitHub's adds `x-mcp-header` to every header-bound
|
|
3045
|
+
// property). Casting sent those through verbatim and every request failed
|
|
3046
|
+
// before the model saw it. See providers/gemini-schema.ts.
|
|
3047
|
+
parameters: toGeminiParameters(tool.inputSchema)
|
|
3048
|
+
}));
|
|
3049
|
+
}
|
|
3050
|
+
function toGeminiFunctionCalls(toolCalls) {
|
|
3051
|
+
return toolCalls.map((tc) => ({
|
|
3052
|
+
functionCall: {
|
|
3053
|
+
name: tc.name,
|
|
3054
|
+
args: tc.input
|
|
3055
|
+
}
|
|
3056
|
+
}));
|
|
3057
|
+
}
|
|
2998
3058
|
var GeminiProvider;
|
|
2999
3059
|
var init_gemini = __esm({
|
|
3000
3060
|
"src/providers/gemini.ts"() {
|
|
@@ -3024,7 +3084,7 @@ var init_gemini = __esm({
|
|
|
3024
3084
|
{ category: HarmCategory.HARM_CATEGORY_HARASSMENT, threshold: HarmBlockThreshold.BLOCK_NONE },
|
|
3025
3085
|
{ category: HarmCategory.HARM_CATEGORY_HATE_SPEECH, threshold: HarmBlockThreshold.BLOCK_NONE }
|
|
3026
3086
|
],
|
|
3027
|
-
tools: options.tools?.length ? [{ functionDeclarations: options.tools
|
|
3087
|
+
tools: options.tools?.length ? [{ functionDeclarations: toGeminiTools(options.tools) }] : void 0,
|
|
3028
3088
|
abortSignal: options.signal
|
|
3029
3089
|
}
|
|
3030
3090
|
});
|
|
@@ -3165,14 +3225,7 @@ var init_gemini = __esm({
|
|
|
3165
3225
|
const parts = [];
|
|
3166
3226
|
const textContent = typeof m.content === "string" ? m.content : "";
|
|
3167
3227
|
if (textContent) parts.push({ text: textContent });
|
|
3168
|
-
|
|
3169
|
-
parts.push({
|
|
3170
|
-
functionCall: {
|
|
3171
|
-
name: tc.name,
|
|
3172
|
-
args: tc.input
|
|
3173
|
-
}
|
|
3174
|
-
});
|
|
3175
|
-
}
|
|
3228
|
+
parts.push(...toGeminiFunctionCalls(m.toolCalls ?? []));
|
|
3176
3229
|
if (parts.length > 0) {
|
|
3177
3230
|
contents.push({ role: "model", parts });
|
|
3178
3231
|
}
|
|
@@ -3219,18 +3272,6 @@ var init_gemini = __esm({
|
|
|
3219
3272
|
}
|
|
3220
3273
|
return parts;
|
|
3221
3274
|
}
|
|
3222
|
-
convertTool(tool) {
|
|
3223
|
-
return {
|
|
3224
|
-
name: tool.name,
|
|
3225
|
-
description: tool.description,
|
|
3226
|
-
// Convert, never cast. Gemini's parameters field is an OpenAPI subset
|
|
3227
|
-
// that 400s on unknown keys, and MCP servers ship schemas carrying their
|
|
3228
|
-
// own extensions (GitHub's adds `x-mcp-header` to every header-bound
|
|
3229
|
-
// property). Casting sent those through verbatim and every request failed
|
|
3230
|
-
// before the model saw it. See providers/gemini-schema.ts.
|
|
3231
|
-
parameters: toGeminiParameters(tool.inputSchema)
|
|
3232
|
-
};
|
|
3233
|
-
}
|
|
3234
3275
|
};
|
|
3235
3276
|
}
|
|
3236
3277
|
});
|
|
@@ -3316,12 +3357,32 @@ var init_net = __esm({
|
|
|
3316
3357
|
// src/providers/ollama.ts
|
|
3317
3358
|
var ollama_exports = {};
|
|
3318
3359
|
__export(ollama_exports, {
|
|
3319
|
-
OllamaProvider: () => OllamaProvider
|
|
3360
|
+
OllamaProvider: () => OllamaProvider,
|
|
3361
|
+
toOllamaToolCalls: () => toOllamaToolCalls,
|
|
3362
|
+
toOllamaTools: () => toOllamaTools
|
|
3320
3363
|
});
|
|
3321
3364
|
function isToolCapable(modelName) {
|
|
3322
3365
|
const name = modelName.toLowerCase();
|
|
3323
3366
|
return TOOL_CAPABLE_FAMILIES.some((family) => name.includes(family));
|
|
3324
3367
|
}
|
|
3368
|
+
function toOllamaTools(tools) {
|
|
3369
|
+
return tools.map((t) => ({
|
|
3370
|
+
type: "function",
|
|
3371
|
+
function: {
|
|
3372
|
+
name: t.name,
|
|
3373
|
+
description: t.description,
|
|
3374
|
+
parameters: t.inputSchema
|
|
3375
|
+
}
|
|
3376
|
+
}));
|
|
3377
|
+
}
|
|
3378
|
+
function toOllamaToolCalls(toolCalls) {
|
|
3379
|
+
return toolCalls.map((tc) => ({
|
|
3380
|
+
function: {
|
|
3381
|
+
name: tc.name,
|
|
3382
|
+
arguments: tc.input
|
|
3383
|
+
}
|
|
3384
|
+
}));
|
|
3385
|
+
}
|
|
3325
3386
|
var TOOL_CAPABLE_FAMILIES, OllamaProvider;
|
|
3326
3387
|
var init_ollama = __esm({
|
|
3327
3388
|
"src/providers/ollama.ts"() {
|
|
@@ -3354,14 +3415,7 @@ var init_ollama = __esm({
|
|
|
3354
3415
|
}
|
|
3355
3416
|
async generateStream(options, onChunk) {
|
|
3356
3417
|
const messages = this.convertMessages(options.messages, options.systemPrompt);
|
|
3357
|
-
const ollamaTools = options.tools
|
|
3358
|
-
type: "function",
|
|
3359
|
-
function: {
|
|
3360
|
-
name: t.name,
|
|
3361
|
-
description: t.description,
|
|
3362
|
-
parameters: t.inputSchema
|
|
3363
|
-
}
|
|
3364
|
-
}));
|
|
3418
|
+
const ollamaTools = options.tools && toOllamaTools(options.tools);
|
|
3365
3419
|
const response = await fetch(`${this.baseUrl}/api/chat`, {
|
|
3366
3420
|
method: "POST",
|
|
3367
3421
|
headers: { "Content-Type": "application/json" },
|
|
@@ -3541,12 +3595,7 @@ var init_ollama = __esm({
|
|
|
3541
3595
|
result.push({
|
|
3542
3596
|
role: "assistant",
|
|
3543
3597
|
content: typeof m.content === "string" ? m.content : "",
|
|
3544
|
-
tool_calls: m.toolCalls
|
|
3545
|
-
function: {
|
|
3546
|
-
name: tc.name,
|
|
3547
|
-
arguments: tc.input
|
|
3548
|
-
}
|
|
3549
|
-
}))
|
|
3598
|
+
tool_calls: toOllamaToolCalls(m.toolCalls)
|
|
3550
3599
|
});
|
|
3551
3600
|
continue;
|
|
3552
3601
|
}
|
|
@@ -6540,6 +6589,21 @@ var DEFAULT_PROVIDER_TPM = {
|
|
|
6540
6589
|
"openai-compatible": 3e4,
|
|
6541
6590
|
ollama: Number.POSITIVE_INFINITY
|
|
6542
6591
|
};
|
|
6592
|
+
function abortReason(signal) {
|
|
6593
|
+
return signal.reason instanceof Error ? signal.reason : new Error("Aborted while waiting for rate-limit capacity");
|
|
6594
|
+
}
|
|
6595
|
+
function sleepUnlessAborted(ms, signal) {
|
|
6596
|
+
if (!signal) return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
6597
|
+
return new Promise((resolve2) => {
|
|
6598
|
+
const done = () => {
|
|
6599
|
+
clearTimeout(timer);
|
|
6600
|
+
signal.removeEventListener("abort", done);
|
|
6601
|
+
resolve2();
|
|
6602
|
+
};
|
|
6603
|
+
const timer = setTimeout(done, ms);
|
|
6604
|
+
signal.addEventListener("abort", done, { once: true });
|
|
6605
|
+
});
|
|
6606
|
+
}
|
|
6543
6607
|
var TpmLimiter = class {
|
|
6544
6608
|
buckets = /* @__PURE__ */ new Map();
|
|
6545
6609
|
constructor(overrides = {}) {
|
|
@@ -6558,9 +6622,10 @@ var TpmLimiter = class {
|
|
|
6558
6622
|
* are reported back via `refund` when short, or simply settled at the next
|
|
6559
6623
|
* refill.
|
|
6560
6624
|
*/
|
|
6561
|
-
async acquire(provider, estimatedTokens) {
|
|
6625
|
+
async acquire(provider, estimatedTokens, signal) {
|
|
6562
6626
|
const bucket = this.buckets.get(provider);
|
|
6563
6627
|
if (!bucket || bucket.tokensPerMinute === Number.POSITIVE_INFINITY) return;
|
|
6628
|
+
if (signal?.aborted) throw abortReason(signal);
|
|
6564
6629
|
const want = Math.min(estimatedTokens, bucket.tokensPerMinute);
|
|
6565
6630
|
for (; ; ) {
|
|
6566
6631
|
this.refill(bucket);
|
|
@@ -6570,7 +6635,8 @@ var TpmLimiter = class {
|
|
|
6570
6635
|
}
|
|
6571
6636
|
const deficit = want - bucket.available;
|
|
6572
6637
|
const waitMs = Math.max(50, Math.ceil(deficit / bucket.tokensPerMinute * 6e4));
|
|
6573
|
-
await
|
|
6638
|
+
await sleepUnlessAborted(waitMs, signal);
|
|
6639
|
+
if (signal?.aborted) throw abortReason(signal);
|
|
6574
6640
|
}
|
|
6575
6641
|
}
|
|
6576
6642
|
/**
|
|
@@ -6616,6 +6682,9 @@ var TpmLimiter = class {
|
|
|
6616
6682
|
};
|
|
6617
6683
|
|
|
6618
6684
|
// src/core/router/local-queue.ts
|
|
6685
|
+
function abortReason2(signal) {
|
|
6686
|
+
return signal.reason instanceof Error ? signal.reason : new Error("Aborted while waiting for a local inference slot");
|
|
6687
|
+
}
|
|
6619
6688
|
var LocalRequestQueue = class {
|
|
6620
6689
|
maxConcurrent;
|
|
6621
6690
|
active = 0;
|
|
@@ -6626,9 +6695,16 @@ var LocalRequestQueue = class {
|
|
|
6626
6695
|
/**
|
|
6627
6696
|
* Acquire a queue slot. Returns a `release` function that MUST be called
|
|
6628
6697
|
* when the inference call is done (even on error). Rejects if the slot
|
|
6629
|
-
* cannot be acquired within `timeoutMs
|
|
6698
|
+
* cannot be acquired within `timeoutMs`, or as soon as `signal` fires.
|
|
6699
|
+
*
|
|
6700
|
+
* The signal matters as much as the timeout: with concurrency of 1 a caller
|
|
6701
|
+
* can sit here for half the inference timeout — 150 seconds by default —
|
|
6702
|
+
* and until it was wired up, neither cancelling the run nor spending its
|
|
6703
|
+
* budget got it out. It waited out the window for a slot it would drop the
|
|
6704
|
+
* moment it received one.
|
|
6630
6705
|
*/
|
|
6631
|
-
async acquire(timeoutMs) {
|
|
6706
|
+
async acquire(timeoutMs, signal) {
|
|
6707
|
+
if (signal?.aborted) throw abortReason2(signal);
|
|
6632
6708
|
if (this.active < this.maxConcurrent) {
|
|
6633
6709
|
this.active++;
|
|
6634
6710
|
return this.makeRelease();
|
|
@@ -6636,23 +6712,36 @@ var LocalRequestQueue = class {
|
|
|
6636
6712
|
return new Promise((resolve2, reject) => {
|
|
6637
6713
|
let settled = false;
|
|
6638
6714
|
let timer;
|
|
6715
|
+
const cleanup = () => {
|
|
6716
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
6717
|
+
signal?.removeEventListener("abort", onAbort);
|
|
6718
|
+
};
|
|
6639
6719
|
const resolver = (release) => {
|
|
6640
6720
|
if (settled) return;
|
|
6641
6721
|
settled = true;
|
|
6642
|
-
|
|
6722
|
+
cleanup();
|
|
6643
6723
|
resolve2(release);
|
|
6644
6724
|
};
|
|
6725
|
+
const abandon = () => {
|
|
6726
|
+
if (settled) return false;
|
|
6727
|
+
settled = true;
|
|
6728
|
+
cleanup();
|
|
6729
|
+
const idx = this.queue.indexOf(resolver);
|
|
6730
|
+
if (idx !== -1) this.queue.splice(idx, 1);
|
|
6731
|
+
return true;
|
|
6732
|
+
};
|
|
6733
|
+
function onAbort() {
|
|
6734
|
+
if (abandon()) reject(abortReason2(signal));
|
|
6735
|
+
}
|
|
6645
6736
|
if (timeoutMs !== void 0 && timeoutMs > 0) {
|
|
6646
6737
|
timer = setTimeout(() => {
|
|
6647
|
-
if (
|
|
6648
|
-
settled = true;
|
|
6649
|
-
const idx = this.queue.indexOf(resolver);
|
|
6650
|
-
if (idx !== -1) this.queue.splice(idx, 1);
|
|
6738
|
+
if (!abandon()) return;
|
|
6651
6739
|
reject(new Error(
|
|
6652
6740
|
`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.`
|
|
6653
6741
|
));
|
|
6654
6742
|
}, timeoutMs);
|
|
6655
6743
|
}
|
|
6744
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
6656
6745
|
this.queue.push(resolver);
|
|
6657
6746
|
});
|
|
6658
6747
|
}
|
|
@@ -6793,6 +6882,116 @@ function fileDeadModelPersistence(filePath) {
|
|
|
6793
6882
|
init_constants();
|
|
6794
6883
|
init_cost();
|
|
6795
6884
|
|
|
6885
|
+
// src/core/context/compaction.ts
|
|
6886
|
+
var CHARS_PER_TOKEN = 4;
|
|
6887
|
+
function estimateTokens(text) {
|
|
6888
|
+
if (!text) return 0;
|
|
6889
|
+
return Math.max(1, Math.ceil(text.length / CHARS_PER_TOKEN));
|
|
6890
|
+
}
|
|
6891
|
+
function contentToText(content) {
|
|
6892
|
+
if (typeof content === "string") return content;
|
|
6893
|
+
return content.map((c) => c.type === "text" ? c.text : c.type === "tool_result" ? c.content : `[${c.type}]`).join(" ");
|
|
6894
|
+
}
|
|
6895
|
+
function messagesTokens(messages) {
|
|
6896
|
+
return messages.reduce((n, m) => n + estimateTokens(contentToText(m.content)), 0);
|
|
6897
|
+
}
|
|
6898
|
+
function atomize(text, maxChars) {
|
|
6899
|
+
const seps = ["\n\n", "\n", ". ", " "];
|
|
6900
|
+
let units = [text];
|
|
6901
|
+
for (const sep of seps) {
|
|
6902
|
+
if (units.every((u) => u.length <= maxChars)) break;
|
|
6903
|
+
const next = [];
|
|
6904
|
+
for (const u of units) {
|
|
6905
|
+
if (u.length <= maxChars) {
|
|
6906
|
+
next.push(u);
|
|
6907
|
+
continue;
|
|
6908
|
+
}
|
|
6909
|
+
const parts = u.split(sep);
|
|
6910
|
+
parts.forEach((p, i) => next.push(i < parts.length - 1 ? p + sep : p));
|
|
6911
|
+
}
|
|
6912
|
+
units = next;
|
|
6913
|
+
}
|
|
6914
|
+
const out = [];
|
|
6915
|
+
for (const u of units) {
|
|
6916
|
+
if (u.length <= maxChars) out.push(u);
|
|
6917
|
+
else for (let i = 0; i < u.length; i += maxChars) out.push(u.slice(i, i + maxChars));
|
|
6918
|
+
}
|
|
6919
|
+
return out;
|
|
6920
|
+
}
|
|
6921
|
+
function chunkText(text, opts) {
|
|
6922
|
+
const maxChars = Math.max(1, Math.floor(opts.maxTokens * 4));
|
|
6923
|
+
const overlap = Math.min(maxChars - 1, Math.max(0, Math.floor(maxChars * (opts.overlapRatio ?? 0.1))));
|
|
6924
|
+
const trimmed = text.trim();
|
|
6925
|
+
if (!trimmed) return [];
|
|
6926
|
+
if (trimmed.length <= maxChars) return [trimmed];
|
|
6927
|
+
const units = atomize(trimmed, maxChars);
|
|
6928
|
+
const chunks = [];
|
|
6929
|
+
let cur = "";
|
|
6930
|
+
for (const u of units) {
|
|
6931
|
+
if (cur && cur.length + u.length > maxChars) {
|
|
6932
|
+
chunks.push(cur.trim());
|
|
6933
|
+
cur = overlap > 0 ? cur.slice(-overlap) : "";
|
|
6934
|
+
}
|
|
6935
|
+
cur += u;
|
|
6936
|
+
}
|
|
6937
|
+
if (cur.trim()) chunks.push(cur.trim());
|
|
6938
|
+
return chunks;
|
|
6939
|
+
}
|
|
6940
|
+
function needsCompaction(totalTokens, contextWindow, reserve = 0.2) {
|
|
6941
|
+
if (!contextWindow || contextWindow <= 0) return false;
|
|
6942
|
+
return totalTokens > contextWindow * (1 - reserve);
|
|
6943
|
+
}
|
|
6944
|
+
async function mapReduceCompact(text, opts) {
|
|
6945
|
+
const capChars = opts.capTokens * 4;
|
|
6946
|
+
let working = text;
|
|
6947
|
+
let truncated = false;
|
|
6948
|
+
if (working.length > capChars) {
|
|
6949
|
+
working = working.slice(0, capChars);
|
|
6950
|
+
truncated = true;
|
|
6951
|
+
}
|
|
6952
|
+
const instruction = opts.instruction ?? "Summarize this content, preserving key facts, names, numbers, and decisions.";
|
|
6953
|
+
let chunks = chunkText(working, { maxTokens: opts.chunkTokens });
|
|
6954
|
+
if (chunks.length <= 1) return { text: working, chunks: chunks.length, calls: 0, truncated };
|
|
6955
|
+
let calls = 0;
|
|
6956
|
+
let parts = await Promise.all(
|
|
6957
|
+
chunks.map((c, i) => {
|
|
6958
|
+
calls++;
|
|
6959
|
+
return opts.summarize(c, `${instruction} (part ${i + 1} of ${chunks.length})`);
|
|
6960
|
+
})
|
|
6961
|
+
);
|
|
6962
|
+
let combined = parts.join("\n\n");
|
|
6963
|
+
const maxPasses = opts.maxReducePasses ?? 3;
|
|
6964
|
+
for (let pass = 0; pass < maxPasses && estimateTokens(combined) > opts.targetTokens; pass++) {
|
|
6965
|
+
const groups = chunkText(combined, { maxTokens: opts.chunkTokens });
|
|
6966
|
+
if (groups.length <= 1) break;
|
|
6967
|
+
parts = await Promise.all(
|
|
6968
|
+
groups.map((g) => {
|
|
6969
|
+
calls++;
|
|
6970
|
+
return opts.summarize(g, "Merge these partial summaries into one cohesive summary; keep all distinct facts.");
|
|
6971
|
+
})
|
|
6972
|
+
);
|
|
6973
|
+
combined = parts.join("\n\n");
|
|
6974
|
+
}
|
|
6975
|
+
const note = truncated ? "\n\n[Note: input exceeded the extended-context cap and was truncated.]" : "";
|
|
6976
|
+
return { text: combined + note, chunks: chunks.length, calls, truncated };
|
|
6977
|
+
}
|
|
6978
|
+
async function rollingSummary(messages, opts) {
|
|
6979
|
+
if (messages.length <= opts.keepRecent) return messages;
|
|
6980
|
+
const older = messages.slice(0, messages.length - opts.keepRecent);
|
|
6981
|
+
const recent = messages.slice(messages.length - opts.keepRecent);
|
|
6982
|
+
const transcript = older.map((m) => `${m.role}: ${contentToText(m.content)}`).join("\n");
|
|
6983
|
+
const summary = await opts.summarize(
|
|
6984
|
+
transcript,
|
|
6985
|
+
"Summarize this earlier conversation. Preserve facts, decisions, names, numbers, and any open threads or todos."
|
|
6986
|
+
);
|
|
6987
|
+
const summaryMessage = {
|
|
6988
|
+
role: "system",
|
|
6989
|
+
content: `Summary of earlier conversation (folded to fit the context window):
|
|
6990
|
+
${summary}`
|
|
6991
|
+
};
|
|
6992
|
+
return [summaryMessage, ...recent];
|
|
6993
|
+
}
|
|
6994
|
+
|
|
6796
6995
|
// src/utils/retry.ts
|
|
6797
6996
|
var CascadeCancelledError = class extends Error {
|
|
6798
6997
|
constructor(reason) {
|
|
@@ -6814,6 +7013,162 @@ async function withTimeout(promise, timeoutMs, errorMessage = "Operation timed o
|
|
|
6814
7013
|
if (timer !== void 0) clearTimeout(timer);
|
|
6815
7014
|
}
|
|
6816
7015
|
}
|
|
7016
|
+
async function withTimeoutAbort(run, timeoutMs, errorMessage = "Operation timed out", outer) {
|
|
7017
|
+
const outers = (Array.isArray(outer) ? outer : [outer]).filter((sig) => sig !== void 0);
|
|
7018
|
+
const controller = new AbortController();
|
|
7019
|
+
let timer;
|
|
7020
|
+
let rejectRace;
|
|
7021
|
+
const settle = (reason) => {
|
|
7022
|
+
rejectRace?.(reason);
|
|
7023
|
+
controller.abort(reason);
|
|
7024
|
+
};
|
|
7025
|
+
const abortOuter = () => {
|
|
7026
|
+
const fired = outers.find((sig) => sig.aborted);
|
|
7027
|
+
settle(fired?.reason instanceof Error ? fired.reason : new CascadeCancelledError("Run cancelled"));
|
|
7028
|
+
};
|
|
7029
|
+
const failPromise = new Promise((_, reject) => {
|
|
7030
|
+
rejectRace = reject;
|
|
7031
|
+
timer = setTimeout(() => settle(new Error(errorMessage)), timeoutMs);
|
|
7032
|
+
});
|
|
7033
|
+
try {
|
|
7034
|
+
if (outers.some((sig) => sig.aborted)) {
|
|
7035
|
+
abortOuter();
|
|
7036
|
+
return await failPromise;
|
|
7037
|
+
}
|
|
7038
|
+
for (const sig of outers) sig.addEventListener("abort", abortOuter, { once: true });
|
|
7039
|
+
return await Promise.race([run(controller.signal), failPromise]);
|
|
7040
|
+
} finally {
|
|
7041
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
7042
|
+
for (const sig of outers) sig.removeEventListener("abort", abortOuter);
|
|
7043
|
+
}
|
|
7044
|
+
}
|
|
7045
|
+
function anySignal(sources) {
|
|
7046
|
+
const live = sources.filter((s) => s !== void 0);
|
|
7047
|
+
if (live.length === 0) return { release: () => {
|
|
7048
|
+
} };
|
|
7049
|
+
if (live.length === 1) return { signal: live[0], release: () => {
|
|
7050
|
+
} };
|
|
7051
|
+
const controller = new AbortController();
|
|
7052
|
+
function detach() {
|
|
7053
|
+
for (const s of live) s.removeEventListener("abort", onAbort);
|
|
7054
|
+
}
|
|
7055
|
+
function onAbort() {
|
|
7056
|
+
const fired = live.find((s) => s.aborted);
|
|
7057
|
+
controller.abort(fired?.reason instanceof Error ? fired.reason : new CascadeCancelledError("Run cancelled"));
|
|
7058
|
+
detach();
|
|
7059
|
+
}
|
|
7060
|
+
if (live.some((s) => s.aborted)) {
|
|
7061
|
+
onAbort();
|
|
7062
|
+
return { signal: controller.signal, release: detach };
|
|
7063
|
+
}
|
|
7064
|
+
for (const s of live) s.addEventListener("abort", onAbort, { once: true });
|
|
7065
|
+
return { signal: controller.signal, release: detach };
|
|
7066
|
+
}
|
|
7067
|
+
|
|
7068
|
+
// src/core/router/wire-profile.ts
|
|
7069
|
+
init_anthropic();
|
|
7070
|
+
init_gemini();
|
|
7071
|
+
init_ollama();
|
|
7072
|
+
init_openai();
|
|
7073
|
+
var textAndImageOnly = (block) => block.type === "text" || block.type === "image";
|
|
7074
|
+
var ANTHROPIC = {
|
|
7075
|
+
// convertMessages: `if (m.role === 'system') continue` — history system
|
|
7076
|
+
// messages are skipped outright; only options.systemPrompt goes as system
|
|
7077
|
+
// input. Compaction emits exactly such a message, holding a summary of the
|
|
7078
|
+
// whole conversation, so charging it refused runs over input never sent.
|
|
7079
|
+
dropsMessage: (m) => m.role === "system",
|
|
7080
|
+
blockHandling: (m) => {
|
|
7081
|
+
if (m.role === "tool") return "stringified";
|
|
7082
|
+
if (m.role === "assistant") return "dropped";
|
|
7083
|
+
return "blocks";
|
|
7084
|
+
},
|
|
7085
|
+
sendsToolCalls: (m) => m.role === "assistant",
|
|
7086
|
+
sendsBlock: textAndImageOnly,
|
|
7087
|
+
sendsToolCallId: true,
|
|
7088
|
+
sizeToolCalls: toAnthropicToolUse,
|
|
7089
|
+
sendsUrlImages: true,
|
|
7090
|
+
readsTopLevelImages: false,
|
|
7091
|
+
sizeTools: toAnthropicTools
|
|
7092
|
+
};
|
|
7093
|
+
var GEMINI = {
|
|
7094
|
+
// buildContents folds a system message into the next user turn, but only
|
|
7095
|
+
// when it has text: array content collapses to '' and the message is
|
|
7096
|
+
// skipped, as is a whitespace-only string.
|
|
7097
|
+
dropsMessage: (m) => m.role === "system" && !(typeof m.content === "string" ? m.content : "").trim(),
|
|
7098
|
+
blockHandling: (m) => {
|
|
7099
|
+
if (m.role === "tool") return "stringified";
|
|
7100
|
+
if (m.role === "assistant" || m.role === "system") return "dropped";
|
|
7101
|
+
return "blocks";
|
|
7102
|
+
},
|
|
7103
|
+
sendsToolCalls: (m) => m.role === "assistant",
|
|
7104
|
+
sendsBlock: textAndImageOnly,
|
|
7105
|
+
sendsToolCallId: true,
|
|
7106
|
+
sizeToolCalls: toGeminiFunctionCalls,
|
|
7107
|
+
sendsUrlImages: false,
|
|
7108
|
+
readsTopLevelImages: true,
|
|
7109
|
+
sizeTools: toGeminiTools
|
|
7110
|
+
};
|
|
7111
|
+
var OPENAI = {
|
|
7112
|
+
dropsMessage: () => false,
|
|
7113
|
+
blockHandling: (m) => {
|
|
7114
|
+
if (m.role === "system" || m.role === "tool") return "dropped";
|
|
7115
|
+
return "blocks";
|
|
7116
|
+
},
|
|
7117
|
+
// The tool_calls branch is inside `if (typeof m.content === 'string')`; an
|
|
7118
|
+
// assistant turn with array content falls through to the parts branch and
|
|
7119
|
+
// its tool calls are never attached.
|
|
7120
|
+
sendsToolCalls: (m) => m.role === "assistant" && typeof m.content === "string",
|
|
7121
|
+
sendsBlock: textAndImageOnly,
|
|
7122
|
+
sendsToolCallId: true,
|
|
7123
|
+
sizeToolCalls: toOpenAIToolCalls,
|
|
7124
|
+
sendsUrlImages: true,
|
|
7125
|
+
readsTopLevelImages: false,
|
|
7126
|
+
sizeTools: toOpenAITools
|
|
7127
|
+
};
|
|
7128
|
+
var OLLAMA = {
|
|
7129
|
+
dropsMessage: () => false,
|
|
7130
|
+
blockHandling: (m) => {
|
|
7131
|
+
if (m.role === "system") return "dropped";
|
|
7132
|
+
if (m.role === "tool") return "stringified";
|
|
7133
|
+
if (m.role === "assistant" && m.toolCalls?.length) return "dropped";
|
|
7134
|
+
return "blocks";
|
|
7135
|
+
},
|
|
7136
|
+
sendsToolCalls: (m) => m.role === "assistant",
|
|
7137
|
+
// The image split maps `b.image.data` with no check on `type`, so a URL is
|
|
7138
|
+
// submitted too (as a URL string in the `images` array).
|
|
7139
|
+
sendsBlock: textAndImageOnly,
|
|
7140
|
+
sendsToolCallId: false,
|
|
7141
|
+
sizeToolCalls: toOllamaToolCalls,
|
|
7142
|
+
sendsUrlImages: true,
|
|
7143
|
+
readsTopLevelImages: false,
|
|
7144
|
+
sizeTools: toOllamaTools
|
|
7145
|
+
};
|
|
7146
|
+
var PROFILES = {
|
|
7147
|
+
anthropic: ANTHROPIC,
|
|
7148
|
+
gemini: GEMINI,
|
|
7149
|
+
openai: OPENAI,
|
|
7150
|
+
// AzureOpenAIProvider and OpenAICompatibleProvider both extend OpenAIProvider
|
|
7151
|
+
// and neither overrides convertMessages.
|
|
7152
|
+
azure: OPENAI,
|
|
7153
|
+
"openai-compatible": OPENAI,
|
|
7154
|
+
ollama: OLLAMA
|
|
7155
|
+
};
|
|
7156
|
+
function wireProfile(provider) {
|
|
7157
|
+
return PROFILES[provider] ?? OPENAI;
|
|
7158
|
+
}
|
|
7159
|
+
function geminiImageCopies(messages) {
|
|
7160
|
+
for (const m of messages) {
|
|
7161
|
+
if (m.role === "user") {
|
|
7162
|
+
const laterUserTurn = messages.slice(messages.indexOf(m) + 1).some((x) => x.role === "user");
|
|
7163
|
+
return laterUserTurn ? 2 : 1;
|
|
7164
|
+
}
|
|
7165
|
+
const text = typeof m.content === "string" ? m.content : "";
|
|
7166
|
+
if (m.role === "system" && text.trim()) return 1;
|
|
7167
|
+
if (m.role === "tool") return 1;
|
|
7168
|
+
if (m.role === "assistant" && (text || m.toolCalls?.length)) return 1;
|
|
7169
|
+
}
|
|
7170
|
+
return 1;
|
|
7171
|
+
}
|
|
6817
7172
|
|
|
6818
7173
|
// src/core/router/model-profiler.ts
|
|
6819
7174
|
var SKIP_PATTERN = /embed|dall-e|whisper|tts|vision|instruct-vision|rerank/i;
|
|
@@ -7323,6 +7678,36 @@ function azureModelForTier(tier, ranked) {
|
|
|
7323
7678
|
if (tier === "T3") return ranked[ranked.length - 1];
|
|
7324
7679
|
return ranked[Math.min(1, ranked.length - 1)];
|
|
7325
7680
|
}
|
|
7681
|
+
var IMAGE_TOKENS_MAX = 2e3;
|
|
7682
|
+
var IMAGE_TOKENS_EACH = IMAGE_TOKENS_MAX;
|
|
7683
|
+
function safeJson(value) {
|
|
7684
|
+
try {
|
|
7685
|
+
return JSON.stringify(value) ?? "";
|
|
7686
|
+
} catch {
|
|
7687
|
+
return "";
|
|
7688
|
+
}
|
|
7689
|
+
}
|
|
7690
|
+
function newRunAbort() {
|
|
7691
|
+
const controller = new AbortController();
|
|
7692
|
+
setMaxListeners(64, controller.signal);
|
|
7693
|
+
return controller;
|
|
7694
|
+
}
|
|
7695
|
+
var TOKENS_PER_MESSAGE_FRAMING = 4;
|
|
7696
|
+
function guardTokens(text) {
|
|
7697
|
+
if (!text) return 0;
|
|
7698
|
+
let asciiChars = 0;
|
|
7699
|
+
let denseBytes = 0;
|
|
7700
|
+
for (const ch of text) {
|
|
7701
|
+
const cp = ch.codePointAt(0);
|
|
7702
|
+
if (cp <= 127) {
|
|
7703
|
+
asciiChars++;
|
|
7704
|
+
continue;
|
|
7705
|
+
}
|
|
7706
|
+
denseBytes += cp <= 2047 ? 2 : cp <= 65535 ? 3 : 4;
|
|
7707
|
+
}
|
|
7708
|
+
if (denseBytes === 0) return estimateTokens(text);
|
|
7709
|
+
return Math.ceil(asciiChars / CHARS_PER_TOKEN) + denseBytes;
|
|
7710
|
+
}
|
|
7326
7711
|
var DISCOVERY_TTL_MS = 15 * 60 * 1e3;
|
|
7327
7712
|
var DISCOVERY_TIMEOUT_MS = 4e3;
|
|
7328
7713
|
var cloudDiscoveryCache = /* @__PURE__ */ new Map();
|
|
@@ -7375,6 +7760,33 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
7375
7760
|
// start of every `cascade run`, independent of the session-wide budget.
|
|
7376
7761
|
runTokens = 0;
|
|
7377
7762
|
runCostUsd = 0;
|
|
7763
|
+
/**
|
|
7764
|
+
* Budget held for calls that are IN FLIGHT — admitted by the preflight check
|
|
7765
|
+
* but not yet counted by recordStats. Without these, concurrent calls all
|
|
7766
|
+
* measure themselves against the same unspent allowance and are all admitted.
|
|
7767
|
+
* Released when each call settles; reset by beginRun() alongside the totals.
|
|
7768
|
+
*/
|
|
7769
|
+
reservedTokens = 0;
|
|
7770
|
+
reservedCostUsd = 0;
|
|
7771
|
+
/**
|
|
7772
|
+
* Fires when this run's budget is spent, cancelling work already in flight.
|
|
7773
|
+
*
|
|
7774
|
+
* The kill switch was previously only a flag, which stops calls that have
|
|
7775
|
+
* not yet submitted. A parallel wave's earlier members are already at the
|
|
7776
|
+
* provider by then, and they carried on generating for a run whose output
|
|
7777
|
+
* would be discarded. Every provider call chains to this, so tripping the
|
|
7778
|
+
* ceiling reaches the requests that are running, not just the ones queued.
|
|
7779
|
+
*/
|
|
7780
|
+
runAbort = newRunAbort();
|
|
7781
|
+
/**
|
|
7782
|
+
* Bumped by every beginRun(). A release handle remembers the generation it
|
|
7783
|
+
* was issued in and does nothing if that has moved on: beginRun() zeroes the
|
|
7784
|
+
* counters, so a call still in flight from the previous run would otherwise
|
|
7785
|
+
* subtract its estimate from the NEW run's totals and drive them negative —
|
|
7786
|
+
* leaving later preflights believing there is more allowance than the cap
|
|
7787
|
+
* actually holds, which is worse than no gate at all.
|
|
7788
|
+
*/
|
|
7789
|
+
runGeneration = 0;
|
|
7378
7790
|
runBudgetExceeded = false;
|
|
7379
7791
|
runBudgetExceededReason;
|
|
7380
7792
|
/**
|
|
@@ -7544,7 +7956,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
7544
7956
|
try {
|
|
7545
7957
|
const cfg = this.config.providers.find((p) => p.type === model.provider) ?? { type: model.provider };
|
|
7546
7958
|
const provider = this.createProvider(cfg, model);
|
|
7547
|
-
const result = await
|
|
7959
|
+
const result = await withTimeoutAbort((signal) => provider.generate({
|
|
7548
7960
|
messages: [{ role: "user", content: "Call the echo tool with text set to 'ping'. Use the tool; do not answer in prose." }],
|
|
7549
7961
|
tools: [{
|
|
7550
7962
|
name: "echo",
|
|
@@ -7552,7 +7964,8 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
7552
7964
|
inputSchema: { type: "object", properties: { text: { type: "string", description: "Text to echo" } }, required: ["text"] }
|
|
7553
7965
|
}],
|
|
7554
7966
|
maxTokens: 80,
|
|
7555
|
-
temperature: 0
|
|
7967
|
+
temperature: 0,
|
|
7968
|
+
signal
|
|
7556
7969
|
}), 3e4, "tool-support probe timed out");
|
|
7557
7970
|
return (result.toolCalls?.length ?? 0) > 0;
|
|
7558
7971
|
} catch {
|
|
@@ -7673,9 +8086,12 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
7673
8086
|
this.runBudgetExceededReason ?? "Per-task budget exceeded."
|
|
7674
8087
|
);
|
|
7675
8088
|
}
|
|
8089
|
+
const runGeneration = this.runGeneration;
|
|
8090
|
+
const runAbort = this.runAbort;
|
|
8091
|
+
const runSignal = this.runSignal;
|
|
7676
8092
|
options = applyTierLimits(options, tier, this.config?.tierLimits);
|
|
7677
|
-
if (
|
|
7678
|
-
options = { ...options, signal:
|
|
8093
|
+
if (runSignal && !options.signal) {
|
|
8094
|
+
options = { ...options, signal: runSignal };
|
|
7679
8095
|
}
|
|
7680
8096
|
if (options.model && !requireVision) {
|
|
7681
8097
|
this.ensureProvider(options.model, this.config.providers);
|
|
@@ -7697,58 +8113,84 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
7697
8113
|
if (!model) throw new Error(`No model available for tier ${tier}`);
|
|
7698
8114
|
const provider = this.getProvider(model);
|
|
7699
8115
|
if (!provider) throw new Error(`No provider for model ${model.id}`);
|
|
8116
|
+
let releaseReservation = this.enforcePreflightBudget(model, options);
|
|
7700
8117
|
const requestedTokens = options.maxTokens ?? model.maxOutputTokens ?? 1024;
|
|
7701
8118
|
const estimatedTokens = Math.min(requestedTokens, model.maxOutputTokens ?? requestedTokens) + 512;
|
|
7702
8119
|
if (this.tpmLimiter) {
|
|
7703
|
-
|
|
8120
|
+
const wait = anySignal([options.signal, runAbort.signal]);
|
|
8121
|
+
try {
|
|
8122
|
+
await this.tpmLimiter.acquire(model.provider, estimatedTokens, wait.signal);
|
|
8123
|
+
} catch (err) {
|
|
8124
|
+
releaseReservation?.();
|
|
8125
|
+
throw this.asAbortFailure(err, options.signal, runSignal);
|
|
8126
|
+
} finally {
|
|
8127
|
+
wait.release();
|
|
8128
|
+
}
|
|
7704
8129
|
}
|
|
7705
8130
|
const useStream = Boolean(onChunk) && model.supportsStreaming && typeof provider.generateStream === "function";
|
|
7706
8131
|
let releaseLocalSlot;
|
|
7707
8132
|
if (model.isLocal) {
|
|
7708
8133
|
const inferenceTimeoutMs = this.config.localInferenceTimeoutMs ?? 3e5;
|
|
7709
8134
|
const queueWaitMs = Math.round(inferenceTimeoutMs / 2);
|
|
7710
|
-
|
|
8135
|
+
const wait = anySignal([options.signal, runAbort.signal]);
|
|
8136
|
+
try {
|
|
8137
|
+
releaseLocalSlot = await this.localQueue.acquire(queueWaitMs, wait.signal);
|
|
8138
|
+
} catch (err) {
|
|
8139
|
+
releaseReservation?.();
|
|
8140
|
+
throw this.asAbortFailure(err, options.signal, runSignal);
|
|
8141
|
+
} finally {
|
|
8142
|
+
wait.release();
|
|
8143
|
+
}
|
|
7711
8144
|
}
|
|
7712
8145
|
try {
|
|
8146
|
+
if (runGeneration === this.runGeneration && this.runBudgetExceeded) {
|
|
8147
|
+
this.tpmLimiter?.refund(model.provider, estimatedTokens);
|
|
8148
|
+
throw new _CascadeRouter.BudgetExceededError(
|
|
8149
|
+
this.runBudgetExceededReason ?? "Per-task budget exceeded."
|
|
8150
|
+
);
|
|
8151
|
+
}
|
|
7713
8152
|
let result;
|
|
7714
8153
|
if (model.isLocal) {
|
|
7715
8154
|
const inferenceTimeoutMs = this.config.localInferenceTimeoutMs ?? 3e5;
|
|
7716
|
-
|
|
7717
|
-
|
|
7718
|
-
|
|
7719
|
-
|
|
7720
|
-
|
|
7721
|
-
inferencePromise,
|
|
8155
|
+
result = await withTimeoutAbort(
|
|
8156
|
+
(signal) => useStream && onChunk ? provider.generateStream({ ...options, signal }, (chunk) => {
|
|
8157
|
+
const text = typeof chunk?.text === "string" ? chunk.text : "";
|
|
8158
|
+
if (text) onChunk({ ...chunk, text });
|
|
8159
|
+
}) : provider.generate({ ...options, signal }),
|
|
7722
8160
|
inferenceTimeoutMs,
|
|
7723
|
-
`Local model ${model.id} inference timed out after ${inferenceTimeoutMs}ms
|
|
8161
|
+
`Local model ${model.id} inference timed out after ${inferenceTimeoutMs}ms`,
|
|
8162
|
+
[options.signal, runAbort.signal]
|
|
7724
8163
|
);
|
|
7725
8164
|
} else if (useStream && onChunk) {
|
|
7726
8165
|
const cloudTimeoutMs = this.config.cloudInferenceTimeoutMs ?? 12e4;
|
|
7727
8166
|
try {
|
|
7728
|
-
result = await
|
|
7729
|
-
provider.generateStream(options, (chunk) => {
|
|
8167
|
+
result = await withTimeoutAbort(
|
|
8168
|
+
(signal) => provider.generateStream({ ...options, signal }, (chunk) => {
|
|
7730
8169
|
const text = typeof chunk?.text === "string" ? chunk.text : "";
|
|
7731
8170
|
if (text) onChunk({ ...chunk, text });
|
|
7732
8171
|
}),
|
|
7733
8172
|
cloudTimeoutMs,
|
|
7734
|
-
`Model ${model.id} stream timed out after ${cloudTimeoutMs}ms
|
|
8173
|
+
`Model ${model.id} stream timed out after ${cloudTimeoutMs}ms`,
|
|
8174
|
+
[options.signal, runAbort.signal]
|
|
7735
8175
|
);
|
|
7736
8176
|
} catch (streamErr) {
|
|
7737
|
-
if (streamErr instanceof Error && streamErr.name === "AbortError" ||
|
|
8177
|
+
if (streamErr instanceof Error && streamErr.name === "AbortError" || runSignal?.aborted || options.signal?.aborted) {
|
|
7738
8178
|
throw streamErr;
|
|
7739
8179
|
}
|
|
7740
|
-
result = await
|
|
7741
|
-
provider.generate(options),
|
|
8180
|
+
result = await withTimeoutAbort(
|
|
8181
|
+
(signal) => provider.generate({ ...options, signal }),
|
|
7742
8182
|
cloudTimeoutMs,
|
|
7743
|
-
`Model ${model.id} inference timed out after ${cloudTimeoutMs}ms
|
|
8183
|
+
`Model ${model.id} inference timed out after ${cloudTimeoutMs}ms`,
|
|
8184
|
+
[options.signal, runAbort.signal]
|
|
7744
8185
|
);
|
|
7745
8186
|
}
|
|
7746
8187
|
} else {
|
|
7747
8188
|
const cloudTimeoutMs = this.config.cloudInferenceTimeoutMs ?? 12e4;
|
|
7748
|
-
result = await
|
|
7749
|
-
provider.generate(options),
|
|
8189
|
+
result = await withTimeoutAbort(
|
|
8190
|
+
(signal) => provider.generate({ ...options, signal }),
|
|
7750
8191
|
cloudTimeoutMs,
|
|
7751
|
-
`Model ${model.id} inference timed out after ${cloudTimeoutMs}ms
|
|
8192
|
+
`Model ${model.id} inference timed out after ${cloudTimeoutMs}ms`,
|
|
8193
|
+
[options.signal, runAbort.signal]
|
|
7752
8194
|
);
|
|
7753
8195
|
}
|
|
7754
8196
|
const corrected = buildTokenUsage(
|
|
@@ -7767,11 +8209,12 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
7767
8209
|
if (!result || typeof result.content !== "string" || !result.usage) {
|
|
7768
8210
|
throw new Error(`Provider ${model.provider}:${model.id} returned an invalid generation result.`);
|
|
7769
8211
|
}
|
|
7770
|
-
this.recordStats(tier, model, result.usage, options.featureTag);
|
|
8212
|
+
this.recordStats(tier, model, result.usage, options.featureTag, runGeneration);
|
|
7771
8213
|
this.failover.recordSuccess(model.provider);
|
|
7772
8214
|
return result;
|
|
7773
8215
|
} catch (err) {
|
|
7774
|
-
if (err instanceof
|
|
8216
|
+
if (err instanceof _CascadeRouter.BudgetExceededError) throw err;
|
|
8217
|
+
if (err instanceof Error && err.name === "AbortError" || runSignal?.aborted || options.signal?.aborted) {
|
|
7775
8218
|
throw new CascadeCancelledError("Run cancelled");
|
|
7776
8219
|
}
|
|
7777
8220
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
@@ -7789,6 +8232,8 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
7789
8232
|
});
|
|
7790
8233
|
releaseLocalSlot?.();
|
|
7791
8234
|
releaseLocalSlot = void 0;
|
|
8235
|
+
releaseReservation?.();
|
|
8236
|
+
releaseReservation = void 0;
|
|
7792
8237
|
const retryOpts = options.model && options.model.id === model.id ? { ...options, model: void 0 } : options;
|
|
7793
8238
|
return this.generate(tier, retryOpts, onChunk, requireVision);
|
|
7794
8239
|
}
|
|
@@ -7810,6 +8255,8 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
7810
8255
|
});
|
|
7811
8256
|
releaseLocalSlot?.();
|
|
7812
8257
|
releaseLocalSlot = void 0;
|
|
8258
|
+
releaseReservation?.();
|
|
8259
|
+
releaseReservation = void 0;
|
|
7813
8260
|
const base = options.model && options.model.id === model.id ? { ...options, model: void 0 } : options;
|
|
7814
8261
|
const retryOpts = { ...base, _notFoundDepth: depth };
|
|
7815
8262
|
return this.generate(tier, retryOpts, onChunk, requireVision);
|
|
@@ -7818,6 +8265,8 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
7818
8265
|
throw err;
|
|
7819
8266
|
} finally {
|
|
7820
8267
|
releaseLocalSlot?.();
|
|
8268
|
+
releaseReservation?.();
|
|
8269
|
+
releaseReservation = void 0;
|
|
7821
8270
|
}
|
|
7822
8271
|
}
|
|
7823
8272
|
getModelForTier(tier) {
|
|
@@ -8188,7 +8637,14 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
8188
8637
|
}
|
|
8189
8638
|
return void 0;
|
|
8190
8639
|
}
|
|
8191
|
-
|
|
8640
|
+
/**
|
|
8641
|
+
* `runGeneration` is the generation the call was ADMITTED in — see
|
|
8642
|
+
* `generate()`. Session-wide accounting always applies (the spend is real
|
|
8643
|
+
* whenever it lands), but the per-RUN ceiling is only meaningful for the run
|
|
8644
|
+
* the call belongs to, so a straggler from a finished run is left out of it.
|
|
8645
|
+
* Omitted by the direct callers in tests, which have no run to straddle.
|
|
8646
|
+
*/
|
|
8647
|
+
recordStats(tier, model, usage, featureTag, runGeneration) {
|
|
8192
8648
|
this.stats.totalTokens += usage.totalTokens;
|
|
8193
8649
|
this.stats.totalCostUsd += usage.estimatedCostUsd;
|
|
8194
8650
|
this.sessionCostUsd += usage.estimatedCostUsd;
|
|
@@ -8213,10 +8669,13 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
8213
8669
|
});
|
|
8214
8670
|
}
|
|
8215
8671
|
}
|
|
8216
|
-
|
|
8217
|
-
|
|
8672
|
+
const currentRun = runGeneration === void 0 || runGeneration === this.runGeneration;
|
|
8673
|
+
if (currentRun) {
|
|
8674
|
+
this.runTokens += usage.totalTokens;
|
|
8675
|
+
this.runCostUsd += usage.estimatedCostUsd;
|
|
8676
|
+
}
|
|
8218
8677
|
this.updateBudgetState();
|
|
8219
|
-
this.enforceRunBudget();
|
|
8678
|
+
if (currentRun) this.enforceRunBudget();
|
|
8220
8679
|
}
|
|
8221
8680
|
/**
|
|
8222
8681
|
* Resets per-run accounting at the start of each `cascade run`. Session
|
|
@@ -8226,9 +8685,158 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
8226
8685
|
beginRun() {
|
|
8227
8686
|
this.runTokens = 0;
|
|
8228
8687
|
this.runCostUsd = 0;
|
|
8688
|
+
this.reservedTokens = 0;
|
|
8689
|
+
this.reservedCostUsd = 0;
|
|
8690
|
+
this.runAbort = newRunAbort();
|
|
8691
|
+
this.runGeneration++;
|
|
8229
8692
|
this.runBudgetExceeded = false;
|
|
8230
8693
|
this.runBudgetExceededReason = void 0;
|
|
8231
8694
|
}
|
|
8695
|
+
/**
|
|
8696
|
+
* Refuse a call whose input alone cannot fit what is left of the run budget.
|
|
8697
|
+
*
|
|
8698
|
+
* `enforceRunBudget()` below runs AFTER a call returns, which makes the caps
|
|
8699
|
+
* a stop rather than a pre-authorisation: the tokens are already bought by
|
|
8700
|
+
* the time it looks. That was tolerable while a prompt could not exceed
|
|
8701
|
+
* 20,000 characters, and stopped being so when that cap was removed in
|
|
8702
|
+
* 0.72.0 — a multi-megabyte prompt is billed in full on the very first call
|
|
8703
|
+
* (the complexity classifier sends the whole thing), and no ceiling
|
|
8704
|
+
* configured downstream can give that money back.
|
|
8705
|
+
*
|
|
8706
|
+
* Deliberately narrow, because a false refusal is worse than a late stop:
|
|
8707
|
+
*
|
|
8708
|
+
* • INPUT ONLY. What a call returns is not knowable in advance, and
|
|
8709
|
+
* charging a worst-case `maxTokens` of output against the budget would
|
|
8710
|
+
* decline runs that would have finished comfortably. Input is the part
|
|
8711
|
+
* that is already determined, and it is the part the removed cap used to
|
|
8712
|
+
* bound.
|
|
8713
|
+
* • Compared against what REMAINS (`cap - spent`), not the whole cap, so a
|
|
8714
|
+
* call is judged on the budget it can actually draw from.
|
|
8715
|
+
* • Skipped when the model has no usable price. An estimate cannot be made,
|
|
8716
|
+
* and refusing on ignorance would break every self-hosted and local model
|
|
8717
|
+
* the moment a cost cap is set. The post-hoc stop still applies there.
|
|
8718
|
+
* • Skipped entirely when no cap is configured — there is nothing to
|
|
8719
|
+
* pre-authorise against.
|
|
8720
|
+
*
|
|
8721
|
+
* The message names the cap AND the estimate, because "too expensive" with
|
|
8722
|
+
* no numbers gives the user nothing to change.
|
|
8723
|
+
*/
|
|
8724
|
+
enforcePreflightBudget(model, options) {
|
|
8725
|
+
const budget = this.config?.budget;
|
|
8726
|
+
const maxCost = budget?.maxCostPerRunUsd;
|
|
8727
|
+
const maxTokens = budget?.maxTokensPerRun;
|
|
8728
|
+
if (maxCost == null && maxTokens == null) return void 0;
|
|
8729
|
+
const inputTokens = this.estimateInputTokens(options, model);
|
|
8730
|
+
if (maxTokens != null) {
|
|
8731
|
+
const remaining = maxTokens - this.runTokens - this.reservedTokens;
|
|
8732
|
+
if (inputTokens > remaining) {
|
|
8733
|
+
this.failPreflight(
|
|
8734
|
+
`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.`
|
|
8735
|
+
);
|
|
8736
|
+
}
|
|
8737
|
+
}
|
|
8738
|
+
let estimatedUsd = 0;
|
|
8739
|
+
if (maxCost != null) {
|
|
8740
|
+
const { input: inputPer1k, unknown } = resolveModelPricing(model, { inputTokens });
|
|
8741
|
+
if (!unknown && inputPer1k > 0) {
|
|
8742
|
+
estimatedUsd = inputTokens / 1e3 * inputPer1k;
|
|
8743
|
+
const remaining = maxCost - this.runCostUsd - this.reservedCostUsd;
|
|
8744
|
+
if (estimatedUsd > remaining) {
|
|
8745
|
+
this.failPreflight(
|
|
8746
|
+
`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.`
|
|
8747
|
+
);
|
|
8748
|
+
}
|
|
8749
|
+
}
|
|
8750
|
+
}
|
|
8751
|
+
this.reservedTokens += inputTokens;
|
|
8752
|
+
this.reservedCostUsd += estimatedUsd;
|
|
8753
|
+
const generation = this.runGeneration;
|
|
8754
|
+
let released = false;
|
|
8755
|
+
return () => {
|
|
8756
|
+
if (released || generation !== this.runGeneration) return;
|
|
8757
|
+
released = true;
|
|
8758
|
+
this.reservedTokens -= inputTokens;
|
|
8759
|
+
this.reservedCostUsd -= estimatedUsd;
|
|
8760
|
+
};
|
|
8761
|
+
}
|
|
8762
|
+
/**
|
|
8763
|
+
* What this call will send, in tokens.
|
|
8764
|
+
*
|
|
8765
|
+
* Text is the bulk of it, but not all of it: providers bill the serialized
|
|
8766
|
+
* TOOL DEFINITIONS on every native-tool call (Anthropic sends each name,
|
|
8767
|
+
* description and input schema), and an image is billed by its own size
|
|
8768
|
+
* rather than as the `[image]` placeholder `contentToText` reduces it to. A
|
|
8769
|
+
* tool-heavy or vision request could therefore pass either cap while its real
|
|
8770
|
+
* input already exceeded the allowance.
|
|
8771
|
+
*/
|
|
8772
|
+
estimateInputTokens(options, model) {
|
|
8773
|
+
let tokens = guardTokens(options.systemPrompt ?? "");
|
|
8774
|
+
let images = 0;
|
|
8775
|
+
const wire = wireProfile(model.provider);
|
|
8776
|
+
const countsImage = (img) => wire.sendsUrlImages || img?.type === "base64";
|
|
8777
|
+
for (const message of options.messages) {
|
|
8778
|
+
if (wire.dropsMessage(message)) continue;
|
|
8779
|
+
tokens += TOKENS_PER_MESSAGE_FRAMING;
|
|
8780
|
+
if (typeof message.content === "string") {
|
|
8781
|
+
tokens += guardTokens(message.content);
|
|
8782
|
+
} else {
|
|
8783
|
+
switch (wire.blockHandling(message)) {
|
|
8784
|
+
case "blocks":
|
|
8785
|
+
for (const block of message.content) {
|
|
8786
|
+
if (!wire.sendsBlock(block)) continue;
|
|
8787
|
+
if (block.type === "image") {
|
|
8788
|
+
if (countsImage(block.image)) images++;
|
|
8789
|
+
continue;
|
|
8790
|
+
}
|
|
8791
|
+
tokens += guardTokens(contentToText([block]));
|
|
8792
|
+
}
|
|
8793
|
+
break;
|
|
8794
|
+
case "stringified":
|
|
8795
|
+
tokens += guardTokens(safeJson(message.content));
|
|
8796
|
+
break;
|
|
8797
|
+
}
|
|
8798
|
+
}
|
|
8799
|
+
if (message.toolCalls?.length && wire.sendsToolCalls(message)) {
|
|
8800
|
+
tokens += guardTokens(safeJson(wire.sizeToolCalls(message.toolCalls)));
|
|
8801
|
+
}
|
|
8802
|
+
if (message.toolCallId && message.role === "tool" && wire.sendsToolCallId) {
|
|
8803
|
+
tokens += guardTokens(message.toolCallId);
|
|
8804
|
+
}
|
|
8805
|
+
}
|
|
8806
|
+
if (options.tools?.length) {
|
|
8807
|
+
tokens += guardTokens(safeJson(wire.sizeTools(options.tools)));
|
|
8808
|
+
}
|
|
8809
|
+
if (wire.readsTopLevelImages) {
|
|
8810
|
+
const topLevel = (options.images ?? []).filter(countsImage).length;
|
|
8811
|
+
images += topLevel * geminiImageCopies(options.messages);
|
|
8812
|
+
}
|
|
8813
|
+
tokens += images * IMAGE_TOKENS_EACH;
|
|
8814
|
+
return tokens;
|
|
8815
|
+
}
|
|
8816
|
+
/**
|
|
8817
|
+
* Report a wait that ended because something asked it to stop, the same way
|
|
8818
|
+
* the provider call below reports one.
|
|
8819
|
+
*
|
|
8820
|
+
* The queue waits sit OUTSIDE the try/catch that maps aborts, so without
|
|
8821
|
+
* this a cancellation surfaced as whatever the queue happened to throw and
|
|
8822
|
+
* was retried or failed over like an ordinary error. A budget abort keeps
|
|
8823
|
+
* saying it ran out of budget — otherwise the reason the run stopped is lost
|
|
8824
|
+
* on the way up — and anything else that was aborted becomes a cancellation.
|
|
8825
|
+
*/
|
|
8826
|
+
asAbortFailure(err, callSignal, runSignal) {
|
|
8827
|
+
if (err instanceof _CascadeRouter.BudgetExceededError) return err;
|
|
8828
|
+
if (err instanceof CascadeCancelledError) return err;
|
|
8829
|
+
if (callSignal?.aborted || runSignal?.aborted) return new CascadeCancelledError("Run cancelled");
|
|
8830
|
+
return err;
|
|
8831
|
+
}
|
|
8832
|
+
/** Trips the same run-level flag a post-hoc overrun does, and reports it the same way. */
|
|
8833
|
+
failPreflight(reason) {
|
|
8834
|
+
this.runBudgetExceeded = true;
|
|
8835
|
+
this.runBudgetExceededReason = reason;
|
|
8836
|
+
this.runAbort.abort(new _CascadeRouter.BudgetExceededError(reason));
|
|
8837
|
+
this.emit("budget:exceeded", { reason, spentUsd: this.sessionCostUsd });
|
|
8838
|
+
throw new _CascadeRouter.BudgetExceededError(reason);
|
|
8839
|
+
}
|
|
8232
8840
|
/**
|
|
8233
8841
|
* Enforce the hard per-task ceiling. Once tripped, the flag makes every
|
|
8234
8842
|
* subsequent (and concurrent) generate() call in this run fail fast.
|
|
@@ -8244,6 +8852,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
8244
8852
|
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.`;
|
|
8245
8853
|
this.runBudgetExceeded = true;
|
|
8246
8854
|
this.runBudgetExceededReason = reason;
|
|
8855
|
+
this.runAbort.abort(new _CascadeRouter.BudgetExceededError(reason));
|
|
8247
8856
|
this.emit("budget:exceeded", { reason, spentUsd: this.sessionCostUsd });
|
|
8248
8857
|
throw new _CascadeRouter.BudgetExceededError(reason);
|
|
8249
8858
|
}
|
|
@@ -9481,6 +10090,12 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
|
|
|
9481
10090
|
maxTokens: 4096,
|
|
9482
10091
|
...subtaskModel ? { model: subtaskModel } : {},
|
|
9483
10092
|
featureTag: this.assignment?.sectionTitle,
|
|
10093
|
+
// The wave signal, so cancel-and-respawn reaches the request that is
|
|
10094
|
+
// RUNNING rather than only stopping the worker at its next checkpoint.
|
|
10095
|
+
// It is a superset of the run signal the router injects when this is
|
|
10096
|
+
// absent (T2 composes it from that signal plus its own per-wave one),
|
|
10097
|
+
// so wiring it here loses no cancellation and adds the wave's.
|
|
10098
|
+
...this.signal ? { signal: this.signal } : {},
|
|
9484
10099
|
...this.localOnlyMatch ? { forceLocal: true } : {}
|
|
9485
10100
|
};
|
|
9486
10101
|
const result = await this.router.generate(
|
|
@@ -9821,6 +10436,22 @@ ${assignment.expectedOutput}`;
|
|
|
9821
10436
|
requiresArtifact() {
|
|
9822
10437
|
return shouldRequireArtifact(this.assignment, this.tools.map((t) => t.name));
|
|
9823
10438
|
}
|
|
10439
|
+
/**
|
|
10440
|
+
* Where a file a tool wrote actually lives.
|
|
10441
|
+
*
|
|
10442
|
+
* This used to be `process.cwd()`, which is only the workspace in a plain
|
|
10443
|
+
* CLI run. The hosted server runs with cwd at the app directory while the
|
|
10444
|
+
* run's workspace is the tenant's scratch dir, and Electron's cwd is the app
|
|
10445
|
+
* bundle rather than the folder the user picked. So the tool wrote
|
|
10446
|
+
* `<workspace>/report.docx`, verification stat'd `<cwd>/report.docx`, found
|
|
10447
|
+
* nothing, and the worker looped and then threw WorkerStallError — surfacing
|
|
10448
|
+
* as "no file was created" on a file that had in fact been created, followed
|
|
10449
|
+
* by a failed node. Asking the registry is exact: it is the same root every
|
|
10450
|
+
* registered tool was configured with (registry.registerDefaults).
|
|
10451
|
+
*/
|
|
10452
|
+
artifactRoot() {
|
|
10453
|
+
return this.toolRegistry.getWorkspaceRoot?.() ?? process.cwd();
|
|
10454
|
+
}
|
|
9824
10455
|
extractArtifactPaths(assignment) {
|
|
9825
10456
|
const declared = (assignment.files ?? []).map((f) => f.trim()).filter((f) => f.includes("."));
|
|
9826
10457
|
const haystack = `${assignment.description}
|
|
@@ -9832,11 +10463,11 @@ ${assignment.expectedOutput}`;
|
|
|
9832
10463
|
const artifactPaths = this.extractArtifactPaths(assignment);
|
|
9833
10464
|
if (!artifactPaths.length) return { ok: true, issues: [] };
|
|
9834
10465
|
const issues = [];
|
|
9835
|
-
const {
|
|
10466
|
+
const { execFile: execFile3 } = await import('child_process');
|
|
9836
10467
|
const { promisify: promisify4 } = await import('util');
|
|
9837
|
-
const
|
|
10468
|
+
const execFileAsync2 = promisify4(execFile3);
|
|
9838
10469
|
for (const artifactPath of artifactPaths) {
|
|
9839
|
-
const absolutePath = path34.resolve(
|
|
10470
|
+
const absolutePath = path34.resolve(this.artifactRoot(), artifactPath);
|
|
9840
10471
|
try {
|
|
9841
10472
|
const stat = await fs11.stat(absolutePath);
|
|
9842
10473
|
if (!stat.isFile()) {
|
|
@@ -9872,20 +10503,18 @@ ${assignment.expectedOutput}`;
|
|
|
9872
10503
|
continue;
|
|
9873
10504
|
}
|
|
9874
10505
|
const ext = path34.extname(absolutePath).toLowerCase();
|
|
9875
|
-
|
|
9876
|
-
|
|
9877
|
-
|
|
9878
|
-
|
|
9879
|
-
|
|
9880
|
-
|
|
9881
|
-
|
|
10506
|
+
const check = ext === ".ts" || ext === ".tsx" ? { cmd: "npx", args: ["tsc", "--noEmit", absolutePath] } : ext === ".js" || ext === ".jsx" ? { cmd: process.execPath, args: ["--check", absolutePath] } : ext === ".py" ? { cmd: "python", args: ["-m", "py_compile", absolutePath] } : null;
|
|
10507
|
+
if (check) {
|
|
10508
|
+
try {
|
|
10509
|
+
await execFileAsync2(check.cmd, check.args, { timeout: 1e4 });
|
|
10510
|
+
} catch (err) {
|
|
10511
|
+
const code = err.code;
|
|
10512
|
+
if (code === "ENOENT" || code === "EINVAL") continue;
|
|
10513
|
+
const e = err;
|
|
10514
|
+
issues.push(`Semantic error in ${artifactPath}:
|
|
10515
|
+
${e.stderr || String(err)}
|
|
10516
|
+
${e.stdout || ""}`);
|
|
9882
10517
|
}
|
|
9883
|
-
} catch (err) {
|
|
9884
|
-
const stderr = err?.stderr || String(err);
|
|
9885
|
-
const stdout = err?.stdout || "";
|
|
9886
|
-
issues.push(`Semantic error in ${artifactPath}:
|
|
9887
|
-
${stderr}
|
|
9888
|
-
${stdout}`);
|
|
9889
10518
|
}
|
|
9890
10519
|
} catch {
|
|
9891
10520
|
issues.push(`Required artifact was not created: ${artifactPath}`);
|
|
@@ -9945,6 +10574,7 @@ ${current}`
|
|
|
9945
10574
|
HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
9946
10575
|
maxTokens: 4096,
|
|
9947
10576
|
featureTag: assignment.sectionTitle,
|
|
10577
|
+
...this.signal ? { signal: this.signal } : {},
|
|
9948
10578
|
...this.localOnlyMatch ? { forceLocal: true } : {}
|
|
9949
10579
|
});
|
|
9950
10580
|
const next = (improved.content ?? "").trim();
|
|
@@ -9964,7 +10594,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
9964
10594
|
async checkAcceptance(assignment) {
|
|
9965
10595
|
const criteria = assignment.acceptance ?? [];
|
|
9966
10596
|
if (!criteria.length) return [];
|
|
9967
|
-
const resolve2 = (target) => path34.resolve(
|
|
10597
|
+
const resolve2 = (target) => path34.resolve(this.artifactRoot(), target);
|
|
9968
10598
|
return evaluateAcceptance(criteria, assignment.files ?? [], {
|
|
9969
10599
|
stat: async (target) => {
|
|
9970
10600
|
try {
|
|
@@ -10006,6 +10636,7 @@ Reply with JSON: { "completeness": "pass"|"fail", "correctness": "pass"|"fail",
|
|
|
10006
10636
|
|
|
10007
10637
|
HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
10008
10638
|
featureTag: assignment.sectionTitle,
|
|
10639
|
+
...this.signal ? { signal: this.signal } : {},
|
|
10009
10640
|
...this.localOnlyMatch ? { forceLocal: true } : {}
|
|
10010
10641
|
});
|
|
10011
10642
|
try {
|
|
@@ -10045,6 +10676,7 @@ ${output.slice(0, 4e3)}`;
|
|
|
10045
10676
|
messages: [{ role: "user", content: prompt }],
|
|
10046
10677
|
maxTokens: 300,
|
|
10047
10678
|
temperature: 0,
|
|
10679
|
+
...this.signal ? { signal: this.signal } : {},
|
|
10048
10680
|
...this.localOnlyMatch ? { forceLocal: true } : {}
|
|
10049
10681
|
});
|
|
10050
10682
|
const match = /\[[\s\S]*\]/.exec(result.content);
|
|
@@ -10156,6 +10788,10 @@ Begin execution now.`;
|
|
|
10156
10788
|
}
|
|
10157
10789
|
};
|
|
10158
10790
|
var PeerBus = class extends EventEmitter {
|
|
10791
|
+
constructor() {
|
|
10792
|
+
super();
|
|
10793
|
+
this.setMaxListeners(64);
|
|
10794
|
+
}
|
|
10159
10795
|
outputs = /* @__PURE__ */ new Map();
|
|
10160
10796
|
waiters = /* @__PURE__ */ new Map();
|
|
10161
10797
|
members = /* @__PURE__ */ new Set();
|
|
@@ -11404,6 +12040,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
11404
12040
|
const waveSignal = AbortSignal.any(
|
|
11405
12041
|
[this.signal, this.waveAbortController.signal].filter(Boolean)
|
|
11406
12042
|
);
|
|
12043
|
+
setMaxListeners(64, waveSignal);
|
|
11407
12044
|
const runOne = async (id) => {
|
|
11408
12045
|
const assignment = sanitizedAssignments.find((a) => a.subtaskId === id);
|
|
11409
12046
|
const worker = workerMap.get(id);
|
|
@@ -15380,6 +16017,18 @@ var ToolRegistry = class extends EventEmitter {
|
|
|
15380
16017
|
this.tools.set(tool.name, tool);
|
|
15381
16018
|
this.emit("tool:added", tool.name);
|
|
15382
16019
|
}
|
|
16020
|
+
/**
|
|
16021
|
+
* The directory every registered tool resolves relative paths against.
|
|
16022
|
+
*
|
|
16023
|
+
* Exposed so a caller that has to reason about a file a tool WROTE can look
|
|
16024
|
+
* in the same place the tool wrote it. `process.cwd()` is not that place
|
|
16025
|
+
* outside a plain CLI run: the hosted server's cwd is the app directory
|
|
16026
|
+
* while the run's workspace is the tenant's scratch dir, and Electron's cwd
|
|
16027
|
+
* is the app bundle rather than the user's chosen folder.
|
|
16028
|
+
*/
|
|
16029
|
+
getWorkspaceRoot() {
|
|
16030
|
+
return this.workspaceRoot;
|
|
16031
|
+
}
|
|
15383
16032
|
/**
|
|
15384
16033
|
* Wait until a named tool is registered, resolving immediately if it already exists.
|
|
15385
16034
|
* T3 workers can call this after encountering a missing-tool error to resume
|
|
@@ -18392,115 +19041,6 @@ var GraphSearchTool = class extends BaseTool {
|
|
|
18392
19041
|
}
|
|
18393
19042
|
};
|
|
18394
19043
|
|
|
18395
|
-
// src/core/context/compaction.ts
|
|
18396
|
-
function estimateTokens(text) {
|
|
18397
|
-
if (!text) return 0;
|
|
18398
|
-
return Math.max(1, Math.ceil(text.length / 4));
|
|
18399
|
-
}
|
|
18400
|
-
function contentToText(content) {
|
|
18401
|
-
if (typeof content === "string") return content;
|
|
18402
|
-
return content.map((c) => c.type === "text" ? c.text : c.type === "tool_result" ? c.content : `[${c.type}]`).join(" ");
|
|
18403
|
-
}
|
|
18404
|
-
function messagesTokens(messages) {
|
|
18405
|
-
return messages.reduce((n, m) => n + estimateTokens(contentToText(m.content)), 0);
|
|
18406
|
-
}
|
|
18407
|
-
function atomize(text, maxChars) {
|
|
18408
|
-
const seps = ["\n\n", "\n", ". ", " "];
|
|
18409
|
-
let units = [text];
|
|
18410
|
-
for (const sep of seps) {
|
|
18411
|
-
if (units.every((u) => u.length <= maxChars)) break;
|
|
18412
|
-
const next = [];
|
|
18413
|
-
for (const u of units) {
|
|
18414
|
-
if (u.length <= maxChars) {
|
|
18415
|
-
next.push(u);
|
|
18416
|
-
continue;
|
|
18417
|
-
}
|
|
18418
|
-
const parts = u.split(sep);
|
|
18419
|
-
parts.forEach((p, i) => next.push(i < parts.length - 1 ? p + sep : p));
|
|
18420
|
-
}
|
|
18421
|
-
units = next;
|
|
18422
|
-
}
|
|
18423
|
-
const out = [];
|
|
18424
|
-
for (const u of units) {
|
|
18425
|
-
if (u.length <= maxChars) out.push(u);
|
|
18426
|
-
else for (let i = 0; i < u.length; i += maxChars) out.push(u.slice(i, i + maxChars));
|
|
18427
|
-
}
|
|
18428
|
-
return out;
|
|
18429
|
-
}
|
|
18430
|
-
function chunkText(text, opts) {
|
|
18431
|
-
const maxChars = Math.max(1, Math.floor(opts.maxTokens * 4));
|
|
18432
|
-
const overlap = Math.min(maxChars - 1, Math.max(0, Math.floor(maxChars * (opts.overlapRatio ?? 0.1))));
|
|
18433
|
-
const trimmed = text.trim();
|
|
18434
|
-
if (!trimmed) return [];
|
|
18435
|
-
if (trimmed.length <= maxChars) return [trimmed];
|
|
18436
|
-
const units = atomize(trimmed, maxChars);
|
|
18437
|
-
const chunks = [];
|
|
18438
|
-
let cur = "";
|
|
18439
|
-
for (const u of units) {
|
|
18440
|
-
if (cur && cur.length + u.length > maxChars) {
|
|
18441
|
-
chunks.push(cur.trim());
|
|
18442
|
-
cur = overlap > 0 ? cur.slice(-overlap) : "";
|
|
18443
|
-
}
|
|
18444
|
-
cur += u;
|
|
18445
|
-
}
|
|
18446
|
-
if (cur.trim()) chunks.push(cur.trim());
|
|
18447
|
-
return chunks;
|
|
18448
|
-
}
|
|
18449
|
-
function needsCompaction(totalTokens, contextWindow, reserve = 0.2) {
|
|
18450
|
-
if (!contextWindow || contextWindow <= 0) return false;
|
|
18451
|
-
return totalTokens > contextWindow * (1 - reserve);
|
|
18452
|
-
}
|
|
18453
|
-
async function mapReduceCompact(text, opts) {
|
|
18454
|
-
const capChars = opts.capTokens * 4;
|
|
18455
|
-
let working = text;
|
|
18456
|
-
let truncated = false;
|
|
18457
|
-
if (working.length > capChars) {
|
|
18458
|
-
working = working.slice(0, capChars);
|
|
18459
|
-
truncated = true;
|
|
18460
|
-
}
|
|
18461
|
-
const instruction = opts.instruction ?? "Summarize this content, preserving key facts, names, numbers, and decisions.";
|
|
18462
|
-
let chunks = chunkText(working, { maxTokens: opts.chunkTokens });
|
|
18463
|
-
if (chunks.length <= 1) return { text: working, chunks: chunks.length, calls: 0, truncated };
|
|
18464
|
-
let calls = 0;
|
|
18465
|
-
let parts = await Promise.all(
|
|
18466
|
-
chunks.map((c, i) => {
|
|
18467
|
-
calls++;
|
|
18468
|
-
return opts.summarize(c, `${instruction} (part ${i + 1} of ${chunks.length})`);
|
|
18469
|
-
})
|
|
18470
|
-
);
|
|
18471
|
-
let combined = parts.join("\n\n");
|
|
18472
|
-
const maxPasses = opts.maxReducePasses ?? 3;
|
|
18473
|
-
for (let pass = 0; pass < maxPasses && estimateTokens(combined) > opts.targetTokens; pass++) {
|
|
18474
|
-
const groups = chunkText(combined, { maxTokens: opts.chunkTokens });
|
|
18475
|
-
if (groups.length <= 1) break;
|
|
18476
|
-
parts = await Promise.all(
|
|
18477
|
-
groups.map((g) => {
|
|
18478
|
-
calls++;
|
|
18479
|
-
return opts.summarize(g, "Merge these partial summaries into one cohesive summary; keep all distinct facts.");
|
|
18480
|
-
})
|
|
18481
|
-
);
|
|
18482
|
-
combined = parts.join("\n\n");
|
|
18483
|
-
}
|
|
18484
|
-
const note = truncated ? "\n\n[Note: input exceeded the extended-context cap and was truncated.]" : "";
|
|
18485
|
-
return { text: combined + note, chunks: chunks.length, calls, truncated };
|
|
18486
|
-
}
|
|
18487
|
-
async function rollingSummary(messages, opts) {
|
|
18488
|
-
if (messages.length <= opts.keepRecent) return messages;
|
|
18489
|
-
const older = messages.slice(0, messages.length - opts.keepRecent);
|
|
18490
|
-
const recent = messages.slice(messages.length - opts.keepRecent);
|
|
18491
|
-
const transcript = older.map((m) => `${m.role}: ${contentToText(m.content)}`).join("\n");
|
|
18492
|
-
const summary = await opts.summarize(
|
|
18493
|
-
transcript,
|
|
18494
|
-
"Summarize this earlier conversation. Preserve facts, decisions, names, numbers, and any open threads or todos."
|
|
18495
|
-
);
|
|
18496
|
-
const summaryMessage = {
|
|
18497
|
-
role: "system",
|
|
18498
|
-
content: `Summary of earlier conversation (folded to fit the context window):
|
|
18499
|
-
${summary}`
|
|
18500
|
-
};
|
|
18501
|
-
return [summaryMessage, ...recent];
|
|
18502
|
-
}
|
|
18503
|
-
|
|
18504
19044
|
// src/core/steering/guidance.ts
|
|
18505
19045
|
var GuidanceQueue = class {
|
|
18506
19046
|
entries = [];
|