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.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import Database from 'better-sqlite3';
|
|
2
|
-
import path24 from 'path';
|
|
3
|
-
import fs23 from 'fs';
|
|
2
|
+
import path24, { dirname } from 'path';
|
|
3
|
+
import fs23, { mkdirSync, writeFileSync, existsSync, readFileSync } from 'fs';
|
|
4
4
|
import os6 from 'os';
|
|
5
5
|
import crypto4, { randomUUID, randomBytes, createHash, timingSafeEqual, webcrypto } from 'crypto';
|
|
6
6
|
import EventEmitter, { setMaxListeners } from 'events';
|
|
@@ -41,12 +41,6 @@ import cron from 'node-cron';
|
|
|
41
41
|
// Cascade AI — Multi-tier AI Orchestration System
|
|
42
42
|
var __defProp = Object.defineProperty;
|
|
43
43
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
44
|
-
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
45
|
-
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
46
|
-
}) : x)(function(x) {
|
|
47
|
-
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
48
|
-
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
49
|
-
});
|
|
50
44
|
var __esm = (fn, res) => function __init() {
|
|
51
45
|
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
52
46
|
};
|
|
@@ -1597,6 +1591,23 @@ var pricing_data_default = {
|
|
|
1597
1591
|
],
|
|
1598
1592
|
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."
|
|
1599
1593
|
},
|
|
1594
|
+
{
|
|
1595
|
+
model: "gemini-2.5-flash-image",
|
|
1596
|
+
provider: "gemini",
|
|
1597
|
+
modality: "image",
|
|
1598
|
+
currency: "USD",
|
|
1599
|
+
asOf: "2026-07-27",
|
|
1600
|
+
source: "https://ai.google.dev/gemini-api/docs/pricing#gemini-2.5-flash-image",
|
|
1601
|
+
provenance: "litellm-catalog",
|
|
1602
|
+
rates: [
|
|
1603
|
+
{
|
|
1604
|
+
unit: "per_image",
|
|
1605
|
+
amount: 0.039,
|
|
1606
|
+
variant: "default 1024px output"
|
|
1607
|
+
}
|
|
1608
|
+
],
|
|
1609
|
+
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."
|
|
1610
|
+
},
|
|
1600
1611
|
{
|
|
1601
1612
|
model: "imagen-4.0-generate-001",
|
|
1602
1613
|
provider: "vertex",
|
|
@@ -1613,7 +1624,7 @@ var pricing_data_default = {
|
|
|
1613
1624
|
]
|
|
1614
1625
|
},
|
|
1615
1626
|
{
|
|
1616
|
-
model: "veo-3.1-generate-
|
|
1627
|
+
model: "veo-3.1-generate-preview",
|
|
1617
1628
|
provider: "gemini",
|
|
1618
1629
|
modality: "video",
|
|
1619
1630
|
currency: "USD",
|
|
@@ -1851,7 +1862,7 @@ function reconcilePrice(model, live) {
|
|
|
1851
1862
|
}
|
|
1852
1863
|
|
|
1853
1864
|
// src/constants.ts
|
|
1854
|
-
var CASCADE_VERSION = "0.
|
|
1865
|
+
var CASCADE_VERSION = "0.64.0";
|
|
1855
1866
|
var CASCADE_CONFIG_DIR = ".cascade";
|
|
1856
1867
|
var CASCADE_MD_FILE = "CASCADE.md";
|
|
1857
1868
|
var CASCADE_IGNORE_FILE = ".cascadeignore";
|
|
@@ -4084,8 +4095,6 @@ var LocalRequestQueue = class {
|
|
|
4084
4095
|
};
|
|
4085
4096
|
}
|
|
4086
4097
|
};
|
|
4087
|
-
|
|
4088
|
-
// src/core/router/dead-models.ts
|
|
4089
4098
|
var DEAD_MODEL_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
4090
4099
|
function deadModelKey(provider, modelId) {
|
|
4091
4100
|
return `${provider}:${modelId}`;
|
|
@@ -4186,16 +4195,13 @@ var DeadModelStore = class {
|
|
|
4186
4195
|
function fileDeadModelPersistence(filePath) {
|
|
4187
4196
|
return {
|
|
4188
4197
|
load() {
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
const raw = JSON.parse(fs25.readFileSync(filePath, "utf-8"));
|
|
4198
|
+
if (!existsSync(filePath)) return [];
|
|
4199
|
+
const raw = JSON.parse(readFileSync(filePath, "utf-8"));
|
|
4192
4200
|
return Array.isArray(raw) ? raw : [];
|
|
4193
4201
|
},
|
|
4194
4202
|
save(records) {
|
|
4195
|
-
|
|
4196
|
-
|
|
4197
|
-
fs25.mkdirSync(path26.dirname(filePath), { recursive: true });
|
|
4198
|
-
fs25.writeFileSync(filePath, JSON.stringify(records, null, 2));
|
|
4203
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
4204
|
+
writeFileSync(filePath, JSON.stringify(records, null, 2));
|
|
4199
4205
|
}
|
|
4200
4206
|
};
|
|
4201
4207
|
}
|
|
@@ -4629,6 +4635,12 @@ var FAMILY_BENCHMARKS = {
|
|
|
4629
4635
|
// Google — strongest at analysis/data and long-context.
|
|
4630
4636
|
"gemini-2.5-pro": { code: 90, analysis: 93, creative: 86, data: 92 },
|
|
4631
4637
|
"gemini-2.5-flash": { code: 82, analysis: 83, creative: 80, data: 82 },
|
|
4638
|
+
// The 2.5-lite tier scores below full 2.5-flash but above the older 2.0
|
|
4639
|
+
// lite tier — it exists because a naive `gemini-?2\.5-flash` regex swallows
|
|
4640
|
+
// "gemini-2.5-flash-lite" as a substring match (no lite variant of its own
|
|
4641
|
+
// to fall into), so the weaker lite model got credited with the full
|
|
4642
|
+
// flash's benchmark score and kept winning Cascade Auto's "best value" pick.
|
|
4643
|
+
"gemini-2.5-flash-lite": { code: 72, analysis: 72, creative: 74, data: 72 },
|
|
4632
4644
|
"gemini-1.5-pro": { code: 82, analysis: 84, creative: 82, data: 85 },
|
|
4633
4645
|
"gemini-2.0-flash": { code: 79, analysis: 80, creative: 79, data: 80 },
|
|
4634
4646
|
"gemini-flash-lite": { code: 68, analysis: 68, creative: 70, data: 68 },
|
|
@@ -4661,6 +4673,11 @@ var FAMILY_MATCHERS = [
|
|
|
4661
4673
|
[/gpt-?4o-mini/i, "gpt-4o-mini"],
|
|
4662
4674
|
[/gpt-?4o/i, "gpt-4o"],
|
|
4663
4675
|
[/gemini-?2\.5-pro/i, "gemini-2.5-pro"],
|
|
4676
|
+
// Must precede the bare `2\.5-flash` rule below: that pattern has no word
|
|
4677
|
+
// boundary after "flash" and matches "gemini-2.5-flash-lite" as a plain
|
|
4678
|
+
// substring, so the lite variant would otherwise be scored as the full
|
|
4679
|
+
// flash model (see the FAMILY_BENCHMARKS comment above).
|
|
4680
|
+
[/gemini-?2\.5-flash-lite/i, "gemini-2.5-flash-lite"],
|
|
4664
4681
|
[/gemini-?2\.5-flash/i, "gemini-2.5-flash"],
|
|
4665
4682
|
[/gemini-?1\.5-pro/i, "gemini-1.5-pro"],
|
|
4666
4683
|
[/gemini-?2\.0-flash-lite/i, "gemini-flash-lite"],
|
|
@@ -6167,6 +6184,92 @@ function truncateForContext(text, maxChars = 12e3) {
|
|
|
6167
6184
|
${tail}`;
|
|
6168
6185
|
}
|
|
6169
6186
|
|
|
6187
|
+
// src/core/router/provider-errors.ts
|
|
6188
|
+
var SYSTEMIC = /* @__PURE__ */ new Set([
|
|
6189
|
+
"rate_limit",
|
|
6190
|
+
"quota_exhausted",
|
|
6191
|
+
"auth",
|
|
6192
|
+
"model_unavailable"
|
|
6193
|
+
]);
|
|
6194
|
+
function isSystemicKind(kind) {
|
|
6195
|
+
return SYSTEMIC.has(kind);
|
|
6196
|
+
}
|
|
6197
|
+
function statusOf(err) {
|
|
6198
|
+
if (!err || typeof err !== "object") return void 0;
|
|
6199
|
+
const e = err;
|
|
6200
|
+
for (const key of ["status", "statusCode", "code"]) {
|
|
6201
|
+
const v = e[key];
|
|
6202
|
+
if (typeof v === "number" && v >= 100 && v < 600) return v;
|
|
6203
|
+
}
|
|
6204
|
+
const resp = e["response"];
|
|
6205
|
+
if (resp && typeof resp["status"] === "number") return resp["status"];
|
|
6206
|
+
const inner = e["error"];
|
|
6207
|
+
if (inner && typeof inner["code"] === "number") return inner["code"];
|
|
6208
|
+
return void 0;
|
|
6209
|
+
}
|
|
6210
|
+
function messageOf(err) {
|
|
6211
|
+
if (err instanceof Error) return err.message;
|
|
6212
|
+
if (typeof err === "string") return err;
|
|
6213
|
+
if (err && typeof err === "object") {
|
|
6214
|
+
const e = err;
|
|
6215
|
+
if (typeof e["message"] === "string") return e["message"];
|
|
6216
|
+
const inner = e["error"];
|
|
6217
|
+
if (inner && typeof inner["message"] === "string") return inner["message"];
|
|
6218
|
+
try {
|
|
6219
|
+
return JSON.stringify(err);
|
|
6220
|
+
} catch {
|
|
6221
|
+
}
|
|
6222
|
+
}
|
|
6223
|
+
return String(err);
|
|
6224
|
+
}
|
|
6225
|
+
function classifyProviderError(err) {
|
|
6226
|
+
const raw = messageOf(err).trim();
|
|
6227
|
+
const status = statusOf(err);
|
|
6228
|
+
const m = raw.toLowerCase();
|
|
6229
|
+
const kind = (() => {
|
|
6230
|
+
if (status === 429) {
|
|
6231
|
+
return /quota|billing|credit|exceeded your current quota|insufficient/.test(m) ? "quota_exhausted" : "rate_limit";
|
|
6232
|
+
}
|
|
6233
|
+
if (status === 401 || status === 403) return "auth";
|
|
6234
|
+
if (status === 404) return "model_unavailable";
|
|
6235
|
+
if (/rate limit|too many requests|resource_exhausted/.test(m)) {
|
|
6236
|
+
return /quota|billing|credit|insufficient/.test(m) ? "quota_exhausted" : "rate_limit";
|
|
6237
|
+
}
|
|
6238
|
+
if (/quota|billing|insufficient_quota|insufficient credit|payment required/.test(m)) return "quota_exhausted";
|
|
6239
|
+
if (/api key|unauthorized|authentication|permission denied|invalid[_ ]api/.test(m)) return "auth";
|
|
6240
|
+
if (/model .*(not found|does not exist|unavailable)|no such model|unknown model|not supported for/.test(m)) {
|
|
6241
|
+
return "model_unavailable";
|
|
6242
|
+
}
|
|
6243
|
+
if (/context length|too many tokens|maximum context|prompt is too long|input too long/.test(m)) return "context_length";
|
|
6244
|
+
if (/safety|content filter|blocked|recitation|prohibited/.test(m)) return "content_filter";
|
|
6245
|
+
if (/econnreset|etimedout|enotfound|socket hang up|network|fetch failed|aborted/.test(m)) return "network";
|
|
6246
|
+
return "unknown";
|
|
6247
|
+
})();
|
|
6248
|
+
return { kind, systemic: isSystemicKind(kind), status, raw };
|
|
6249
|
+
}
|
|
6250
|
+
function describeProviderError(c, modelId) {
|
|
6251
|
+
const on = modelId ? ` on ${modelId}` : "";
|
|
6252
|
+
const detail = c.raw ? ` Provider said: ${c.raw}` : "";
|
|
6253
|
+
switch (c.kind) {
|
|
6254
|
+
case "rate_limit":
|
|
6255
|
+
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}`;
|
|
6256
|
+
case "quota_exhausted":
|
|
6257
|
+
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}`;
|
|
6258
|
+
case "auth":
|
|
6259
|
+
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}`;
|
|
6260
|
+
case "model_unavailable":
|
|
6261
|
+
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}`;
|
|
6262
|
+
case "context_length":
|
|
6263
|
+
return `The prompt was too long${on}. Reduce the attached context, or use a model with a larger context window for this tier.${detail}`;
|
|
6264
|
+
case "content_filter":
|
|
6265
|
+
return `The provider's safety filter refused this request${on}.${detail}`;
|
|
6266
|
+
case "network":
|
|
6267
|
+
return `Network error reaching the provider${on}. This is usually transient.${detail}`;
|
|
6268
|
+
default:
|
|
6269
|
+
return `The model call failed${on}.${detail}`;
|
|
6270
|
+
}
|
|
6271
|
+
}
|
|
6272
|
+
|
|
6170
6273
|
// src/core/tiers/t3-worker.ts
|
|
6171
6274
|
var CriticalToolError = class extends Error {
|
|
6172
6275
|
constructor(message, toolName) {
|
|
@@ -6200,8 +6303,15 @@ var KNOWN_TOOLS = [
|
|
|
6200
6303
|
"web_search",
|
|
6201
6304
|
"glob",
|
|
6202
6305
|
"grep",
|
|
6203
|
-
"web_fetch"
|
|
6306
|
+
"web_fetch",
|
|
6307
|
+
"generate_image"
|
|
6204
6308
|
];
|
|
6309
|
+
var PROVIDER_BACKED_TOOLS = /* @__PURE__ */ new Set([
|
|
6310
|
+
"generate_image",
|
|
6311
|
+
"generate_speech",
|
|
6312
|
+
"generate_video",
|
|
6313
|
+
"transcribe_audio"
|
|
6314
|
+
]);
|
|
6205
6315
|
function buildWorkerRules(has) {
|
|
6206
6316
|
const canWriteFiles = has("file_write") || has("file_edit") || has("run_code");
|
|
6207
6317
|
const hasAnyTool = KNOWN_TOOLS.some(has);
|
|
@@ -6211,9 +6321,22 @@ function buildWorkerRules(has) {
|
|
|
6211
6321
|
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.",
|
|
6212
6322
|
has("web_search") && '- Use the "web_search" tool to find current information, documentation, news, or general web data.',
|
|
6213
6323
|
has("pdf_create") && '- Use the "pdf_create" tool for PDF requests.',
|
|
6324
|
+
// Without this the model "writes the image" as prose — a bracketed
|
|
6325
|
+
// description sitting in the deck where the picture should be — even with a
|
|
6326
|
+
// working image model registered. Naming the reference syntax matters as
|
|
6327
|
+
// much as naming the tool: a generated image nobody embeds is still a
|
|
6328
|
+
// missing image.
|
|
6329
|
+
//
|
|
6330
|
+
// Scoped to PowerPoint/Word specifically: those are the only exporters
|
|
6331
|
+
// that actually embed a Markdown image reference (cloud/web/exporters.ts
|
|
6332
|
+
// toPptx/toDocx). PDF and plain-text/Markdown deliverables flatten a
|
|
6333
|
+
// reference straight to caption text — telling the model to call
|
|
6334
|
+
// generate_image for THOSE just pays for an image nobody ever sees.
|
|
6335
|
+
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.',
|
|
6214
6336
|
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.',
|
|
6215
6337
|
"- If you are not making meaningful progress, stop and escalate rather than looping or padding the response.",
|
|
6216
6338
|
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.',
|
|
6339
|
+
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.",
|
|
6217
6340
|
"- Return structured output that directly addresses the expected output specification."
|
|
6218
6341
|
];
|
|
6219
6342
|
return `You are a T3 Worker agent in the Cascade AI system. Your job is to execute a specific subtask completely and accurately.
|
|
@@ -6747,7 +6870,9 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
|
|
|
6747
6870
|
const durationMs = Date.now() - toolStartMs;
|
|
6748
6871
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
6749
6872
|
this.emit("tool:result", { id: tc.id, tierId: this.id, toolName: tc.name, error: errMsg, durationMs });
|
|
6750
|
-
|
|
6873
|
+
const isTaggedSystemic = err instanceof Error && err.systemic === true;
|
|
6874
|
+
const isProviderBacked = PROVIDER_BACKED_TOOLS.has(tc.name);
|
|
6875
|
+
if (isTaggedSystemic || isProviderBacked && classifyProviderError(err).systemic) {
|
|
6751
6876
|
throw new CriticalToolError(errMsg, tc.name);
|
|
6752
6877
|
}
|
|
6753
6878
|
return await this.adaptiveFallback(tc, `Tool error: ${errMsg}`);
|
|
@@ -7035,8 +7160,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
7035
7160
|
} catch {
|
|
7036
7161
|
return {
|
|
7037
7162
|
checksRun: ["completeness", "correctness", "compliance"],
|
|
7038
|
-
passed: [
|
|
7039
|
-
failed: []
|
|
7163
|
+
passed: [],
|
|
7164
|
+
failed: ["completeness"]
|
|
7040
7165
|
};
|
|
7041
7166
|
}
|
|
7042
7167
|
}
|
|
@@ -9105,17 +9230,17 @@ var CAPABILITIES = [
|
|
|
9105
9230
|
api: "openai-images"
|
|
9106
9231
|
},
|
|
9107
9232
|
{
|
|
9108
|
-
//
|
|
9109
|
-
//
|
|
9110
|
-
//
|
|
9111
|
-
// generateContent
|
|
9112
|
-
//
|
|
9113
|
-
//
|
|
9114
|
-
//
|
|
9233
|
+
// Was `imagen-4.0-generate-001` on `:predict`, which now 404s for new keys
|
|
9234
|
+
// ("no longer available to new users") — Imagen's :predict API is deprecated
|
|
9235
|
+
// and shuts down 2026-08-17. Google's own migration path is this model on
|
|
9236
|
+
// the ordinary generateContent endpoint, so the fix was never another Imagen
|
|
9237
|
+
// id: the whole request/response SHAPE changes, which is why `api` moved to
|
|
9238
|
+
// 'gemini-generate-content' rather than just the modelId moving.
|
|
9239
|
+
// https://ai.google.dev/gemini-api/docs/imagen
|
|
9115
9240
|
modality: "image",
|
|
9116
9241
|
provider: "gemini",
|
|
9117
|
-
modelId: "
|
|
9118
|
-
api: "gemini-
|
|
9242
|
+
modelId: "gemini-2.5-flash-image",
|
|
9243
|
+
api: "gemini-generate-content"
|
|
9119
9244
|
},
|
|
9120
9245
|
// ── Speech (text → audio) ──
|
|
9121
9246
|
{
|
|
@@ -9147,9 +9272,17 @@ var CAPABILITIES = [
|
|
|
9147
9272
|
// Long-running: submit returns an operation name, the video lands minutes
|
|
9148
9273
|
// later, and the bytes come from a short-lived signed URL. generate.ts owns
|
|
9149
9274
|
// that polling loop; the registry only records the shape via `api`.
|
|
9275
|
+
//
|
|
9276
|
+
// `veo-3.1-generate-001` (no "-preview") is VERTEX AI's id for this model,
|
|
9277
|
+
// not the Gemini Developer API's — this file calls
|
|
9278
|
+
// generativelanguage.googleapis.com (see generate.ts), which 404s on it:
|
|
9279
|
+
// "models/veo-3.1-generate-001 is not found for API version v1beta". On the
|
|
9280
|
+
// Gemini API specifically, Veo 3.1 is still preview-only, so the correct id
|
|
9281
|
+
// there is `veo-3.1-generate-preview`.
|
|
9282
|
+
// https://ai.google.dev/gemini-api/docs/veo
|
|
9150
9283
|
modality: "video",
|
|
9151
9284
|
provider: "gemini",
|
|
9152
|
-
modelId: "veo-3.1-generate-
|
|
9285
|
+
modelId: "veo-3.1-generate-preview",
|
|
9153
9286
|
api: "gemini-predict-lro"
|
|
9154
9287
|
}
|
|
9155
9288
|
];
|
|
@@ -9170,6 +9303,14 @@ function capabilityCost(cap) {
|
|
|
9170
9303
|
const shown = amount < 1e-3 ? amount.toExponential(2) : amount.toFixed(4);
|
|
9171
9304
|
return { label: `$${shown} ${unit}`, amount, unit };
|
|
9172
9305
|
}
|
|
9306
|
+
function explainChoice(c, cost, modality, isBest) {
|
|
9307
|
+
const priced = cost ? ` ${cost.label}.` : "";
|
|
9308
|
+
if (c.quality !== void 0) {
|
|
9309
|
+
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}`;
|
|
9310
|
+
}
|
|
9311
|
+
const noRanking = ". No published quality ranking is applied for this modality, so this is a cost choice, not a quality one.";
|
|
9312
|
+
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;
|
|
9313
|
+
}
|
|
9173
9314
|
var MultimodalRegistry = class {
|
|
9174
9315
|
/** Provider types the user has actually configured a key for. */
|
|
9175
9316
|
available;
|
|
@@ -9187,22 +9328,20 @@ var MultimodalRegistry = class {
|
|
|
9187
9328
|
return [...new Set(this.usable().map((c) => c.modality))];
|
|
9188
9329
|
}
|
|
9189
9330
|
/**
|
|
9190
|
-
*
|
|
9331
|
+
* Every usable capability for a modality, best first.
|
|
9191
9332
|
*
|
|
9192
9333
|
* Ranked models beat unranked ones, then higher quality, then lower unit
|
|
9193
|
-
* price. An explicit `preferModel`
|
|
9194
|
-
* naming a model is a stronger signal than any prior.
|
|
9334
|
+
* price. An explicit `preferModel` is moved to the head when it is usable —
|
|
9335
|
+
* the user naming a model is a stronger signal than any prior.
|
|
9336
|
+
*
|
|
9337
|
+
* `select()` is just the head of this list. The TAIL is the point: a systemic
|
|
9338
|
+
* provider failure (dead key, 404, exhausted quota) is scoped to one
|
|
9339
|
+
* provider+model, so "this model cannot draw" is not the same fact as "this
|
|
9340
|
+
* account cannot draw". A caller holding the ordered alternatives can try the
|
|
9341
|
+
* next provider instead of aborting the whole run — see generate-media.ts.
|
|
9195
9342
|
*/
|
|
9196
|
-
|
|
9197
|
-
const
|
|
9198
|
-
if (!pool.length) return null;
|
|
9199
|
-
if (opts.preferModel) {
|
|
9200
|
-
const wanted = pool.find((c) => c.modelId === opts.preferModel);
|
|
9201
|
-
if (wanted) {
|
|
9202
|
-
return { capability: wanted, cost: capabilityCost(wanted), reason: `Requested explicitly: ${wanted.modelId}.` };
|
|
9203
|
-
}
|
|
9204
|
-
}
|
|
9205
|
-
const scored = pool.map((c) => ({ c, cost: capabilityCost(c) })).sort((a, b) => {
|
|
9343
|
+
rank(modality, opts = {}) {
|
|
9344
|
+
const scored = this.usable(modality).map((c) => ({ c, cost: capabilityCost(c) })).sort((a, b) => {
|
|
9206
9345
|
const qa = a.c.quality ?? -1;
|
|
9207
9346
|
const qb = b.c.quality ?? -1;
|
|
9208
9347
|
if (qa !== qb) return qb - qa;
|
|
@@ -9210,10 +9349,25 @@ var MultimodalRegistry = class {
|
|
|
9210
9349
|
const pb = b.cost?.amount ?? Number.POSITIVE_INFINITY;
|
|
9211
9350
|
return pa - pb;
|
|
9212
9351
|
});
|
|
9213
|
-
const
|
|
9214
|
-
|
|
9215
|
-
|
|
9216
|
-
|
|
9352
|
+
const results = scored.map(({ c, cost }, i) => ({
|
|
9353
|
+
capability: c,
|
|
9354
|
+
cost,
|
|
9355
|
+
reason: explainChoice(c, cost, modality, i === 0)
|
|
9356
|
+
}));
|
|
9357
|
+
if (opts.preferModel) {
|
|
9358
|
+
const i = results.findIndex((r) => r.capability.modelId === opts.preferModel);
|
|
9359
|
+
if (i > -1) {
|
|
9360
|
+
const [wanted] = results.splice(i, 1);
|
|
9361
|
+
results.unshift({ ...wanted, reason: `Requested explicitly: ${wanted.capability.modelId}.` });
|
|
9362
|
+
}
|
|
9363
|
+
}
|
|
9364
|
+
return results;
|
|
9365
|
+
}
|
|
9366
|
+
/**
|
|
9367
|
+
* Pick a model for a modality — the best usable one, or null if there is none.
|
|
9368
|
+
*/
|
|
9369
|
+
select(modality, opts = {}) {
|
|
9370
|
+
return this.rank(modality, opts)[0] ?? null;
|
|
9217
9371
|
}
|
|
9218
9372
|
/**
|
|
9219
9373
|
* A plain-language account of what this account can and cannot generate —
|
|
@@ -9266,92 +9420,6 @@ var BaseTool = class {
|
|
|
9266
9420
|
}
|
|
9267
9421
|
};
|
|
9268
9422
|
|
|
9269
|
-
// src/core/router/provider-errors.ts
|
|
9270
|
-
var SYSTEMIC = /* @__PURE__ */ new Set([
|
|
9271
|
-
"rate_limit",
|
|
9272
|
-
"quota_exhausted",
|
|
9273
|
-
"auth",
|
|
9274
|
-
"model_unavailable"
|
|
9275
|
-
]);
|
|
9276
|
-
function isSystemicKind(kind) {
|
|
9277
|
-
return SYSTEMIC.has(kind);
|
|
9278
|
-
}
|
|
9279
|
-
function statusOf(err) {
|
|
9280
|
-
if (!err || typeof err !== "object") return void 0;
|
|
9281
|
-
const e = err;
|
|
9282
|
-
for (const key of ["status", "statusCode", "code"]) {
|
|
9283
|
-
const v = e[key];
|
|
9284
|
-
if (typeof v === "number" && v >= 100 && v < 600) return v;
|
|
9285
|
-
}
|
|
9286
|
-
const resp = e["response"];
|
|
9287
|
-
if (resp && typeof resp["status"] === "number") return resp["status"];
|
|
9288
|
-
const inner = e["error"];
|
|
9289
|
-
if (inner && typeof inner["code"] === "number") return inner["code"];
|
|
9290
|
-
return void 0;
|
|
9291
|
-
}
|
|
9292
|
-
function messageOf(err) {
|
|
9293
|
-
if (err instanceof Error) return err.message;
|
|
9294
|
-
if (typeof err === "string") return err;
|
|
9295
|
-
if (err && typeof err === "object") {
|
|
9296
|
-
const e = err;
|
|
9297
|
-
if (typeof e["message"] === "string") return e["message"];
|
|
9298
|
-
const inner = e["error"];
|
|
9299
|
-
if (inner && typeof inner["message"] === "string") return inner["message"];
|
|
9300
|
-
try {
|
|
9301
|
-
return JSON.stringify(err);
|
|
9302
|
-
} catch {
|
|
9303
|
-
}
|
|
9304
|
-
}
|
|
9305
|
-
return String(err);
|
|
9306
|
-
}
|
|
9307
|
-
function classifyProviderError(err) {
|
|
9308
|
-
const raw = messageOf(err).trim();
|
|
9309
|
-
const status = statusOf(err);
|
|
9310
|
-
const m = raw.toLowerCase();
|
|
9311
|
-
const kind = (() => {
|
|
9312
|
-
if (status === 429) {
|
|
9313
|
-
return /quota|billing|credit|exceeded your current quota|insufficient/.test(m) ? "quota_exhausted" : "rate_limit";
|
|
9314
|
-
}
|
|
9315
|
-
if (status === 401 || status === 403) return "auth";
|
|
9316
|
-
if (status === 404) return "model_unavailable";
|
|
9317
|
-
if (/rate limit|too many requests|resource_exhausted/.test(m)) {
|
|
9318
|
-
return /quota|billing|credit|insufficient/.test(m) ? "quota_exhausted" : "rate_limit";
|
|
9319
|
-
}
|
|
9320
|
-
if (/quota|billing|insufficient_quota|insufficient credit|payment required/.test(m)) return "quota_exhausted";
|
|
9321
|
-
if (/api key|unauthorized|authentication|permission denied|invalid[_ ]api/.test(m)) return "auth";
|
|
9322
|
-
if (/model .*(not found|does not exist|unavailable)|no such model|unknown model|not supported for/.test(m)) {
|
|
9323
|
-
return "model_unavailable";
|
|
9324
|
-
}
|
|
9325
|
-
if (/context length|too many tokens|maximum context|prompt is too long|input too long/.test(m)) return "context_length";
|
|
9326
|
-
if (/safety|content filter|blocked|recitation|prohibited/.test(m)) return "content_filter";
|
|
9327
|
-
if (/econnreset|etimedout|enotfound|socket hang up|network|fetch failed|aborted/.test(m)) return "network";
|
|
9328
|
-
return "unknown";
|
|
9329
|
-
})();
|
|
9330
|
-
return { kind, systemic: isSystemicKind(kind), status, raw };
|
|
9331
|
-
}
|
|
9332
|
-
function describeProviderError(c, modelId) {
|
|
9333
|
-
const on = modelId ? ` on ${modelId}` : "";
|
|
9334
|
-
const detail = c.raw ? ` Provider said: ${c.raw}` : "";
|
|
9335
|
-
switch (c.kind) {
|
|
9336
|
-
case "rate_limit":
|
|
9337
|
-
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}`;
|
|
9338
|
-
case "quota_exhausted":
|
|
9339
|
-
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}`;
|
|
9340
|
-
case "auth":
|
|
9341
|
-
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}`;
|
|
9342
|
-
case "model_unavailable":
|
|
9343
|
-
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}`;
|
|
9344
|
-
case "context_length":
|
|
9345
|
-
return `The prompt was too long${on}. Reduce the attached context, or use a model with a larger context window for this tier.${detail}`;
|
|
9346
|
-
case "content_filter":
|
|
9347
|
-
return `The provider's safety filter refused this request${on}.${detail}`;
|
|
9348
|
-
case "network":
|
|
9349
|
-
return `Network error reaching the provider${on}. This is usually transient.${detail}`;
|
|
9350
|
-
default:
|
|
9351
|
-
return `The model call failed${on}.${detail}`;
|
|
9352
|
-
}
|
|
9353
|
-
}
|
|
9354
|
-
|
|
9355
9423
|
// src/core/multimodal/generate.ts
|
|
9356
9424
|
var DEFAULT_TIMEOUT_MS = 12e4;
|
|
9357
9425
|
async function callProvider(fn, modelId) {
|
|
@@ -9390,6 +9458,29 @@ function baseUrlFor(cfg, fallback) {
|
|
|
9390
9458
|
function stamp(ext) {
|
|
9391
9459
|
return `cascade-${Date.now()}.${ext}`;
|
|
9392
9460
|
}
|
|
9461
|
+
var GEMINI_ASPECT_RATIOS = [
|
|
9462
|
+
["1:1", 1 / 1],
|
|
9463
|
+
["3:2", 3 / 2],
|
|
9464
|
+
["2:3", 2 / 3],
|
|
9465
|
+
["3:4", 3 / 4],
|
|
9466
|
+
["4:3", 4 / 3],
|
|
9467
|
+
["4:5", 4 / 5],
|
|
9468
|
+
["5:4", 5 / 4],
|
|
9469
|
+
["9:16", 9 / 16],
|
|
9470
|
+
["16:9", 16 / 9],
|
|
9471
|
+
["21:9", 21 / 9]
|
|
9472
|
+
];
|
|
9473
|
+
function closestGeminiAspectRatio(size) {
|
|
9474
|
+
const m = size.match(/^(\d+)\s*[x×]\s*(\d+)$/i);
|
|
9475
|
+
if (!m) return void 0;
|
|
9476
|
+
const w = Number(m[1]);
|
|
9477
|
+
const h = Number(m[2]);
|
|
9478
|
+
if (!w || !h) return void 0;
|
|
9479
|
+
const ratio = w / h;
|
|
9480
|
+
return GEMINI_ASPECT_RATIOS.reduce(
|
|
9481
|
+
(best, cand) => Math.abs(cand[1] - ratio) < Math.abs(best[1] - ratio) ? cand : best
|
|
9482
|
+
)[0];
|
|
9483
|
+
}
|
|
9393
9484
|
async function generateImage(cap, cfg, req, signal) {
|
|
9394
9485
|
return callProvider(async () => {
|
|
9395
9486
|
if (cap.api === "openai-images") {
|
|
@@ -9414,19 +9505,37 @@ async function generateImage(cap, cfg, req, signal) {
|
|
|
9414
9505
|
...first.revised_prompt ? { revisedPrompt: first.revised_prompt } : {}
|
|
9415
9506
|
};
|
|
9416
9507
|
}
|
|
9417
|
-
if (cap.api === "gemini-
|
|
9508
|
+
if (cap.api === "gemini-generate-content") {
|
|
9418
9509
|
const base = baseUrlFor(cfg, "https://generativelanguage.googleapis.com/v1beta");
|
|
9419
|
-
const url = `${base}/models/${encodeURIComponent(cap.modelId)}:
|
|
9510
|
+
const url = `${base}/models/${encodeURIComponent(cap.modelId)}:generateContent`;
|
|
9511
|
+
const aspectRatio = req.size ? closestGeminiAspectRatio(req.size) : void 0;
|
|
9420
9512
|
const res = await postJson(url, {
|
|
9421
|
-
|
|
9422
|
-
|
|
9513
|
+
contents: [{ parts: [{ text: req.prompt }] }],
|
|
9514
|
+
generationConfig: {
|
|
9515
|
+
// Asked for explicitly: these models can answer with text only, and a
|
|
9516
|
+
// text-only reply to "draw me a cat" is a failed image generation.
|
|
9517
|
+
responseModalities: ["IMAGE", "TEXT"],
|
|
9518
|
+
// Gemini has no free-form pixel size — only a named aspect ratio (see
|
|
9519
|
+
// closestGeminiAspectRatio). Omitted entirely when there's no
|
|
9520
|
+
// requested size, or no supported ratio close enough to bother with.
|
|
9521
|
+
...aspectRatio ? { imageConfig: { aspectRatio } } : {}
|
|
9522
|
+
}
|
|
9423
9523
|
}, { "x-goog-api-key": cfg.apiKey ?? "" }, signal);
|
|
9424
9524
|
const body = await res.json();
|
|
9425
|
-
const
|
|
9426
|
-
|
|
9427
|
-
const
|
|
9525
|
+
const candidate = body.candidates?.[0];
|
|
9526
|
+
const parts = candidate?.content?.parts ?? [];
|
|
9527
|
+
const image = parts.find((p) => p.inlineData?.data)?.inlineData;
|
|
9528
|
+
if (!image?.data) {
|
|
9529
|
+
const blocked = body.promptFeedback?.blockReason ?? (candidate?.finishReason && candidate.finishReason !== "STOP" ? candidate.finishReason : void 0);
|
|
9530
|
+
if (blocked) throw new Error(`Image request was blocked by the provider (${blocked}).`);
|
|
9531
|
+
const said = parts.find((p) => p.text)?.text?.trim();
|
|
9532
|
+
throw new Error(
|
|
9533
|
+
"Image response contained no image data." + (said ? ` The model replied with text instead: ${said}` : "")
|
|
9534
|
+
);
|
|
9535
|
+
}
|
|
9536
|
+
const mimeType = image.mimeType ?? "image/png";
|
|
9428
9537
|
return {
|
|
9429
|
-
data: Buffer.from(
|
|
9538
|
+
data: Buffer.from(image.data, "base64"),
|
|
9430
9539
|
mimeType,
|
|
9431
9540
|
filename: stamp(mimeType.includes("jpeg") ? "jpg" : "png"),
|
|
9432
9541
|
modelId: cap.modelId,
|
|
@@ -9579,6 +9688,49 @@ function resolve(deps, modality, preferModel) {
|
|
|
9579
9688
|
}
|
|
9580
9689
|
return { ok: true, selected, cfg };
|
|
9581
9690
|
}
|
|
9691
|
+
function messageOf2(err) {
|
|
9692
|
+
return err instanceof Error ? err.message : String(err);
|
|
9693
|
+
}
|
|
9694
|
+
async function runWithProviderFallback(deps, modality, first, run, signal) {
|
|
9695
|
+
const failed = first.selected.capability;
|
|
9696
|
+
try {
|
|
9697
|
+
return { value: await run(failed, first.cfg), selected: first.selected };
|
|
9698
|
+
} catch (err) {
|
|
9699
|
+
if (signal?.aborted || !classifyProviderError(err).systemic) throw err;
|
|
9700
|
+
const tried = [`${failed.modelId} (${failed.provider}): ${messageOf2(err)}`];
|
|
9701
|
+
let allSystemic = true;
|
|
9702
|
+
let lastNonSystemic;
|
|
9703
|
+
const seen = /* @__PURE__ */ new Set([failed.provider]);
|
|
9704
|
+
for (const alt of deps.registry.rank(modality)) {
|
|
9705
|
+
if (seen.has(alt.capability.provider)) continue;
|
|
9706
|
+
const cfg = deps.lookupProvider(alt.capability.provider);
|
|
9707
|
+
if (!cfg) continue;
|
|
9708
|
+
seen.add(alt.capability.provider);
|
|
9709
|
+
try {
|
|
9710
|
+
return {
|
|
9711
|
+
value: await run(alt.capability, cfg),
|
|
9712
|
+
selected: alt,
|
|
9713
|
+
fellBackFrom: { capability: failed, error: messageOf2(err) }
|
|
9714
|
+
};
|
|
9715
|
+
} catch (retryErr) {
|
|
9716
|
+
if (signal?.aborted) throw retryErr;
|
|
9717
|
+
tried.push(`${alt.capability.modelId} (${alt.capability.provider}): ${messageOf2(retryErr)}`);
|
|
9718
|
+
if (classifyProviderError(retryErr).systemic) {
|
|
9719
|
+
continue;
|
|
9720
|
+
}
|
|
9721
|
+
allSystemic = false;
|
|
9722
|
+
lastNonSystemic = retryErr;
|
|
9723
|
+
}
|
|
9724
|
+
}
|
|
9725
|
+
if (tried.length === 1) throw err;
|
|
9726
|
+
if (!allSystemic) throw lastNonSystemic;
|
|
9727
|
+
const exhausted = new Error(
|
|
9728
|
+
`${modality} generation failed on every configured provider (${seen.size} tried). ${tried.join(" ")}`
|
|
9729
|
+
);
|
|
9730
|
+
Object.assign(exhausted, { systemic: true });
|
|
9731
|
+
throw exhausted;
|
|
9732
|
+
}
|
|
9733
|
+
}
|
|
9582
9734
|
var GenerateImageTool = class extends BaseTool {
|
|
9583
9735
|
constructor(deps) {
|
|
9584
9736
|
super();
|
|
@@ -9602,16 +9754,32 @@ var GenerateImageTool = class extends BaseTool {
|
|
|
9602
9754
|
const r = resolve(this.deps, "image", input["model"] ? String(input["model"]) : void 0);
|
|
9603
9755
|
if (!r.ok) return r.error;
|
|
9604
9756
|
const size = input["size"] ? String(input["size"]) : void 0;
|
|
9605
|
-
const
|
|
9606
|
-
|
|
9607
|
-
|
|
9608
|
-
|
|
9757
|
+
const attempt = await runWithProviderFallback(
|
|
9758
|
+
this.deps,
|
|
9759
|
+
"image",
|
|
9760
|
+
r,
|
|
9761
|
+
(cap, cfg) => generateImage(cap, cfg, { prompt, ...size ? { size } : {} }, options.signal),
|
|
9609
9762
|
options.signal
|
|
9610
9763
|
);
|
|
9764
|
+
const asset = attempt.value;
|
|
9765
|
+
const fell = attempt.fellBackFrom;
|
|
9611
9766
|
const location = await this.deps.sink(asset);
|
|
9612
9767
|
return [
|
|
9613
9768
|
`Image generated and saved to ${location}.`,
|
|
9614
|
-
|
|
9769
|
+
// Knowing WHERE the image is isn't the same as knowing how to use it: a
|
|
9770
|
+
// model told only "saved to X" tends to describe the picture in prose
|
|
9771
|
+
// instead of embedding it. Spelling out the reference syntax here makes
|
|
9772
|
+
// the worker's "never write a placeholder" rule concretely actionable,
|
|
9773
|
+
// and it works for every host because `location` is whatever that host's
|
|
9774
|
+
// sink returned — a workspace path on desktop/CLI, a URL in the cloud.
|
|
9775
|
+
`To include this image in your output, reference it as: `,
|
|
9776
|
+
// The reported model has to be the one that actually drew the picture, not
|
|
9777
|
+
// the one that was picked and then failed.
|
|
9778
|
+
`Model: ${asset.modelId} \u2014 ${fell ? `used as a fallback after ${fell.capability.modelId} (${fell.capability.provider}) failed with a systemic error.` : attempt.selected.reason}`,
|
|
9779
|
+
// Worth one line: the run succeeded, so nothing else will report that a
|
|
9780
|
+
// configured provider is broken, and "your Gemini key is dead" is exactly
|
|
9781
|
+
// the kind of thing the user wants to hear once rather than never.
|
|
9782
|
+
fell ? `Note: ${fell.capability.provider} failed first \u2014 ${fell.error}` : "",
|
|
9615
9783
|
// DALL·E rewrites the prompt before drawing; the model needs to know what
|
|
9616
9784
|
// was actually rendered, not what it asked for, or its description of the
|
|
9617
9785
|
// result will not match the image.
|
|
@@ -9854,6 +10022,87 @@ function mcpToolName(serverName, toolName) {
|
|
|
9854
10022
|
function isMcpToolName(name) {
|
|
9855
10023
|
return name.startsWith(MCP_TOOL_PREFIX);
|
|
9856
10024
|
}
|
|
10025
|
+
var READ_ONLY_MCP_VERBS = ["list", "get", "search", "read", "describe", "find", "query", "fetch", "show", "view"];
|
|
10026
|
+
var MUTATING_MCP_TOKENS = /* @__PURE__ */ new Set([
|
|
10027
|
+
"create",
|
|
10028
|
+
"delete",
|
|
10029
|
+
"remove",
|
|
10030
|
+
"update",
|
|
10031
|
+
"write",
|
|
10032
|
+
"modify",
|
|
10033
|
+
"set",
|
|
10034
|
+
"put",
|
|
10035
|
+
"post",
|
|
10036
|
+
"patch",
|
|
10037
|
+
"push",
|
|
10038
|
+
"merge",
|
|
10039
|
+
"send",
|
|
10040
|
+
"execute",
|
|
10041
|
+
"exec",
|
|
10042
|
+
"run",
|
|
10043
|
+
"deploy",
|
|
10044
|
+
"install",
|
|
10045
|
+
"uninstall",
|
|
10046
|
+
"upload",
|
|
10047
|
+
"publish",
|
|
10048
|
+
"revoke",
|
|
10049
|
+
"grant",
|
|
10050
|
+
"approve",
|
|
10051
|
+
"reject",
|
|
10052
|
+
"cancel",
|
|
10053
|
+
"enable",
|
|
10054
|
+
"disable",
|
|
10055
|
+
"add",
|
|
10056
|
+
"insert",
|
|
10057
|
+
"drop",
|
|
10058
|
+
"truncate",
|
|
10059
|
+
"clear",
|
|
10060
|
+
"reset",
|
|
10061
|
+
"restart",
|
|
10062
|
+
"stop",
|
|
10063
|
+
"kill",
|
|
10064
|
+
"terminate",
|
|
10065
|
+
"rename",
|
|
10066
|
+
"move",
|
|
10067
|
+
"fork",
|
|
10068
|
+
"sync",
|
|
10069
|
+
"replace",
|
|
10070
|
+
"overwrite",
|
|
10071
|
+
"invite",
|
|
10072
|
+
"ban",
|
|
10073
|
+
"block",
|
|
10074
|
+
"assign",
|
|
10075
|
+
"lock",
|
|
10076
|
+
"unlock",
|
|
10077
|
+
"archive",
|
|
10078
|
+
"star",
|
|
10079
|
+
"comment",
|
|
10080
|
+
"reply",
|
|
10081
|
+
"submit",
|
|
10082
|
+
"apply",
|
|
10083
|
+
"trigger",
|
|
10084
|
+
"invoke",
|
|
10085
|
+
"purge",
|
|
10086
|
+
"migrate",
|
|
10087
|
+
"edit",
|
|
10088
|
+
"save",
|
|
10089
|
+
"commit"
|
|
10090
|
+
]);
|
|
10091
|
+
function mcpTokens(rawToolName) {
|
|
10092
|
+
return rawToolName.split(/[_-]+/).flatMap((segment) => segment.split(/(?<=[a-z0-9])(?=[A-Z])/)).map((t) => t.toLowerCase()).filter(Boolean);
|
|
10093
|
+
}
|
|
10094
|
+
function isReadOnlyMcpToolName(rawToolName) {
|
|
10095
|
+
if (mcpTokens(rawToolName).some((t) => MUTATING_MCP_TOKENS.has(t))) return false;
|
|
10096
|
+
const lower = rawToolName.toLowerCase();
|
|
10097
|
+
for (const verb of READ_ONLY_MCP_VERBS) {
|
|
10098
|
+
if (!lower.startsWith(verb)) continue;
|
|
10099
|
+
const boundary = rawToolName[verb.length];
|
|
10100
|
+
if (boundary === void 0 || boundary === "_" || boundary === "-" || boundary >= "A" && boundary <= "Z") {
|
|
10101
|
+
return true;
|
|
10102
|
+
}
|
|
10103
|
+
}
|
|
10104
|
+
return false;
|
|
10105
|
+
}
|
|
9857
10106
|
function uniqueMcpServerName(desired, existingNames) {
|
|
9858
10107
|
const taken = new Set(existingNames.map(mcpServerPrefix));
|
|
9859
10108
|
if (!taken.has(mcpServerPrefix(desired))) return desired;
|
|
@@ -10947,9 +11196,11 @@ var WebSearchTool = class extends BaseTool {
|
|
|
10947
11196
|
);
|
|
10948
11197
|
const errors = [];
|
|
10949
11198
|
let results = [];
|
|
11199
|
+
let anyBackendReachable = false;
|
|
10950
11200
|
if (this.config.searxngUrl) {
|
|
10951
11201
|
try {
|
|
10952
11202
|
results = await searchSearXNG(query, this.config.searxngUrl, maxResults);
|
|
11203
|
+
anyBackendReachable = true;
|
|
10953
11204
|
if (results.length > 0) return this.formatResults(query, results);
|
|
10954
11205
|
errors.push("SearXNG: returned 0 results");
|
|
10955
11206
|
} catch (err) {
|
|
@@ -10959,6 +11210,7 @@ var WebSearchTool = class extends BaseTool {
|
|
|
10959
11210
|
if (this.config.braveApiKey) {
|
|
10960
11211
|
try {
|
|
10961
11212
|
results = await searchBrave(query, this.config.braveApiKey, maxResults);
|
|
11213
|
+
anyBackendReachable = true;
|
|
10962
11214
|
if (results.length > 0) return this.formatResults(query, results);
|
|
10963
11215
|
errors.push("Brave: returned 0 results");
|
|
10964
11216
|
} catch (err) {
|
|
@@ -10968,6 +11220,7 @@ var WebSearchTool = class extends BaseTool {
|
|
|
10968
11220
|
if (this.config.tavilyApiKey) {
|
|
10969
11221
|
try {
|
|
10970
11222
|
results = await searchTavily(query, this.config.tavilyApiKey, maxResults);
|
|
11223
|
+
anyBackendReachable = true;
|
|
10971
11224
|
if (results.length > 0) return this.formatResults(query, results);
|
|
10972
11225
|
errors.push("Tavily: returned 0 results");
|
|
10973
11226
|
} catch (err) {
|
|
@@ -10977,6 +11230,7 @@ var WebSearchTool = class extends BaseTool {
|
|
|
10977
11230
|
for (const variant of ["html", "lite"]) {
|
|
10978
11231
|
try {
|
|
10979
11232
|
results = await searchDuckDuckGo(query, maxResults, variant);
|
|
11233
|
+
anyBackendReachable = true;
|
|
10980
11234
|
if (results.length > 0) return this.formatResults(query, results);
|
|
10981
11235
|
errors.push(`DuckDuckGo ${variant}: returned 0 results`);
|
|
10982
11236
|
} catch (err) {
|
|
@@ -10984,11 +11238,11 @@ var WebSearchTool = class extends BaseTool {
|
|
|
10984
11238
|
}
|
|
10985
11239
|
}
|
|
10986
11240
|
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" : "";
|
|
10987
|
-
|
|
10988
|
-
`Web search for "${query}"
|
|
11241
|
+
throw Object.assign(new Error([
|
|
11242
|
+
`Web search for "${query}" found nothing across all backends:`,
|
|
10989
11243
|
...errors.map((e) => ` \u2022 ${e}`),
|
|
10990
11244
|
configHint
|
|
10991
|
-
].join("\n");
|
|
11245
|
+
].join("\n")), { systemic: !anyBackendReachable });
|
|
10992
11246
|
}
|
|
10993
11247
|
formatResults(query, results) {
|
|
10994
11248
|
const lines = [`Web search results for: "${query}"`, ""];
|
|
@@ -11356,6 +11610,15 @@ var McpToolWrapper = class extends BaseTool {
|
|
|
11356
11610
|
this.description = `[MCP:${serverName}] ${description}`;
|
|
11357
11611
|
this.inputSchema = inputSchema;
|
|
11358
11612
|
}
|
|
11613
|
+
// Every built-in tool of comparable risk (shell, git, file writes, the
|
|
11614
|
+
// built-in github tool) overrides this to true. An MCP tool never did —
|
|
11615
|
+
// BaseTool's default (false) meant a connected server's create/delete/push
|
|
11616
|
+
// actions ran with zero human approval, whatever they did. MCP carries no
|
|
11617
|
+
// standard "this mutates state" flag, so default to dangerous and name the
|
|
11618
|
+
// exception (a read-only-looking verb) rather than the other way around.
|
|
11619
|
+
isDangerous() {
|
|
11620
|
+
return !isReadOnlyMcpToolName(this.toolName);
|
|
11621
|
+
}
|
|
11359
11622
|
async execute(input, _options) {
|
|
11360
11623
|
return this.mcpClient.callTool(this.serverName, this.toolName, input);
|
|
11361
11624
|
}
|
|
@@ -11468,7 +11731,7 @@ var ToolRegistry = class extends EventEmitter {
|
|
|
11468
11731
|
requiresApproval(toolName) {
|
|
11469
11732
|
const defaults = DEFAULT_APPROVAL_REQUIRED;
|
|
11470
11733
|
const configured = this.config.requireApprovalFor;
|
|
11471
|
-
return defaults.includes(toolName) || configured.includes(toolName);
|
|
11734
|
+
return defaults.includes(toolName) || configured.includes(toolName) || this.isDangerous(toolName);
|
|
11472
11735
|
}
|
|
11473
11736
|
isDangerous(toolName) {
|
|
11474
11737
|
return this.tools.get(toolName)?.isDangerous() ?? false;
|
|
@@ -14836,9 +15099,8 @@ var Cascade = class _Cascade extends EventEmitter {
|
|
|
14836
15099
|
});
|
|
14837
15100
|
this.toolRegistry = new ToolRegistry(this.config.tools, workspacePath);
|
|
14838
15101
|
try {
|
|
14839
|
-
const nodePath = __require("path");
|
|
14840
15102
|
this.router.setDeadModelStore(new DeadModelStore(
|
|
14841
|
-
fileDeadModelPersistence(
|
|
15103
|
+
fileDeadModelPersistence(path24.join(workspacePath, ".cascade", "dead-models.json"))
|
|
14842
15104
|
));
|
|
14843
15105
|
} catch {
|
|
14844
15106
|
}
|