cascade-ai 0.72.0 → 0.74.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.cjs +839 -233
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +839 -233
- package/dist/cli.js.map +1 -1
- package/dist/desktop-core.cjs +825 -232
- package/dist/index.cjs +824 -231
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +104 -0
- package/dist/index.d.ts +104 -0
- package/dist/index.js +824 -231
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.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.74.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,
|
|
@@ -2240,10 +2264,17 @@ var init_cost = __esm({
|
|
|
2240
2264
|
});
|
|
2241
2265
|
|
|
2242
2266
|
// src/providers/base.ts
|
|
2243
|
-
var BaseProvider;
|
|
2267
|
+
var ProviderUnreachableError, BaseProvider;
|
|
2244
2268
|
var init_base = __esm({
|
|
2245
2269
|
"src/providers/base.ts"() {
|
|
2246
2270
|
init_cost();
|
|
2271
|
+
ProviderUnreachableError = class extends Error {
|
|
2272
|
+
transient = true;
|
|
2273
|
+
constructor(message) {
|
|
2274
|
+
super(message);
|
|
2275
|
+
this.name = "ProviderUnreachableError";
|
|
2276
|
+
}
|
|
2277
|
+
};
|
|
2247
2278
|
BaseProvider = class {
|
|
2248
2279
|
config;
|
|
2249
2280
|
model;
|
|
@@ -2315,7 +2346,9 @@ var init_model_filter = __esm({
|
|
|
2315
2346
|
// src/providers/anthropic.ts
|
|
2316
2347
|
var anthropic_exports = {};
|
|
2317
2348
|
__export(anthropic_exports, {
|
|
2318
|
-
AnthropicProvider: () => AnthropicProvider
|
|
2349
|
+
AnthropicProvider: () => AnthropicProvider,
|
|
2350
|
+
toAnthropicToolUse: () => toAnthropicToolUse,
|
|
2351
|
+
toAnthropicTools: () => toAnthropicTools
|
|
2319
2352
|
});
|
|
2320
2353
|
function anthropicThinkingParam(modelId, maxTokens) {
|
|
2321
2354
|
if (!/claude-(opus|sonnet)-4/i.test(modelId)) return {};
|
|
@@ -2323,6 +2356,21 @@ function anthropicThinkingParam(modelId, maxTokens) {
|
|
|
2323
2356
|
if (budget < 1024) return {};
|
|
2324
2357
|
return { thinking: { type: "enabled", budget_tokens: budget } };
|
|
2325
2358
|
}
|
|
2359
|
+
function toAnthropicTools(tools) {
|
|
2360
|
+
return tools.map((t) => ({
|
|
2361
|
+
name: t.name,
|
|
2362
|
+
description: t.description,
|
|
2363
|
+
input_schema: t.inputSchema
|
|
2364
|
+
}));
|
|
2365
|
+
}
|
|
2366
|
+
function toAnthropicToolUse(toolCalls) {
|
|
2367
|
+
return toolCalls.map((tc) => ({
|
|
2368
|
+
type: "tool_use",
|
|
2369
|
+
id: tc.id,
|
|
2370
|
+
name: tc.name,
|
|
2371
|
+
input: tc.input
|
|
2372
|
+
}));
|
|
2373
|
+
}
|
|
2326
2374
|
var AnthropicProvider;
|
|
2327
2375
|
var init_anthropic = __esm({
|
|
2328
2376
|
"src/providers/anthropic.ts"() {
|
|
@@ -2351,11 +2399,7 @@ var init_anthropic = __esm({
|
|
|
2351
2399
|
}
|
|
2352
2400
|
async generateStream(options, onChunk) {
|
|
2353
2401
|
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
|
-
}));
|
|
2402
|
+
const tools = options.tools && toAnthropicTools(options.tools);
|
|
2359
2403
|
let fullContent = "";
|
|
2360
2404
|
let inputTokens = 0;
|
|
2361
2405
|
let outputTokens = 0;
|
|
@@ -2483,14 +2527,7 @@ var init_anthropic = __esm({
|
|
|
2483
2527
|
const content = [];
|
|
2484
2528
|
const text = typeof m.content === "string" ? m.content : "";
|
|
2485
2529
|
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
|
-
}
|
|
2530
|
+
content.push(...toAnthropicToolUse(m.toolCalls ?? []));
|
|
2494
2531
|
if (content.length > 0) {
|
|
2495
2532
|
result.push({ role: "assistant", content });
|
|
2496
2533
|
}
|
|
@@ -2529,7 +2566,9 @@ var openai_exports = {};
|
|
|
2529
2566
|
__export(openai_exports, {
|
|
2530
2567
|
OpenAIProvider: () => OpenAIProvider,
|
|
2531
2568
|
isParamShapeError: () => isParamShapeError,
|
|
2532
|
-
isReasoningModel: () => isReasoningModel
|
|
2569
|
+
isReasoningModel: () => isReasoningModel,
|
|
2570
|
+
toOpenAIToolCalls: () => toOpenAIToolCalls,
|
|
2571
|
+
toOpenAITools: () => toOpenAITools
|
|
2533
2572
|
});
|
|
2534
2573
|
function isReasoningModel(id) {
|
|
2535
2574
|
const m = (id || "").toLowerCase();
|
|
@@ -2540,6 +2579,26 @@ function isParamShapeError(err) {
|
|
|
2540
2579
|
const msg = String(e?.message ?? e?.error?.message ?? "").toLowerCase();
|
|
2541
2580
|
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
2581
|
}
|
|
2582
|
+
function toOpenAITools(tools) {
|
|
2583
|
+
return tools.map((t) => ({
|
|
2584
|
+
type: "function",
|
|
2585
|
+
function: {
|
|
2586
|
+
name: t.name,
|
|
2587
|
+
description: t.description,
|
|
2588
|
+
parameters: t.inputSchema
|
|
2589
|
+
}
|
|
2590
|
+
}));
|
|
2591
|
+
}
|
|
2592
|
+
function toOpenAIToolCalls(toolCalls) {
|
|
2593
|
+
return toolCalls.map((tc) => ({
|
|
2594
|
+
id: tc.id,
|
|
2595
|
+
type: "function",
|
|
2596
|
+
function: {
|
|
2597
|
+
name: tc.name,
|
|
2598
|
+
arguments: JSON.stringify(tc.input)
|
|
2599
|
+
}
|
|
2600
|
+
}));
|
|
2601
|
+
}
|
|
2543
2602
|
var OpenAIProvider;
|
|
2544
2603
|
var init_openai = __esm({
|
|
2545
2604
|
"src/providers/openai.ts"() {
|
|
@@ -2566,14 +2625,7 @@ var init_openai = __esm({
|
|
|
2566
2625
|
}
|
|
2567
2626
|
async generateStream(options, onChunk) {
|
|
2568
2627
|
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
|
-
}));
|
|
2628
|
+
const tools = options.tools && toOpenAITools(options.tools);
|
|
2577
2629
|
let fullContent = "";
|
|
2578
2630
|
let inputTokens = 0;
|
|
2579
2631
|
let outputTokens = 0;
|
|
@@ -2719,14 +2771,7 @@ var init_openai = __esm({
|
|
|
2719
2771
|
result.push({
|
|
2720
2772
|
role: "assistant",
|
|
2721
2773
|
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
|
-
}))
|
|
2774
|
+
tool_calls: toOpenAIToolCalls(m.toolCalls)
|
|
2730
2775
|
});
|
|
2731
2776
|
} else {
|
|
2732
2777
|
result.push({ role: m.role, content: m.content });
|
|
@@ -2993,8 +3038,42 @@ var init_gemini_schema = __esm({
|
|
|
2993
3038
|
// src/providers/gemini.ts
|
|
2994
3039
|
var gemini_exports = {};
|
|
2995
3040
|
__export(gemini_exports, {
|
|
2996
|
-
GeminiProvider: () => GeminiProvider
|
|
3041
|
+
GeminiProvider: () => GeminiProvider,
|
|
3042
|
+
toGeminiFunctionCalls: () => toGeminiFunctionCalls,
|
|
3043
|
+
toGeminiTools: () => toGeminiTools
|
|
2997
3044
|
});
|
|
3045
|
+
function toGeminiTools(tools) {
|
|
3046
|
+
return tools.map((tool) => ({
|
|
3047
|
+
name: tool.name,
|
|
3048
|
+
description: tool.description,
|
|
3049
|
+
// Convert, never cast. Gemini's parameters field is an OpenAPI subset
|
|
3050
|
+
// that 400s on unknown keys, and MCP servers ship schemas carrying their
|
|
3051
|
+
// own extensions (GitHub's adds `x-mcp-header` to every header-bound
|
|
3052
|
+
// property). Casting sent those through verbatim and every request failed
|
|
3053
|
+
// before the model saw it. See providers/gemini-schema.ts.
|
|
3054
|
+
parameters: toGeminiParameters(tool.inputSchema)
|
|
3055
|
+
}));
|
|
3056
|
+
}
|
|
3057
|
+
function toGeminiFunctionCalls(toolCalls) {
|
|
3058
|
+
return toolCalls.map((tc) => ({
|
|
3059
|
+
functionCall: {
|
|
3060
|
+
name: tc.name,
|
|
3061
|
+
args: tc.input
|
|
3062
|
+
}
|
|
3063
|
+
}));
|
|
3064
|
+
}
|
|
3065
|
+
function isGeminiAuthStatus(status) {
|
|
3066
|
+
return status === 400 || status === 401 || status === 403;
|
|
3067
|
+
}
|
|
3068
|
+
async function describeGeminiError(resp) {
|
|
3069
|
+
try {
|
|
3070
|
+
const body = await resp.json();
|
|
3071
|
+
const detail = body?.error?.message ?? body?.error?.status;
|
|
3072
|
+
return detail ? ` \u2014 ${detail}` : "";
|
|
3073
|
+
} catch {
|
|
3074
|
+
return "";
|
|
3075
|
+
}
|
|
3076
|
+
}
|
|
2998
3077
|
var GeminiProvider;
|
|
2999
3078
|
var init_gemini = __esm({
|
|
3000
3079
|
"src/providers/gemini.ts"() {
|
|
@@ -3024,7 +3103,7 @@ var init_gemini = __esm({
|
|
|
3024
3103
|
{ category: HarmCategory.HARM_CATEGORY_HARASSMENT, threshold: HarmBlockThreshold.BLOCK_NONE },
|
|
3025
3104
|
{ category: HarmCategory.HARM_CATEGORY_HATE_SPEECH, threshold: HarmBlockThreshold.BLOCK_NONE }
|
|
3026
3105
|
],
|
|
3027
|
-
tools: options.tools?.length ? [{ functionDeclarations: options.tools
|
|
3106
|
+
tools: options.tools?.length ? [{ functionDeclarations: toGeminiTools(options.tools) }] : void 0,
|
|
3028
3107
|
abortSignal: options.signal
|
|
3029
3108
|
}
|
|
3030
3109
|
});
|
|
@@ -3086,9 +3165,7 @@ var init_gemini = __esm({
|
|
|
3086
3165
|
}
|
|
3087
3166
|
async listModels() {
|
|
3088
3167
|
try {
|
|
3089
|
-
const resp = await
|
|
3090
|
-
`https://generativelanguage.googleapis.com/v1beta/models?key=${this.config.apiKey}`
|
|
3091
|
-
);
|
|
3168
|
+
const resp = await this.fetchModelList();
|
|
3092
3169
|
if (!resp.ok) {
|
|
3093
3170
|
return Object.values(MODELS).filter((m) => m.provider === "gemini");
|
|
3094
3171
|
}
|
|
@@ -3119,18 +3196,61 @@ var init_gemini = __esm({
|
|
|
3119
3196
|
return Object.values(MODELS).filter((m) => m.provider === "gemini");
|
|
3120
3197
|
}
|
|
3121
3198
|
}
|
|
3199
|
+
/**
|
|
3200
|
+
* Is this KEY usable — not "does one particular model answer".
|
|
3201
|
+
*
|
|
3202
|
+
* This used to call countTokens() against `this.model.id`, which the router
|
|
3203
|
+
* fills from the first Gemini entry in the bundled catalogue. That made the
|
|
3204
|
+
* whole provider's fate depend on one hard-coded model id: a key that cannot
|
|
3205
|
+
* reach that one model — retired, not enabled for the key's project, served
|
|
3206
|
+
* on a different API version — failed the probe, and because every later step
|
|
3207
|
+
* is gated on the result (`validateCloudProviderModels` and
|
|
3208
|
+
* `discoverProviderModels` both return early for an unavailable provider) the
|
|
3209
|
+
* real model list was never fetched. Gemini vanished entirely, and the CLI
|
|
3210
|
+
* reported "add a provider API key first" about a key that was present and
|
|
3211
|
+
* working.
|
|
3212
|
+
*
|
|
3213
|
+
* Listing models asks the account-level question instead, the same one
|
|
3214
|
+
* OpenAI's probe asks, and it is the same request `listModels()` already
|
|
3215
|
+
* makes for discovery.
|
|
3216
|
+
*
|
|
3217
|
+
* Throws rather than returning false. The router logs the reason it catches,
|
|
3218
|
+
* and "bad key, wrong endpoint/deployment, or unreachable" — three guesses,
|
|
3219
|
+
* no answer — is what made this take a user report to find. A swallowed probe
|
|
3220
|
+
* error is the same defect that once hid a misconfigured Azure deployment.
|
|
3221
|
+
*/
|
|
3122
3222
|
async isAvailable() {
|
|
3223
|
+
if (!this.config.apiKey) throw new Error("no Gemini API key configured");
|
|
3224
|
+
let resp;
|
|
3123
3225
|
try {
|
|
3124
|
-
await this.
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
}
|
|
3130
|
-
|
|
3226
|
+
resp = await this.fetchModelList();
|
|
3227
|
+
} catch (err) {
|
|
3228
|
+
throw new ProviderUnreachableError(
|
|
3229
|
+
`could not reach the Gemini API: ${err instanceof Error ? err.message : String(err)}`
|
|
3230
|
+
);
|
|
3231
|
+
}
|
|
3232
|
+
if (resp.ok) return true;
|
|
3233
|
+
const status = `HTTP ${resp.status} ${resp.statusText}`.trim();
|
|
3234
|
+
const detail = await describeGeminiError(resp);
|
|
3235
|
+
if (isGeminiAuthStatus(resp.status)) {
|
|
3236
|
+
throw new Error(`Gemini rejected the API key: ${status}${detail}`);
|
|
3131
3237
|
}
|
|
3238
|
+
throw new ProviderUnreachableError(`Gemini model list failed: ${status}${detail}`);
|
|
3132
3239
|
}
|
|
3133
3240
|
// ── Private ──────────────────────────────────
|
|
3241
|
+
/**
|
|
3242
|
+
* The account's model list. One place so the availability probe and
|
|
3243
|
+
* discovery cannot ask different questions of the same endpoint.
|
|
3244
|
+
*
|
|
3245
|
+
* The key goes in a header, not the query string: a URL carries into proxy
|
|
3246
|
+
* logs, error reports and shell history, and this one would carry the key
|
|
3247
|
+
* with it.
|
|
3248
|
+
*/
|
|
3249
|
+
fetchModelList() {
|
|
3250
|
+
return fetch("https://generativelanguage.googleapis.com/v1beta/models", {
|
|
3251
|
+
headers: { "x-goog-api-key": this.config.apiKey ?? "" }
|
|
3252
|
+
});
|
|
3253
|
+
}
|
|
3134
3254
|
buildContents(messages, extraImages) {
|
|
3135
3255
|
const contents = [];
|
|
3136
3256
|
for (const m of messages) {
|
|
@@ -3165,14 +3285,7 @@ var init_gemini = __esm({
|
|
|
3165
3285
|
const parts = [];
|
|
3166
3286
|
const textContent = typeof m.content === "string" ? m.content : "";
|
|
3167
3287
|
if (textContent) parts.push({ text: textContent });
|
|
3168
|
-
|
|
3169
|
-
parts.push({
|
|
3170
|
-
functionCall: {
|
|
3171
|
-
name: tc.name,
|
|
3172
|
-
args: tc.input
|
|
3173
|
-
}
|
|
3174
|
-
});
|
|
3175
|
-
}
|
|
3288
|
+
parts.push(...toGeminiFunctionCalls(m.toolCalls ?? []));
|
|
3176
3289
|
if (parts.length > 0) {
|
|
3177
3290
|
contents.push({ role: "model", parts });
|
|
3178
3291
|
}
|
|
@@ -3219,18 +3332,6 @@ var init_gemini = __esm({
|
|
|
3219
3332
|
}
|
|
3220
3333
|
return parts;
|
|
3221
3334
|
}
|
|
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
3335
|
};
|
|
3235
3336
|
}
|
|
3236
3337
|
});
|
|
@@ -3316,12 +3417,32 @@ var init_net = __esm({
|
|
|
3316
3417
|
// src/providers/ollama.ts
|
|
3317
3418
|
var ollama_exports = {};
|
|
3318
3419
|
__export(ollama_exports, {
|
|
3319
|
-
OllamaProvider: () => OllamaProvider
|
|
3420
|
+
OllamaProvider: () => OllamaProvider,
|
|
3421
|
+
toOllamaToolCalls: () => toOllamaToolCalls,
|
|
3422
|
+
toOllamaTools: () => toOllamaTools
|
|
3320
3423
|
});
|
|
3321
3424
|
function isToolCapable(modelName) {
|
|
3322
3425
|
const name = modelName.toLowerCase();
|
|
3323
3426
|
return TOOL_CAPABLE_FAMILIES.some((family) => name.includes(family));
|
|
3324
3427
|
}
|
|
3428
|
+
function toOllamaTools(tools) {
|
|
3429
|
+
return tools.map((t) => ({
|
|
3430
|
+
type: "function",
|
|
3431
|
+
function: {
|
|
3432
|
+
name: t.name,
|
|
3433
|
+
description: t.description,
|
|
3434
|
+
parameters: t.inputSchema
|
|
3435
|
+
}
|
|
3436
|
+
}));
|
|
3437
|
+
}
|
|
3438
|
+
function toOllamaToolCalls(toolCalls) {
|
|
3439
|
+
return toolCalls.map((tc) => ({
|
|
3440
|
+
function: {
|
|
3441
|
+
name: tc.name,
|
|
3442
|
+
arguments: tc.input
|
|
3443
|
+
}
|
|
3444
|
+
}));
|
|
3445
|
+
}
|
|
3325
3446
|
var TOOL_CAPABLE_FAMILIES, OllamaProvider;
|
|
3326
3447
|
var init_ollama = __esm({
|
|
3327
3448
|
"src/providers/ollama.ts"() {
|
|
@@ -3354,14 +3475,7 @@ var init_ollama = __esm({
|
|
|
3354
3475
|
}
|
|
3355
3476
|
async generateStream(options, onChunk) {
|
|
3356
3477
|
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
|
-
}));
|
|
3478
|
+
const ollamaTools = options.tools && toOllamaTools(options.tools);
|
|
3365
3479
|
const response = await fetch(`${this.baseUrl}/api/chat`, {
|
|
3366
3480
|
method: "POST",
|
|
3367
3481
|
headers: { "Content-Type": "application/json" },
|
|
@@ -3541,12 +3655,7 @@ var init_ollama = __esm({
|
|
|
3541
3655
|
result.push({
|
|
3542
3656
|
role: "assistant",
|
|
3543
3657
|
content: typeof m.content === "string" ? m.content : "",
|
|
3544
|
-
tool_calls: m.toolCalls
|
|
3545
|
-
function: {
|
|
3546
|
-
name: tc.name,
|
|
3547
|
-
arguments: tc.input
|
|
3548
|
-
}
|
|
3549
|
-
}))
|
|
3658
|
+
tool_calls: toOllamaToolCalls(m.toolCalls)
|
|
3550
3659
|
});
|
|
3551
3660
|
continue;
|
|
3552
3661
|
}
|
|
@@ -6221,6 +6330,7 @@ init_gemini();
|
|
|
6221
6330
|
init_ollama();
|
|
6222
6331
|
init_openai_compatible();
|
|
6223
6332
|
init_openai();
|
|
6333
|
+
init_base();
|
|
6224
6334
|
|
|
6225
6335
|
// src/core/router/selector.ts
|
|
6226
6336
|
init_constants();
|
|
@@ -6540,6 +6650,21 @@ var DEFAULT_PROVIDER_TPM = {
|
|
|
6540
6650
|
"openai-compatible": 3e4,
|
|
6541
6651
|
ollama: Number.POSITIVE_INFINITY
|
|
6542
6652
|
};
|
|
6653
|
+
function abortReason(signal) {
|
|
6654
|
+
return signal.reason instanceof Error ? signal.reason : new Error("Aborted while waiting for rate-limit capacity");
|
|
6655
|
+
}
|
|
6656
|
+
function sleepUnlessAborted(ms, signal) {
|
|
6657
|
+
if (!signal) return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
6658
|
+
return new Promise((resolve2) => {
|
|
6659
|
+
const done = () => {
|
|
6660
|
+
clearTimeout(timer);
|
|
6661
|
+
signal.removeEventListener("abort", done);
|
|
6662
|
+
resolve2();
|
|
6663
|
+
};
|
|
6664
|
+
const timer = setTimeout(done, ms);
|
|
6665
|
+
signal.addEventListener("abort", done, { once: true });
|
|
6666
|
+
});
|
|
6667
|
+
}
|
|
6543
6668
|
var TpmLimiter = class {
|
|
6544
6669
|
buckets = /* @__PURE__ */ new Map();
|
|
6545
6670
|
constructor(overrides = {}) {
|
|
@@ -6558,9 +6683,10 @@ var TpmLimiter = class {
|
|
|
6558
6683
|
* are reported back via `refund` when short, or simply settled at the next
|
|
6559
6684
|
* refill.
|
|
6560
6685
|
*/
|
|
6561
|
-
async acquire(provider, estimatedTokens) {
|
|
6686
|
+
async acquire(provider, estimatedTokens, signal) {
|
|
6562
6687
|
const bucket = this.buckets.get(provider);
|
|
6563
6688
|
if (!bucket || bucket.tokensPerMinute === Number.POSITIVE_INFINITY) return;
|
|
6689
|
+
if (signal?.aborted) throw abortReason(signal);
|
|
6564
6690
|
const want = Math.min(estimatedTokens, bucket.tokensPerMinute);
|
|
6565
6691
|
for (; ; ) {
|
|
6566
6692
|
this.refill(bucket);
|
|
@@ -6570,7 +6696,8 @@ var TpmLimiter = class {
|
|
|
6570
6696
|
}
|
|
6571
6697
|
const deficit = want - bucket.available;
|
|
6572
6698
|
const waitMs = Math.max(50, Math.ceil(deficit / bucket.tokensPerMinute * 6e4));
|
|
6573
|
-
await
|
|
6699
|
+
await sleepUnlessAborted(waitMs, signal);
|
|
6700
|
+
if (signal?.aborted) throw abortReason(signal);
|
|
6574
6701
|
}
|
|
6575
6702
|
}
|
|
6576
6703
|
/**
|
|
@@ -6616,6 +6743,9 @@ var TpmLimiter = class {
|
|
|
6616
6743
|
};
|
|
6617
6744
|
|
|
6618
6745
|
// src/core/router/local-queue.ts
|
|
6746
|
+
function abortReason2(signal) {
|
|
6747
|
+
return signal.reason instanceof Error ? signal.reason : new Error("Aborted while waiting for a local inference slot");
|
|
6748
|
+
}
|
|
6619
6749
|
var LocalRequestQueue = class {
|
|
6620
6750
|
maxConcurrent;
|
|
6621
6751
|
active = 0;
|
|
@@ -6626,9 +6756,16 @@ var LocalRequestQueue = class {
|
|
|
6626
6756
|
/**
|
|
6627
6757
|
* Acquire a queue slot. Returns a `release` function that MUST be called
|
|
6628
6758
|
* when the inference call is done (even on error). Rejects if the slot
|
|
6629
|
-
* cannot be acquired within `timeoutMs
|
|
6759
|
+
* cannot be acquired within `timeoutMs`, or as soon as `signal` fires.
|
|
6760
|
+
*
|
|
6761
|
+
* The signal matters as much as the timeout: with concurrency of 1 a caller
|
|
6762
|
+
* can sit here for half the inference timeout — 150 seconds by default —
|
|
6763
|
+
* and until it was wired up, neither cancelling the run nor spending its
|
|
6764
|
+
* budget got it out. It waited out the window for a slot it would drop the
|
|
6765
|
+
* moment it received one.
|
|
6630
6766
|
*/
|
|
6631
|
-
async acquire(timeoutMs) {
|
|
6767
|
+
async acquire(timeoutMs, signal) {
|
|
6768
|
+
if (signal?.aborted) throw abortReason2(signal);
|
|
6632
6769
|
if (this.active < this.maxConcurrent) {
|
|
6633
6770
|
this.active++;
|
|
6634
6771
|
return this.makeRelease();
|
|
@@ -6636,23 +6773,36 @@ var LocalRequestQueue = class {
|
|
|
6636
6773
|
return new Promise((resolve2, reject) => {
|
|
6637
6774
|
let settled = false;
|
|
6638
6775
|
let timer;
|
|
6776
|
+
const cleanup = () => {
|
|
6777
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
6778
|
+
signal?.removeEventListener("abort", onAbort);
|
|
6779
|
+
};
|
|
6639
6780
|
const resolver = (release) => {
|
|
6640
6781
|
if (settled) return;
|
|
6641
6782
|
settled = true;
|
|
6642
|
-
|
|
6783
|
+
cleanup();
|
|
6643
6784
|
resolve2(release);
|
|
6644
6785
|
};
|
|
6786
|
+
const abandon = () => {
|
|
6787
|
+
if (settled) return false;
|
|
6788
|
+
settled = true;
|
|
6789
|
+
cleanup();
|
|
6790
|
+
const idx = this.queue.indexOf(resolver);
|
|
6791
|
+
if (idx !== -1) this.queue.splice(idx, 1);
|
|
6792
|
+
return true;
|
|
6793
|
+
};
|
|
6794
|
+
function onAbort() {
|
|
6795
|
+
if (abandon()) reject(abortReason2(signal));
|
|
6796
|
+
}
|
|
6645
6797
|
if (timeoutMs !== void 0 && timeoutMs > 0) {
|
|
6646
6798
|
timer = setTimeout(() => {
|
|
6647
|
-
if (
|
|
6648
|
-
settled = true;
|
|
6649
|
-
const idx = this.queue.indexOf(resolver);
|
|
6650
|
-
if (idx !== -1) this.queue.splice(idx, 1);
|
|
6799
|
+
if (!abandon()) return;
|
|
6651
6800
|
reject(new Error(
|
|
6652
6801
|
`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
6802
|
));
|
|
6654
6803
|
}, timeoutMs);
|
|
6655
6804
|
}
|
|
6805
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
6656
6806
|
this.queue.push(resolver);
|
|
6657
6807
|
});
|
|
6658
6808
|
}
|
|
@@ -6793,6 +6943,116 @@ function fileDeadModelPersistence(filePath) {
|
|
|
6793
6943
|
init_constants();
|
|
6794
6944
|
init_cost();
|
|
6795
6945
|
|
|
6946
|
+
// src/core/context/compaction.ts
|
|
6947
|
+
var CHARS_PER_TOKEN = 4;
|
|
6948
|
+
function estimateTokens(text) {
|
|
6949
|
+
if (!text) return 0;
|
|
6950
|
+
return Math.max(1, Math.ceil(text.length / CHARS_PER_TOKEN));
|
|
6951
|
+
}
|
|
6952
|
+
function contentToText(content) {
|
|
6953
|
+
if (typeof content === "string") return content;
|
|
6954
|
+
return content.map((c) => c.type === "text" ? c.text : c.type === "tool_result" ? c.content : `[${c.type}]`).join(" ");
|
|
6955
|
+
}
|
|
6956
|
+
function messagesTokens(messages) {
|
|
6957
|
+
return messages.reduce((n, m) => n + estimateTokens(contentToText(m.content)), 0);
|
|
6958
|
+
}
|
|
6959
|
+
function atomize(text, maxChars) {
|
|
6960
|
+
const seps = ["\n\n", "\n", ". ", " "];
|
|
6961
|
+
let units = [text];
|
|
6962
|
+
for (const sep of seps) {
|
|
6963
|
+
if (units.every((u) => u.length <= maxChars)) break;
|
|
6964
|
+
const next = [];
|
|
6965
|
+
for (const u of units) {
|
|
6966
|
+
if (u.length <= maxChars) {
|
|
6967
|
+
next.push(u);
|
|
6968
|
+
continue;
|
|
6969
|
+
}
|
|
6970
|
+
const parts = u.split(sep);
|
|
6971
|
+
parts.forEach((p, i) => next.push(i < parts.length - 1 ? p + sep : p));
|
|
6972
|
+
}
|
|
6973
|
+
units = next;
|
|
6974
|
+
}
|
|
6975
|
+
const out = [];
|
|
6976
|
+
for (const u of units) {
|
|
6977
|
+
if (u.length <= maxChars) out.push(u);
|
|
6978
|
+
else for (let i = 0; i < u.length; i += maxChars) out.push(u.slice(i, i + maxChars));
|
|
6979
|
+
}
|
|
6980
|
+
return out;
|
|
6981
|
+
}
|
|
6982
|
+
function chunkText(text, opts) {
|
|
6983
|
+
const maxChars = Math.max(1, Math.floor(opts.maxTokens * 4));
|
|
6984
|
+
const overlap = Math.min(maxChars - 1, Math.max(0, Math.floor(maxChars * (opts.overlapRatio ?? 0.1))));
|
|
6985
|
+
const trimmed = text.trim();
|
|
6986
|
+
if (!trimmed) return [];
|
|
6987
|
+
if (trimmed.length <= maxChars) return [trimmed];
|
|
6988
|
+
const units = atomize(trimmed, maxChars);
|
|
6989
|
+
const chunks = [];
|
|
6990
|
+
let cur = "";
|
|
6991
|
+
for (const u of units) {
|
|
6992
|
+
if (cur && cur.length + u.length > maxChars) {
|
|
6993
|
+
chunks.push(cur.trim());
|
|
6994
|
+
cur = overlap > 0 ? cur.slice(-overlap) : "";
|
|
6995
|
+
}
|
|
6996
|
+
cur += u;
|
|
6997
|
+
}
|
|
6998
|
+
if (cur.trim()) chunks.push(cur.trim());
|
|
6999
|
+
return chunks;
|
|
7000
|
+
}
|
|
7001
|
+
function needsCompaction(totalTokens, contextWindow, reserve = 0.2) {
|
|
7002
|
+
if (!contextWindow || contextWindow <= 0) return false;
|
|
7003
|
+
return totalTokens > contextWindow * (1 - reserve);
|
|
7004
|
+
}
|
|
7005
|
+
async function mapReduceCompact(text, opts) {
|
|
7006
|
+
const capChars = opts.capTokens * 4;
|
|
7007
|
+
let working = text;
|
|
7008
|
+
let truncated = false;
|
|
7009
|
+
if (working.length > capChars) {
|
|
7010
|
+
working = working.slice(0, capChars);
|
|
7011
|
+
truncated = true;
|
|
7012
|
+
}
|
|
7013
|
+
const instruction = opts.instruction ?? "Summarize this content, preserving key facts, names, numbers, and decisions.";
|
|
7014
|
+
let chunks = chunkText(working, { maxTokens: opts.chunkTokens });
|
|
7015
|
+
if (chunks.length <= 1) return { text: working, chunks: chunks.length, calls: 0, truncated };
|
|
7016
|
+
let calls = 0;
|
|
7017
|
+
let parts = await Promise.all(
|
|
7018
|
+
chunks.map((c, i) => {
|
|
7019
|
+
calls++;
|
|
7020
|
+
return opts.summarize(c, `${instruction} (part ${i + 1} of ${chunks.length})`);
|
|
7021
|
+
})
|
|
7022
|
+
);
|
|
7023
|
+
let combined = parts.join("\n\n");
|
|
7024
|
+
const maxPasses = opts.maxReducePasses ?? 3;
|
|
7025
|
+
for (let pass = 0; pass < maxPasses && estimateTokens(combined) > opts.targetTokens; pass++) {
|
|
7026
|
+
const groups = chunkText(combined, { maxTokens: opts.chunkTokens });
|
|
7027
|
+
if (groups.length <= 1) break;
|
|
7028
|
+
parts = await Promise.all(
|
|
7029
|
+
groups.map((g) => {
|
|
7030
|
+
calls++;
|
|
7031
|
+
return opts.summarize(g, "Merge these partial summaries into one cohesive summary; keep all distinct facts.");
|
|
7032
|
+
})
|
|
7033
|
+
);
|
|
7034
|
+
combined = parts.join("\n\n");
|
|
7035
|
+
}
|
|
7036
|
+
const note = truncated ? "\n\n[Note: input exceeded the extended-context cap and was truncated.]" : "";
|
|
7037
|
+
return { text: combined + note, chunks: chunks.length, calls, truncated };
|
|
7038
|
+
}
|
|
7039
|
+
async function rollingSummary(messages, opts) {
|
|
7040
|
+
if (messages.length <= opts.keepRecent) return messages;
|
|
7041
|
+
const older = messages.slice(0, messages.length - opts.keepRecent);
|
|
7042
|
+
const recent = messages.slice(messages.length - opts.keepRecent);
|
|
7043
|
+
const transcript = older.map((m) => `${m.role}: ${contentToText(m.content)}`).join("\n");
|
|
7044
|
+
const summary = await opts.summarize(
|
|
7045
|
+
transcript,
|
|
7046
|
+
"Summarize this earlier conversation. Preserve facts, decisions, names, numbers, and any open threads or todos."
|
|
7047
|
+
);
|
|
7048
|
+
const summaryMessage = {
|
|
7049
|
+
role: "system",
|
|
7050
|
+
content: `Summary of earlier conversation (folded to fit the context window):
|
|
7051
|
+
${summary}`
|
|
7052
|
+
};
|
|
7053
|
+
return [summaryMessage, ...recent];
|
|
7054
|
+
}
|
|
7055
|
+
|
|
6796
7056
|
// src/utils/retry.ts
|
|
6797
7057
|
var CascadeCancelledError = class extends Error {
|
|
6798
7058
|
constructor(reason) {
|
|
@@ -6814,6 +7074,162 @@ async function withTimeout(promise, timeoutMs, errorMessage = "Operation timed o
|
|
|
6814
7074
|
if (timer !== void 0) clearTimeout(timer);
|
|
6815
7075
|
}
|
|
6816
7076
|
}
|
|
7077
|
+
async function withTimeoutAbort(run, timeoutMs, errorMessage = "Operation timed out", outer) {
|
|
7078
|
+
const outers = (Array.isArray(outer) ? outer : [outer]).filter((sig) => sig !== void 0);
|
|
7079
|
+
const controller = new AbortController();
|
|
7080
|
+
let timer;
|
|
7081
|
+
let rejectRace;
|
|
7082
|
+
const settle = (reason) => {
|
|
7083
|
+
rejectRace?.(reason);
|
|
7084
|
+
controller.abort(reason);
|
|
7085
|
+
};
|
|
7086
|
+
const abortOuter = () => {
|
|
7087
|
+
const fired = outers.find((sig) => sig.aborted);
|
|
7088
|
+
settle(fired?.reason instanceof Error ? fired.reason : new CascadeCancelledError("Run cancelled"));
|
|
7089
|
+
};
|
|
7090
|
+
const failPromise = new Promise((_, reject) => {
|
|
7091
|
+
rejectRace = reject;
|
|
7092
|
+
timer = setTimeout(() => settle(new Error(errorMessage)), timeoutMs);
|
|
7093
|
+
});
|
|
7094
|
+
try {
|
|
7095
|
+
if (outers.some((sig) => sig.aborted)) {
|
|
7096
|
+
abortOuter();
|
|
7097
|
+
return await failPromise;
|
|
7098
|
+
}
|
|
7099
|
+
for (const sig of outers) sig.addEventListener("abort", abortOuter, { once: true });
|
|
7100
|
+
return await Promise.race([run(controller.signal), failPromise]);
|
|
7101
|
+
} finally {
|
|
7102
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
7103
|
+
for (const sig of outers) sig.removeEventListener("abort", abortOuter);
|
|
7104
|
+
}
|
|
7105
|
+
}
|
|
7106
|
+
function anySignal(sources) {
|
|
7107
|
+
const live = sources.filter((s) => s !== void 0);
|
|
7108
|
+
if (live.length === 0) return { release: () => {
|
|
7109
|
+
} };
|
|
7110
|
+
if (live.length === 1) return { signal: live[0], release: () => {
|
|
7111
|
+
} };
|
|
7112
|
+
const controller = new AbortController();
|
|
7113
|
+
function detach() {
|
|
7114
|
+
for (const s of live) s.removeEventListener("abort", onAbort);
|
|
7115
|
+
}
|
|
7116
|
+
function onAbort() {
|
|
7117
|
+
const fired = live.find((s) => s.aborted);
|
|
7118
|
+
controller.abort(fired?.reason instanceof Error ? fired.reason : new CascadeCancelledError("Run cancelled"));
|
|
7119
|
+
detach();
|
|
7120
|
+
}
|
|
7121
|
+
if (live.some((s) => s.aborted)) {
|
|
7122
|
+
onAbort();
|
|
7123
|
+
return { signal: controller.signal, release: detach };
|
|
7124
|
+
}
|
|
7125
|
+
for (const s of live) s.addEventListener("abort", onAbort, { once: true });
|
|
7126
|
+
return { signal: controller.signal, release: detach };
|
|
7127
|
+
}
|
|
7128
|
+
|
|
7129
|
+
// src/core/router/wire-profile.ts
|
|
7130
|
+
init_anthropic();
|
|
7131
|
+
init_gemini();
|
|
7132
|
+
init_ollama();
|
|
7133
|
+
init_openai();
|
|
7134
|
+
var textAndImageOnly = (block) => block.type === "text" || block.type === "image";
|
|
7135
|
+
var ANTHROPIC = {
|
|
7136
|
+
// convertMessages: `if (m.role === 'system') continue` — history system
|
|
7137
|
+
// messages are skipped outright; only options.systemPrompt goes as system
|
|
7138
|
+
// input. Compaction emits exactly such a message, holding a summary of the
|
|
7139
|
+
// whole conversation, so charging it refused runs over input never sent.
|
|
7140
|
+
dropsMessage: (m) => m.role === "system",
|
|
7141
|
+
blockHandling: (m) => {
|
|
7142
|
+
if (m.role === "tool") return "stringified";
|
|
7143
|
+
if (m.role === "assistant") return "dropped";
|
|
7144
|
+
return "blocks";
|
|
7145
|
+
},
|
|
7146
|
+
sendsToolCalls: (m) => m.role === "assistant",
|
|
7147
|
+
sendsBlock: textAndImageOnly,
|
|
7148
|
+
sendsToolCallId: true,
|
|
7149
|
+
sizeToolCalls: toAnthropicToolUse,
|
|
7150
|
+
sendsUrlImages: true,
|
|
7151
|
+
readsTopLevelImages: false,
|
|
7152
|
+
sizeTools: toAnthropicTools
|
|
7153
|
+
};
|
|
7154
|
+
var GEMINI = {
|
|
7155
|
+
// buildContents folds a system message into the next user turn, but only
|
|
7156
|
+
// when it has text: array content collapses to '' and the message is
|
|
7157
|
+
// skipped, as is a whitespace-only string.
|
|
7158
|
+
dropsMessage: (m) => m.role === "system" && !(typeof m.content === "string" ? m.content : "").trim(),
|
|
7159
|
+
blockHandling: (m) => {
|
|
7160
|
+
if (m.role === "tool") return "stringified";
|
|
7161
|
+
if (m.role === "assistant" || m.role === "system") return "dropped";
|
|
7162
|
+
return "blocks";
|
|
7163
|
+
},
|
|
7164
|
+
sendsToolCalls: (m) => m.role === "assistant",
|
|
7165
|
+
sendsBlock: textAndImageOnly,
|
|
7166
|
+
sendsToolCallId: true,
|
|
7167
|
+
sizeToolCalls: toGeminiFunctionCalls,
|
|
7168
|
+
sendsUrlImages: false,
|
|
7169
|
+
readsTopLevelImages: true,
|
|
7170
|
+
sizeTools: toGeminiTools
|
|
7171
|
+
};
|
|
7172
|
+
var OPENAI = {
|
|
7173
|
+
dropsMessage: () => false,
|
|
7174
|
+
blockHandling: (m) => {
|
|
7175
|
+
if (m.role === "system" || m.role === "tool") return "dropped";
|
|
7176
|
+
return "blocks";
|
|
7177
|
+
},
|
|
7178
|
+
// The tool_calls branch is inside `if (typeof m.content === 'string')`; an
|
|
7179
|
+
// assistant turn with array content falls through to the parts branch and
|
|
7180
|
+
// its tool calls are never attached.
|
|
7181
|
+
sendsToolCalls: (m) => m.role === "assistant" && typeof m.content === "string",
|
|
7182
|
+
sendsBlock: textAndImageOnly,
|
|
7183
|
+
sendsToolCallId: true,
|
|
7184
|
+
sizeToolCalls: toOpenAIToolCalls,
|
|
7185
|
+
sendsUrlImages: true,
|
|
7186
|
+
readsTopLevelImages: false,
|
|
7187
|
+
sizeTools: toOpenAITools
|
|
7188
|
+
};
|
|
7189
|
+
var OLLAMA = {
|
|
7190
|
+
dropsMessage: () => false,
|
|
7191
|
+
blockHandling: (m) => {
|
|
7192
|
+
if (m.role === "system") return "dropped";
|
|
7193
|
+
if (m.role === "tool") return "stringified";
|
|
7194
|
+
if (m.role === "assistant" && m.toolCalls?.length) return "dropped";
|
|
7195
|
+
return "blocks";
|
|
7196
|
+
},
|
|
7197
|
+
sendsToolCalls: (m) => m.role === "assistant",
|
|
7198
|
+
// The image split maps `b.image.data` with no check on `type`, so a URL is
|
|
7199
|
+
// submitted too (as a URL string in the `images` array).
|
|
7200
|
+
sendsBlock: textAndImageOnly,
|
|
7201
|
+
sendsToolCallId: false,
|
|
7202
|
+
sizeToolCalls: toOllamaToolCalls,
|
|
7203
|
+
sendsUrlImages: true,
|
|
7204
|
+
readsTopLevelImages: false,
|
|
7205
|
+
sizeTools: toOllamaTools
|
|
7206
|
+
};
|
|
7207
|
+
var PROFILES = {
|
|
7208
|
+
anthropic: ANTHROPIC,
|
|
7209
|
+
gemini: GEMINI,
|
|
7210
|
+
openai: OPENAI,
|
|
7211
|
+
// AzureOpenAIProvider and OpenAICompatibleProvider both extend OpenAIProvider
|
|
7212
|
+
// and neither overrides convertMessages.
|
|
7213
|
+
azure: OPENAI,
|
|
7214
|
+
"openai-compatible": OPENAI,
|
|
7215
|
+
ollama: OLLAMA
|
|
7216
|
+
};
|
|
7217
|
+
function wireProfile(provider) {
|
|
7218
|
+
return PROFILES[provider] ?? OPENAI;
|
|
7219
|
+
}
|
|
7220
|
+
function geminiImageCopies(messages) {
|
|
7221
|
+
for (const m of messages) {
|
|
7222
|
+
if (m.role === "user") {
|
|
7223
|
+
const laterUserTurn = messages.slice(messages.indexOf(m) + 1).some((x) => x.role === "user");
|
|
7224
|
+
return laterUserTurn ? 2 : 1;
|
|
7225
|
+
}
|
|
7226
|
+
const text = typeof m.content === "string" ? m.content : "";
|
|
7227
|
+
if (m.role === "system" && text.trim()) return 1;
|
|
7228
|
+
if (m.role === "tool") return 1;
|
|
7229
|
+
if (m.role === "assistant" && (text || m.toolCalls?.length)) return 1;
|
|
7230
|
+
}
|
|
7231
|
+
return 1;
|
|
7232
|
+
}
|
|
6817
7233
|
|
|
6818
7234
|
// src/core/router/model-profiler.ts
|
|
6819
7235
|
var SKIP_PATTERN = /embed|dall-e|whisper|tts|vision|instruct-vision|rerank/i;
|
|
@@ -7323,6 +7739,36 @@ function azureModelForTier(tier, ranked) {
|
|
|
7323
7739
|
if (tier === "T3") return ranked[ranked.length - 1];
|
|
7324
7740
|
return ranked[Math.min(1, ranked.length - 1)];
|
|
7325
7741
|
}
|
|
7742
|
+
var IMAGE_TOKENS_MAX = 2e3;
|
|
7743
|
+
var IMAGE_TOKENS_EACH = IMAGE_TOKENS_MAX;
|
|
7744
|
+
function safeJson(value) {
|
|
7745
|
+
try {
|
|
7746
|
+
return JSON.stringify(value) ?? "";
|
|
7747
|
+
} catch {
|
|
7748
|
+
return "";
|
|
7749
|
+
}
|
|
7750
|
+
}
|
|
7751
|
+
function newRunAbort() {
|
|
7752
|
+
const controller = new AbortController();
|
|
7753
|
+
setMaxListeners(64, controller.signal);
|
|
7754
|
+
return controller;
|
|
7755
|
+
}
|
|
7756
|
+
var TOKENS_PER_MESSAGE_FRAMING = 4;
|
|
7757
|
+
function guardTokens(text) {
|
|
7758
|
+
if (!text) return 0;
|
|
7759
|
+
let asciiChars = 0;
|
|
7760
|
+
let denseBytes = 0;
|
|
7761
|
+
for (const ch of text) {
|
|
7762
|
+
const cp = ch.codePointAt(0);
|
|
7763
|
+
if (cp <= 127) {
|
|
7764
|
+
asciiChars++;
|
|
7765
|
+
continue;
|
|
7766
|
+
}
|
|
7767
|
+
denseBytes += cp <= 2047 ? 2 : cp <= 65535 ? 3 : 4;
|
|
7768
|
+
}
|
|
7769
|
+
if (denseBytes === 0) return estimateTokens(text);
|
|
7770
|
+
return Math.ceil(asciiChars / CHARS_PER_TOKEN) + denseBytes;
|
|
7771
|
+
}
|
|
7326
7772
|
var DISCOVERY_TTL_MS = 15 * 60 * 1e3;
|
|
7327
7773
|
var DISCOVERY_TIMEOUT_MS = 4e3;
|
|
7328
7774
|
var cloudDiscoveryCache = /* @__PURE__ */ new Map();
|
|
@@ -7375,6 +7821,33 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
7375
7821
|
// start of every `cascade run`, independent of the session-wide budget.
|
|
7376
7822
|
runTokens = 0;
|
|
7377
7823
|
runCostUsd = 0;
|
|
7824
|
+
/**
|
|
7825
|
+
* Budget held for calls that are IN FLIGHT — admitted by the preflight check
|
|
7826
|
+
* but not yet counted by recordStats. Without these, concurrent calls all
|
|
7827
|
+
* measure themselves against the same unspent allowance and are all admitted.
|
|
7828
|
+
* Released when each call settles; reset by beginRun() alongside the totals.
|
|
7829
|
+
*/
|
|
7830
|
+
reservedTokens = 0;
|
|
7831
|
+
reservedCostUsd = 0;
|
|
7832
|
+
/**
|
|
7833
|
+
* Fires when this run's budget is spent, cancelling work already in flight.
|
|
7834
|
+
*
|
|
7835
|
+
* The kill switch was previously only a flag, which stops calls that have
|
|
7836
|
+
* not yet submitted. A parallel wave's earlier members are already at the
|
|
7837
|
+
* provider by then, and they carried on generating for a run whose output
|
|
7838
|
+
* would be discarded. Every provider call chains to this, so tripping the
|
|
7839
|
+
* ceiling reaches the requests that are running, not just the ones queued.
|
|
7840
|
+
*/
|
|
7841
|
+
runAbort = newRunAbort();
|
|
7842
|
+
/**
|
|
7843
|
+
* Bumped by every beginRun(). A release handle remembers the generation it
|
|
7844
|
+
* was issued in and does nothing if that has moved on: beginRun() zeroes the
|
|
7845
|
+
* counters, so a call still in flight from the previous run would otherwise
|
|
7846
|
+
* subtract its estimate from the NEW run's totals and drive them negative —
|
|
7847
|
+
* leaving later preflights believing there is more allowance than the cap
|
|
7848
|
+
* actually holds, which is worse than no gate at all.
|
|
7849
|
+
*/
|
|
7850
|
+
runGeneration = 0;
|
|
7378
7851
|
runBudgetExceeded = false;
|
|
7379
7852
|
runBudgetExceededReason;
|
|
7380
7853
|
/**
|
|
@@ -7544,7 +8017,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
7544
8017
|
try {
|
|
7545
8018
|
const cfg = this.config.providers.find((p) => p.type === model.provider) ?? { type: model.provider };
|
|
7546
8019
|
const provider = this.createProvider(cfg, model);
|
|
7547
|
-
const result = await
|
|
8020
|
+
const result = await withTimeoutAbort((signal) => provider.generate({
|
|
7548
8021
|
messages: [{ role: "user", content: "Call the echo tool with text set to 'ping'. Use the tool; do not answer in prose." }],
|
|
7549
8022
|
tools: [{
|
|
7550
8023
|
name: "echo",
|
|
@@ -7552,7 +8025,8 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
7552
8025
|
inputSchema: { type: "object", properties: { text: { type: "string", description: "Text to echo" } }, required: ["text"] }
|
|
7553
8026
|
}],
|
|
7554
8027
|
maxTokens: 80,
|
|
7555
|
-
temperature: 0
|
|
8028
|
+
temperature: 0,
|
|
8029
|
+
signal
|
|
7556
8030
|
}), 3e4, "tool-support probe timed out");
|
|
7557
8031
|
return (result.toolCalls?.length ?? 0) > 0;
|
|
7558
8032
|
} catch {
|
|
@@ -7673,9 +8147,12 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
7673
8147
|
this.runBudgetExceededReason ?? "Per-task budget exceeded."
|
|
7674
8148
|
);
|
|
7675
8149
|
}
|
|
8150
|
+
const runGeneration = this.runGeneration;
|
|
8151
|
+
const runAbort = this.runAbort;
|
|
8152
|
+
const runSignal = this.runSignal;
|
|
7676
8153
|
options = applyTierLimits(options, tier, this.config?.tierLimits);
|
|
7677
|
-
if (
|
|
7678
|
-
options = { ...options, signal:
|
|
8154
|
+
if (runSignal && !options.signal) {
|
|
8155
|
+
options = { ...options, signal: runSignal };
|
|
7679
8156
|
}
|
|
7680
8157
|
if (options.model && !requireVision) {
|
|
7681
8158
|
this.ensureProvider(options.model, this.config.providers);
|
|
@@ -7697,58 +8174,84 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
7697
8174
|
if (!model) throw new Error(`No model available for tier ${tier}`);
|
|
7698
8175
|
const provider = this.getProvider(model);
|
|
7699
8176
|
if (!provider) throw new Error(`No provider for model ${model.id}`);
|
|
8177
|
+
let releaseReservation = this.enforcePreflightBudget(model, options);
|
|
7700
8178
|
const requestedTokens = options.maxTokens ?? model.maxOutputTokens ?? 1024;
|
|
7701
8179
|
const estimatedTokens = Math.min(requestedTokens, model.maxOutputTokens ?? requestedTokens) + 512;
|
|
7702
8180
|
if (this.tpmLimiter) {
|
|
7703
|
-
|
|
8181
|
+
const wait = anySignal([options.signal, runAbort.signal]);
|
|
8182
|
+
try {
|
|
8183
|
+
await this.tpmLimiter.acquire(model.provider, estimatedTokens, wait.signal);
|
|
8184
|
+
} catch (err) {
|
|
8185
|
+
releaseReservation?.();
|
|
8186
|
+
throw this.asAbortFailure(err, options.signal, runSignal);
|
|
8187
|
+
} finally {
|
|
8188
|
+
wait.release();
|
|
8189
|
+
}
|
|
7704
8190
|
}
|
|
7705
8191
|
const useStream = Boolean(onChunk) && model.supportsStreaming && typeof provider.generateStream === "function";
|
|
7706
8192
|
let releaseLocalSlot;
|
|
7707
8193
|
if (model.isLocal) {
|
|
7708
8194
|
const inferenceTimeoutMs = this.config.localInferenceTimeoutMs ?? 3e5;
|
|
7709
8195
|
const queueWaitMs = Math.round(inferenceTimeoutMs / 2);
|
|
7710
|
-
|
|
8196
|
+
const wait = anySignal([options.signal, runAbort.signal]);
|
|
8197
|
+
try {
|
|
8198
|
+
releaseLocalSlot = await this.localQueue.acquire(queueWaitMs, wait.signal);
|
|
8199
|
+
} catch (err) {
|
|
8200
|
+
releaseReservation?.();
|
|
8201
|
+
throw this.asAbortFailure(err, options.signal, runSignal);
|
|
8202
|
+
} finally {
|
|
8203
|
+
wait.release();
|
|
8204
|
+
}
|
|
7711
8205
|
}
|
|
7712
8206
|
try {
|
|
8207
|
+
if (runGeneration === this.runGeneration && this.runBudgetExceeded) {
|
|
8208
|
+
this.tpmLimiter?.refund(model.provider, estimatedTokens);
|
|
8209
|
+
throw new _CascadeRouter.BudgetExceededError(
|
|
8210
|
+
this.runBudgetExceededReason ?? "Per-task budget exceeded."
|
|
8211
|
+
);
|
|
8212
|
+
}
|
|
7713
8213
|
let result;
|
|
7714
8214
|
if (model.isLocal) {
|
|
7715
8215
|
const inferenceTimeoutMs = this.config.localInferenceTimeoutMs ?? 3e5;
|
|
7716
|
-
|
|
7717
|
-
|
|
7718
|
-
|
|
7719
|
-
|
|
7720
|
-
|
|
7721
|
-
inferencePromise,
|
|
8216
|
+
result = await withTimeoutAbort(
|
|
8217
|
+
(signal) => useStream && onChunk ? provider.generateStream({ ...options, signal }, (chunk) => {
|
|
8218
|
+
const text = typeof chunk?.text === "string" ? chunk.text : "";
|
|
8219
|
+
if (text) onChunk({ ...chunk, text });
|
|
8220
|
+
}) : provider.generate({ ...options, signal }),
|
|
7722
8221
|
inferenceTimeoutMs,
|
|
7723
|
-
`Local model ${model.id} inference timed out after ${inferenceTimeoutMs}ms
|
|
8222
|
+
`Local model ${model.id} inference timed out after ${inferenceTimeoutMs}ms`,
|
|
8223
|
+
[options.signal, runAbort.signal]
|
|
7724
8224
|
);
|
|
7725
8225
|
} else if (useStream && onChunk) {
|
|
7726
8226
|
const cloudTimeoutMs = this.config.cloudInferenceTimeoutMs ?? 12e4;
|
|
7727
8227
|
try {
|
|
7728
|
-
result = await
|
|
7729
|
-
provider.generateStream(options, (chunk) => {
|
|
8228
|
+
result = await withTimeoutAbort(
|
|
8229
|
+
(signal) => provider.generateStream({ ...options, signal }, (chunk) => {
|
|
7730
8230
|
const text = typeof chunk?.text === "string" ? chunk.text : "";
|
|
7731
8231
|
if (text) onChunk({ ...chunk, text });
|
|
7732
8232
|
}),
|
|
7733
8233
|
cloudTimeoutMs,
|
|
7734
|
-
`Model ${model.id} stream timed out after ${cloudTimeoutMs}ms
|
|
8234
|
+
`Model ${model.id} stream timed out after ${cloudTimeoutMs}ms`,
|
|
8235
|
+
[options.signal, runAbort.signal]
|
|
7735
8236
|
);
|
|
7736
8237
|
} catch (streamErr) {
|
|
7737
|
-
if (streamErr instanceof Error && streamErr.name === "AbortError" ||
|
|
8238
|
+
if (streamErr instanceof Error && streamErr.name === "AbortError" || runSignal?.aborted || options.signal?.aborted) {
|
|
7738
8239
|
throw streamErr;
|
|
7739
8240
|
}
|
|
7740
|
-
result = await
|
|
7741
|
-
provider.generate(options),
|
|
8241
|
+
result = await withTimeoutAbort(
|
|
8242
|
+
(signal) => provider.generate({ ...options, signal }),
|
|
7742
8243
|
cloudTimeoutMs,
|
|
7743
|
-
`Model ${model.id} inference timed out after ${cloudTimeoutMs}ms
|
|
8244
|
+
`Model ${model.id} inference timed out after ${cloudTimeoutMs}ms`,
|
|
8245
|
+
[options.signal, runAbort.signal]
|
|
7744
8246
|
);
|
|
7745
8247
|
}
|
|
7746
8248
|
} else {
|
|
7747
8249
|
const cloudTimeoutMs = this.config.cloudInferenceTimeoutMs ?? 12e4;
|
|
7748
|
-
result = await
|
|
7749
|
-
provider.generate(options),
|
|
8250
|
+
result = await withTimeoutAbort(
|
|
8251
|
+
(signal) => provider.generate({ ...options, signal }),
|
|
7750
8252
|
cloudTimeoutMs,
|
|
7751
|
-
`Model ${model.id} inference timed out after ${cloudTimeoutMs}ms
|
|
8253
|
+
`Model ${model.id} inference timed out after ${cloudTimeoutMs}ms`,
|
|
8254
|
+
[options.signal, runAbort.signal]
|
|
7752
8255
|
);
|
|
7753
8256
|
}
|
|
7754
8257
|
const corrected = buildTokenUsage(
|
|
@@ -7767,11 +8270,12 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
7767
8270
|
if (!result || typeof result.content !== "string" || !result.usage) {
|
|
7768
8271
|
throw new Error(`Provider ${model.provider}:${model.id} returned an invalid generation result.`);
|
|
7769
8272
|
}
|
|
7770
|
-
this.recordStats(tier, model, result.usage, options.featureTag);
|
|
8273
|
+
this.recordStats(tier, model, result.usage, options.featureTag, runGeneration);
|
|
7771
8274
|
this.failover.recordSuccess(model.provider);
|
|
7772
8275
|
return result;
|
|
7773
8276
|
} catch (err) {
|
|
7774
|
-
if (err instanceof
|
|
8277
|
+
if (err instanceof _CascadeRouter.BudgetExceededError) throw err;
|
|
8278
|
+
if (err instanceof Error && err.name === "AbortError" || runSignal?.aborted || options.signal?.aborted) {
|
|
7775
8279
|
throw new CascadeCancelledError("Run cancelled");
|
|
7776
8280
|
}
|
|
7777
8281
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
@@ -7789,6 +8293,8 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
7789
8293
|
});
|
|
7790
8294
|
releaseLocalSlot?.();
|
|
7791
8295
|
releaseLocalSlot = void 0;
|
|
8296
|
+
releaseReservation?.();
|
|
8297
|
+
releaseReservation = void 0;
|
|
7792
8298
|
const retryOpts = options.model && options.model.id === model.id ? { ...options, model: void 0 } : options;
|
|
7793
8299
|
return this.generate(tier, retryOpts, onChunk, requireVision);
|
|
7794
8300
|
}
|
|
@@ -7810,6 +8316,8 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
7810
8316
|
});
|
|
7811
8317
|
releaseLocalSlot?.();
|
|
7812
8318
|
releaseLocalSlot = void 0;
|
|
8319
|
+
releaseReservation?.();
|
|
8320
|
+
releaseReservation = void 0;
|
|
7813
8321
|
const base = options.model && options.model.id === model.id ? { ...options, model: void 0 } : options;
|
|
7814
8322
|
const retryOpts = { ...base, _notFoundDepth: depth };
|
|
7815
8323
|
return this.generate(tier, retryOpts, onChunk, requireVision);
|
|
@@ -7818,6 +8326,8 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
7818
8326
|
throw err;
|
|
7819
8327
|
} finally {
|
|
7820
8328
|
releaseLocalSlot?.();
|
|
8329
|
+
releaseReservation?.();
|
|
8330
|
+
releaseReservation = void 0;
|
|
7821
8331
|
}
|
|
7822
8332
|
}
|
|
7823
8333
|
getModelForTier(tier) {
|
|
@@ -8080,12 +8590,28 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
8080
8590
|
return this.selector?.getAllAvailableModels() ?? [];
|
|
8081
8591
|
}
|
|
8082
8592
|
// ── Private ──────────────────────────────────
|
|
8593
|
+
/**
|
|
8594
|
+
* Why each configured provider failed its availability probe, kept so the
|
|
8595
|
+
* surfaces can say something better than "add an API key" to a user who
|
|
8596
|
+
* already has one. Cleared and refilled by each detectAvailableProviders().
|
|
8597
|
+
*/
|
|
8598
|
+
probeFailures = /* @__PURE__ */ new Map();
|
|
8599
|
+
/**
|
|
8600
|
+
* What stopped the configured providers from being usable, if anything.
|
|
8601
|
+
* Empty when every configured provider passed — or when none was configured,
|
|
8602
|
+
* which is a different problem with a different answer.
|
|
8603
|
+
*/
|
|
8604
|
+
providerProbeFailures() {
|
|
8605
|
+
return [...this.probeFailures].map(([provider, reason]) => ({ provider, reason }));
|
|
8606
|
+
}
|
|
8083
8607
|
/** Logs why a configured provider failed its availability probe. */
|
|
8084
8608
|
emitProbeFailure(type, reason) {
|
|
8609
|
+
this.probeFailures.set(type, reason);
|
|
8085
8610
|
console.warn(`[router] provider "${type}" is not available: ${reason}`);
|
|
8086
8611
|
}
|
|
8087
8612
|
async detectAvailableProviders(configs) {
|
|
8088
8613
|
const available = /* @__PURE__ */ new Set();
|
|
8614
|
+
this.probeFailures.clear();
|
|
8089
8615
|
const checks = configs.map(async (cfg) => {
|
|
8090
8616
|
try {
|
|
8091
8617
|
const testModel = this.getAnyModelForProvider(cfg.type);
|
|
@@ -8095,7 +8621,13 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
8095
8621
|
if (ok) available.add(cfg.type);
|
|
8096
8622
|
else this.emitProbeFailure(cfg.type, "availability check returned false (bad key, wrong endpoint/deployment, or unreachable)");
|
|
8097
8623
|
} catch (err) {
|
|
8098
|
-
|
|
8624
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
8625
|
+
if (err instanceof ProviderUnreachableError) {
|
|
8626
|
+
available.add(cfg.type);
|
|
8627
|
+
console.warn(`[router] provider "${cfg.type}" probe did not complete: ${reason} \u2014 continuing with it enabled`);
|
|
8628
|
+
return;
|
|
8629
|
+
}
|
|
8630
|
+
this.emitProbeFailure(cfg.type, reason);
|
|
8099
8631
|
}
|
|
8100
8632
|
});
|
|
8101
8633
|
await Promise.allSettled(checks);
|
|
@@ -8188,7 +8720,14 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
8188
8720
|
}
|
|
8189
8721
|
return void 0;
|
|
8190
8722
|
}
|
|
8191
|
-
|
|
8723
|
+
/**
|
|
8724
|
+
* `runGeneration` is the generation the call was ADMITTED in — see
|
|
8725
|
+
* `generate()`. Session-wide accounting always applies (the spend is real
|
|
8726
|
+
* whenever it lands), but the per-RUN ceiling is only meaningful for the run
|
|
8727
|
+
* the call belongs to, so a straggler from a finished run is left out of it.
|
|
8728
|
+
* Omitted by the direct callers in tests, which have no run to straddle.
|
|
8729
|
+
*/
|
|
8730
|
+
recordStats(tier, model, usage, featureTag, runGeneration) {
|
|
8192
8731
|
this.stats.totalTokens += usage.totalTokens;
|
|
8193
8732
|
this.stats.totalCostUsd += usage.estimatedCostUsd;
|
|
8194
8733
|
this.sessionCostUsd += usage.estimatedCostUsd;
|
|
@@ -8213,10 +8752,13 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
8213
8752
|
});
|
|
8214
8753
|
}
|
|
8215
8754
|
}
|
|
8216
|
-
|
|
8217
|
-
|
|
8755
|
+
const currentRun = runGeneration === void 0 || runGeneration === this.runGeneration;
|
|
8756
|
+
if (currentRun) {
|
|
8757
|
+
this.runTokens += usage.totalTokens;
|
|
8758
|
+
this.runCostUsd += usage.estimatedCostUsd;
|
|
8759
|
+
}
|
|
8218
8760
|
this.updateBudgetState();
|
|
8219
|
-
this.enforceRunBudget();
|
|
8761
|
+
if (currentRun) this.enforceRunBudget();
|
|
8220
8762
|
}
|
|
8221
8763
|
/**
|
|
8222
8764
|
* Resets per-run accounting at the start of each `cascade run`. Session
|
|
@@ -8226,9 +8768,158 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
8226
8768
|
beginRun() {
|
|
8227
8769
|
this.runTokens = 0;
|
|
8228
8770
|
this.runCostUsd = 0;
|
|
8771
|
+
this.reservedTokens = 0;
|
|
8772
|
+
this.reservedCostUsd = 0;
|
|
8773
|
+
this.runAbort = newRunAbort();
|
|
8774
|
+
this.runGeneration++;
|
|
8229
8775
|
this.runBudgetExceeded = false;
|
|
8230
8776
|
this.runBudgetExceededReason = void 0;
|
|
8231
8777
|
}
|
|
8778
|
+
/**
|
|
8779
|
+
* Refuse a call whose input alone cannot fit what is left of the run budget.
|
|
8780
|
+
*
|
|
8781
|
+
* `enforceRunBudget()` below runs AFTER a call returns, which makes the caps
|
|
8782
|
+
* a stop rather than a pre-authorisation: the tokens are already bought by
|
|
8783
|
+
* the time it looks. That was tolerable while a prompt could not exceed
|
|
8784
|
+
* 20,000 characters, and stopped being so when that cap was removed in
|
|
8785
|
+
* 0.72.0 — a multi-megabyte prompt is billed in full on the very first call
|
|
8786
|
+
* (the complexity classifier sends the whole thing), and no ceiling
|
|
8787
|
+
* configured downstream can give that money back.
|
|
8788
|
+
*
|
|
8789
|
+
* Deliberately narrow, because a false refusal is worse than a late stop:
|
|
8790
|
+
*
|
|
8791
|
+
* • INPUT ONLY. What a call returns is not knowable in advance, and
|
|
8792
|
+
* charging a worst-case `maxTokens` of output against the budget would
|
|
8793
|
+
* decline runs that would have finished comfortably. Input is the part
|
|
8794
|
+
* that is already determined, and it is the part the removed cap used to
|
|
8795
|
+
* bound.
|
|
8796
|
+
* • Compared against what REMAINS (`cap - spent`), not the whole cap, so a
|
|
8797
|
+
* call is judged on the budget it can actually draw from.
|
|
8798
|
+
* • Skipped when the model has no usable price. An estimate cannot be made,
|
|
8799
|
+
* and refusing on ignorance would break every self-hosted and local model
|
|
8800
|
+
* the moment a cost cap is set. The post-hoc stop still applies there.
|
|
8801
|
+
* • Skipped entirely when no cap is configured — there is nothing to
|
|
8802
|
+
* pre-authorise against.
|
|
8803
|
+
*
|
|
8804
|
+
* The message names the cap AND the estimate, because "too expensive" with
|
|
8805
|
+
* no numbers gives the user nothing to change.
|
|
8806
|
+
*/
|
|
8807
|
+
enforcePreflightBudget(model, options) {
|
|
8808
|
+
const budget = this.config?.budget;
|
|
8809
|
+
const maxCost = budget?.maxCostPerRunUsd;
|
|
8810
|
+
const maxTokens = budget?.maxTokensPerRun;
|
|
8811
|
+
if (maxCost == null && maxTokens == null) return void 0;
|
|
8812
|
+
const inputTokens = this.estimateInputTokens(options, model);
|
|
8813
|
+
if (maxTokens != null) {
|
|
8814
|
+
const remaining = maxTokens - this.runTokens - this.reservedTokens;
|
|
8815
|
+
if (inputTokens > remaining) {
|
|
8816
|
+
this.failPreflight(
|
|
8817
|
+
`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.`
|
|
8818
|
+
);
|
|
8819
|
+
}
|
|
8820
|
+
}
|
|
8821
|
+
let estimatedUsd = 0;
|
|
8822
|
+
if (maxCost != null) {
|
|
8823
|
+
const { input: inputPer1k, unknown } = resolveModelPricing(model, { inputTokens });
|
|
8824
|
+
if (!unknown && inputPer1k > 0) {
|
|
8825
|
+
estimatedUsd = inputTokens / 1e3 * inputPer1k;
|
|
8826
|
+
const remaining = maxCost - this.runCostUsd - this.reservedCostUsd;
|
|
8827
|
+
if (estimatedUsd > remaining) {
|
|
8828
|
+
this.failPreflight(
|
|
8829
|
+
`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.`
|
|
8830
|
+
);
|
|
8831
|
+
}
|
|
8832
|
+
}
|
|
8833
|
+
}
|
|
8834
|
+
this.reservedTokens += inputTokens;
|
|
8835
|
+
this.reservedCostUsd += estimatedUsd;
|
|
8836
|
+
const generation = this.runGeneration;
|
|
8837
|
+
let released = false;
|
|
8838
|
+
return () => {
|
|
8839
|
+
if (released || generation !== this.runGeneration) return;
|
|
8840
|
+
released = true;
|
|
8841
|
+
this.reservedTokens -= inputTokens;
|
|
8842
|
+
this.reservedCostUsd -= estimatedUsd;
|
|
8843
|
+
};
|
|
8844
|
+
}
|
|
8845
|
+
/**
|
|
8846
|
+
* What this call will send, in tokens.
|
|
8847
|
+
*
|
|
8848
|
+
* Text is the bulk of it, but not all of it: providers bill the serialized
|
|
8849
|
+
* TOOL DEFINITIONS on every native-tool call (Anthropic sends each name,
|
|
8850
|
+
* description and input schema), and an image is billed by its own size
|
|
8851
|
+
* rather than as the `[image]` placeholder `contentToText` reduces it to. A
|
|
8852
|
+
* tool-heavy or vision request could therefore pass either cap while its real
|
|
8853
|
+
* input already exceeded the allowance.
|
|
8854
|
+
*/
|
|
8855
|
+
estimateInputTokens(options, model) {
|
|
8856
|
+
let tokens = guardTokens(options.systemPrompt ?? "");
|
|
8857
|
+
let images = 0;
|
|
8858
|
+
const wire = wireProfile(model.provider);
|
|
8859
|
+
const countsImage = (img) => wire.sendsUrlImages || img?.type === "base64";
|
|
8860
|
+
for (const message of options.messages) {
|
|
8861
|
+
if (wire.dropsMessage(message)) continue;
|
|
8862
|
+
tokens += TOKENS_PER_MESSAGE_FRAMING;
|
|
8863
|
+
if (typeof message.content === "string") {
|
|
8864
|
+
tokens += guardTokens(message.content);
|
|
8865
|
+
} else {
|
|
8866
|
+
switch (wire.blockHandling(message)) {
|
|
8867
|
+
case "blocks":
|
|
8868
|
+
for (const block of message.content) {
|
|
8869
|
+
if (!wire.sendsBlock(block)) continue;
|
|
8870
|
+
if (block.type === "image") {
|
|
8871
|
+
if (countsImage(block.image)) images++;
|
|
8872
|
+
continue;
|
|
8873
|
+
}
|
|
8874
|
+
tokens += guardTokens(contentToText([block]));
|
|
8875
|
+
}
|
|
8876
|
+
break;
|
|
8877
|
+
case "stringified":
|
|
8878
|
+
tokens += guardTokens(safeJson(message.content));
|
|
8879
|
+
break;
|
|
8880
|
+
}
|
|
8881
|
+
}
|
|
8882
|
+
if (message.toolCalls?.length && wire.sendsToolCalls(message)) {
|
|
8883
|
+
tokens += guardTokens(safeJson(wire.sizeToolCalls(message.toolCalls)));
|
|
8884
|
+
}
|
|
8885
|
+
if (message.toolCallId && message.role === "tool" && wire.sendsToolCallId) {
|
|
8886
|
+
tokens += guardTokens(message.toolCallId);
|
|
8887
|
+
}
|
|
8888
|
+
}
|
|
8889
|
+
if (options.tools?.length) {
|
|
8890
|
+
tokens += guardTokens(safeJson(wire.sizeTools(options.tools)));
|
|
8891
|
+
}
|
|
8892
|
+
if (wire.readsTopLevelImages) {
|
|
8893
|
+
const topLevel = (options.images ?? []).filter(countsImage).length;
|
|
8894
|
+
images += topLevel * geminiImageCopies(options.messages);
|
|
8895
|
+
}
|
|
8896
|
+
tokens += images * IMAGE_TOKENS_EACH;
|
|
8897
|
+
return tokens;
|
|
8898
|
+
}
|
|
8899
|
+
/**
|
|
8900
|
+
* Report a wait that ended because something asked it to stop, the same way
|
|
8901
|
+
* the provider call below reports one.
|
|
8902
|
+
*
|
|
8903
|
+
* The queue waits sit OUTSIDE the try/catch that maps aborts, so without
|
|
8904
|
+
* this a cancellation surfaced as whatever the queue happened to throw and
|
|
8905
|
+
* was retried or failed over like an ordinary error. A budget abort keeps
|
|
8906
|
+
* saying it ran out of budget — otherwise the reason the run stopped is lost
|
|
8907
|
+
* on the way up — and anything else that was aborted becomes a cancellation.
|
|
8908
|
+
*/
|
|
8909
|
+
asAbortFailure(err, callSignal, runSignal) {
|
|
8910
|
+
if (err instanceof _CascadeRouter.BudgetExceededError) return err;
|
|
8911
|
+
if (err instanceof CascadeCancelledError) return err;
|
|
8912
|
+
if (callSignal?.aborted || runSignal?.aborted) return new CascadeCancelledError("Run cancelled");
|
|
8913
|
+
return err;
|
|
8914
|
+
}
|
|
8915
|
+
/** Trips the same run-level flag a post-hoc overrun does, and reports it the same way. */
|
|
8916
|
+
failPreflight(reason) {
|
|
8917
|
+
this.runBudgetExceeded = true;
|
|
8918
|
+
this.runBudgetExceededReason = reason;
|
|
8919
|
+
this.runAbort.abort(new _CascadeRouter.BudgetExceededError(reason));
|
|
8920
|
+
this.emit("budget:exceeded", { reason, spentUsd: this.sessionCostUsd });
|
|
8921
|
+
throw new _CascadeRouter.BudgetExceededError(reason);
|
|
8922
|
+
}
|
|
8232
8923
|
/**
|
|
8233
8924
|
* Enforce the hard per-task ceiling. Once tripped, the flag makes every
|
|
8234
8925
|
* subsequent (and concurrent) generate() call in this run fail fast.
|
|
@@ -8244,6 +8935,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
8244
8935
|
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
8936
|
this.runBudgetExceeded = true;
|
|
8246
8937
|
this.runBudgetExceededReason = reason;
|
|
8938
|
+
this.runAbort.abort(new _CascadeRouter.BudgetExceededError(reason));
|
|
8247
8939
|
this.emit("budget:exceeded", { reason, spentUsd: this.sessionCostUsd });
|
|
8248
8940
|
throw new _CascadeRouter.BudgetExceededError(reason);
|
|
8249
8941
|
}
|
|
@@ -9481,6 +10173,12 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
|
|
|
9481
10173
|
maxTokens: 4096,
|
|
9482
10174
|
...subtaskModel ? { model: subtaskModel } : {},
|
|
9483
10175
|
featureTag: this.assignment?.sectionTitle,
|
|
10176
|
+
// The wave signal, so cancel-and-respawn reaches the request that is
|
|
10177
|
+
// RUNNING rather than only stopping the worker at its next checkpoint.
|
|
10178
|
+
// It is a superset of the run signal the router injects when this is
|
|
10179
|
+
// absent (T2 composes it from that signal plus its own per-wave one),
|
|
10180
|
+
// so wiring it here loses no cancellation and adds the wave's.
|
|
10181
|
+
...this.signal ? { signal: this.signal } : {},
|
|
9484
10182
|
...this.localOnlyMatch ? { forceLocal: true } : {}
|
|
9485
10183
|
};
|
|
9486
10184
|
const result = await this.router.generate(
|
|
@@ -9959,6 +10657,7 @@ ${current}`
|
|
|
9959
10657
|
HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
9960
10658
|
maxTokens: 4096,
|
|
9961
10659
|
featureTag: assignment.sectionTitle,
|
|
10660
|
+
...this.signal ? { signal: this.signal } : {},
|
|
9962
10661
|
...this.localOnlyMatch ? { forceLocal: true } : {}
|
|
9963
10662
|
});
|
|
9964
10663
|
const next = (improved.content ?? "").trim();
|
|
@@ -10020,6 +10719,7 @@ Reply with JSON: { "completeness": "pass"|"fail", "correctness": "pass"|"fail",
|
|
|
10020
10719
|
|
|
10021
10720
|
HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
10022
10721
|
featureTag: assignment.sectionTitle,
|
|
10722
|
+
...this.signal ? { signal: this.signal } : {},
|
|
10023
10723
|
...this.localOnlyMatch ? { forceLocal: true } : {}
|
|
10024
10724
|
});
|
|
10025
10725
|
try {
|
|
@@ -10059,6 +10759,7 @@ ${output.slice(0, 4e3)}`;
|
|
|
10059
10759
|
messages: [{ role: "user", content: prompt }],
|
|
10060
10760
|
maxTokens: 300,
|
|
10061
10761
|
temperature: 0,
|
|
10762
|
+
...this.signal ? { signal: this.signal } : {},
|
|
10062
10763
|
...this.localOnlyMatch ? { forceLocal: true } : {}
|
|
10063
10764
|
});
|
|
10064
10765
|
const match = /\[[\s\S]*\]/.exec(result.content);
|
|
@@ -10170,6 +10871,10 @@ Begin execution now.`;
|
|
|
10170
10871
|
}
|
|
10171
10872
|
};
|
|
10172
10873
|
var PeerBus = class extends EventEmitter {
|
|
10874
|
+
constructor() {
|
|
10875
|
+
super();
|
|
10876
|
+
this.setMaxListeners(64);
|
|
10877
|
+
}
|
|
10173
10878
|
outputs = /* @__PURE__ */ new Map();
|
|
10174
10879
|
waiters = /* @__PURE__ */ new Map();
|
|
10175
10880
|
members = /* @__PURE__ */ new Set();
|
|
@@ -11418,6 +12123,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
11418
12123
|
const waveSignal = AbortSignal.any(
|
|
11419
12124
|
[this.signal, this.waveAbortController.signal].filter(Boolean)
|
|
11420
12125
|
);
|
|
12126
|
+
setMaxListeners(64, waveSignal);
|
|
11421
12127
|
const runOne = async (id) => {
|
|
11422
12128
|
const assignment = sanitizedAssignments.find((a) => a.subtaskId === id);
|
|
11423
12129
|
const worker = workerMap.get(id);
|
|
@@ -18418,115 +19124,6 @@ var GraphSearchTool = class extends BaseTool {
|
|
|
18418
19124
|
}
|
|
18419
19125
|
};
|
|
18420
19126
|
|
|
18421
|
-
// src/core/context/compaction.ts
|
|
18422
|
-
function estimateTokens(text) {
|
|
18423
|
-
if (!text) return 0;
|
|
18424
|
-
return Math.max(1, Math.ceil(text.length / 4));
|
|
18425
|
-
}
|
|
18426
|
-
function contentToText(content) {
|
|
18427
|
-
if (typeof content === "string") return content;
|
|
18428
|
-
return content.map((c) => c.type === "text" ? c.text : c.type === "tool_result" ? c.content : `[${c.type}]`).join(" ");
|
|
18429
|
-
}
|
|
18430
|
-
function messagesTokens(messages) {
|
|
18431
|
-
return messages.reduce((n, m) => n + estimateTokens(contentToText(m.content)), 0);
|
|
18432
|
-
}
|
|
18433
|
-
function atomize(text, maxChars) {
|
|
18434
|
-
const seps = ["\n\n", "\n", ". ", " "];
|
|
18435
|
-
let units = [text];
|
|
18436
|
-
for (const sep of seps) {
|
|
18437
|
-
if (units.every((u) => u.length <= maxChars)) break;
|
|
18438
|
-
const next = [];
|
|
18439
|
-
for (const u of units) {
|
|
18440
|
-
if (u.length <= maxChars) {
|
|
18441
|
-
next.push(u);
|
|
18442
|
-
continue;
|
|
18443
|
-
}
|
|
18444
|
-
const parts = u.split(sep);
|
|
18445
|
-
parts.forEach((p, i) => next.push(i < parts.length - 1 ? p + sep : p));
|
|
18446
|
-
}
|
|
18447
|
-
units = next;
|
|
18448
|
-
}
|
|
18449
|
-
const out = [];
|
|
18450
|
-
for (const u of units) {
|
|
18451
|
-
if (u.length <= maxChars) out.push(u);
|
|
18452
|
-
else for (let i = 0; i < u.length; i += maxChars) out.push(u.slice(i, i + maxChars));
|
|
18453
|
-
}
|
|
18454
|
-
return out;
|
|
18455
|
-
}
|
|
18456
|
-
function chunkText(text, opts) {
|
|
18457
|
-
const maxChars = Math.max(1, Math.floor(opts.maxTokens * 4));
|
|
18458
|
-
const overlap = Math.min(maxChars - 1, Math.max(0, Math.floor(maxChars * (opts.overlapRatio ?? 0.1))));
|
|
18459
|
-
const trimmed = text.trim();
|
|
18460
|
-
if (!trimmed) return [];
|
|
18461
|
-
if (trimmed.length <= maxChars) return [trimmed];
|
|
18462
|
-
const units = atomize(trimmed, maxChars);
|
|
18463
|
-
const chunks = [];
|
|
18464
|
-
let cur = "";
|
|
18465
|
-
for (const u of units) {
|
|
18466
|
-
if (cur && cur.length + u.length > maxChars) {
|
|
18467
|
-
chunks.push(cur.trim());
|
|
18468
|
-
cur = overlap > 0 ? cur.slice(-overlap) : "";
|
|
18469
|
-
}
|
|
18470
|
-
cur += u;
|
|
18471
|
-
}
|
|
18472
|
-
if (cur.trim()) chunks.push(cur.trim());
|
|
18473
|
-
return chunks;
|
|
18474
|
-
}
|
|
18475
|
-
function needsCompaction(totalTokens, contextWindow, reserve = 0.2) {
|
|
18476
|
-
if (!contextWindow || contextWindow <= 0) return false;
|
|
18477
|
-
return totalTokens > contextWindow * (1 - reserve);
|
|
18478
|
-
}
|
|
18479
|
-
async function mapReduceCompact(text, opts) {
|
|
18480
|
-
const capChars = opts.capTokens * 4;
|
|
18481
|
-
let working = text;
|
|
18482
|
-
let truncated = false;
|
|
18483
|
-
if (working.length > capChars) {
|
|
18484
|
-
working = working.slice(0, capChars);
|
|
18485
|
-
truncated = true;
|
|
18486
|
-
}
|
|
18487
|
-
const instruction = opts.instruction ?? "Summarize this content, preserving key facts, names, numbers, and decisions.";
|
|
18488
|
-
let chunks = chunkText(working, { maxTokens: opts.chunkTokens });
|
|
18489
|
-
if (chunks.length <= 1) return { text: working, chunks: chunks.length, calls: 0, truncated };
|
|
18490
|
-
let calls = 0;
|
|
18491
|
-
let parts = await Promise.all(
|
|
18492
|
-
chunks.map((c, i) => {
|
|
18493
|
-
calls++;
|
|
18494
|
-
return opts.summarize(c, `${instruction} (part ${i + 1} of ${chunks.length})`);
|
|
18495
|
-
})
|
|
18496
|
-
);
|
|
18497
|
-
let combined = parts.join("\n\n");
|
|
18498
|
-
const maxPasses = opts.maxReducePasses ?? 3;
|
|
18499
|
-
for (let pass = 0; pass < maxPasses && estimateTokens(combined) > opts.targetTokens; pass++) {
|
|
18500
|
-
const groups = chunkText(combined, { maxTokens: opts.chunkTokens });
|
|
18501
|
-
if (groups.length <= 1) break;
|
|
18502
|
-
parts = await Promise.all(
|
|
18503
|
-
groups.map((g) => {
|
|
18504
|
-
calls++;
|
|
18505
|
-
return opts.summarize(g, "Merge these partial summaries into one cohesive summary; keep all distinct facts.");
|
|
18506
|
-
})
|
|
18507
|
-
);
|
|
18508
|
-
combined = parts.join("\n\n");
|
|
18509
|
-
}
|
|
18510
|
-
const note = truncated ? "\n\n[Note: input exceeded the extended-context cap and was truncated.]" : "";
|
|
18511
|
-
return { text: combined + note, chunks: chunks.length, calls, truncated };
|
|
18512
|
-
}
|
|
18513
|
-
async function rollingSummary(messages, opts) {
|
|
18514
|
-
if (messages.length <= opts.keepRecent) return messages;
|
|
18515
|
-
const older = messages.slice(0, messages.length - opts.keepRecent);
|
|
18516
|
-
const recent = messages.slice(messages.length - opts.keepRecent);
|
|
18517
|
-
const transcript = older.map((m) => `${m.role}: ${contentToText(m.content)}`).join("\n");
|
|
18518
|
-
const summary = await opts.summarize(
|
|
18519
|
-
transcript,
|
|
18520
|
-
"Summarize this earlier conversation. Preserve facts, decisions, names, numbers, and any open threads or todos."
|
|
18521
|
-
);
|
|
18522
|
-
const summaryMessage = {
|
|
18523
|
-
role: "system",
|
|
18524
|
-
content: `Summary of earlier conversation (folded to fit the context window):
|
|
18525
|
-
${summary}`
|
|
18526
|
-
};
|
|
18527
|
-
return [summaryMessage, ...recent];
|
|
18528
|
-
}
|
|
18529
|
-
|
|
18530
19127
|
// src/core/steering/guidance.ts
|
|
18531
19128
|
var GuidanceQueue = class {
|
|
18532
19129
|
entries = [];
|
|
@@ -20078,7 +20675,16 @@ ${finalOutput}` : ""
|
|
|
20078
20675
|
const selector = this.router.getSelector();
|
|
20079
20676
|
const model = options.fastAnswerModel ? selector.selectForTier(tier, options.fastAnswerModel) : selector.getCandidatesForTier(tier)[0] ?? selector.selectForTier(tier);
|
|
20080
20677
|
if (!model) {
|
|
20081
|
-
|
|
20678
|
+
const failures2 = this.router.providerProbeFailures();
|
|
20679
|
+
const context = failures2.length ? ` Configured providers: ${failures2.map((f) => `${f.provider}: ${f.reason}`).join("; ")}` : "";
|
|
20680
|
+
if (options.fastAnswerModel) {
|
|
20681
|
+
throw new Error(
|
|
20682
|
+
`No model is available for a fast answer: the pinned model "${options.fastAnswerModel}" could not be resolved \u2014 check it is spelled correctly and served by a configured provider.${context}`
|
|
20683
|
+
);
|
|
20684
|
+
}
|
|
20685
|
+
throw new Error(
|
|
20686
|
+
failures2.length ? `No model is available for a fast answer.${context}` : "No model is available for a fast answer \u2014 add a provider API key first."
|
|
20687
|
+
);
|
|
20082
20688
|
}
|
|
20083
20689
|
this.recordDecision(
|
|
20084
20690
|
"model",
|