cascade-ai 0.56.0 → 0.64.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 +415 -153
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +417 -155
- package/dist/cli.js.map +1 -1
- package/dist/desktop-core.cjs +417 -149
- package/dist/index.cjs +415 -153
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +417 -155
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -87,12 +87,6 @@ var cron__default = /*#__PURE__*/_interopDefault(cron);
|
|
|
87
87
|
// Cascade AI — Multi-tier AI Orchestration System
|
|
88
88
|
var __defProp = Object.defineProperty;
|
|
89
89
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
90
|
-
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
91
|
-
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
92
|
-
}) : x)(function(x) {
|
|
93
|
-
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
94
|
-
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
95
|
-
});
|
|
96
90
|
var __esm = (fn, res) => function __init() {
|
|
97
91
|
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
98
92
|
};
|
|
@@ -1643,6 +1637,23 @@ var pricing_data_default = {
|
|
|
1643
1637
|
],
|
|
1644
1638
|
notes: "Google bills image output per output-token ($120 per 1M image output tokens); $0.134/image is that rate applied to one default-resolution image. Cost per image therefore varies with resolution."
|
|
1645
1639
|
},
|
|
1640
|
+
{
|
|
1641
|
+
model: "gemini-2.5-flash-image",
|
|
1642
|
+
provider: "gemini",
|
|
1643
|
+
modality: "image",
|
|
1644
|
+
currency: "USD",
|
|
1645
|
+
asOf: "2026-07-27",
|
|
1646
|
+
source: "https://ai.google.dev/gemini-api/docs/pricing#gemini-2.5-flash-image",
|
|
1647
|
+
provenance: "litellm-catalog",
|
|
1648
|
+
rates: [
|
|
1649
|
+
{
|
|
1650
|
+
unit: "per_image",
|
|
1651
|
+
amount: 0.039,
|
|
1652
|
+
variant: "default 1024px output"
|
|
1653
|
+
}
|
|
1654
|
+
],
|
|
1655
|
+
notes: "Google bills image output per output-token ($30 per 1M image output tokens); a default 1024px image is 1290 output tokens, i.e. $0.039. Cost per image therefore varies with resolution."
|
|
1656
|
+
},
|
|
1646
1657
|
{
|
|
1647
1658
|
model: "imagen-4.0-generate-001",
|
|
1648
1659
|
provider: "vertex",
|
|
@@ -1659,7 +1670,7 @@ var pricing_data_default = {
|
|
|
1659
1670
|
]
|
|
1660
1671
|
},
|
|
1661
1672
|
{
|
|
1662
|
-
model: "veo-3.1-generate-
|
|
1673
|
+
model: "veo-3.1-generate-preview",
|
|
1663
1674
|
provider: "gemini",
|
|
1664
1675
|
modality: "video",
|
|
1665
1676
|
currency: "USD",
|
|
@@ -1897,7 +1908,7 @@ function reconcilePrice(model, live) {
|
|
|
1897
1908
|
}
|
|
1898
1909
|
|
|
1899
1910
|
// src/constants.ts
|
|
1900
|
-
var CASCADE_VERSION = "0.
|
|
1911
|
+
var CASCADE_VERSION = "0.64.0";
|
|
1901
1912
|
var CASCADE_CONFIG_DIR = ".cascade";
|
|
1902
1913
|
var CASCADE_MD_FILE = "CASCADE.md";
|
|
1903
1914
|
var CASCADE_IGNORE_FILE = ".cascadeignore";
|
|
@@ -4130,8 +4141,6 @@ var LocalRequestQueue = class {
|
|
|
4130
4141
|
};
|
|
4131
4142
|
}
|
|
4132
4143
|
};
|
|
4133
|
-
|
|
4134
|
-
// src/core/router/dead-models.ts
|
|
4135
4144
|
var DEAD_MODEL_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
4136
4145
|
function deadModelKey(provider, modelId) {
|
|
4137
4146
|
return `${provider}:${modelId}`;
|
|
@@ -4232,16 +4241,13 @@ var DeadModelStore = class {
|
|
|
4232
4241
|
function fileDeadModelPersistence(filePath) {
|
|
4233
4242
|
return {
|
|
4234
4243
|
load() {
|
|
4235
|
-
|
|
4236
|
-
|
|
4237
|
-
const raw = JSON.parse(fs25.readFileSync(filePath, "utf-8"));
|
|
4244
|
+
if (!fs23.existsSync(filePath)) return [];
|
|
4245
|
+
const raw = JSON.parse(fs23.readFileSync(filePath, "utf-8"));
|
|
4238
4246
|
return Array.isArray(raw) ? raw : [];
|
|
4239
4247
|
},
|
|
4240
4248
|
save(records) {
|
|
4241
|
-
|
|
4242
|
-
|
|
4243
|
-
fs25.mkdirSync(path26.dirname(filePath), { recursive: true });
|
|
4244
|
-
fs25.writeFileSync(filePath, JSON.stringify(records, null, 2));
|
|
4249
|
+
fs23.mkdirSync(path24.dirname(filePath), { recursive: true });
|
|
4250
|
+
fs23.writeFileSync(filePath, JSON.stringify(records, null, 2));
|
|
4245
4251
|
}
|
|
4246
4252
|
};
|
|
4247
4253
|
}
|
|
@@ -4675,6 +4681,12 @@ var FAMILY_BENCHMARKS = {
|
|
|
4675
4681
|
// Google — strongest at analysis/data and long-context.
|
|
4676
4682
|
"gemini-2.5-pro": { code: 90, analysis: 93, creative: 86, data: 92 },
|
|
4677
4683
|
"gemini-2.5-flash": { code: 82, analysis: 83, creative: 80, data: 82 },
|
|
4684
|
+
// The 2.5-lite tier scores below full 2.5-flash but above the older 2.0
|
|
4685
|
+
// lite tier — it exists because a naive `gemini-?2\.5-flash` regex swallows
|
|
4686
|
+
// "gemini-2.5-flash-lite" as a substring match (no lite variant of its own
|
|
4687
|
+
// to fall into), so the weaker lite model got credited with the full
|
|
4688
|
+
// flash's benchmark score and kept winning Cascade Auto's "best value" pick.
|
|
4689
|
+
"gemini-2.5-flash-lite": { code: 72, analysis: 72, creative: 74, data: 72 },
|
|
4678
4690
|
"gemini-1.5-pro": { code: 82, analysis: 84, creative: 82, data: 85 },
|
|
4679
4691
|
"gemini-2.0-flash": { code: 79, analysis: 80, creative: 79, data: 80 },
|
|
4680
4692
|
"gemini-flash-lite": { code: 68, analysis: 68, creative: 70, data: 68 },
|
|
@@ -4707,6 +4719,11 @@ var FAMILY_MATCHERS = [
|
|
|
4707
4719
|
[/gpt-?4o-mini/i, "gpt-4o-mini"],
|
|
4708
4720
|
[/gpt-?4o/i, "gpt-4o"],
|
|
4709
4721
|
[/gemini-?2\.5-pro/i, "gemini-2.5-pro"],
|
|
4722
|
+
// Must precede the bare `2\.5-flash` rule below: that pattern has no word
|
|
4723
|
+
// boundary after "flash" and matches "gemini-2.5-flash-lite" as a plain
|
|
4724
|
+
// substring, so the lite variant would otherwise be scored as the full
|
|
4725
|
+
// flash model (see the FAMILY_BENCHMARKS comment above).
|
|
4726
|
+
[/gemini-?2\.5-flash-lite/i, "gemini-2.5-flash-lite"],
|
|
4710
4727
|
[/gemini-?2\.5-flash/i, "gemini-2.5-flash"],
|
|
4711
4728
|
[/gemini-?1\.5-pro/i, "gemini-1.5-pro"],
|
|
4712
4729
|
[/gemini-?2\.0-flash-lite/i, "gemini-flash-lite"],
|
|
@@ -6213,6 +6230,92 @@ function truncateForContext(text, maxChars = 12e3) {
|
|
|
6213
6230
|
${tail}`;
|
|
6214
6231
|
}
|
|
6215
6232
|
|
|
6233
|
+
// src/core/router/provider-errors.ts
|
|
6234
|
+
var SYSTEMIC = /* @__PURE__ */ new Set([
|
|
6235
|
+
"rate_limit",
|
|
6236
|
+
"quota_exhausted",
|
|
6237
|
+
"auth",
|
|
6238
|
+
"model_unavailable"
|
|
6239
|
+
]);
|
|
6240
|
+
function isSystemicKind(kind) {
|
|
6241
|
+
return SYSTEMIC.has(kind);
|
|
6242
|
+
}
|
|
6243
|
+
function statusOf(err) {
|
|
6244
|
+
if (!err || typeof err !== "object") return void 0;
|
|
6245
|
+
const e = err;
|
|
6246
|
+
for (const key of ["status", "statusCode", "code"]) {
|
|
6247
|
+
const v = e[key];
|
|
6248
|
+
if (typeof v === "number" && v >= 100 && v < 600) return v;
|
|
6249
|
+
}
|
|
6250
|
+
const resp = e["response"];
|
|
6251
|
+
if (resp && typeof resp["status"] === "number") return resp["status"];
|
|
6252
|
+
const inner = e["error"];
|
|
6253
|
+
if (inner && typeof inner["code"] === "number") return inner["code"];
|
|
6254
|
+
return void 0;
|
|
6255
|
+
}
|
|
6256
|
+
function messageOf(err) {
|
|
6257
|
+
if (err instanceof Error) return err.message;
|
|
6258
|
+
if (typeof err === "string") return err;
|
|
6259
|
+
if (err && typeof err === "object") {
|
|
6260
|
+
const e = err;
|
|
6261
|
+
if (typeof e["message"] === "string") return e["message"];
|
|
6262
|
+
const inner = e["error"];
|
|
6263
|
+
if (inner && typeof inner["message"] === "string") return inner["message"];
|
|
6264
|
+
try {
|
|
6265
|
+
return JSON.stringify(err);
|
|
6266
|
+
} catch {
|
|
6267
|
+
}
|
|
6268
|
+
}
|
|
6269
|
+
return String(err);
|
|
6270
|
+
}
|
|
6271
|
+
function classifyProviderError(err) {
|
|
6272
|
+
const raw = messageOf(err).trim();
|
|
6273
|
+
const status = statusOf(err);
|
|
6274
|
+
const m = raw.toLowerCase();
|
|
6275
|
+
const kind = (() => {
|
|
6276
|
+
if (status === 429) {
|
|
6277
|
+
return /quota|billing|credit|exceeded your current quota|insufficient/.test(m) ? "quota_exhausted" : "rate_limit";
|
|
6278
|
+
}
|
|
6279
|
+
if (status === 401 || status === 403) return "auth";
|
|
6280
|
+
if (status === 404) return "model_unavailable";
|
|
6281
|
+
if (/rate limit|too many requests|resource_exhausted/.test(m)) {
|
|
6282
|
+
return /quota|billing|credit|insufficient/.test(m) ? "quota_exhausted" : "rate_limit";
|
|
6283
|
+
}
|
|
6284
|
+
if (/quota|billing|insufficient_quota|insufficient credit|payment required/.test(m)) return "quota_exhausted";
|
|
6285
|
+
if (/api key|unauthorized|authentication|permission denied|invalid[_ ]api/.test(m)) return "auth";
|
|
6286
|
+
if (/model .*(not found|does not exist|unavailable)|no such model|unknown model|not supported for/.test(m)) {
|
|
6287
|
+
return "model_unavailable";
|
|
6288
|
+
}
|
|
6289
|
+
if (/context length|too many tokens|maximum context|prompt is too long|input too long/.test(m)) return "context_length";
|
|
6290
|
+
if (/safety|content filter|blocked|recitation|prohibited/.test(m)) return "content_filter";
|
|
6291
|
+
if (/econnreset|etimedout|enotfound|socket hang up|network|fetch failed|aborted/.test(m)) return "network";
|
|
6292
|
+
return "unknown";
|
|
6293
|
+
})();
|
|
6294
|
+
return { kind, systemic: isSystemicKind(kind), status, raw };
|
|
6295
|
+
}
|
|
6296
|
+
function describeProviderError(c, modelId) {
|
|
6297
|
+
const on = modelId ? ` on ${modelId}` : "";
|
|
6298
|
+
const detail = c.raw ? ` Provider said: ${c.raw}` : "";
|
|
6299
|
+
switch (c.kind) {
|
|
6300
|
+
case "rate_limit":
|
|
6301
|
+
return `Rate limit hit${on}. The provider is throttling requests \u2014 lower the number of parallel workers, or use a model with a higher requests-per-minute allowance.${detail}`;
|
|
6302
|
+
case "quota_exhausted":
|
|
6303
|
+
return `Quota or billing limit reached${on}. This will not recover on its own \u2014 check the provider's billing page, or switch this tier to a different provider.${detail}`;
|
|
6304
|
+
case "auth":
|
|
6305
|
+
return `Authentication failed${on}. The API key is missing, expired, or lacks access to this model \u2014 re-check the key in Settings > Providers.${detail}`;
|
|
6306
|
+
case "model_unavailable":
|
|
6307
|
+
return `Model unavailable${on}. The id may be wrong, retired, or not enabled for this API key \u2014 pick a different model for this tier.${detail}`;
|
|
6308
|
+
case "context_length":
|
|
6309
|
+
return `The prompt was too long${on}. Reduce the attached context, or use a model with a larger context window for this tier.${detail}`;
|
|
6310
|
+
case "content_filter":
|
|
6311
|
+
return `The provider's safety filter refused this request${on}.${detail}`;
|
|
6312
|
+
case "network":
|
|
6313
|
+
return `Network error reaching the provider${on}. This is usually transient.${detail}`;
|
|
6314
|
+
default:
|
|
6315
|
+
return `The model call failed${on}.${detail}`;
|
|
6316
|
+
}
|
|
6317
|
+
}
|
|
6318
|
+
|
|
6216
6319
|
// src/core/tiers/t3-worker.ts
|
|
6217
6320
|
var CriticalToolError = class extends Error {
|
|
6218
6321
|
constructor(message, toolName) {
|
|
@@ -6246,8 +6349,15 @@ var KNOWN_TOOLS = [
|
|
|
6246
6349
|
"web_search",
|
|
6247
6350
|
"glob",
|
|
6248
6351
|
"grep",
|
|
6249
|
-
"web_fetch"
|
|
6352
|
+
"web_fetch",
|
|
6353
|
+
"generate_image"
|
|
6250
6354
|
];
|
|
6355
|
+
var PROVIDER_BACKED_TOOLS = /* @__PURE__ */ new Set([
|
|
6356
|
+
"generate_image",
|
|
6357
|
+
"generate_speech",
|
|
6358
|
+
"generate_video",
|
|
6359
|
+
"transcribe_audio"
|
|
6360
|
+
]);
|
|
6251
6361
|
function buildWorkerRules(has) {
|
|
6252
6362
|
const canWriteFiles = has("file_write") || has("file_edit") || has("run_code");
|
|
6253
6363
|
const hasAnyTool = KNOWN_TOOLS.some(has);
|
|
@@ -6257,9 +6367,22 @@ function buildWorkerRules(has) {
|
|
|
6257
6367
|
canWriteFiles && "- If the task asks for a file or artifact, you must actually create it in the workspace, verify that it exists, and inspect it before claiming success.",
|
|
6258
6368
|
has("web_search") && '- Use the "web_search" tool to find current information, documentation, news, or general web data.',
|
|
6259
6369
|
has("pdf_create") && '- Use the "pdf_create" tool for PDF requests.',
|
|
6370
|
+
// Without this the model "writes the image" as prose — a bracketed
|
|
6371
|
+
// description sitting in the deck where the picture should be — even with a
|
|
6372
|
+
// working image model registered. Naming the reference syntax matters as
|
|
6373
|
+
// much as naming the tool: a generated image nobody embeds is still a
|
|
6374
|
+
// missing image.
|
|
6375
|
+
//
|
|
6376
|
+
// Scoped to PowerPoint/Word specifically: those are the only exporters
|
|
6377
|
+
// that actually embed a Markdown image reference (cloud/web/exporters.ts
|
|
6378
|
+
// toPptx/toDocx). PDF and plain-text/Markdown deliverables flatten a
|
|
6379
|
+
// reference straight to caption text — telling the model to call
|
|
6380
|
+
// generate_image for THOSE just pays for an image nobody ever sees.
|
|
6381
|
+
has("generate_image") && '- When a slide deck (PowerPoint) or Word document deliverable calls for a decorative image or illustration, you MUST call the "generate_image" tool and then reference its result using Markdown image syntax: , where "location" is exactly the string the tool reported back. NEVER write a text placeholder or a bracketed description such as [image: a cat] in its place. Do NOT use "generate_image" for a chart, graph, or diagram that must show exact data (numbers, axes, labels) \u2014 an image model cannot guarantee those values are correct, so render that data as a Markdown table instead. For any OTHER deliverable format (PDF, plain text, code, etc.) do NOT call "generate_image" \u2014 those cannot embed the result, so describe the visual in words instead.',
|
|
6260
6382
|
has("run_code") && '- Use the "run_code" tool for any file types (Excel, Zip, csv, etc.) or complex processing not covered by other tools. Always cleanup after code execution.',
|
|
6261
6383
|
"- If you are not making meaningful progress, stop and escalate rather than looping or padding the response.",
|
|
6262
6384
|
has("peer_message") && '- Use the "peer_message" tool to communicate with other T3 workers if your tasks have dependencies or shared state. You can send updates or wait for signals.',
|
|
6385
|
+
hasAnyTool && "- Only use tools directly relevant to THIS subtask. Do not reach for an unrelated connected-service action (e.g. creating, deleting, or modifying a repository, issue, or PR; sending a message) unless the subtask explicitly calls for it.",
|
|
6263
6386
|
"- Return structured output that directly addresses the expected output specification."
|
|
6264
6387
|
];
|
|
6265
6388
|
return `You are a T3 Worker agent in the Cascade AI system. Your job is to execute a specific subtask completely and accurately.
|
|
@@ -6793,7 +6916,9 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
|
|
|
6793
6916
|
const durationMs = Date.now() - toolStartMs;
|
|
6794
6917
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
6795
6918
|
this.emit("tool:result", { id: tc.id, tierId: this.id, toolName: tc.name, error: errMsg, durationMs });
|
|
6796
|
-
|
|
6919
|
+
const isTaggedSystemic = err instanceof Error && err.systemic === true;
|
|
6920
|
+
const isProviderBacked = PROVIDER_BACKED_TOOLS.has(tc.name);
|
|
6921
|
+
if (isTaggedSystemic || isProviderBacked && classifyProviderError(err).systemic) {
|
|
6797
6922
|
throw new CriticalToolError(errMsg, tc.name);
|
|
6798
6923
|
}
|
|
6799
6924
|
return await this.adaptiveFallback(tc, `Tool error: ${errMsg}`);
|
|
@@ -7081,8 +7206,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
7081
7206
|
} catch {
|
|
7082
7207
|
return {
|
|
7083
7208
|
checksRun: ["completeness", "correctness", "compliance"],
|
|
7084
|
-
passed: [
|
|
7085
|
-
failed: []
|
|
7209
|
+
passed: [],
|
|
7210
|
+
failed: ["completeness"]
|
|
7086
7211
|
};
|
|
7087
7212
|
}
|
|
7088
7213
|
}
|
|
@@ -9151,17 +9276,17 @@ var CAPABILITIES = [
|
|
|
9151
9276
|
api: "openai-images"
|
|
9152
9277
|
},
|
|
9153
9278
|
{
|
|
9154
|
-
//
|
|
9155
|
-
//
|
|
9156
|
-
//
|
|
9157
|
-
// generateContent
|
|
9158
|
-
//
|
|
9159
|
-
//
|
|
9160
|
-
//
|
|
9279
|
+
// Was `imagen-4.0-generate-001` on `:predict`, which now 404s for new keys
|
|
9280
|
+
// ("no longer available to new users") — Imagen's :predict API is deprecated
|
|
9281
|
+
// and shuts down 2026-08-17. Google's own migration path is this model on
|
|
9282
|
+
// the ordinary generateContent endpoint, so the fix was never another Imagen
|
|
9283
|
+
// id: the whole request/response SHAPE changes, which is why `api` moved to
|
|
9284
|
+
// 'gemini-generate-content' rather than just the modelId moving.
|
|
9285
|
+
// https://ai.google.dev/gemini-api/docs/imagen
|
|
9161
9286
|
modality: "image",
|
|
9162
9287
|
provider: "gemini",
|
|
9163
|
-
modelId: "
|
|
9164
|
-
api: "gemini-
|
|
9288
|
+
modelId: "gemini-2.5-flash-image",
|
|
9289
|
+
api: "gemini-generate-content"
|
|
9165
9290
|
},
|
|
9166
9291
|
// ── Speech (text → audio) ──
|
|
9167
9292
|
{
|
|
@@ -9193,9 +9318,17 @@ var CAPABILITIES = [
|
|
|
9193
9318
|
// Long-running: submit returns an operation name, the video lands minutes
|
|
9194
9319
|
// later, and the bytes come from a short-lived signed URL. generate.ts owns
|
|
9195
9320
|
// that polling loop; the registry only records the shape via `api`.
|
|
9321
|
+
//
|
|
9322
|
+
// `veo-3.1-generate-001` (no "-preview") is VERTEX AI's id for this model,
|
|
9323
|
+
// not the Gemini Developer API's — this file calls
|
|
9324
|
+
// generativelanguage.googleapis.com (see generate.ts), which 404s on it:
|
|
9325
|
+
// "models/veo-3.1-generate-001 is not found for API version v1beta". On the
|
|
9326
|
+
// Gemini API specifically, Veo 3.1 is still preview-only, so the correct id
|
|
9327
|
+
// there is `veo-3.1-generate-preview`.
|
|
9328
|
+
// https://ai.google.dev/gemini-api/docs/veo
|
|
9196
9329
|
modality: "video",
|
|
9197
9330
|
provider: "gemini",
|
|
9198
|
-
modelId: "veo-3.1-generate-
|
|
9331
|
+
modelId: "veo-3.1-generate-preview",
|
|
9199
9332
|
api: "gemini-predict-lro"
|
|
9200
9333
|
}
|
|
9201
9334
|
];
|
|
@@ -9216,6 +9349,14 @@ function capabilityCost(cap) {
|
|
|
9216
9349
|
const shown = amount < 1e-3 ? amount.toExponential(2) : amount.toFixed(4);
|
|
9217
9350
|
return { label: `$${shown} ${unit}`, amount, unit };
|
|
9218
9351
|
}
|
|
9352
|
+
function explainChoice(c, cost, modality, isBest) {
|
|
9353
|
+
const priced = cost ? ` ${cost.label}.` : "";
|
|
9354
|
+
if (c.quality !== void 0) {
|
|
9355
|
+
return isBest ? `${c.modelId} (${c.provider}) \u2014 ranked highest for ${modality}: ${c.qualityBasis}.${priced}` : `${c.modelId} (${c.provider}) \u2014 ranked alternative for ${modality}: ${c.qualityBasis}.${priced}`;
|
|
9356
|
+
}
|
|
9357
|
+
const noRanking = ". No published quality ranking is applied for this modality, so this is a cost choice, not a quality one.";
|
|
9358
|
+
return isBest ? `${c.modelId} (${c.provider}) \u2014 cheapest usable ${modality} model` + (cost ? ` at ${cost.label}` : "") + noRanking : `${c.modelId} (${c.provider}) \u2014 alternative usable ${modality} model` + (cost ? ` at ${cost.label}` : "") + noRanking;
|
|
9359
|
+
}
|
|
9219
9360
|
var MultimodalRegistry = class {
|
|
9220
9361
|
/** Provider types the user has actually configured a key for. */
|
|
9221
9362
|
available;
|
|
@@ -9233,22 +9374,20 @@ var MultimodalRegistry = class {
|
|
|
9233
9374
|
return [...new Set(this.usable().map((c) => c.modality))];
|
|
9234
9375
|
}
|
|
9235
9376
|
/**
|
|
9236
|
-
*
|
|
9377
|
+
* Every usable capability for a modality, best first.
|
|
9237
9378
|
*
|
|
9238
9379
|
* Ranked models beat unranked ones, then higher quality, then lower unit
|
|
9239
|
-
* price. An explicit `preferModel`
|
|
9240
|
-
* naming a model is a stronger signal than any prior.
|
|
9380
|
+
* price. An explicit `preferModel` is moved to the head when it is usable —
|
|
9381
|
+
* the user naming a model is a stronger signal than any prior.
|
|
9382
|
+
*
|
|
9383
|
+
* `select()` is just the head of this list. The TAIL is the point: a systemic
|
|
9384
|
+
* provider failure (dead key, 404, exhausted quota) is scoped to one
|
|
9385
|
+
* provider+model, so "this model cannot draw" is not the same fact as "this
|
|
9386
|
+
* account cannot draw". A caller holding the ordered alternatives can try the
|
|
9387
|
+
* next provider instead of aborting the whole run — see generate-media.ts.
|
|
9241
9388
|
*/
|
|
9242
|
-
|
|
9243
|
-
const
|
|
9244
|
-
if (!pool.length) return null;
|
|
9245
|
-
if (opts.preferModel) {
|
|
9246
|
-
const wanted = pool.find((c) => c.modelId === opts.preferModel);
|
|
9247
|
-
if (wanted) {
|
|
9248
|
-
return { capability: wanted, cost: capabilityCost(wanted), reason: `Requested explicitly: ${wanted.modelId}.` };
|
|
9249
|
-
}
|
|
9250
|
-
}
|
|
9251
|
-
const scored = pool.map((c) => ({ c, cost: capabilityCost(c) })).sort((a, b) => {
|
|
9389
|
+
rank(modality, opts = {}) {
|
|
9390
|
+
const scored = this.usable(modality).map((c) => ({ c, cost: capabilityCost(c) })).sort((a, b) => {
|
|
9252
9391
|
const qa = a.c.quality ?? -1;
|
|
9253
9392
|
const qb = b.c.quality ?? -1;
|
|
9254
9393
|
if (qa !== qb) return qb - qa;
|
|
@@ -9256,10 +9395,25 @@ var MultimodalRegistry = class {
|
|
|
9256
9395
|
const pb = b.cost?.amount ?? Number.POSITIVE_INFINITY;
|
|
9257
9396
|
return pa - pb;
|
|
9258
9397
|
});
|
|
9259
|
-
const
|
|
9260
|
-
|
|
9261
|
-
|
|
9262
|
-
|
|
9398
|
+
const results = scored.map(({ c, cost }, i) => ({
|
|
9399
|
+
capability: c,
|
|
9400
|
+
cost,
|
|
9401
|
+
reason: explainChoice(c, cost, modality, i === 0)
|
|
9402
|
+
}));
|
|
9403
|
+
if (opts.preferModel) {
|
|
9404
|
+
const i = results.findIndex((r) => r.capability.modelId === opts.preferModel);
|
|
9405
|
+
if (i > -1) {
|
|
9406
|
+
const [wanted] = results.splice(i, 1);
|
|
9407
|
+
results.unshift({ ...wanted, reason: `Requested explicitly: ${wanted.capability.modelId}.` });
|
|
9408
|
+
}
|
|
9409
|
+
}
|
|
9410
|
+
return results;
|
|
9411
|
+
}
|
|
9412
|
+
/**
|
|
9413
|
+
* Pick a model for a modality — the best usable one, or null if there is none.
|
|
9414
|
+
*/
|
|
9415
|
+
select(modality, opts = {}) {
|
|
9416
|
+
return this.rank(modality, opts)[0] ?? null;
|
|
9263
9417
|
}
|
|
9264
9418
|
/**
|
|
9265
9419
|
* A plain-language account of what this account can and cannot generate —
|
|
@@ -9312,92 +9466,6 @@ var BaseTool = class {
|
|
|
9312
9466
|
}
|
|
9313
9467
|
};
|
|
9314
9468
|
|
|
9315
|
-
// src/core/router/provider-errors.ts
|
|
9316
|
-
var SYSTEMIC = /* @__PURE__ */ new Set([
|
|
9317
|
-
"rate_limit",
|
|
9318
|
-
"quota_exhausted",
|
|
9319
|
-
"auth",
|
|
9320
|
-
"model_unavailable"
|
|
9321
|
-
]);
|
|
9322
|
-
function isSystemicKind(kind) {
|
|
9323
|
-
return SYSTEMIC.has(kind);
|
|
9324
|
-
}
|
|
9325
|
-
function statusOf(err) {
|
|
9326
|
-
if (!err || typeof err !== "object") return void 0;
|
|
9327
|
-
const e = err;
|
|
9328
|
-
for (const key of ["status", "statusCode", "code"]) {
|
|
9329
|
-
const v = e[key];
|
|
9330
|
-
if (typeof v === "number" && v >= 100 && v < 600) return v;
|
|
9331
|
-
}
|
|
9332
|
-
const resp = e["response"];
|
|
9333
|
-
if (resp && typeof resp["status"] === "number") return resp["status"];
|
|
9334
|
-
const inner = e["error"];
|
|
9335
|
-
if (inner && typeof inner["code"] === "number") return inner["code"];
|
|
9336
|
-
return void 0;
|
|
9337
|
-
}
|
|
9338
|
-
function messageOf(err) {
|
|
9339
|
-
if (err instanceof Error) return err.message;
|
|
9340
|
-
if (typeof err === "string") return err;
|
|
9341
|
-
if (err && typeof err === "object") {
|
|
9342
|
-
const e = err;
|
|
9343
|
-
if (typeof e["message"] === "string") return e["message"];
|
|
9344
|
-
const inner = e["error"];
|
|
9345
|
-
if (inner && typeof inner["message"] === "string") return inner["message"];
|
|
9346
|
-
try {
|
|
9347
|
-
return JSON.stringify(err);
|
|
9348
|
-
} catch {
|
|
9349
|
-
}
|
|
9350
|
-
}
|
|
9351
|
-
return String(err);
|
|
9352
|
-
}
|
|
9353
|
-
function classifyProviderError(err) {
|
|
9354
|
-
const raw = messageOf(err).trim();
|
|
9355
|
-
const status = statusOf(err);
|
|
9356
|
-
const m = raw.toLowerCase();
|
|
9357
|
-
const kind = (() => {
|
|
9358
|
-
if (status === 429) {
|
|
9359
|
-
return /quota|billing|credit|exceeded your current quota|insufficient/.test(m) ? "quota_exhausted" : "rate_limit";
|
|
9360
|
-
}
|
|
9361
|
-
if (status === 401 || status === 403) return "auth";
|
|
9362
|
-
if (status === 404) return "model_unavailable";
|
|
9363
|
-
if (/rate limit|too many requests|resource_exhausted/.test(m)) {
|
|
9364
|
-
return /quota|billing|credit|insufficient/.test(m) ? "quota_exhausted" : "rate_limit";
|
|
9365
|
-
}
|
|
9366
|
-
if (/quota|billing|insufficient_quota|insufficient credit|payment required/.test(m)) return "quota_exhausted";
|
|
9367
|
-
if (/api key|unauthorized|authentication|permission denied|invalid[_ ]api/.test(m)) return "auth";
|
|
9368
|
-
if (/model .*(not found|does not exist|unavailable)|no such model|unknown model|not supported for/.test(m)) {
|
|
9369
|
-
return "model_unavailable";
|
|
9370
|
-
}
|
|
9371
|
-
if (/context length|too many tokens|maximum context|prompt is too long|input too long/.test(m)) return "context_length";
|
|
9372
|
-
if (/safety|content filter|blocked|recitation|prohibited/.test(m)) return "content_filter";
|
|
9373
|
-
if (/econnreset|etimedout|enotfound|socket hang up|network|fetch failed|aborted/.test(m)) return "network";
|
|
9374
|
-
return "unknown";
|
|
9375
|
-
})();
|
|
9376
|
-
return { kind, systemic: isSystemicKind(kind), status, raw };
|
|
9377
|
-
}
|
|
9378
|
-
function describeProviderError(c, modelId) {
|
|
9379
|
-
const on = modelId ? ` on ${modelId}` : "";
|
|
9380
|
-
const detail = c.raw ? ` Provider said: ${c.raw}` : "";
|
|
9381
|
-
switch (c.kind) {
|
|
9382
|
-
case "rate_limit":
|
|
9383
|
-
return `Rate limit hit${on}. The provider is throttling requests \u2014 lower the number of parallel workers, or use a model with a higher requests-per-minute allowance.${detail}`;
|
|
9384
|
-
case "quota_exhausted":
|
|
9385
|
-
return `Quota or billing limit reached${on}. This will not recover on its own \u2014 check the provider's billing page, or switch this tier to a different provider.${detail}`;
|
|
9386
|
-
case "auth":
|
|
9387
|
-
return `Authentication failed${on}. The API key is missing, expired, or lacks access to this model \u2014 re-check the key in Settings > Providers.${detail}`;
|
|
9388
|
-
case "model_unavailable":
|
|
9389
|
-
return `Model unavailable${on}. The id may be wrong, retired, or not enabled for this API key \u2014 pick a different model for this tier.${detail}`;
|
|
9390
|
-
case "context_length":
|
|
9391
|
-
return `The prompt was too long${on}. Reduce the attached context, or use a model with a larger context window for this tier.${detail}`;
|
|
9392
|
-
case "content_filter":
|
|
9393
|
-
return `The provider's safety filter refused this request${on}.${detail}`;
|
|
9394
|
-
case "network":
|
|
9395
|
-
return `Network error reaching the provider${on}. This is usually transient.${detail}`;
|
|
9396
|
-
default:
|
|
9397
|
-
return `The model call failed${on}.${detail}`;
|
|
9398
|
-
}
|
|
9399
|
-
}
|
|
9400
|
-
|
|
9401
9469
|
// src/core/multimodal/generate.ts
|
|
9402
9470
|
var DEFAULT_TIMEOUT_MS = 12e4;
|
|
9403
9471
|
async function callProvider(fn, modelId) {
|
|
@@ -9436,6 +9504,29 @@ function baseUrlFor(cfg, fallback) {
|
|
|
9436
9504
|
function stamp(ext) {
|
|
9437
9505
|
return `cascade-${Date.now()}.${ext}`;
|
|
9438
9506
|
}
|
|
9507
|
+
var GEMINI_ASPECT_RATIOS = [
|
|
9508
|
+
["1:1", 1 / 1],
|
|
9509
|
+
["3:2", 3 / 2],
|
|
9510
|
+
["2:3", 2 / 3],
|
|
9511
|
+
["3:4", 3 / 4],
|
|
9512
|
+
["4:3", 4 / 3],
|
|
9513
|
+
["4:5", 4 / 5],
|
|
9514
|
+
["5:4", 5 / 4],
|
|
9515
|
+
["9:16", 9 / 16],
|
|
9516
|
+
["16:9", 16 / 9],
|
|
9517
|
+
["21:9", 21 / 9]
|
|
9518
|
+
];
|
|
9519
|
+
function closestGeminiAspectRatio(size) {
|
|
9520
|
+
const m = size.match(/^(\d+)\s*[x×]\s*(\d+)$/i);
|
|
9521
|
+
if (!m) return void 0;
|
|
9522
|
+
const w = Number(m[1]);
|
|
9523
|
+
const h = Number(m[2]);
|
|
9524
|
+
if (!w || !h) return void 0;
|
|
9525
|
+
const ratio = w / h;
|
|
9526
|
+
return GEMINI_ASPECT_RATIOS.reduce(
|
|
9527
|
+
(best, cand) => Math.abs(cand[1] - ratio) < Math.abs(best[1] - ratio) ? cand : best
|
|
9528
|
+
)[0];
|
|
9529
|
+
}
|
|
9439
9530
|
async function generateImage(cap, cfg, req, signal) {
|
|
9440
9531
|
return callProvider(async () => {
|
|
9441
9532
|
if (cap.api === "openai-images") {
|
|
@@ -9460,19 +9551,37 @@ async function generateImage(cap, cfg, req, signal) {
|
|
|
9460
9551
|
...first.revised_prompt ? { revisedPrompt: first.revised_prompt } : {}
|
|
9461
9552
|
};
|
|
9462
9553
|
}
|
|
9463
|
-
if (cap.api === "gemini-
|
|
9554
|
+
if (cap.api === "gemini-generate-content") {
|
|
9464
9555
|
const base = baseUrlFor(cfg, "https://generativelanguage.googleapis.com/v1beta");
|
|
9465
|
-
const url = `${base}/models/${encodeURIComponent(cap.modelId)}:
|
|
9556
|
+
const url = `${base}/models/${encodeURIComponent(cap.modelId)}:generateContent`;
|
|
9557
|
+
const aspectRatio = req.size ? closestGeminiAspectRatio(req.size) : void 0;
|
|
9466
9558
|
const res = await postJson(url, {
|
|
9467
|
-
|
|
9468
|
-
|
|
9559
|
+
contents: [{ parts: [{ text: req.prompt }] }],
|
|
9560
|
+
generationConfig: {
|
|
9561
|
+
// Asked for explicitly: these models can answer with text only, and a
|
|
9562
|
+
// text-only reply to "draw me a cat" is a failed image generation.
|
|
9563
|
+
responseModalities: ["IMAGE", "TEXT"],
|
|
9564
|
+
// Gemini has no free-form pixel size — only a named aspect ratio (see
|
|
9565
|
+
// closestGeminiAspectRatio). Omitted entirely when there's no
|
|
9566
|
+
// requested size, or no supported ratio close enough to bother with.
|
|
9567
|
+
...aspectRatio ? { imageConfig: { aspectRatio } } : {}
|
|
9568
|
+
}
|
|
9469
9569
|
}, { "x-goog-api-key": cfg.apiKey ?? "" }, signal);
|
|
9470
9570
|
const body = await res.json();
|
|
9471
|
-
const
|
|
9472
|
-
|
|
9473
|
-
const
|
|
9571
|
+
const candidate = body.candidates?.[0];
|
|
9572
|
+
const parts = candidate?.content?.parts ?? [];
|
|
9573
|
+
const image = parts.find((p) => p.inlineData?.data)?.inlineData;
|
|
9574
|
+
if (!image?.data) {
|
|
9575
|
+
const blocked = body.promptFeedback?.blockReason ?? (candidate?.finishReason && candidate.finishReason !== "STOP" ? candidate.finishReason : void 0);
|
|
9576
|
+
if (blocked) throw new Error(`Image request was blocked by the provider (${blocked}).`);
|
|
9577
|
+
const said = parts.find((p) => p.text)?.text?.trim();
|
|
9578
|
+
throw new Error(
|
|
9579
|
+
"Image response contained no image data." + (said ? ` The model replied with text instead: ${said}` : "")
|
|
9580
|
+
);
|
|
9581
|
+
}
|
|
9582
|
+
const mimeType = image.mimeType ?? "image/png";
|
|
9474
9583
|
return {
|
|
9475
|
-
data: Buffer.from(
|
|
9584
|
+
data: Buffer.from(image.data, "base64"),
|
|
9476
9585
|
mimeType,
|
|
9477
9586
|
filename: stamp(mimeType.includes("jpeg") ? "jpg" : "png"),
|
|
9478
9587
|
modelId: cap.modelId,
|
|
@@ -9625,6 +9734,49 @@ function resolve(deps, modality, preferModel) {
|
|
|
9625
9734
|
}
|
|
9626
9735
|
return { ok: true, selected, cfg };
|
|
9627
9736
|
}
|
|
9737
|
+
function messageOf2(err) {
|
|
9738
|
+
return err instanceof Error ? err.message : String(err);
|
|
9739
|
+
}
|
|
9740
|
+
async function runWithProviderFallback(deps, modality, first, run, signal) {
|
|
9741
|
+
const failed = first.selected.capability;
|
|
9742
|
+
try {
|
|
9743
|
+
return { value: await run(failed, first.cfg), selected: first.selected };
|
|
9744
|
+
} catch (err) {
|
|
9745
|
+
if (signal?.aborted || !classifyProviderError(err).systemic) throw err;
|
|
9746
|
+
const tried = [`${failed.modelId} (${failed.provider}): ${messageOf2(err)}`];
|
|
9747
|
+
let allSystemic = true;
|
|
9748
|
+
let lastNonSystemic;
|
|
9749
|
+
const seen = /* @__PURE__ */ new Set([failed.provider]);
|
|
9750
|
+
for (const alt of deps.registry.rank(modality)) {
|
|
9751
|
+
if (seen.has(alt.capability.provider)) continue;
|
|
9752
|
+
const cfg = deps.lookupProvider(alt.capability.provider);
|
|
9753
|
+
if (!cfg) continue;
|
|
9754
|
+
seen.add(alt.capability.provider);
|
|
9755
|
+
try {
|
|
9756
|
+
return {
|
|
9757
|
+
value: await run(alt.capability, cfg),
|
|
9758
|
+
selected: alt,
|
|
9759
|
+
fellBackFrom: { capability: failed, error: messageOf2(err) }
|
|
9760
|
+
};
|
|
9761
|
+
} catch (retryErr) {
|
|
9762
|
+
if (signal?.aborted) throw retryErr;
|
|
9763
|
+
tried.push(`${alt.capability.modelId} (${alt.capability.provider}): ${messageOf2(retryErr)}`);
|
|
9764
|
+
if (classifyProviderError(retryErr).systemic) {
|
|
9765
|
+
continue;
|
|
9766
|
+
}
|
|
9767
|
+
allSystemic = false;
|
|
9768
|
+
lastNonSystemic = retryErr;
|
|
9769
|
+
}
|
|
9770
|
+
}
|
|
9771
|
+
if (tried.length === 1) throw err;
|
|
9772
|
+
if (!allSystemic) throw lastNonSystemic;
|
|
9773
|
+
const exhausted = new Error(
|
|
9774
|
+
`${modality} generation failed on every configured provider (${seen.size} tried). ${tried.join(" ")}`
|
|
9775
|
+
);
|
|
9776
|
+
Object.assign(exhausted, { systemic: true });
|
|
9777
|
+
throw exhausted;
|
|
9778
|
+
}
|
|
9779
|
+
}
|
|
9628
9780
|
var GenerateImageTool = class extends BaseTool {
|
|
9629
9781
|
constructor(deps) {
|
|
9630
9782
|
super();
|
|
@@ -9648,16 +9800,32 @@ var GenerateImageTool = class extends BaseTool {
|
|
|
9648
9800
|
const r = resolve(this.deps, "image", input["model"] ? String(input["model"]) : void 0);
|
|
9649
9801
|
if (!r.ok) return r.error;
|
|
9650
9802
|
const size = input["size"] ? String(input["size"]) : void 0;
|
|
9651
|
-
const
|
|
9652
|
-
|
|
9653
|
-
|
|
9654
|
-
|
|
9803
|
+
const attempt = await runWithProviderFallback(
|
|
9804
|
+
this.deps,
|
|
9805
|
+
"image",
|
|
9806
|
+
r,
|
|
9807
|
+
(cap, cfg) => generateImage(cap, cfg, { prompt, ...size ? { size } : {} }, options.signal),
|
|
9655
9808
|
options.signal
|
|
9656
9809
|
);
|
|
9810
|
+
const asset = attempt.value;
|
|
9811
|
+
const fell = attempt.fellBackFrom;
|
|
9657
9812
|
const location = await this.deps.sink(asset);
|
|
9658
9813
|
return [
|
|
9659
9814
|
`Image generated and saved to ${location}.`,
|
|
9660
|
-
|
|
9815
|
+
// Knowing WHERE the image is isn't the same as knowing how to use it: a
|
|
9816
|
+
// model told only "saved to X" tends to describe the picture in prose
|
|
9817
|
+
// instead of embedding it. Spelling out the reference syntax here makes
|
|
9818
|
+
// the worker's "never write a placeholder" rule concretely actionable,
|
|
9819
|
+
// and it works for every host because `location` is whatever that host's
|
|
9820
|
+
// sink returned — a workspace path on desktop/CLI, a URL in the cloud.
|
|
9821
|
+
`To include this image in your output, reference it as: `,
|
|
9822
|
+
// The reported model has to be the one that actually drew the picture, not
|
|
9823
|
+
// the one that was picked and then failed.
|
|
9824
|
+
`Model: ${asset.modelId} \u2014 ${fell ? `used as a fallback after ${fell.capability.modelId} (${fell.capability.provider}) failed with a systemic error.` : attempt.selected.reason}`,
|
|
9825
|
+
// Worth one line: the run succeeded, so nothing else will report that a
|
|
9826
|
+
// configured provider is broken, and "your Gemini key is dead" is exactly
|
|
9827
|
+
// the kind of thing the user wants to hear once rather than never.
|
|
9828
|
+
fell ? `Note: ${fell.capability.provider} failed first \u2014 ${fell.error}` : "",
|
|
9661
9829
|
// DALL·E rewrites the prompt before drawing; the model needs to know what
|
|
9662
9830
|
// was actually rendered, not what it asked for, or its description of the
|
|
9663
9831
|
// result will not match the image.
|
|
@@ -9900,6 +10068,87 @@ function mcpToolName(serverName, toolName) {
|
|
|
9900
10068
|
function isMcpToolName(name) {
|
|
9901
10069
|
return name.startsWith(MCP_TOOL_PREFIX);
|
|
9902
10070
|
}
|
|
10071
|
+
var READ_ONLY_MCP_VERBS = ["list", "get", "search", "read", "describe", "find", "query", "fetch", "show", "view"];
|
|
10072
|
+
var MUTATING_MCP_TOKENS = /* @__PURE__ */ new Set([
|
|
10073
|
+
"create",
|
|
10074
|
+
"delete",
|
|
10075
|
+
"remove",
|
|
10076
|
+
"update",
|
|
10077
|
+
"write",
|
|
10078
|
+
"modify",
|
|
10079
|
+
"set",
|
|
10080
|
+
"put",
|
|
10081
|
+
"post",
|
|
10082
|
+
"patch",
|
|
10083
|
+
"push",
|
|
10084
|
+
"merge",
|
|
10085
|
+
"send",
|
|
10086
|
+
"execute",
|
|
10087
|
+
"exec",
|
|
10088
|
+
"run",
|
|
10089
|
+
"deploy",
|
|
10090
|
+
"install",
|
|
10091
|
+
"uninstall",
|
|
10092
|
+
"upload",
|
|
10093
|
+
"publish",
|
|
10094
|
+
"revoke",
|
|
10095
|
+
"grant",
|
|
10096
|
+
"approve",
|
|
10097
|
+
"reject",
|
|
10098
|
+
"cancel",
|
|
10099
|
+
"enable",
|
|
10100
|
+
"disable",
|
|
10101
|
+
"add",
|
|
10102
|
+
"insert",
|
|
10103
|
+
"drop",
|
|
10104
|
+
"truncate",
|
|
10105
|
+
"clear",
|
|
10106
|
+
"reset",
|
|
10107
|
+
"restart",
|
|
10108
|
+
"stop",
|
|
10109
|
+
"kill",
|
|
10110
|
+
"terminate",
|
|
10111
|
+
"rename",
|
|
10112
|
+
"move",
|
|
10113
|
+
"fork",
|
|
10114
|
+
"sync",
|
|
10115
|
+
"replace",
|
|
10116
|
+
"overwrite",
|
|
10117
|
+
"invite",
|
|
10118
|
+
"ban",
|
|
10119
|
+
"block",
|
|
10120
|
+
"assign",
|
|
10121
|
+
"lock",
|
|
10122
|
+
"unlock",
|
|
10123
|
+
"archive",
|
|
10124
|
+
"star",
|
|
10125
|
+
"comment",
|
|
10126
|
+
"reply",
|
|
10127
|
+
"submit",
|
|
10128
|
+
"apply",
|
|
10129
|
+
"trigger",
|
|
10130
|
+
"invoke",
|
|
10131
|
+
"purge",
|
|
10132
|
+
"migrate",
|
|
10133
|
+
"edit",
|
|
10134
|
+
"save",
|
|
10135
|
+
"commit"
|
|
10136
|
+
]);
|
|
10137
|
+
function mcpTokens(rawToolName) {
|
|
10138
|
+
return rawToolName.split(/[_-]+/).flatMap((segment) => segment.split(/(?<=[a-z0-9])(?=[A-Z])/)).map((t) => t.toLowerCase()).filter(Boolean);
|
|
10139
|
+
}
|
|
10140
|
+
function isReadOnlyMcpToolName(rawToolName) {
|
|
10141
|
+
if (mcpTokens(rawToolName).some((t) => MUTATING_MCP_TOKENS.has(t))) return false;
|
|
10142
|
+
const lower = rawToolName.toLowerCase();
|
|
10143
|
+
for (const verb of READ_ONLY_MCP_VERBS) {
|
|
10144
|
+
if (!lower.startsWith(verb)) continue;
|
|
10145
|
+
const boundary = rawToolName[verb.length];
|
|
10146
|
+
if (boundary === void 0 || boundary === "_" || boundary === "-" || boundary >= "A" && boundary <= "Z") {
|
|
10147
|
+
return true;
|
|
10148
|
+
}
|
|
10149
|
+
}
|
|
10150
|
+
return false;
|
|
10151
|
+
}
|
|
9903
10152
|
function uniqueMcpServerName(desired, existingNames) {
|
|
9904
10153
|
const taken = new Set(existingNames.map(mcpServerPrefix));
|
|
9905
10154
|
if (!taken.has(mcpServerPrefix(desired))) return desired;
|
|
@@ -10993,9 +11242,11 @@ var WebSearchTool = class extends BaseTool {
|
|
|
10993
11242
|
);
|
|
10994
11243
|
const errors = [];
|
|
10995
11244
|
let results = [];
|
|
11245
|
+
let anyBackendReachable = false;
|
|
10996
11246
|
if (this.config.searxngUrl) {
|
|
10997
11247
|
try {
|
|
10998
11248
|
results = await searchSearXNG(query, this.config.searxngUrl, maxResults);
|
|
11249
|
+
anyBackendReachable = true;
|
|
10999
11250
|
if (results.length > 0) return this.formatResults(query, results);
|
|
11000
11251
|
errors.push("SearXNG: returned 0 results");
|
|
11001
11252
|
} catch (err) {
|
|
@@ -11005,6 +11256,7 @@ var WebSearchTool = class extends BaseTool {
|
|
|
11005
11256
|
if (this.config.braveApiKey) {
|
|
11006
11257
|
try {
|
|
11007
11258
|
results = await searchBrave(query, this.config.braveApiKey, maxResults);
|
|
11259
|
+
anyBackendReachable = true;
|
|
11008
11260
|
if (results.length > 0) return this.formatResults(query, results);
|
|
11009
11261
|
errors.push("Brave: returned 0 results");
|
|
11010
11262
|
} catch (err) {
|
|
@@ -11014,6 +11266,7 @@ var WebSearchTool = class extends BaseTool {
|
|
|
11014
11266
|
if (this.config.tavilyApiKey) {
|
|
11015
11267
|
try {
|
|
11016
11268
|
results = await searchTavily(query, this.config.tavilyApiKey, maxResults);
|
|
11269
|
+
anyBackendReachable = true;
|
|
11017
11270
|
if (results.length > 0) return this.formatResults(query, results);
|
|
11018
11271
|
errors.push("Tavily: returned 0 results");
|
|
11019
11272
|
} catch (err) {
|
|
@@ -11023,6 +11276,7 @@ var WebSearchTool = class extends BaseTool {
|
|
|
11023
11276
|
for (const variant of ["html", "lite"]) {
|
|
11024
11277
|
try {
|
|
11025
11278
|
results = await searchDuckDuckGo(query, maxResults, variant);
|
|
11279
|
+
anyBackendReachable = true;
|
|
11026
11280
|
if (results.length > 0) return this.formatResults(query, results);
|
|
11027
11281
|
errors.push(`DuckDuckGo ${variant}: returned 0 results`);
|
|
11028
11282
|
} catch (err) {
|
|
@@ -11030,11 +11284,11 @@ var WebSearchTool = class extends BaseTool {
|
|
|
11030
11284
|
}
|
|
11031
11285
|
}
|
|
11032
11286
|
const configHint = !this.config.searxngUrl && !this.config.braveApiKey && !this.config.tavilyApiKey ? "\nTip: Configure a search backend for better results:\n \u2022 Self-hosted: set SEARXNG_URL in your environment\n \u2022 Brave Search API: set BRAVE_SEARCH_API_KEY\n \u2022 Tavily API: set TAVILY_API_KEY" : "";
|
|
11033
|
-
|
|
11034
|
-
`Web search for "${query}"
|
|
11287
|
+
throw Object.assign(new Error([
|
|
11288
|
+
`Web search for "${query}" found nothing across all backends:`,
|
|
11035
11289
|
...errors.map((e) => ` \u2022 ${e}`),
|
|
11036
11290
|
configHint
|
|
11037
|
-
].join("\n");
|
|
11291
|
+
].join("\n")), { systemic: !anyBackendReachable });
|
|
11038
11292
|
}
|
|
11039
11293
|
formatResults(query, results) {
|
|
11040
11294
|
const lines = [`Web search results for: "${query}"`, ""];
|
|
@@ -11402,6 +11656,15 @@ var McpToolWrapper = class extends BaseTool {
|
|
|
11402
11656
|
this.description = `[MCP:${serverName}] ${description}`;
|
|
11403
11657
|
this.inputSchema = inputSchema;
|
|
11404
11658
|
}
|
|
11659
|
+
// Every built-in tool of comparable risk (shell, git, file writes, the
|
|
11660
|
+
// built-in github tool) overrides this to true. An MCP tool never did —
|
|
11661
|
+
// BaseTool's default (false) meant a connected server's create/delete/push
|
|
11662
|
+
// actions ran with zero human approval, whatever they did. MCP carries no
|
|
11663
|
+
// standard "this mutates state" flag, so default to dangerous and name the
|
|
11664
|
+
// exception (a read-only-looking verb) rather than the other way around.
|
|
11665
|
+
isDangerous() {
|
|
11666
|
+
return !isReadOnlyMcpToolName(this.toolName);
|
|
11667
|
+
}
|
|
11405
11668
|
async execute(input, _options) {
|
|
11406
11669
|
return this.mcpClient.callTool(this.serverName, this.toolName, input);
|
|
11407
11670
|
}
|
|
@@ -11514,7 +11777,7 @@ var ToolRegistry = class extends EventEmitter__default.default {
|
|
|
11514
11777
|
requiresApproval(toolName) {
|
|
11515
11778
|
const defaults = DEFAULT_APPROVAL_REQUIRED;
|
|
11516
11779
|
const configured = this.config.requireApprovalFor;
|
|
11517
|
-
return defaults.includes(toolName) || configured.includes(toolName);
|
|
11780
|
+
return defaults.includes(toolName) || configured.includes(toolName) || this.isDangerous(toolName);
|
|
11518
11781
|
}
|
|
11519
11782
|
isDangerous(toolName) {
|
|
11520
11783
|
return this.tools.get(toolName)?.isDangerous() ?? false;
|
|
@@ -14882,9 +15145,8 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
|
|
|
14882
15145
|
});
|
|
14883
15146
|
this.toolRegistry = new ToolRegistry(this.config.tools, workspacePath);
|
|
14884
15147
|
try {
|
|
14885
|
-
const nodePath = __require("path");
|
|
14886
15148
|
this.router.setDeadModelStore(new DeadModelStore(
|
|
14887
|
-
fileDeadModelPersistence(
|
|
15149
|
+
fileDeadModelPersistence(path24__default.default.join(workspacePath, ".cascade", "dead-models.json"))
|
|
14888
15150
|
));
|
|
14889
15151
|
} catch {
|
|
14890
15152
|
}
|