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/cli.js
CHANGED
|
@@ -7,8 +7,8 @@ import https from 'https';
|
|
|
7
7
|
import zlib from 'zlib';
|
|
8
8
|
import { Readable } from 'stream';
|
|
9
9
|
import Database2 from 'better-sqlite3';
|
|
10
|
-
import path32 from 'path';
|
|
11
|
-
import fs29 from 'fs';
|
|
10
|
+
import path32, { dirname } from 'path';
|
|
11
|
+
import fs29, { mkdirSync, writeFileSync, existsSync, readFileSync } from 'fs';
|
|
12
12
|
import os10 from 'os';
|
|
13
13
|
import crypto, { randomUUID, randomBytes, webcrypto, timingSafeEqual, createHash } from 'crypto';
|
|
14
14
|
import { render, Box, Text, useStdout, useApp, useInput, Static } from 'ink';
|
|
@@ -51,12 +51,6 @@ import cron from 'node-cron';
|
|
|
51
51
|
// Cascade AI — Multi-tier AI Orchestration System
|
|
52
52
|
var __defProp = Object.defineProperty;
|
|
53
53
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
54
|
-
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
55
|
-
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
56
|
-
}) : x)(function(x) {
|
|
57
|
-
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
58
|
-
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
59
|
-
});
|
|
60
54
|
var __esm = (fn, res) => function __init() {
|
|
61
55
|
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
62
56
|
};
|
|
@@ -1476,6 +1470,23 @@ var init_pricing_data = __esm({
|
|
|
1476
1470
|
],
|
|
1477
1471
|
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."
|
|
1478
1472
|
},
|
|
1473
|
+
{
|
|
1474
|
+
model: "gemini-2.5-flash-image",
|
|
1475
|
+
provider: "gemini",
|
|
1476
|
+
modality: "image",
|
|
1477
|
+
currency: "USD",
|
|
1478
|
+
asOf: "2026-07-27",
|
|
1479
|
+
source: "https://ai.google.dev/gemini-api/docs/pricing#gemini-2.5-flash-image",
|
|
1480
|
+
provenance: "litellm-catalog",
|
|
1481
|
+
rates: [
|
|
1482
|
+
{
|
|
1483
|
+
unit: "per_image",
|
|
1484
|
+
amount: 0.039,
|
|
1485
|
+
variant: "default 1024px output"
|
|
1486
|
+
}
|
|
1487
|
+
],
|
|
1488
|
+
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."
|
|
1489
|
+
},
|
|
1479
1490
|
{
|
|
1480
1491
|
model: "imagen-4.0-generate-001",
|
|
1481
1492
|
provider: "vertex",
|
|
@@ -1492,7 +1503,7 @@ var init_pricing_data = __esm({
|
|
|
1492
1503
|
]
|
|
1493
1504
|
},
|
|
1494
1505
|
{
|
|
1495
|
-
model: "veo-3.1-generate-
|
|
1506
|
+
model: "veo-3.1-generate-preview",
|
|
1496
1507
|
provider: "gemini",
|
|
1497
1508
|
modality: "video",
|
|
1498
1509
|
currency: "USD",
|
|
@@ -1746,7 +1757,7 @@ var CASCADE_VERSION, CASCADE_CONFIG_FILE, CASCADE_DB_FILE, CASCADE_DASHBOARD_SEC
|
|
|
1746
1757
|
var init_constants = __esm({
|
|
1747
1758
|
"src/constants.ts"() {
|
|
1748
1759
|
init_pricing();
|
|
1749
|
-
CASCADE_VERSION = "0.
|
|
1760
|
+
CASCADE_VERSION = "0.64.0";
|
|
1750
1761
|
CASCADE_CONFIG_FILE = ".cascade/config.json";
|
|
1751
1762
|
CASCADE_DB_FILE = ".cascade/memory.db";
|
|
1752
1763
|
CASCADE_DASHBOARD_SECRET_FILE = ".cascade/dashboard-secret";
|
|
@@ -5415,6 +5426,87 @@ function mcpToolName(serverName, toolName) {
|
|
|
5415
5426
|
function isMcpToolName(name) {
|
|
5416
5427
|
return name.startsWith(MCP_TOOL_PREFIX);
|
|
5417
5428
|
}
|
|
5429
|
+
var READ_ONLY_MCP_VERBS = ["list", "get", "search", "read", "describe", "find", "query", "fetch", "show", "view"];
|
|
5430
|
+
var MUTATING_MCP_TOKENS = /* @__PURE__ */ new Set([
|
|
5431
|
+
"create",
|
|
5432
|
+
"delete",
|
|
5433
|
+
"remove",
|
|
5434
|
+
"update",
|
|
5435
|
+
"write",
|
|
5436
|
+
"modify",
|
|
5437
|
+
"set",
|
|
5438
|
+
"put",
|
|
5439
|
+
"post",
|
|
5440
|
+
"patch",
|
|
5441
|
+
"push",
|
|
5442
|
+
"merge",
|
|
5443
|
+
"send",
|
|
5444
|
+
"execute",
|
|
5445
|
+
"exec",
|
|
5446
|
+
"run",
|
|
5447
|
+
"deploy",
|
|
5448
|
+
"install",
|
|
5449
|
+
"uninstall",
|
|
5450
|
+
"upload",
|
|
5451
|
+
"publish",
|
|
5452
|
+
"revoke",
|
|
5453
|
+
"grant",
|
|
5454
|
+
"approve",
|
|
5455
|
+
"reject",
|
|
5456
|
+
"cancel",
|
|
5457
|
+
"enable",
|
|
5458
|
+
"disable",
|
|
5459
|
+
"add",
|
|
5460
|
+
"insert",
|
|
5461
|
+
"drop",
|
|
5462
|
+
"truncate",
|
|
5463
|
+
"clear",
|
|
5464
|
+
"reset",
|
|
5465
|
+
"restart",
|
|
5466
|
+
"stop",
|
|
5467
|
+
"kill",
|
|
5468
|
+
"terminate",
|
|
5469
|
+
"rename",
|
|
5470
|
+
"move",
|
|
5471
|
+
"fork",
|
|
5472
|
+
"sync",
|
|
5473
|
+
"replace",
|
|
5474
|
+
"overwrite",
|
|
5475
|
+
"invite",
|
|
5476
|
+
"ban",
|
|
5477
|
+
"block",
|
|
5478
|
+
"assign",
|
|
5479
|
+
"lock",
|
|
5480
|
+
"unlock",
|
|
5481
|
+
"archive",
|
|
5482
|
+
"star",
|
|
5483
|
+
"comment",
|
|
5484
|
+
"reply",
|
|
5485
|
+
"submit",
|
|
5486
|
+
"apply",
|
|
5487
|
+
"trigger",
|
|
5488
|
+
"invoke",
|
|
5489
|
+
"purge",
|
|
5490
|
+
"migrate",
|
|
5491
|
+
"edit",
|
|
5492
|
+
"save",
|
|
5493
|
+
"commit"
|
|
5494
|
+
]);
|
|
5495
|
+
function mcpTokens(rawToolName) {
|
|
5496
|
+
return rawToolName.split(/[_-]+/).flatMap((segment) => segment.split(/(?<=[a-z0-9])(?=[A-Z])/)).map((t) => t.toLowerCase()).filter(Boolean);
|
|
5497
|
+
}
|
|
5498
|
+
function isReadOnlyMcpToolName(rawToolName) {
|
|
5499
|
+
if (mcpTokens(rawToolName).some((t) => MUTATING_MCP_TOKENS.has(t))) return false;
|
|
5500
|
+
const lower = rawToolName.toLowerCase();
|
|
5501
|
+
for (const verb of READ_ONLY_MCP_VERBS) {
|
|
5502
|
+
if (!lower.startsWith(verb)) continue;
|
|
5503
|
+
const boundary = rawToolName[verb.length];
|
|
5504
|
+
if (boundary === void 0 || boundary === "_" || boundary === "-" || boundary >= "A" && boundary <= "Z") {
|
|
5505
|
+
return true;
|
|
5506
|
+
}
|
|
5507
|
+
}
|
|
5508
|
+
return false;
|
|
5509
|
+
}
|
|
5418
5510
|
function uniqueMcpServerName(desired, existingNames) {
|
|
5419
5511
|
const taken = new Set(existingNames.map(mcpServerPrefix));
|
|
5420
5512
|
if (!taken.has(mcpServerPrefix(desired))) return desired;
|
|
@@ -6426,8 +6518,6 @@ var LocalRequestQueue = class {
|
|
|
6426
6518
|
};
|
|
6427
6519
|
}
|
|
6428
6520
|
};
|
|
6429
|
-
|
|
6430
|
-
// src/core/router/dead-models.ts
|
|
6431
6521
|
var DEAD_MODEL_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
6432
6522
|
function deadModelKey(provider, modelId) {
|
|
6433
6523
|
return `${provider}:${modelId}`;
|
|
@@ -6528,16 +6618,13 @@ var DeadModelStore = class {
|
|
|
6528
6618
|
function fileDeadModelPersistence(filePath) {
|
|
6529
6619
|
return {
|
|
6530
6620
|
load() {
|
|
6531
|
-
|
|
6532
|
-
|
|
6533
|
-
const raw = JSON.parse(fs32.readFileSync(filePath, "utf-8"));
|
|
6621
|
+
if (!existsSync(filePath)) return [];
|
|
6622
|
+
const raw = JSON.parse(readFileSync(filePath, "utf-8"));
|
|
6534
6623
|
return Array.isArray(raw) ? raw : [];
|
|
6535
6624
|
},
|
|
6536
6625
|
save(records) {
|
|
6537
|
-
|
|
6538
|
-
|
|
6539
|
-
fs32.mkdirSync(path37.dirname(filePath), { recursive: true });
|
|
6540
|
-
fs32.writeFileSync(filePath, JSON.stringify(records, null, 2));
|
|
6626
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
6627
|
+
writeFileSync(filePath, JSON.stringify(records, null, 2));
|
|
6541
6628
|
}
|
|
6542
6629
|
};
|
|
6543
6630
|
}
|
|
@@ -6967,6 +7054,12 @@ var FAMILY_BENCHMARKS = {
|
|
|
6967
7054
|
// Google — strongest at analysis/data and long-context.
|
|
6968
7055
|
"gemini-2.5-pro": { code: 90, analysis: 93, creative: 86, data: 92 },
|
|
6969
7056
|
"gemini-2.5-flash": { code: 82, analysis: 83, creative: 80, data: 82 },
|
|
7057
|
+
// The 2.5-lite tier scores below full 2.5-flash but above the older 2.0
|
|
7058
|
+
// lite tier — it exists because a naive `gemini-?2\.5-flash` regex swallows
|
|
7059
|
+
// "gemini-2.5-flash-lite" as a substring match (no lite variant of its own
|
|
7060
|
+
// to fall into), so the weaker lite model got credited with the full
|
|
7061
|
+
// flash's benchmark score and kept winning Cascade Auto's "best value" pick.
|
|
7062
|
+
"gemini-2.5-flash-lite": { code: 72, analysis: 72, creative: 74, data: 72 },
|
|
6970
7063
|
"gemini-1.5-pro": { code: 82, analysis: 84, creative: 82, data: 85 },
|
|
6971
7064
|
"gemini-2.0-flash": { code: 79, analysis: 80, creative: 79, data: 80 },
|
|
6972
7065
|
"gemini-flash-lite": { code: 68, analysis: 68, creative: 70, data: 68 },
|
|
@@ -6999,6 +7092,11 @@ var FAMILY_MATCHERS = [
|
|
|
6999
7092
|
[/gpt-?4o-mini/i, "gpt-4o-mini"],
|
|
7000
7093
|
[/gpt-?4o/i, "gpt-4o"],
|
|
7001
7094
|
[/gemini-?2\.5-pro/i, "gemini-2.5-pro"],
|
|
7095
|
+
// Must precede the bare `2\.5-flash` rule below: that pattern has no word
|
|
7096
|
+
// boundary after "flash" and matches "gemini-2.5-flash-lite" as a plain
|
|
7097
|
+
// substring, so the lite variant would otherwise be scored as the full
|
|
7098
|
+
// flash model (see the FAMILY_BENCHMARKS comment above).
|
|
7099
|
+
[/gemini-?2\.5-flash-lite/i, "gemini-2.5-flash-lite"],
|
|
7002
7100
|
[/gemini-?2\.5-flash/i, "gemini-2.5-flash"],
|
|
7003
7101
|
[/gemini-?1\.5-pro/i, "gemini-1.5-pro"],
|
|
7004
7102
|
[/gemini-?2\.0-flash-lite/i, "gemini-flash-lite"],
|
|
@@ -8506,6 +8604,92 @@ function truncateForContext(text, maxChars = 12e3) {
|
|
|
8506
8604
|
${tail}`;
|
|
8507
8605
|
}
|
|
8508
8606
|
|
|
8607
|
+
// src/core/router/provider-errors.ts
|
|
8608
|
+
var SYSTEMIC = /* @__PURE__ */ new Set([
|
|
8609
|
+
"rate_limit",
|
|
8610
|
+
"quota_exhausted",
|
|
8611
|
+
"auth",
|
|
8612
|
+
"model_unavailable"
|
|
8613
|
+
]);
|
|
8614
|
+
function isSystemicKind(kind) {
|
|
8615
|
+
return SYSTEMIC.has(kind);
|
|
8616
|
+
}
|
|
8617
|
+
function statusOf(err) {
|
|
8618
|
+
if (!err || typeof err !== "object") return void 0;
|
|
8619
|
+
const e = err;
|
|
8620
|
+
for (const key of ["status", "statusCode", "code"]) {
|
|
8621
|
+
const v = e[key];
|
|
8622
|
+
if (typeof v === "number" && v >= 100 && v < 600) return v;
|
|
8623
|
+
}
|
|
8624
|
+
const resp = e["response"];
|
|
8625
|
+
if (resp && typeof resp["status"] === "number") return resp["status"];
|
|
8626
|
+
const inner = e["error"];
|
|
8627
|
+
if (inner && typeof inner["code"] === "number") return inner["code"];
|
|
8628
|
+
return void 0;
|
|
8629
|
+
}
|
|
8630
|
+
function messageOf(err) {
|
|
8631
|
+
if (err instanceof Error) return err.message;
|
|
8632
|
+
if (typeof err === "string") return err;
|
|
8633
|
+
if (err && typeof err === "object") {
|
|
8634
|
+
const e = err;
|
|
8635
|
+
if (typeof e["message"] === "string") return e["message"];
|
|
8636
|
+
const inner = e["error"];
|
|
8637
|
+
if (inner && typeof inner["message"] === "string") return inner["message"];
|
|
8638
|
+
try {
|
|
8639
|
+
return JSON.stringify(err);
|
|
8640
|
+
} catch {
|
|
8641
|
+
}
|
|
8642
|
+
}
|
|
8643
|
+
return String(err);
|
|
8644
|
+
}
|
|
8645
|
+
function classifyProviderError(err) {
|
|
8646
|
+
const raw = messageOf(err).trim();
|
|
8647
|
+
const status = statusOf(err);
|
|
8648
|
+
const m = raw.toLowerCase();
|
|
8649
|
+
const kind = (() => {
|
|
8650
|
+
if (status === 429) {
|
|
8651
|
+
return /quota|billing|credit|exceeded your current quota|insufficient/.test(m) ? "quota_exhausted" : "rate_limit";
|
|
8652
|
+
}
|
|
8653
|
+
if (status === 401 || status === 403) return "auth";
|
|
8654
|
+
if (status === 404) return "model_unavailable";
|
|
8655
|
+
if (/rate limit|too many requests|resource_exhausted/.test(m)) {
|
|
8656
|
+
return /quota|billing|credit|insufficient/.test(m) ? "quota_exhausted" : "rate_limit";
|
|
8657
|
+
}
|
|
8658
|
+
if (/quota|billing|insufficient_quota|insufficient credit|payment required/.test(m)) return "quota_exhausted";
|
|
8659
|
+
if (/api key|unauthorized|authentication|permission denied|invalid[_ ]api/.test(m)) return "auth";
|
|
8660
|
+
if (/model .*(not found|does not exist|unavailable)|no such model|unknown model|not supported for/.test(m)) {
|
|
8661
|
+
return "model_unavailable";
|
|
8662
|
+
}
|
|
8663
|
+
if (/context length|too many tokens|maximum context|prompt is too long|input too long/.test(m)) return "context_length";
|
|
8664
|
+
if (/safety|content filter|blocked|recitation|prohibited/.test(m)) return "content_filter";
|
|
8665
|
+
if (/econnreset|etimedout|enotfound|socket hang up|network|fetch failed|aborted/.test(m)) return "network";
|
|
8666
|
+
return "unknown";
|
|
8667
|
+
})();
|
|
8668
|
+
return { kind, systemic: isSystemicKind(kind), status, raw };
|
|
8669
|
+
}
|
|
8670
|
+
function describeProviderError(c, modelId) {
|
|
8671
|
+
const on = modelId ? ` on ${modelId}` : "";
|
|
8672
|
+
const detail = c.raw ? ` Provider said: ${c.raw}` : "";
|
|
8673
|
+
switch (c.kind) {
|
|
8674
|
+
case "rate_limit":
|
|
8675
|
+
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}`;
|
|
8676
|
+
case "quota_exhausted":
|
|
8677
|
+
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}`;
|
|
8678
|
+
case "auth":
|
|
8679
|
+
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}`;
|
|
8680
|
+
case "model_unavailable":
|
|
8681
|
+
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}`;
|
|
8682
|
+
case "context_length":
|
|
8683
|
+
return `The prompt was too long${on}. Reduce the attached context, or use a model with a larger context window for this tier.${detail}`;
|
|
8684
|
+
case "content_filter":
|
|
8685
|
+
return `The provider's safety filter refused this request${on}.${detail}`;
|
|
8686
|
+
case "network":
|
|
8687
|
+
return `Network error reaching the provider${on}. This is usually transient.${detail}`;
|
|
8688
|
+
default:
|
|
8689
|
+
return `The model call failed${on}.${detail}`;
|
|
8690
|
+
}
|
|
8691
|
+
}
|
|
8692
|
+
|
|
8509
8693
|
// src/core/tiers/t3-worker.ts
|
|
8510
8694
|
var CriticalToolError = class extends Error {
|
|
8511
8695
|
constructor(message, toolName) {
|
|
@@ -8539,8 +8723,15 @@ var KNOWN_TOOLS = [
|
|
|
8539
8723
|
"web_search",
|
|
8540
8724
|
"glob",
|
|
8541
8725
|
"grep",
|
|
8542
|
-
"web_fetch"
|
|
8726
|
+
"web_fetch",
|
|
8727
|
+
"generate_image"
|
|
8543
8728
|
];
|
|
8729
|
+
var PROVIDER_BACKED_TOOLS = /* @__PURE__ */ new Set([
|
|
8730
|
+
"generate_image",
|
|
8731
|
+
"generate_speech",
|
|
8732
|
+
"generate_video",
|
|
8733
|
+
"transcribe_audio"
|
|
8734
|
+
]);
|
|
8544
8735
|
function buildWorkerRules(has) {
|
|
8545
8736
|
const canWriteFiles = has("file_write") || has("file_edit") || has("run_code");
|
|
8546
8737
|
const hasAnyTool = KNOWN_TOOLS.some(has);
|
|
@@ -8550,9 +8741,22 @@ function buildWorkerRules(has) {
|
|
|
8550
8741
|
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.",
|
|
8551
8742
|
has("web_search") && '- Use the "web_search" tool to find current information, documentation, news, or general web data.',
|
|
8552
8743
|
has("pdf_create") && '- Use the "pdf_create" tool for PDF requests.',
|
|
8744
|
+
// Without this the model "writes the image" as prose — a bracketed
|
|
8745
|
+
// description sitting in the deck where the picture should be — even with a
|
|
8746
|
+
// working image model registered. Naming the reference syntax matters as
|
|
8747
|
+
// much as naming the tool: a generated image nobody embeds is still a
|
|
8748
|
+
// missing image.
|
|
8749
|
+
//
|
|
8750
|
+
// Scoped to PowerPoint/Word specifically: those are the only exporters
|
|
8751
|
+
// that actually embed a Markdown image reference (cloud/web/exporters.ts
|
|
8752
|
+
// toPptx/toDocx). PDF and plain-text/Markdown deliverables flatten a
|
|
8753
|
+
// reference straight to caption text — telling the model to call
|
|
8754
|
+
// generate_image for THOSE just pays for an image nobody ever sees.
|
|
8755
|
+
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.',
|
|
8553
8756
|
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.',
|
|
8554
8757
|
"- If you are not making meaningful progress, stop and escalate rather than looping or padding the response.",
|
|
8555
8758
|
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.',
|
|
8759
|
+
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.",
|
|
8556
8760
|
"- Return structured output that directly addresses the expected output specification."
|
|
8557
8761
|
];
|
|
8558
8762
|
return `You are a T3 Worker agent in the Cascade AI system. Your job is to execute a specific subtask completely and accurately.
|
|
@@ -9086,7 +9290,9 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
|
|
|
9086
9290
|
const durationMs = Date.now() - toolStartMs;
|
|
9087
9291
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
9088
9292
|
this.emit("tool:result", { id: tc.id, tierId: this.id, toolName: tc.name, error: errMsg, durationMs });
|
|
9089
|
-
|
|
9293
|
+
const isTaggedSystemic = err instanceof Error && err.systemic === true;
|
|
9294
|
+
const isProviderBacked = PROVIDER_BACKED_TOOLS.has(tc.name);
|
|
9295
|
+
if (isTaggedSystemic || isProviderBacked && classifyProviderError(err).systemic) {
|
|
9090
9296
|
throw new CriticalToolError(errMsg, tc.name);
|
|
9091
9297
|
}
|
|
9092
9298
|
return await this.adaptiveFallback(tc, `Tool error: ${errMsg}`);
|
|
@@ -9374,8 +9580,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
9374
9580
|
} catch {
|
|
9375
9581
|
return {
|
|
9376
9582
|
checksRun: ["completeness", "correctness", "compliance"],
|
|
9377
|
-
passed: [
|
|
9378
|
-
failed: []
|
|
9583
|
+
passed: [],
|
|
9584
|
+
failed: ["completeness"]
|
|
9379
9585
|
};
|
|
9380
9586
|
}
|
|
9381
9587
|
}
|
|
@@ -11451,17 +11657,17 @@ var CAPABILITIES = [
|
|
|
11451
11657
|
api: "openai-images"
|
|
11452
11658
|
},
|
|
11453
11659
|
{
|
|
11454
|
-
//
|
|
11455
|
-
//
|
|
11456
|
-
//
|
|
11457
|
-
// generateContent
|
|
11458
|
-
//
|
|
11459
|
-
//
|
|
11460
|
-
//
|
|
11660
|
+
// Was `imagen-4.0-generate-001` on `:predict`, which now 404s for new keys
|
|
11661
|
+
// ("no longer available to new users") — Imagen's :predict API is deprecated
|
|
11662
|
+
// and shuts down 2026-08-17. Google's own migration path is this model on
|
|
11663
|
+
// the ordinary generateContent endpoint, so the fix was never another Imagen
|
|
11664
|
+
// id: the whole request/response SHAPE changes, which is why `api` moved to
|
|
11665
|
+
// 'gemini-generate-content' rather than just the modelId moving.
|
|
11666
|
+
// https://ai.google.dev/gemini-api/docs/imagen
|
|
11461
11667
|
modality: "image",
|
|
11462
11668
|
provider: "gemini",
|
|
11463
|
-
modelId: "
|
|
11464
|
-
api: "gemini-
|
|
11669
|
+
modelId: "gemini-2.5-flash-image",
|
|
11670
|
+
api: "gemini-generate-content"
|
|
11465
11671
|
},
|
|
11466
11672
|
// ── Speech (text → audio) ──
|
|
11467
11673
|
{
|
|
@@ -11493,9 +11699,17 @@ var CAPABILITIES = [
|
|
|
11493
11699
|
// Long-running: submit returns an operation name, the video lands minutes
|
|
11494
11700
|
// later, and the bytes come from a short-lived signed URL. generate.ts owns
|
|
11495
11701
|
// that polling loop; the registry only records the shape via `api`.
|
|
11702
|
+
//
|
|
11703
|
+
// `veo-3.1-generate-001` (no "-preview") is VERTEX AI's id for this model,
|
|
11704
|
+
// not the Gemini Developer API's — this file calls
|
|
11705
|
+
// generativelanguage.googleapis.com (see generate.ts), which 404s on it:
|
|
11706
|
+
// "models/veo-3.1-generate-001 is not found for API version v1beta". On the
|
|
11707
|
+
// Gemini API specifically, Veo 3.1 is still preview-only, so the correct id
|
|
11708
|
+
// there is `veo-3.1-generate-preview`.
|
|
11709
|
+
// https://ai.google.dev/gemini-api/docs/veo
|
|
11496
11710
|
modality: "video",
|
|
11497
11711
|
provider: "gemini",
|
|
11498
|
-
modelId: "veo-3.1-generate-
|
|
11712
|
+
modelId: "veo-3.1-generate-preview",
|
|
11499
11713
|
api: "gemini-predict-lro"
|
|
11500
11714
|
}
|
|
11501
11715
|
];
|
|
@@ -11516,6 +11730,14 @@ function capabilityCost(cap) {
|
|
|
11516
11730
|
const shown = amount < 1e-3 ? amount.toExponential(2) : amount.toFixed(4);
|
|
11517
11731
|
return { label: `$${shown} ${unit}`, amount, unit };
|
|
11518
11732
|
}
|
|
11733
|
+
function explainChoice(c, cost, modality, isBest) {
|
|
11734
|
+
const priced = cost ? ` ${cost.label}.` : "";
|
|
11735
|
+
if (c.quality !== void 0) {
|
|
11736
|
+
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}`;
|
|
11737
|
+
}
|
|
11738
|
+
const noRanking = ". No published quality ranking is applied for this modality, so this is a cost choice, not a quality one.";
|
|
11739
|
+
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;
|
|
11740
|
+
}
|
|
11519
11741
|
var MultimodalRegistry = class {
|
|
11520
11742
|
/** Provider types the user has actually configured a key for. */
|
|
11521
11743
|
available;
|
|
@@ -11533,22 +11755,20 @@ var MultimodalRegistry = class {
|
|
|
11533
11755
|
return [...new Set(this.usable().map((c) => c.modality))];
|
|
11534
11756
|
}
|
|
11535
11757
|
/**
|
|
11536
|
-
*
|
|
11758
|
+
* Every usable capability for a modality, best first.
|
|
11537
11759
|
*
|
|
11538
11760
|
* Ranked models beat unranked ones, then higher quality, then lower unit
|
|
11539
|
-
* price. An explicit `preferModel`
|
|
11540
|
-
* naming a model is a stronger signal than any prior.
|
|
11761
|
+
* price. An explicit `preferModel` is moved to the head when it is usable —
|
|
11762
|
+
* the user naming a model is a stronger signal than any prior.
|
|
11763
|
+
*
|
|
11764
|
+
* `select()` is just the head of this list. The TAIL is the point: a systemic
|
|
11765
|
+
* provider failure (dead key, 404, exhausted quota) is scoped to one
|
|
11766
|
+
* provider+model, so "this model cannot draw" is not the same fact as "this
|
|
11767
|
+
* account cannot draw". A caller holding the ordered alternatives can try the
|
|
11768
|
+
* next provider instead of aborting the whole run — see generate-media.ts.
|
|
11541
11769
|
*/
|
|
11542
|
-
|
|
11543
|
-
const
|
|
11544
|
-
if (!pool.length) return null;
|
|
11545
|
-
if (opts.preferModel) {
|
|
11546
|
-
const wanted = pool.find((c) => c.modelId === opts.preferModel);
|
|
11547
|
-
if (wanted) {
|
|
11548
|
-
return { capability: wanted, cost: capabilityCost(wanted), reason: `Requested explicitly: ${wanted.modelId}.` };
|
|
11549
|
-
}
|
|
11550
|
-
}
|
|
11551
|
-
const scored = pool.map((c) => ({ c, cost: capabilityCost(c) })).sort((a, b) => {
|
|
11770
|
+
rank(modality, opts = {}) {
|
|
11771
|
+
const scored = this.usable(modality).map((c) => ({ c, cost: capabilityCost(c) })).sort((a, b) => {
|
|
11552
11772
|
const qa = a.c.quality ?? -1;
|
|
11553
11773
|
const qb = b.c.quality ?? -1;
|
|
11554
11774
|
if (qa !== qb) return qb - qa;
|
|
@@ -11556,10 +11776,25 @@ var MultimodalRegistry = class {
|
|
|
11556
11776
|
const pb = b.cost?.amount ?? Number.POSITIVE_INFINITY;
|
|
11557
11777
|
return pa - pb;
|
|
11558
11778
|
});
|
|
11559
|
-
const
|
|
11560
|
-
|
|
11561
|
-
|
|
11562
|
-
|
|
11779
|
+
const results = scored.map(({ c, cost }, i) => ({
|
|
11780
|
+
capability: c,
|
|
11781
|
+
cost,
|
|
11782
|
+
reason: explainChoice(c, cost, modality, i === 0)
|
|
11783
|
+
}));
|
|
11784
|
+
if (opts.preferModel) {
|
|
11785
|
+
const i = results.findIndex((r) => r.capability.modelId === opts.preferModel);
|
|
11786
|
+
if (i > -1) {
|
|
11787
|
+
const [wanted] = results.splice(i, 1);
|
|
11788
|
+
results.unshift({ ...wanted, reason: `Requested explicitly: ${wanted.capability.modelId}.` });
|
|
11789
|
+
}
|
|
11790
|
+
}
|
|
11791
|
+
return results;
|
|
11792
|
+
}
|
|
11793
|
+
/**
|
|
11794
|
+
* Pick a model for a modality — the best usable one, or null if there is none.
|
|
11795
|
+
*/
|
|
11796
|
+
select(modality, opts = {}) {
|
|
11797
|
+
return this.rank(modality, opts)[0] ?? null;
|
|
11563
11798
|
}
|
|
11564
11799
|
/**
|
|
11565
11800
|
* A plain-language account of what this account can and cannot generate —
|
|
@@ -11612,92 +11847,6 @@ var BaseTool = class {
|
|
|
11612
11847
|
}
|
|
11613
11848
|
};
|
|
11614
11849
|
|
|
11615
|
-
// src/core/router/provider-errors.ts
|
|
11616
|
-
var SYSTEMIC = /* @__PURE__ */ new Set([
|
|
11617
|
-
"rate_limit",
|
|
11618
|
-
"quota_exhausted",
|
|
11619
|
-
"auth",
|
|
11620
|
-
"model_unavailable"
|
|
11621
|
-
]);
|
|
11622
|
-
function isSystemicKind(kind) {
|
|
11623
|
-
return SYSTEMIC.has(kind);
|
|
11624
|
-
}
|
|
11625
|
-
function statusOf(err) {
|
|
11626
|
-
if (!err || typeof err !== "object") return void 0;
|
|
11627
|
-
const e = err;
|
|
11628
|
-
for (const key of ["status", "statusCode", "code"]) {
|
|
11629
|
-
const v = e[key];
|
|
11630
|
-
if (typeof v === "number" && v >= 100 && v < 600) return v;
|
|
11631
|
-
}
|
|
11632
|
-
const resp = e["response"];
|
|
11633
|
-
if (resp && typeof resp["status"] === "number") return resp["status"];
|
|
11634
|
-
const inner = e["error"];
|
|
11635
|
-
if (inner && typeof inner["code"] === "number") return inner["code"];
|
|
11636
|
-
return void 0;
|
|
11637
|
-
}
|
|
11638
|
-
function messageOf(err) {
|
|
11639
|
-
if (err instanceof Error) return err.message;
|
|
11640
|
-
if (typeof err === "string") return err;
|
|
11641
|
-
if (err && typeof err === "object") {
|
|
11642
|
-
const e = err;
|
|
11643
|
-
if (typeof e["message"] === "string") return e["message"];
|
|
11644
|
-
const inner = e["error"];
|
|
11645
|
-
if (inner && typeof inner["message"] === "string") return inner["message"];
|
|
11646
|
-
try {
|
|
11647
|
-
return JSON.stringify(err);
|
|
11648
|
-
} catch {
|
|
11649
|
-
}
|
|
11650
|
-
}
|
|
11651
|
-
return String(err);
|
|
11652
|
-
}
|
|
11653
|
-
function classifyProviderError(err) {
|
|
11654
|
-
const raw = messageOf(err).trim();
|
|
11655
|
-
const status = statusOf(err);
|
|
11656
|
-
const m = raw.toLowerCase();
|
|
11657
|
-
const kind = (() => {
|
|
11658
|
-
if (status === 429) {
|
|
11659
|
-
return /quota|billing|credit|exceeded your current quota|insufficient/.test(m) ? "quota_exhausted" : "rate_limit";
|
|
11660
|
-
}
|
|
11661
|
-
if (status === 401 || status === 403) return "auth";
|
|
11662
|
-
if (status === 404) return "model_unavailable";
|
|
11663
|
-
if (/rate limit|too many requests|resource_exhausted/.test(m)) {
|
|
11664
|
-
return /quota|billing|credit|insufficient/.test(m) ? "quota_exhausted" : "rate_limit";
|
|
11665
|
-
}
|
|
11666
|
-
if (/quota|billing|insufficient_quota|insufficient credit|payment required/.test(m)) return "quota_exhausted";
|
|
11667
|
-
if (/api key|unauthorized|authentication|permission denied|invalid[_ ]api/.test(m)) return "auth";
|
|
11668
|
-
if (/model .*(not found|does not exist|unavailable)|no such model|unknown model|not supported for/.test(m)) {
|
|
11669
|
-
return "model_unavailable";
|
|
11670
|
-
}
|
|
11671
|
-
if (/context length|too many tokens|maximum context|prompt is too long|input too long/.test(m)) return "context_length";
|
|
11672
|
-
if (/safety|content filter|blocked|recitation|prohibited/.test(m)) return "content_filter";
|
|
11673
|
-
if (/econnreset|etimedout|enotfound|socket hang up|network|fetch failed|aborted/.test(m)) return "network";
|
|
11674
|
-
return "unknown";
|
|
11675
|
-
})();
|
|
11676
|
-
return { kind, systemic: isSystemicKind(kind), status, raw };
|
|
11677
|
-
}
|
|
11678
|
-
function describeProviderError(c, modelId) {
|
|
11679
|
-
const on = modelId ? ` on ${modelId}` : "";
|
|
11680
|
-
const detail = c.raw ? ` Provider said: ${c.raw}` : "";
|
|
11681
|
-
switch (c.kind) {
|
|
11682
|
-
case "rate_limit":
|
|
11683
|
-
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}`;
|
|
11684
|
-
case "quota_exhausted":
|
|
11685
|
-
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}`;
|
|
11686
|
-
case "auth":
|
|
11687
|
-
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}`;
|
|
11688
|
-
case "model_unavailable":
|
|
11689
|
-
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}`;
|
|
11690
|
-
case "context_length":
|
|
11691
|
-
return `The prompt was too long${on}. Reduce the attached context, or use a model with a larger context window for this tier.${detail}`;
|
|
11692
|
-
case "content_filter":
|
|
11693
|
-
return `The provider's safety filter refused this request${on}.${detail}`;
|
|
11694
|
-
case "network":
|
|
11695
|
-
return `Network error reaching the provider${on}. This is usually transient.${detail}`;
|
|
11696
|
-
default:
|
|
11697
|
-
return `The model call failed${on}.${detail}`;
|
|
11698
|
-
}
|
|
11699
|
-
}
|
|
11700
|
-
|
|
11701
11850
|
// src/core/multimodal/generate.ts
|
|
11702
11851
|
var DEFAULT_TIMEOUT_MS = 12e4;
|
|
11703
11852
|
async function callProvider(fn, modelId) {
|
|
@@ -11736,6 +11885,29 @@ function baseUrlFor(cfg, fallback) {
|
|
|
11736
11885
|
function stamp(ext) {
|
|
11737
11886
|
return `cascade-${Date.now()}.${ext}`;
|
|
11738
11887
|
}
|
|
11888
|
+
var GEMINI_ASPECT_RATIOS = [
|
|
11889
|
+
["1:1", 1 / 1],
|
|
11890
|
+
["3:2", 3 / 2],
|
|
11891
|
+
["2:3", 2 / 3],
|
|
11892
|
+
["3:4", 3 / 4],
|
|
11893
|
+
["4:3", 4 / 3],
|
|
11894
|
+
["4:5", 4 / 5],
|
|
11895
|
+
["5:4", 5 / 4],
|
|
11896
|
+
["9:16", 9 / 16],
|
|
11897
|
+
["16:9", 16 / 9],
|
|
11898
|
+
["21:9", 21 / 9]
|
|
11899
|
+
];
|
|
11900
|
+
function closestGeminiAspectRatio(size) {
|
|
11901
|
+
const m = size.match(/^(\d+)\s*[x×]\s*(\d+)$/i);
|
|
11902
|
+
if (!m) return void 0;
|
|
11903
|
+
const w = Number(m[1]);
|
|
11904
|
+
const h = Number(m[2]);
|
|
11905
|
+
if (!w || !h) return void 0;
|
|
11906
|
+
const ratio = w / h;
|
|
11907
|
+
return GEMINI_ASPECT_RATIOS.reduce(
|
|
11908
|
+
(best, cand) => Math.abs(cand[1] - ratio) < Math.abs(best[1] - ratio) ? cand : best
|
|
11909
|
+
)[0];
|
|
11910
|
+
}
|
|
11739
11911
|
async function generateImage(cap, cfg, req, signal) {
|
|
11740
11912
|
return callProvider(async () => {
|
|
11741
11913
|
if (cap.api === "openai-images") {
|
|
@@ -11760,19 +11932,37 @@ async function generateImage(cap, cfg, req, signal) {
|
|
|
11760
11932
|
...first.revised_prompt ? { revisedPrompt: first.revised_prompt } : {}
|
|
11761
11933
|
};
|
|
11762
11934
|
}
|
|
11763
|
-
if (cap.api === "gemini-
|
|
11935
|
+
if (cap.api === "gemini-generate-content") {
|
|
11764
11936
|
const base = baseUrlFor(cfg, "https://generativelanguage.googleapis.com/v1beta");
|
|
11765
|
-
const url = `${base}/models/${encodeURIComponent(cap.modelId)}:
|
|
11937
|
+
const url = `${base}/models/${encodeURIComponent(cap.modelId)}:generateContent`;
|
|
11938
|
+
const aspectRatio = req.size ? closestGeminiAspectRatio(req.size) : void 0;
|
|
11766
11939
|
const res = await postJson(url, {
|
|
11767
|
-
|
|
11768
|
-
|
|
11940
|
+
contents: [{ parts: [{ text: req.prompt }] }],
|
|
11941
|
+
generationConfig: {
|
|
11942
|
+
// Asked for explicitly: these models can answer with text only, and a
|
|
11943
|
+
// text-only reply to "draw me a cat" is a failed image generation.
|
|
11944
|
+
responseModalities: ["IMAGE", "TEXT"],
|
|
11945
|
+
// Gemini has no free-form pixel size — only a named aspect ratio (see
|
|
11946
|
+
// closestGeminiAspectRatio). Omitted entirely when there's no
|
|
11947
|
+
// requested size, or no supported ratio close enough to bother with.
|
|
11948
|
+
...aspectRatio ? { imageConfig: { aspectRatio } } : {}
|
|
11949
|
+
}
|
|
11769
11950
|
}, { "x-goog-api-key": cfg.apiKey ?? "" }, signal);
|
|
11770
11951
|
const body = await res.json();
|
|
11771
|
-
const
|
|
11772
|
-
|
|
11773
|
-
const
|
|
11952
|
+
const candidate = body.candidates?.[0];
|
|
11953
|
+
const parts = candidate?.content?.parts ?? [];
|
|
11954
|
+
const image = parts.find((p) => p.inlineData?.data)?.inlineData;
|
|
11955
|
+
if (!image?.data) {
|
|
11956
|
+
const blocked = body.promptFeedback?.blockReason ?? (candidate?.finishReason && candidate.finishReason !== "STOP" ? candidate.finishReason : void 0);
|
|
11957
|
+
if (blocked) throw new Error(`Image request was blocked by the provider (${blocked}).`);
|
|
11958
|
+
const said = parts.find((p) => p.text)?.text?.trim();
|
|
11959
|
+
throw new Error(
|
|
11960
|
+
"Image response contained no image data." + (said ? ` The model replied with text instead: ${said}` : "")
|
|
11961
|
+
);
|
|
11962
|
+
}
|
|
11963
|
+
const mimeType = image.mimeType ?? "image/png";
|
|
11774
11964
|
return {
|
|
11775
|
-
data: Buffer.from(
|
|
11965
|
+
data: Buffer.from(image.data, "base64"),
|
|
11776
11966
|
mimeType,
|
|
11777
11967
|
filename: stamp(mimeType.includes("jpeg") ? "jpg" : "png"),
|
|
11778
11968
|
modelId: cap.modelId,
|
|
@@ -11925,6 +12115,49 @@ function resolve(deps, modality, preferModel) {
|
|
|
11925
12115
|
}
|
|
11926
12116
|
return { ok: true, selected, cfg };
|
|
11927
12117
|
}
|
|
12118
|
+
function messageOf2(err) {
|
|
12119
|
+
return err instanceof Error ? err.message : String(err);
|
|
12120
|
+
}
|
|
12121
|
+
async function runWithProviderFallback(deps, modality, first, run, signal) {
|
|
12122
|
+
const failed = first.selected.capability;
|
|
12123
|
+
try {
|
|
12124
|
+
return { value: await run(failed, first.cfg), selected: first.selected };
|
|
12125
|
+
} catch (err) {
|
|
12126
|
+
if (signal?.aborted || !classifyProviderError(err).systemic) throw err;
|
|
12127
|
+
const tried = [`${failed.modelId} (${failed.provider}): ${messageOf2(err)}`];
|
|
12128
|
+
let allSystemic = true;
|
|
12129
|
+
let lastNonSystemic;
|
|
12130
|
+
const seen = /* @__PURE__ */ new Set([failed.provider]);
|
|
12131
|
+
for (const alt of deps.registry.rank(modality)) {
|
|
12132
|
+
if (seen.has(alt.capability.provider)) continue;
|
|
12133
|
+
const cfg = deps.lookupProvider(alt.capability.provider);
|
|
12134
|
+
if (!cfg) continue;
|
|
12135
|
+
seen.add(alt.capability.provider);
|
|
12136
|
+
try {
|
|
12137
|
+
return {
|
|
12138
|
+
value: await run(alt.capability, cfg),
|
|
12139
|
+
selected: alt,
|
|
12140
|
+
fellBackFrom: { capability: failed, error: messageOf2(err) }
|
|
12141
|
+
};
|
|
12142
|
+
} catch (retryErr) {
|
|
12143
|
+
if (signal?.aborted) throw retryErr;
|
|
12144
|
+
tried.push(`${alt.capability.modelId} (${alt.capability.provider}): ${messageOf2(retryErr)}`);
|
|
12145
|
+
if (classifyProviderError(retryErr).systemic) {
|
|
12146
|
+
continue;
|
|
12147
|
+
}
|
|
12148
|
+
allSystemic = false;
|
|
12149
|
+
lastNonSystemic = retryErr;
|
|
12150
|
+
}
|
|
12151
|
+
}
|
|
12152
|
+
if (tried.length === 1) throw err;
|
|
12153
|
+
if (!allSystemic) throw lastNonSystemic;
|
|
12154
|
+
const exhausted = new Error(
|
|
12155
|
+
`${modality} generation failed on every configured provider (${seen.size} tried). ${tried.join(" ")}`
|
|
12156
|
+
);
|
|
12157
|
+
Object.assign(exhausted, { systemic: true });
|
|
12158
|
+
throw exhausted;
|
|
12159
|
+
}
|
|
12160
|
+
}
|
|
11928
12161
|
var GenerateImageTool = class extends BaseTool {
|
|
11929
12162
|
constructor(deps) {
|
|
11930
12163
|
super();
|
|
@@ -11948,16 +12181,32 @@ var GenerateImageTool = class extends BaseTool {
|
|
|
11948
12181
|
const r = resolve(this.deps, "image", input["model"] ? String(input["model"]) : void 0);
|
|
11949
12182
|
if (!r.ok) return r.error;
|
|
11950
12183
|
const size = input["size"] ? String(input["size"]) : void 0;
|
|
11951
|
-
const
|
|
11952
|
-
|
|
11953
|
-
|
|
11954
|
-
|
|
12184
|
+
const attempt = await runWithProviderFallback(
|
|
12185
|
+
this.deps,
|
|
12186
|
+
"image",
|
|
12187
|
+
r,
|
|
12188
|
+
(cap, cfg) => generateImage(cap, cfg, { prompt, ...size ? { size } : {} }, options.signal),
|
|
11955
12189
|
options.signal
|
|
11956
12190
|
);
|
|
12191
|
+
const asset = attempt.value;
|
|
12192
|
+
const fell = attempt.fellBackFrom;
|
|
11957
12193
|
const location = await this.deps.sink(asset);
|
|
11958
12194
|
return [
|
|
11959
12195
|
`Image generated and saved to ${location}.`,
|
|
11960
|
-
|
|
12196
|
+
// Knowing WHERE the image is isn't the same as knowing how to use it: a
|
|
12197
|
+
// model told only "saved to X" tends to describe the picture in prose
|
|
12198
|
+
// instead of embedding it. Spelling out the reference syntax here makes
|
|
12199
|
+
// the worker's "never write a placeholder" rule concretely actionable,
|
|
12200
|
+
// and it works for every host because `location` is whatever that host's
|
|
12201
|
+
// sink returned — a workspace path on desktop/CLI, a URL in the cloud.
|
|
12202
|
+
`To include this image in your output, reference it as: `,
|
|
12203
|
+
// The reported model has to be the one that actually drew the picture, not
|
|
12204
|
+
// the one that was picked and then failed.
|
|
12205
|
+
`Model: ${asset.modelId} \u2014 ${fell ? `used as a fallback after ${fell.capability.modelId} (${fell.capability.provider}) failed with a systemic error.` : attempt.selected.reason}`,
|
|
12206
|
+
// Worth one line: the run succeeded, so nothing else will report that a
|
|
12207
|
+
// configured provider is broken, and "your Gemini key is dead" is exactly
|
|
12208
|
+
// the kind of thing the user wants to hear once rather than never.
|
|
12209
|
+
fell ? `Note: ${fell.capability.provider} failed first \u2014 ${fell.error}` : "",
|
|
11961
12210
|
// DALL·E rewrites the prompt before drawing; the model needs to know what
|
|
11962
12211
|
// was actually rendered, not what it asked for, or its description of the
|
|
11963
12212
|
// result will not match the image.
|
|
@@ -13200,9 +13449,11 @@ var WebSearchTool = class extends BaseTool {
|
|
|
13200
13449
|
);
|
|
13201
13450
|
const errors = [];
|
|
13202
13451
|
let results = [];
|
|
13452
|
+
let anyBackendReachable = false;
|
|
13203
13453
|
if (this.config.searxngUrl) {
|
|
13204
13454
|
try {
|
|
13205
13455
|
results = await searchSearXNG(query, this.config.searxngUrl, maxResults);
|
|
13456
|
+
anyBackendReachable = true;
|
|
13206
13457
|
if (results.length > 0) return this.formatResults(query, results);
|
|
13207
13458
|
errors.push("SearXNG: returned 0 results");
|
|
13208
13459
|
} catch (err) {
|
|
@@ -13212,6 +13463,7 @@ var WebSearchTool = class extends BaseTool {
|
|
|
13212
13463
|
if (this.config.braveApiKey) {
|
|
13213
13464
|
try {
|
|
13214
13465
|
results = await searchBrave(query, this.config.braveApiKey, maxResults);
|
|
13466
|
+
anyBackendReachable = true;
|
|
13215
13467
|
if (results.length > 0) return this.formatResults(query, results);
|
|
13216
13468
|
errors.push("Brave: returned 0 results");
|
|
13217
13469
|
} catch (err) {
|
|
@@ -13221,6 +13473,7 @@ var WebSearchTool = class extends BaseTool {
|
|
|
13221
13473
|
if (this.config.tavilyApiKey) {
|
|
13222
13474
|
try {
|
|
13223
13475
|
results = await searchTavily(query, this.config.tavilyApiKey, maxResults);
|
|
13476
|
+
anyBackendReachable = true;
|
|
13224
13477
|
if (results.length > 0) return this.formatResults(query, results);
|
|
13225
13478
|
errors.push("Tavily: returned 0 results");
|
|
13226
13479
|
} catch (err) {
|
|
@@ -13230,6 +13483,7 @@ var WebSearchTool = class extends BaseTool {
|
|
|
13230
13483
|
for (const variant of ["html", "lite"]) {
|
|
13231
13484
|
try {
|
|
13232
13485
|
results = await searchDuckDuckGo(query, maxResults, variant);
|
|
13486
|
+
anyBackendReachable = true;
|
|
13233
13487
|
if (results.length > 0) return this.formatResults(query, results);
|
|
13234
13488
|
errors.push(`DuckDuckGo ${variant}: returned 0 results`);
|
|
13235
13489
|
} catch (err) {
|
|
@@ -13237,11 +13491,11 @@ var WebSearchTool = class extends BaseTool {
|
|
|
13237
13491
|
}
|
|
13238
13492
|
}
|
|
13239
13493
|
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" : "";
|
|
13240
|
-
|
|
13241
|
-
`Web search for "${query}"
|
|
13494
|
+
throw Object.assign(new Error([
|
|
13495
|
+
`Web search for "${query}" found nothing across all backends:`,
|
|
13242
13496
|
...errors.map((e) => ` \u2022 ${e}`),
|
|
13243
13497
|
configHint
|
|
13244
|
-
].join("\n");
|
|
13498
|
+
].join("\n")), { systemic: !anyBackendReachable });
|
|
13245
13499
|
}
|
|
13246
13500
|
formatResults(query, results) {
|
|
13247
13501
|
const lines = [`Web search results for: "${query}"`, ""];
|
|
@@ -13609,6 +13863,15 @@ var McpToolWrapper = class extends BaseTool {
|
|
|
13609
13863
|
this.description = `[MCP:${serverName}] ${description}`;
|
|
13610
13864
|
this.inputSchema = inputSchema;
|
|
13611
13865
|
}
|
|
13866
|
+
// Every built-in tool of comparable risk (shell, git, file writes, the
|
|
13867
|
+
// built-in github tool) overrides this to true. An MCP tool never did —
|
|
13868
|
+
// BaseTool's default (false) meant a connected server's create/delete/push
|
|
13869
|
+
// actions ran with zero human approval, whatever they did. MCP carries no
|
|
13870
|
+
// standard "this mutates state" flag, so default to dangerous and name the
|
|
13871
|
+
// exception (a read-only-looking verb) rather than the other way around.
|
|
13872
|
+
isDangerous() {
|
|
13873
|
+
return !isReadOnlyMcpToolName(this.toolName);
|
|
13874
|
+
}
|
|
13612
13875
|
async execute(input, _options) {
|
|
13613
13876
|
return this.mcpClient.callTool(this.serverName, this.toolName, input);
|
|
13614
13877
|
}
|
|
@@ -13721,7 +13984,7 @@ var ToolRegistry = class extends EventEmitter {
|
|
|
13721
13984
|
requiresApproval(toolName) {
|
|
13722
13985
|
const defaults = DEFAULT_APPROVAL_REQUIRED;
|
|
13723
13986
|
const configured = this.config.requireApprovalFor;
|
|
13724
|
-
return defaults.includes(toolName) || configured.includes(toolName);
|
|
13987
|
+
return defaults.includes(toolName) || configured.includes(toolName) || this.isDangerous(toolName);
|
|
13725
13988
|
}
|
|
13726
13989
|
isDangerous(toolName) {
|
|
13727
13990
|
return this.tools.get(toolName)?.isDangerous() ?? false;
|
|
@@ -16631,9 +16894,8 @@ var Cascade = class _Cascade extends EventEmitter {
|
|
|
16631
16894
|
});
|
|
16632
16895
|
this.toolRegistry = new ToolRegistry(this.config.tools, workspacePath);
|
|
16633
16896
|
try {
|
|
16634
|
-
const nodePath = __require("path");
|
|
16635
16897
|
this.router.setDeadModelStore(new DeadModelStore(
|
|
16636
|
-
fileDeadModelPersistence(
|
|
16898
|
+
fileDeadModelPersistence(path32.join(workspacePath, ".cascade", "dead-models.json"))
|
|
16637
16899
|
));
|
|
16638
16900
|
} catch {
|
|
16639
16901
|
}
|