cascade-ai 0.72.0 → 0.74.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -64564,7 +64564,7 @@ var init_dist2 = __esm({
64564
64564
  }
64565
64565
  this._maxListeners = this._maxListeners || void 0;
64566
64566
  };
64567
- EventEmitter9.prototype.setMaxListeners = function setMaxListeners2(n8) {
64567
+ EventEmitter9.prototype.setMaxListeners = function setMaxListeners4(n8) {
64568
64568
  if (typeof n8 !== "number" || n8 < 0 || NumberIsNaN(n8)) throw new RangeError('The value of "n" is out of range. It must be a non-negative number. Received ' + n8 + ".");
64569
64569
  this._maxListeners = n8;
64570
64570
  return this;
@@ -291665,6 +291665,15 @@ function findPricing(modelId, provider, opts = {}) {
291665
291665
  }
291666
291666
  return null;
291667
291667
  }
291668
+ function hasContextBands(model) {
291669
+ if (model.isLocal) return false;
291670
+ const fallbackProviders = model.provider === "azure" ? ["openai"] : model.provider === "gemini" ? ["vertex"] : [];
291671
+ const match = findPricing(model.id, model.provider, {
291672
+ baseModelId: model.baseModelId,
291673
+ fallbackProviders
291674
+ });
291675
+ return (match?.entry.tiers?.length ?? 0) > 1;
291676
+ }
291668
291677
  function tierFor(entry, inputTokens = 0) {
291669
291678
  const tiers = entry.tiers;
291670
291679
  if (!tiers?.length) return null;
@@ -291781,7 +291790,7 @@ function reconcilePrice(model, live) {
291781
291790
  }
291782
291791
 
291783
291792
  // src/constants.ts
291784
- var CASCADE_VERSION = "0.72.0";
291793
+ var CASCADE_VERSION = "0.74.0";
291785
291794
  var CASCADE_CONFIG_DIR = ".cascade";
291786
291795
  var CASCADE_MD_FILE = "CASCADE.md";
291787
291796
  var CASCADE_IGNORE_FILE = ".cascadeignore";
@@ -292226,8 +292235,23 @@ var PROVIDER_DISPLAY_NAMES = {
292226
292235
  };
292227
292236
 
292228
292237
  // src/utils/cost.ts
292229
- function resolveModelPricing(model) {
292238
+ function resolveModelPricing(model, opts = {}) {
292230
292239
  if (model.isLocal) return { input: 0, output: 0, unknown: false };
292240
+ if (opts.inputTokens != null && hasContextBands(model)) {
292241
+ const banded = resolvePricing(model, { inputTokens: opts.inputTokens });
292242
+ const base = resolvePricing(model, { inputTokens: 0 });
292243
+ if (!banded.unknown && !base.unknown) {
292244
+ const scale = (stamped, baseRate) => stamped > 0 && baseRate > 0 ? stamped / baseRate : 1;
292245
+ const inScale = scale(model.inputCostPer1kTokens, base.input);
292246
+ const outScale = scale(model.outputCostPer1kTokens, base.output);
292247
+ return {
292248
+ input: banded.input * inScale,
292249
+ output: banded.output * outScale,
292250
+ unknown: false,
292251
+ ...banded.estimatedFromProvider ? { estimatedFromProvider: banded.estimatedFromProvider } : {}
292252
+ };
292253
+ }
292254
+ }
292231
292255
  if (model.inputCostPer1kTokens > 0 || model.outputCostPer1kTokens > 0) {
292232
292256
  return {
292233
292257
  input: model.inputCostPer1kTokens,
@@ -292235,7 +292259,7 @@ function resolveModelPricing(model) {
292235
292259
  unknown: false
292236
292260
  };
292237
292261
  }
292238
- const fromDataset = resolvePricing(model);
292262
+ const fromDataset = resolvePricing(model, { inputTokens: opts.inputTokens });
292239
292263
  if (!fromDataset.unknown) {
292240
292264
  return {
292241
292265
  input: fromDataset.input,
@@ -292257,11 +292281,11 @@ function resolveModelPricing(model) {
292257
292281
  return { input: 0, output: 0, unknown: true };
292258
292282
  }
292259
292283
  function calculateCost(inputTokens, outputTokens, model) {
292260
- const { input, output } = resolveModelPricing(model);
292284
+ const { input, output } = resolveModelPricing(model, { inputTokens });
292261
292285
  return inputTokens / 1e3 * input + outputTokens / 1e3 * output;
292262
292286
  }
292263
292287
  function buildTokenUsage(inputTokens, outputTokens, model) {
292264
- const { input, output, unknown: unknown2 } = resolveModelPricing(model);
292288
+ const { input, output, unknown: unknown2 } = resolveModelPricing(model, { inputTokens });
292265
292289
  return {
292266
292290
  inputTokens,
292267
292291
  outputTokens,
@@ -292272,6 +292296,13 @@ function buildTokenUsage(inputTokens, outputTokens, model) {
292272
292296
  }
292273
292297
 
292274
292298
  // src/providers/base.ts
292299
+ var ProviderUnreachableError = class extends Error {
292300
+ transient = true;
292301
+ constructor(message) {
292302
+ super(message);
292303
+ this.name = "ProviderUnreachableError";
292304
+ }
292305
+ };
292275
292306
  var BaseProvider = class {
292276
292307
  config;
292277
292308
  model;
@@ -292340,6 +292371,21 @@ function anthropicThinkingParam(modelId, maxTokens) {
292340
292371
  if (budget < 1024) return {};
292341
292372
  return { thinking: { type: "enabled", budget_tokens: budget } };
292342
292373
  }
292374
+ function toAnthropicTools(tools) {
292375
+ return tools.map((t4) => ({
292376
+ name: t4.name,
292377
+ description: t4.description,
292378
+ input_schema: t4.inputSchema
292379
+ }));
292380
+ }
292381
+ function toAnthropicToolUse(toolCalls) {
292382
+ return toolCalls.map((tc) => ({
292383
+ type: "tool_use",
292384
+ id: tc.id,
292385
+ name: tc.name,
292386
+ input: tc.input
292387
+ }));
292388
+ }
292343
292389
  var AnthropicProvider = class extends BaseProvider {
292344
292390
  client;
292345
292391
  constructor(config2, model) {
@@ -292361,11 +292407,7 @@ var AnthropicProvider = class extends BaseProvider {
292361
292407
  }
292362
292408
  async generateStream(options, onChunk) {
292363
292409
  const messages = this.convertMessages(options.messages);
292364
- const tools = options.tools?.map((t4) => ({
292365
- name: t4.name,
292366
- description: t4.description,
292367
- input_schema: t4.inputSchema
292368
- }));
292410
+ const tools = options.tools && toAnthropicTools(options.tools);
292369
292411
  let fullContent = "";
292370
292412
  let inputTokens = 0;
292371
292413
  let outputTokens = 0;
@@ -292493,14 +292535,7 @@ var AnthropicProvider = class extends BaseProvider {
292493
292535
  const content = [];
292494
292536
  const text = typeof m3.content === "string" ? m3.content : "";
292495
292537
  if (text) content.push({ type: "text", text });
292496
- for (const tc of m3.toolCalls ?? []) {
292497
- content.push({
292498
- type: "tool_use",
292499
- id: tc.id,
292500
- name: tc.name,
292501
- input: tc.input
292502
- });
292503
- }
292538
+ content.push(...toAnthropicToolUse(m3.toolCalls ?? []));
292504
292539
  if (content.length > 0) {
292505
292540
  result.push({ role: "assistant", content });
292506
292541
  }
@@ -300413,6 +300448,26 @@ function isParamShapeError(err) {
300413
300448
  const msg = String(e3?.message ?? e3?.error?.message ?? "").toLowerCase();
300414
300449
  return msg.includes("max_completion_tokens") || msg.includes("max_tokens") && (msg.includes("unsupported") || msg.includes("not supported") || msg.includes("use")) || msg.includes("temperature") && (msg.includes("unsupported") || msg.includes("does not support") || msg.includes("only the default"));
300415
300450
  }
300451
+ function toOpenAITools(tools) {
300452
+ return tools.map((t4) => ({
300453
+ type: "function",
300454
+ function: {
300455
+ name: t4.name,
300456
+ description: t4.description,
300457
+ parameters: t4.inputSchema
300458
+ }
300459
+ }));
300460
+ }
300461
+ function toOpenAIToolCalls(toolCalls) {
300462
+ return toolCalls.map((tc) => ({
300463
+ id: tc.id,
300464
+ type: "function",
300465
+ function: {
300466
+ name: tc.name,
300467
+ arguments: JSON.stringify(tc.input)
300468
+ }
300469
+ }));
300470
+ }
300416
300471
  var OpenAIProvider = class extends BaseProvider {
300417
300472
  client;
300418
300473
  /** Once we learn (from the model id or an API error) that this deployment
@@ -300432,14 +300487,7 @@ var OpenAIProvider = class extends BaseProvider {
300432
300487
  }
300433
300488
  async generateStream(options, onChunk) {
300434
300489
  const messages = this.convertMessages(options.messages, options.systemPrompt);
300435
- const tools = options.tools?.map((t4) => ({
300436
- type: "function",
300437
- function: {
300438
- name: t4.name,
300439
- description: t4.description,
300440
- parameters: t4.inputSchema
300441
- }
300442
- }));
300490
+ const tools = options.tools && toOpenAITools(options.tools);
300443
300491
  let fullContent = "";
300444
300492
  let inputTokens = 0;
300445
300493
  let outputTokens = 0;
@@ -300585,14 +300633,7 @@ var OpenAIProvider = class extends BaseProvider {
300585
300633
  result.push({
300586
300634
  role: "assistant",
300587
300635
  content: m3.content || "",
300588
- tool_calls: m3.toolCalls.map((toolCall) => ({
300589
- id: toolCall.id,
300590
- type: "function",
300591
- function: {
300592
- name: toolCall.name,
300593
- arguments: JSON.stringify(toolCall.input)
300594
- }
300595
- }))
300636
+ tool_calls: toOpenAIToolCalls(m3.toolCalls)
300596
300637
  });
300597
300638
  } else {
300598
300639
  result.push({ role: m3.role, content: m3.content });
@@ -318863,6 +318904,38 @@ function toGeminiParameters(inputSchema) {
318863
318904
  }
318864
318905
 
318865
318906
  // src/providers/gemini.ts
318907
+ function toGeminiTools(tools) {
318908
+ return tools.map((tool) => ({
318909
+ name: tool.name,
318910
+ description: tool.description,
318911
+ // Convert, never cast. Gemini's parameters field is an OpenAPI subset
318912
+ // that 400s on unknown keys, and MCP servers ship schemas carrying their
318913
+ // own extensions (GitHub's adds `x-mcp-header` to every header-bound
318914
+ // property). Casting sent those through verbatim and every request failed
318915
+ // before the model saw it. See providers/gemini-schema.ts.
318916
+ parameters: toGeminiParameters(tool.inputSchema)
318917
+ }));
318918
+ }
318919
+ function toGeminiFunctionCalls(toolCalls) {
318920
+ return toolCalls.map((tc) => ({
318921
+ functionCall: {
318922
+ name: tc.name,
318923
+ args: tc.input
318924
+ }
318925
+ }));
318926
+ }
318927
+ function isGeminiAuthStatus(status) {
318928
+ return status === 400 || status === 401 || status === 403;
318929
+ }
318930
+ async function describeGeminiError(resp) {
318931
+ try {
318932
+ const body = await resp.json();
318933
+ const detail = body?.error?.message ?? body?.error?.status;
318934
+ return detail ? ` \u2014 ${detail}` : "";
318935
+ } catch {
318936
+ return "";
318937
+ }
318938
+ }
318866
318939
  var GeminiProvider = class extends BaseProvider {
318867
318940
  client;
318868
318941
  constructor(config2, model) {
@@ -318884,7 +318957,7 @@ var GeminiProvider = class extends BaseProvider {
318884
318957
  { category: HarmCategory.HARM_CATEGORY_HARASSMENT, threshold: HarmBlockThreshold.BLOCK_NONE },
318885
318958
  { category: HarmCategory.HARM_CATEGORY_HATE_SPEECH, threshold: HarmBlockThreshold.BLOCK_NONE }
318886
318959
  ],
318887
- tools: options.tools?.length ? [{ functionDeclarations: options.tools.map(this.convertTool) }] : void 0,
318960
+ tools: options.tools?.length ? [{ functionDeclarations: toGeminiTools(options.tools) }] : void 0,
318888
318961
  abortSignal: options.signal
318889
318962
  }
318890
318963
  });
@@ -318946,9 +319019,7 @@ var GeminiProvider = class extends BaseProvider {
318946
319019
  }
318947
319020
  async listModels() {
318948
319021
  try {
318949
- const resp = await fetch(
318950
- `https://generativelanguage.googleapis.com/v1beta/models?key=${this.config.apiKey}`
318951
- );
319022
+ const resp = await this.fetchModelList();
318952
319023
  if (!resp.ok) {
318953
319024
  return Object.values(MODELS).filter((m3) => m3.provider === "gemini");
318954
319025
  }
@@ -318979,18 +319050,61 @@ var GeminiProvider = class extends BaseProvider {
318979
319050
  return Object.values(MODELS).filter((m3) => m3.provider === "gemini");
318980
319051
  }
318981
319052
  }
319053
+ /**
319054
+ * Is this KEY usable — not "does one particular model answer".
319055
+ *
319056
+ * This used to call countTokens() against `this.model.id`, which the router
319057
+ * fills from the first Gemini entry in the bundled catalogue. That made the
319058
+ * whole provider's fate depend on one hard-coded model id: a key that cannot
319059
+ * reach that one model — retired, not enabled for the key's project, served
319060
+ * on a different API version — failed the probe, and because every later step
319061
+ * is gated on the result (`validateCloudProviderModels` and
319062
+ * `discoverProviderModels` both return early for an unavailable provider) the
319063
+ * real model list was never fetched. Gemini vanished entirely, and the CLI
319064
+ * reported "add a provider API key first" about a key that was present and
319065
+ * working.
319066
+ *
319067
+ * Listing models asks the account-level question instead, the same one
319068
+ * OpenAI's probe asks, and it is the same request `listModels()` already
319069
+ * makes for discovery.
319070
+ *
319071
+ * Throws rather than returning false. The router logs the reason it catches,
319072
+ * and "bad key, wrong endpoint/deployment, or unreachable" — three guesses,
319073
+ * no answer — is what made this take a user report to find. A swallowed probe
319074
+ * error is the same defect that once hid a misconfigured Azure deployment.
319075
+ */
318982
319076
  async isAvailable() {
319077
+ if (!this.config.apiKey) throw new Error("no Gemini API key configured");
319078
+ let resp;
318983
319079
  try {
318984
- await this.client.models.countTokens({
318985
- model: this.model.id,
318986
- contents: [{ role: "user", parts: [{ text: "ping" }] }]
318987
- });
318988
- return true;
318989
- } catch {
318990
- return false;
319080
+ resp = await this.fetchModelList();
319081
+ } catch (err) {
319082
+ throw new ProviderUnreachableError(
319083
+ `could not reach the Gemini API: ${err instanceof Error ? err.message : String(err)}`
319084
+ );
319085
+ }
319086
+ if (resp.ok) return true;
319087
+ const status = `HTTP ${resp.status} ${resp.statusText}`.trim();
319088
+ const detail = await describeGeminiError(resp);
319089
+ if (isGeminiAuthStatus(resp.status)) {
319090
+ throw new Error(`Gemini rejected the API key: ${status}${detail}`);
318991
319091
  }
319092
+ throw new ProviderUnreachableError(`Gemini model list failed: ${status}${detail}`);
318992
319093
  }
318993
319094
  // ── Private ──────────────────────────────────
319095
+ /**
319096
+ * The account's model list. One place so the availability probe and
319097
+ * discovery cannot ask different questions of the same endpoint.
319098
+ *
319099
+ * The key goes in a header, not the query string: a URL carries into proxy
319100
+ * logs, error reports and shell history, and this one would carry the key
319101
+ * with it.
319102
+ */
319103
+ fetchModelList() {
319104
+ return fetch("https://generativelanguage.googleapis.com/v1beta/models", {
319105
+ headers: { "x-goog-api-key": this.config.apiKey ?? "" }
319106
+ });
319107
+ }
318994
319108
  buildContents(messages, extraImages) {
318995
319109
  const contents = [];
318996
319110
  for (const m3 of messages) {
@@ -319025,14 +319139,7 @@ var GeminiProvider = class extends BaseProvider {
319025
319139
  const parts = [];
319026
319140
  const textContent = typeof m3.content === "string" ? m3.content : "";
319027
319141
  if (textContent) parts.push({ text: textContent });
319028
- for (const tc of m3.toolCalls ?? []) {
319029
- parts.push({
319030
- functionCall: {
319031
- name: tc.name,
319032
- args: tc.input
319033
- }
319034
- });
319035
- }
319142
+ parts.push(...toGeminiFunctionCalls(m3.toolCalls ?? []));
319036
319143
  if (parts.length > 0) {
319037
319144
  contents.push({ role: "model", parts });
319038
319145
  }
@@ -319079,18 +319186,6 @@ var GeminiProvider = class extends BaseProvider {
319079
319186
  }
319080
319187
  return parts;
319081
319188
  }
319082
- convertTool(tool) {
319083
- return {
319084
- name: tool.name,
319085
- description: tool.description,
319086
- // Convert, never cast. Gemini's parameters field is an OpenAPI subset
319087
- // that 400s on unknown keys, and MCP servers ship schemas carrying their
319088
- // own extensions (GitHub's adds `x-mcp-header` to every header-bound
319089
- // property). Casting sent those through verbatim and every request failed
319090
- // before the model saw it. See providers/gemini-schema.ts.
319091
- parameters: toGeminiParameters(tool.inputSchema)
319092
- };
319093
- }
319094
319189
  };
319095
319190
  try {
319096
319191
  dns__default.default.setDefaultResultOrder("ipv4first");
@@ -319183,6 +319278,24 @@ function isToolCapable(modelName) {
319183
319278
  const name = modelName.toLowerCase();
319184
319279
  return TOOL_CAPABLE_FAMILIES.some((family) => name.includes(family));
319185
319280
  }
319281
+ function toOllamaTools(tools) {
319282
+ return tools.map((t4) => ({
319283
+ type: "function",
319284
+ function: {
319285
+ name: t4.name,
319286
+ description: t4.description,
319287
+ parameters: t4.inputSchema
319288
+ }
319289
+ }));
319290
+ }
319291
+ function toOllamaToolCalls(toolCalls) {
319292
+ return toolCalls.map((tc) => ({
319293
+ function: {
319294
+ name: tc.name,
319295
+ arguments: tc.input
319296
+ }
319297
+ }));
319298
+ }
319186
319299
  var OllamaProvider = class extends BaseProvider {
319187
319300
  baseUrl;
319188
319301
  constructor(config2, model) {
@@ -319195,14 +319308,7 @@ var OllamaProvider = class extends BaseProvider {
319195
319308
  }
319196
319309
  async generateStream(options, onChunk) {
319197
319310
  const messages = this.convertMessages(options.messages, options.systemPrompt);
319198
- const ollamaTools = options.tools?.map((t4) => ({
319199
- type: "function",
319200
- function: {
319201
- name: t4.name,
319202
- description: t4.description,
319203
- parameters: t4.inputSchema
319204
- }
319205
- }));
319311
+ const ollamaTools = options.tools && toOllamaTools(options.tools);
319206
319312
  const response = await fetch(`${this.baseUrl}/api/chat`, {
319207
319313
  method: "POST",
319208
319314
  headers: { "Content-Type": "application/json" },
@@ -319382,12 +319488,7 @@ var OllamaProvider = class extends BaseProvider {
319382
319488
  result.push({
319383
319489
  role: "assistant",
319384
319490
  content: typeof m3.content === "string" ? m3.content : "",
319385
- tool_calls: m3.toolCalls.map((tc) => ({
319386
- function: {
319387
- name: tc.name,
319388
- arguments: tc.input
319389
- }
319390
- }))
319491
+ tool_calls: toOllamaToolCalls(m3.toolCalls)
319391
319492
  });
319392
319493
  continue;
319393
319494
  }
@@ -319790,6 +319891,21 @@ var DEFAULT_PROVIDER_TPM = {
319790
319891
  "openai-compatible": 3e4,
319791
319892
  ollama: Number.POSITIVE_INFINITY
319792
319893
  };
319894
+ function abortReason(signal) {
319895
+ return signal.reason instanceof Error ? signal.reason : new Error("Aborted while waiting for rate-limit capacity");
319896
+ }
319897
+ function sleepUnlessAborted(ms2, signal) {
319898
+ if (!signal) return new Promise((resolve2) => setTimeout(resolve2, ms2));
319899
+ return new Promise((resolve2) => {
319900
+ const done = () => {
319901
+ clearTimeout(timer);
319902
+ signal.removeEventListener("abort", done);
319903
+ resolve2();
319904
+ };
319905
+ const timer = setTimeout(done, ms2);
319906
+ signal.addEventListener("abort", done, { once: true });
319907
+ });
319908
+ }
319793
319909
  var TpmLimiter = class {
319794
319910
  buckets = /* @__PURE__ */ new Map();
319795
319911
  constructor(overrides = {}) {
@@ -319808,9 +319924,10 @@ var TpmLimiter = class {
319808
319924
  * are reported back via `refund` when short, or simply settled at the next
319809
319925
  * refill.
319810
319926
  */
319811
- async acquire(provider, estimatedTokens) {
319927
+ async acquire(provider, estimatedTokens, signal) {
319812
319928
  const bucket = this.buckets.get(provider);
319813
319929
  if (!bucket || bucket.tokensPerMinute === Number.POSITIVE_INFINITY) return;
319930
+ if (signal?.aborted) throw abortReason(signal);
319814
319931
  const want = Math.min(estimatedTokens, bucket.tokensPerMinute);
319815
319932
  for (; ; ) {
319816
319933
  this.refill(bucket);
@@ -319820,7 +319937,8 @@ var TpmLimiter = class {
319820
319937
  }
319821
319938
  const deficit = want - bucket.available;
319822
319939
  const waitMs = Math.max(50, Math.ceil(deficit / bucket.tokensPerMinute * 6e4));
319823
- await new Promise((resolve2) => setTimeout(resolve2, waitMs));
319940
+ await sleepUnlessAborted(waitMs, signal);
319941
+ if (signal?.aborted) throw abortReason(signal);
319824
319942
  }
319825
319943
  }
319826
319944
  /**
@@ -319866,6 +319984,9 @@ var TpmLimiter = class {
319866
319984
  };
319867
319985
 
319868
319986
  // src/core/router/local-queue.ts
319987
+ function abortReason2(signal) {
319988
+ return signal.reason instanceof Error ? signal.reason : new Error("Aborted while waiting for a local inference slot");
319989
+ }
319869
319990
  var LocalRequestQueue = class {
319870
319991
  maxConcurrent;
319871
319992
  active = 0;
@@ -319876,9 +319997,16 @@ var LocalRequestQueue = class {
319876
319997
  /**
319877
319998
  * Acquire a queue slot. Returns a `release` function that MUST be called
319878
319999
  * when the inference call is done (even on error). Rejects if the slot
319879
- * cannot be acquired within `timeoutMs`.
320000
+ * cannot be acquired within `timeoutMs`, or as soon as `signal` fires.
320001
+ *
320002
+ * The signal matters as much as the timeout: with concurrency of 1 a caller
320003
+ * can sit here for half the inference timeout — 150 seconds by default —
320004
+ * and until it was wired up, neither cancelling the run nor spending its
320005
+ * budget got it out. It waited out the window for a slot it would drop the
320006
+ * moment it received one.
319880
320007
  */
319881
- async acquire(timeoutMs) {
320008
+ async acquire(timeoutMs, signal) {
320009
+ if (signal?.aborted) throw abortReason2(signal);
319882
320010
  if (this.active < this.maxConcurrent) {
319883
320011
  this.active++;
319884
320012
  return this.makeRelease();
@@ -319886,23 +320014,36 @@ var LocalRequestQueue = class {
319886
320014
  return new Promise((resolve2, reject) => {
319887
320015
  let settled = false;
319888
320016
  let timer;
320017
+ const cleanup = () => {
320018
+ if (timer !== void 0) clearTimeout(timer);
320019
+ signal?.removeEventListener("abort", onAbort);
320020
+ };
319889
320021
  const resolver = (release) => {
319890
320022
  if (settled) return;
319891
320023
  settled = true;
319892
- if (timer !== void 0) clearTimeout(timer);
320024
+ cleanup();
319893
320025
  resolve2(release);
319894
320026
  };
320027
+ const abandon = () => {
320028
+ if (settled) return false;
320029
+ settled = true;
320030
+ cleanup();
320031
+ const idx = this.queue.indexOf(resolver);
320032
+ if (idx !== -1) this.queue.splice(idx, 1);
320033
+ return true;
320034
+ };
320035
+ function onAbort() {
320036
+ if (abandon()) reject(abortReason2(signal));
320037
+ }
319895
320038
  if (timeoutMs !== void 0 && timeoutMs > 0) {
319896
320039
  timer = setTimeout(() => {
319897
- if (settled) return;
319898
- settled = true;
319899
- const idx = this.queue.indexOf(resolver);
319900
- if (idx !== -1) this.queue.splice(idx, 1);
320040
+ if (!abandon()) return;
319901
320041
  reject(new Error(
319902
320042
  `Local model queue: timed out waiting for a free slot after ${timeoutMs}ms. Active: ${this.active}, Queued: ${this.queue.length}. Consider increasing localConcurrency or localInferenceTimeoutMs in your config.`
319903
320043
  ));
319904
320044
  }, timeoutMs);
319905
320045
  }
320046
+ signal?.addEventListener("abort", onAbort, { once: true });
319906
320047
  this.queue.push(resolver);
319907
320048
  });
319908
320049
  }
@@ -320039,6 +320180,116 @@ function fileDeadModelPersistence(filePath) {
320039
320180
  };
320040
320181
  }
320041
320182
 
320183
+ // src/core/context/compaction.ts
320184
+ var CHARS_PER_TOKEN = 4;
320185
+ function estimateTokens(text) {
320186
+ if (!text) return 0;
320187
+ return Math.max(1, Math.ceil(text.length / CHARS_PER_TOKEN));
320188
+ }
320189
+ function contentToText(content) {
320190
+ if (typeof content === "string") return content;
320191
+ return content.map((c4) => c4.type === "text" ? c4.text : c4.type === "tool_result" ? c4.content : `[${c4.type}]`).join(" ");
320192
+ }
320193
+ function messagesTokens(messages) {
320194
+ return messages.reduce((n8, m3) => n8 + estimateTokens(contentToText(m3.content)), 0);
320195
+ }
320196
+ function atomize(text, maxChars) {
320197
+ const seps = ["\n\n", "\n", ". ", " "];
320198
+ let units = [text];
320199
+ for (const sep3 of seps) {
320200
+ if (units.every((u2) => u2.length <= maxChars)) break;
320201
+ const next = [];
320202
+ for (const u2 of units) {
320203
+ if (u2.length <= maxChars) {
320204
+ next.push(u2);
320205
+ continue;
320206
+ }
320207
+ const parts = u2.split(sep3);
320208
+ parts.forEach((p3, i4) => next.push(i4 < parts.length - 1 ? p3 + sep3 : p3));
320209
+ }
320210
+ units = next;
320211
+ }
320212
+ const out = [];
320213
+ for (const u2 of units) {
320214
+ if (u2.length <= maxChars) out.push(u2);
320215
+ else for (let i4 = 0; i4 < u2.length; i4 += maxChars) out.push(u2.slice(i4, i4 + maxChars));
320216
+ }
320217
+ return out;
320218
+ }
320219
+ function chunkText(text, opts) {
320220
+ const maxChars = Math.max(1, Math.floor(opts.maxTokens * 4));
320221
+ const overlap = Math.min(maxChars - 1, Math.max(0, Math.floor(maxChars * (opts.overlapRatio ?? 0.1))));
320222
+ const trimmed2 = text.trim();
320223
+ if (!trimmed2) return [];
320224
+ if (trimmed2.length <= maxChars) return [trimmed2];
320225
+ const units = atomize(trimmed2, maxChars);
320226
+ const chunks = [];
320227
+ let cur = "";
320228
+ for (const u2 of units) {
320229
+ if (cur && cur.length + u2.length > maxChars) {
320230
+ chunks.push(cur.trim());
320231
+ cur = overlap > 0 ? cur.slice(-overlap) : "";
320232
+ }
320233
+ cur += u2;
320234
+ }
320235
+ if (cur.trim()) chunks.push(cur.trim());
320236
+ return chunks;
320237
+ }
320238
+ function needsCompaction(totalTokens, contextWindow, reserve = 0.2) {
320239
+ if (!contextWindow || contextWindow <= 0) return false;
320240
+ return totalTokens > contextWindow * (1 - reserve);
320241
+ }
320242
+ async function mapReduceCompact(text, opts) {
320243
+ const capChars = opts.capTokens * 4;
320244
+ let working = text;
320245
+ let truncated = false;
320246
+ if (working.length > capChars) {
320247
+ working = working.slice(0, capChars);
320248
+ truncated = true;
320249
+ }
320250
+ const instruction = opts.instruction ?? "Summarize this content, preserving key facts, names, numbers, and decisions.";
320251
+ let chunks = chunkText(working, { maxTokens: opts.chunkTokens });
320252
+ if (chunks.length <= 1) return { text: working, chunks: chunks.length, calls: 0, truncated };
320253
+ let calls = 0;
320254
+ let parts = await Promise.all(
320255
+ chunks.map((c4, i4) => {
320256
+ calls++;
320257
+ return opts.summarize(c4, `${instruction} (part ${i4 + 1} of ${chunks.length})`);
320258
+ })
320259
+ );
320260
+ let combined = parts.join("\n\n");
320261
+ const maxPasses = opts.maxReducePasses ?? 3;
320262
+ for (let pass = 0; pass < maxPasses && estimateTokens(combined) > opts.targetTokens; pass++) {
320263
+ const groups = chunkText(combined, { maxTokens: opts.chunkTokens });
320264
+ if (groups.length <= 1) break;
320265
+ parts = await Promise.all(
320266
+ groups.map((g2) => {
320267
+ calls++;
320268
+ return opts.summarize(g2, "Merge these partial summaries into one cohesive summary; keep all distinct facts.");
320269
+ })
320270
+ );
320271
+ combined = parts.join("\n\n");
320272
+ }
320273
+ const note = truncated ? "\n\n[Note: input exceeded the extended-context cap and was truncated.]" : "";
320274
+ return { text: combined + note, chunks: chunks.length, calls, truncated };
320275
+ }
320276
+ async function rollingSummary(messages, opts) {
320277
+ if (messages.length <= opts.keepRecent) return messages;
320278
+ const older = messages.slice(0, messages.length - opts.keepRecent);
320279
+ const recent = messages.slice(messages.length - opts.keepRecent);
320280
+ const transcript = older.map((m3) => `${m3.role}: ${contentToText(m3.content)}`).join("\n");
320281
+ const summary = await opts.summarize(
320282
+ transcript,
320283
+ "Summarize this earlier conversation. Preserve facts, decisions, names, numbers, and any open threads or todos."
320284
+ );
320285
+ const summaryMessage = {
320286
+ role: "system",
320287
+ content: `Summary of earlier conversation (folded to fit the context window):
320288
+ ${summary}`
320289
+ };
320290
+ return [summaryMessage, ...recent];
320291
+ }
320292
+
320042
320293
  // src/utils/retry.ts
320043
320294
  var CascadeCancelledError = class extends Error {
320044
320295
  constructor(reason) {
@@ -320073,6 +320324,158 @@ async function withTimeout(promise, timeoutMs, errorMessage = "Operation timed o
320073
320324
  if (timer !== void 0) clearTimeout(timer);
320074
320325
  }
320075
320326
  }
320327
+ async function withTimeoutAbort(run, timeoutMs, errorMessage = "Operation timed out", outer) {
320328
+ const outers = (Array.isArray(outer) ? outer : [outer]).filter((sig) => sig !== void 0);
320329
+ const controller = new AbortController();
320330
+ let timer;
320331
+ let rejectRace;
320332
+ const settle = (reason) => {
320333
+ rejectRace?.(reason);
320334
+ controller.abort(reason);
320335
+ };
320336
+ const abortOuter = () => {
320337
+ const fired = outers.find((sig) => sig.aborted);
320338
+ settle(fired?.reason instanceof Error ? fired.reason : new CascadeCancelledError("Run cancelled"));
320339
+ };
320340
+ const failPromise = new Promise((_4, reject) => {
320341
+ rejectRace = reject;
320342
+ timer = setTimeout(() => settle(new Error(errorMessage)), timeoutMs);
320343
+ });
320344
+ try {
320345
+ if (outers.some((sig) => sig.aborted)) {
320346
+ abortOuter();
320347
+ return await failPromise;
320348
+ }
320349
+ for (const sig of outers) sig.addEventListener("abort", abortOuter, { once: true });
320350
+ return await Promise.race([run(controller.signal), failPromise]);
320351
+ } finally {
320352
+ if (timer !== void 0) clearTimeout(timer);
320353
+ for (const sig of outers) sig.removeEventListener("abort", abortOuter);
320354
+ }
320355
+ }
320356
+ function anySignal(sources) {
320357
+ const live = sources.filter((s3) => s3 !== void 0);
320358
+ if (live.length === 0) return { release: () => {
320359
+ } };
320360
+ if (live.length === 1) return { signal: live[0], release: () => {
320361
+ } };
320362
+ const controller = new AbortController();
320363
+ function detach() {
320364
+ for (const s3 of live) s3.removeEventListener("abort", onAbort);
320365
+ }
320366
+ function onAbort() {
320367
+ const fired = live.find((s3) => s3.aborted);
320368
+ controller.abort(fired?.reason instanceof Error ? fired.reason : new CascadeCancelledError("Run cancelled"));
320369
+ detach();
320370
+ }
320371
+ if (live.some((s3) => s3.aborted)) {
320372
+ onAbort();
320373
+ return { signal: controller.signal, release: detach };
320374
+ }
320375
+ for (const s3 of live) s3.addEventListener("abort", onAbort, { once: true });
320376
+ return { signal: controller.signal, release: detach };
320377
+ }
320378
+
320379
+ // src/core/router/wire-profile.ts
320380
+ var textAndImageOnly = (block) => block.type === "text" || block.type === "image";
320381
+ var ANTHROPIC = {
320382
+ // convertMessages: `if (m.role === 'system') continue` — history system
320383
+ // messages are skipped outright; only options.systemPrompt goes as system
320384
+ // input. Compaction emits exactly such a message, holding a summary of the
320385
+ // whole conversation, so charging it refused runs over input never sent.
320386
+ dropsMessage: (m3) => m3.role === "system",
320387
+ blockHandling: (m3) => {
320388
+ if (m3.role === "tool") return "stringified";
320389
+ if (m3.role === "assistant") return "dropped";
320390
+ return "blocks";
320391
+ },
320392
+ sendsToolCalls: (m3) => m3.role === "assistant",
320393
+ sendsBlock: textAndImageOnly,
320394
+ sendsToolCallId: true,
320395
+ sizeToolCalls: toAnthropicToolUse,
320396
+ sendsUrlImages: true,
320397
+ readsTopLevelImages: false,
320398
+ sizeTools: toAnthropicTools
320399
+ };
320400
+ var GEMINI = {
320401
+ // buildContents folds a system message into the next user turn, but only
320402
+ // when it has text: array content collapses to '' and the message is
320403
+ // skipped, as is a whitespace-only string.
320404
+ dropsMessage: (m3) => m3.role === "system" && !(typeof m3.content === "string" ? m3.content : "").trim(),
320405
+ blockHandling: (m3) => {
320406
+ if (m3.role === "tool") return "stringified";
320407
+ if (m3.role === "assistant" || m3.role === "system") return "dropped";
320408
+ return "blocks";
320409
+ },
320410
+ sendsToolCalls: (m3) => m3.role === "assistant",
320411
+ sendsBlock: textAndImageOnly,
320412
+ sendsToolCallId: true,
320413
+ sizeToolCalls: toGeminiFunctionCalls,
320414
+ sendsUrlImages: false,
320415
+ readsTopLevelImages: true,
320416
+ sizeTools: toGeminiTools
320417
+ };
320418
+ var OPENAI = {
320419
+ dropsMessage: () => false,
320420
+ blockHandling: (m3) => {
320421
+ if (m3.role === "system" || m3.role === "tool") return "dropped";
320422
+ return "blocks";
320423
+ },
320424
+ // The tool_calls branch is inside `if (typeof m.content === 'string')`; an
320425
+ // assistant turn with array content falls through to the parts branch and
320426
+ // its tool calls are never attached.
320427
+ sendsToolCalls: (m3) => m3.role === "assistant" && typeof m3.content === "string",
320428
+ sendsBlock: textAndImageOnly,
320429
+ sendsToolCallId: true,
320430
+ sizeToolCalls: toOpenAIToolCalls,
320431
+ sendsUrlImages: true,
320432
+ readsTopLevelImages: false,
320433
+ sizeTools: toOpenAITools
320434
+ };
320435
+ var OLLAMA = {
320436
+ dropsMessage: () => false,
320437
+ blockHandling: (m3) => {
320438
+ if (m3.role === "system") return "dropped";
320439
+ if (m3.role === "tool") return "stringified";
320440
+ if (m3.role === "assistant" && m3.toolCalls?.length) return "dropped";
320441
+ return "blocks";
320442
+ },
320443
+ sendsToolCalls: (m3) => m3.role === "assistant",
320444
+ // The image split maps `b.image.data` with no check on `type`, so a URL is
320445
+ // submitted too (as a URL string in the `images` array).
320446
+ sendsBlock: textAndImageOnly,
320447
+ sendsToolCallId: false,
320448
+ sizeToolCalls: toOllamaToolCalls,
320449
+ sendsUrlImages: true,
320450
+ readsTopLevelImages: false,
320451
+ sizeTools: toOllamaTools
320452
+ };
320453
+ var PROFILES = {
320454
+ anthropic: ANTHROPIC,
320455
+ gemini: GEMINI,
320456
+ openai: OPENAI,
320457
+ // AzureOpenAIProvider and OpenAICompatibleProvider both extend OpenAIProvider
320458
+ // and neither overrides convertMessages.
320459
+ azure: OPENAI,
320460
+ "openai-compatible": OPENAI,
320461
+ ollama: OLLAMA
320462
+ };
320463
+ function wireProfile(provider) {
320464
+ return PROFILES[provider] ?? OPENAI;
320465
+ }
320466
+ function geminiImageCopies(messages) {
320467
+ for (const m3 of messages) {
320468
+ if (m3.role === "user") {
320469
+ const laterUserTurn = messages.slice(messages.indexOf(m3) + 1).some((x5) => x5.role === "user");
320470
+ return laterUserTurn ? 2 : 1;
320471
+ }
320472
+ const text = typeof m3.content === "string" ? m3.content : "";
320473
+ if (m3.role === "system" && text.trim()) return 1;
320474
+ if (m3.role === "tool") return 1;
320475
+ if (m3.role === "assistant" && (text || m3.toolCalls?.length)) return 1;
320476
+ }
320477
+ return 1;
320478
+ }
320076
320479
 
320077
320480
  // src/core/router/model-profiler.ts
320078
320481
  var SKIP_PATTERN = /embed|dall-e|whisper|tts|vision|instruct-vision|rerank/i;
@@ -320577,6 +320980,36 @@ function azureModelForTier(tier, ranked) {
320577
320980
  if (tier === "T3") return ranked[ranked.length - 1];
320578
320981
  return ranked[Math.min(1, ranked.length - 1)];
320579
320982
  }
320983
+ var IMAGE_TOKENS_MAX = 2e3;
320984
+ var IMAGE_TOKENS_EACH = IMAGE_TOKENS_MAX;
320985
+ function safeJson(value) {
320986
+ try {
320987
+ return JSON.stringify(value) ?? "";
320988
+ } catch {
320989
+ return "";
320990
+ }
320991
+ }
320992
+ function newRunAbort() {
320993
+ const controller = new AbortController();
320994
+ EventEmitter.setMaxListeners(64, controller.signal);
320995
+ return controller;
320996
+ }
320997
+ var TOKENS_PER_MESSAGE_FRAMING = 4;
320998
+ function guardTokens(text) {
320999
+ if (!text) return 0;
321000
+ let asciiChars = 0;
321001
+ let denseBytes = 0;
321002
+ for (const ch of text) {
321003
+ const cp = ch.codePointAt(0);
321004
+ if (cp <= 127) {
321005
+ asciiChars++;
321006
+ continue;
321007
+ }
321008
+ denseBytes += cp <= 2047 ? 2 : cp <= 65535 ? 3 : 4;
321009
+ }
321010
+ if (denseBytes === 0) return estimateTokens(text);
321011
+ return Math.ceil(asciiChars / CHARS_PER_TOKEN) + denseBytes;
321012
+ }
320580
321013
  var DISCOVERY_TTL_MS = 15 * 60 * 1e3;
320581
321014
  var DISCOVERY_TIMEOUT_MS = 4e3;
320582
321015
  var cloudDiscoveryCache = /* @__PURE__ */ new Map();
@@ -320629,6 +321062,33 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
320629
321062
  // start of every `cascade run`, independent of the session-wide budget.
320630
321063
  runTokens = 0;
320631
321064
  runCostUsd = 0;
321065
+ /**
321066
+ * Budget held for calls that are IN FLIGHT — admitted by the preflight check
321067
+ * but not yet counted by recordStats. Without these, concurrent calls all
321068
+ * measure themselves against the same unspent allowance and are all admitted.
321069
+ * Released when each call settles; reset by beginRun() alongside the totals.
321070
+ */
321071
+ reservedTokens = 0;
321072
+ reservedCostUsd = 0;
321073
+ /**
321074
+ * Fires when this run's budget is spent, cancelling work already in flight.
321075
+ *
321076
+ * The kill switch was previously only a flag, which stops calls that have
321077
+ * not yet submitted. A parallel wave's earlier members are already at the
321078
+ * provider by then, and they carried on generating for a run whose output
321079
+ * would be discarded. Every provider call chains to this, so tripping the
321080
+ * ceiling reaches the requests that are running, not just the ones queued.
321081
+ */
321082
+ runAbort = newRunAbort();
321083
+ /**
321084
+ * Bumped by every beginRun(). A release handle remembers the generation it
321085
+ * was issued in and does nothing if that has moved on: beginRun() zeroes the
321086
+ * counters, so a call still in flight from the previous run would otherwise
321087
+ * subtract its estimate from the NEW run's totals and drive them negative —
321088
+ * leaving later preflights believing there is more allowance than the cap
321089
+ * actually holds, which is worse than no gate at all.
321090
+ */
321091
+ runGeneration = 0;
320632
321092
  runBudgetExceeded = false;
320633
321093
  runBudgetExceededReason;
320634
321094
  /**
@@ -320798,7 +321258,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
320798
321258
  try {
320799
321259
  const cfg = this.config.providers.find((p3) => p3.type === model.provider) ?? { type: model.provider };
320800
321260
  const provider = this.createProvider(cfg, model);
320801
- const result = await withTimeout(provider.generate({
321261
+ const result = await withTimeoutAbort((signal) => provider.generate({
320802
321262
  messages: [{ role: "user", content: "Call the echo tool with text set to 'ping'. Use the tool; do not answer in prose." }],
320803
321263
  tools: [{
320804
321264
  name: "echo",
@@ -320806,7 +321266,8 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
320806
321266
  inputSchema: { type: "object", properties: { text: { type: "string", description: "Text to echo" } }, required: ["text"] }
320807
321267
  }],
320808
321268
  maxTokens: 80,
320809
- temperature: 0
321269
+ temperature: 0,
321270
+ signal
320810
321271
  }), 3e4, "tool-support probe timed out");
320811
321272
  return (result.toolCalls?.length ?? 0) > 0;
320812
321273
  } catch {
@@ -320927,9 +321388,12 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
320927
321388
  this.runBudgetExceededReason ?? "Per-task budget exceeded."
320928
321389
  );
320929
321390
  }
321391
+ const runGeneration = this.runGeneration;
321392
+ const runAbort = this.runAbort;
321393
+ const runSignal = this.runSignal;
320930
321394
  options = applyTierLimits(options, tier, this.config?.tierLimits);
320931
- if (this.runSignal && !options.signal) {
320932
- options = { ...options, signal: this.runSignal };
321395
+ if (runSignal && !options.signal) {
321396
+ options = { ...options, signal: runSignal };
320933
321397
  }
320934
321398
  if (options.model && !requireVision) {
320935
321399
  this.ensureProvider(options.model, this.config.providers);
@@ -320951,58 +321415,84 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
320951
321415
  if (!model) throw new Error(`No model available for tier ${tier}`);
320952
321416
  const provider = this.getProvider(model);
320953
321417
  if (!provider) throw new Error(`No provider for model ${model.id}`);
321418
+ let releaseReservation = this.enforcePreflightBudget(model, options);
320954
321419
  const requestedTokens = options.maxTokens ?? model.maxOutputTokens ?? 1024;
320955
321420
  const estimatedTokens = Math.min(requestedTokens, model.maxOutputTokens ?? requestedTokens) + 512;
320956
321421
  if (this.tpmLimiter) {
320957
- await this.tpmLimiter.acquire(model.provider, estimatedTokens);
321422
+ const wait = anySignal([options.signal, runAbort.signal]);
321423
+ try {
321424
+ await this.tpmLimiter.acquire(model.provider, estimatedTokens, wait.signal);
321425
+ } catch (err) {
321426
+ releaseReservation?.();
321427
+ throw this.asAbortFailure(err, options.signal, runSignal);
321428
+ } finally {
321429
+ wait.release();
321430
+ }
320958
321431
  }
320959
321432
  const useStream = Boolean(onChunk) && model.supportsStreaming && typeof provider.generateStream === "function";
320960
321433
  let releaseLocalSlot;
320961
321434
  if (model.isLocal) {
320962
321435
  const inferenceTimeoutMs = this.config.localInferenceTimeoutMs ?? 3e5;
320963
321436
  const queueWaitMs = Math.round(inferenceTimeoutMs / 2);
320964
- releaseLocalSlot = await this.localQueue.acquire(queueWaitMs);
321437
+ const wait = anySignal([options.signal, runAbort.signal]);
321438
+ try {
321439
+ releaseLocalSlot = await this.localQueue.acquire(queueWaitMs, wait.signal);
321440
+ } catch (err) {
321441
+ releaseReservation?.();
321442
+ throw this.asAbortFailure(err, options.signal, runSignal);
321443
+ } finally {
321444
+ wait.release();
321445
+ }
320965
321446
  }
320966
321447
  try {
321448
+ if (runGeneration === this.runGeneration && this.runBudgetExceeded) {
321449
+ this.tpmLimiter?.refund(model.provider, estimatedTokens);
321450
+ throw new _CascadeRouter.BudgetExceededError(
321451
+ this.runBudgetExceededReason ?? "Per-task budget exceeded."
321452
+ );
321453
+ }
320967
321454
  let result;
320968
321455
  if (model.isLocal) {
320969
321456
  const inferenceTimeoutMs = this.config.localInferenceTimeoutMs ?? 3e5;
320970
- const inferencePromise = useStream && onChunk ? provider.generateStream(options, (chunk) => {
320971
- const text = typeof chunk?.text === "string" ? chunk.text : "";
320972
- if (text) onChunk({ ...chunk, text });
320973
- }) : provider.generate(options);
320974
- result = await withTimeout(
320975
- inferencePromise,
321457
+ result = await withTimeoutAbort(
321458
+ (signal) => useStream && onChunk ? provider.generateStream({ ...options, signal }, (chunk) => {
321459
+ const text = typeof chunk?.text === "string" ? chunk.text : "";
321460
+ if (text) onChunk({ ...chunk, text });
321461
+ }) : provider.generate({ ...options, signal }),
320976
321462
  inferenceTimeoutMs,
320977
- `Local model ${model.id} inference timed out after ${inferenceTimeoutMs}ms`
321463
+ `Local model ${model.id} inference timed out after ${inferenceTimeoutMs}ms`,
321464
+ [options.signal, runAbort.signal]
320978
321465
  );
320979
321466
  } else if (useStream && onChunk) {
320980
321467
  const cloudTimeoutMs = this.config.cloudInferenceTimeoutMs ?? 12e4;
320981
321468
  try {
320982
- result = await withTimeout(
320983
- provider.generateStream(options, (chunk) => {
321469
+ result = await withTimeoutAbort(
321470
+ (signal) => provider.generateStream({ ...options, signal }, (chunk) => {
320984
321471
  const text = typeof chunk?.text === "string" ? chunk.text : "";
320985
321472
  if (text) onChunk({ ...chunk, text });
320986
321473
  }),
320987
321474
  cloudTimeoutMs,
320988
- `Model ${model.id} stream timed out after ${cloudTimeoutMs}ms`
321475
+ `Model ${model.id} stream timed out after ${cloudTimeoutMs}ms`,
321476
+ [options.signal, runAbort.signal]
320989
321477
  );
320990
321478
  } catch (streamErr) {
320991
- if (streamErr instanceof Error && streamErr.name === "AbortError" || this.runSignal?.aborted || options.signal?.aborted) {
321479
+ if (streamErr instanceof Error && streamErr.name === "AbortError" || runSignal?.aborted || options.signal?.aborted) {
320992
321480
  throw streamErr;
320993
321481
  }
320994
- result = await withTimeout(
320995
- provider.generate(options),
321482
+ result = await withTimeoutAbort(
321483
+ (signal) => provider.generate({ ...options, signal }),
320996
321484
  cloudTimeoutMs,
320997
- `Model ${model.id} inference timed out after ${cloudTimeoutMs}ms`
321485
+ `Model ${model.id} inference timed out after ${cloudTimeoutMs}ms`,
321486
+ [options.signal, runAbort.signal]
320998
321487
  );
320999
321488
  }
321000
321489
  } else {
321001
321490
  const cloudTimeoutMs = this.config.cloudInferenceTimeoutMs ?? 12e4;
321002
- result = await withTimeout(
321003
- provider.generate(options),
321491
+ result = await withTimeoutAbort(
321492
+ (signal) => provider.generate({ ...options, signal }),
321004
321493
  cloudTimeoutMs,
321005
- `Model ${model.id} inference timed out after ${cloudTimeoutMs}ms`
321494
+ `Model ${model.id} inference timed out after ${cloudTimeoutMs}ms`,
321495
+ [options.signal, runAbort.signal]
321006
321496
  );
321007
321497
  }
321008
321498
  const corrected = buildTokenUsage(
@@ -321021,11 +321511,12 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
321021
321511
  if (!result || typeof result.content !== "string" || !result.usage) {
321022
321512
  throw new Error(`Provider ${model.provider}:${model.id} returned an invalid generation result.`);
321023
321513
  }
321024
- this.recordStats(tier, model, result.usage, options.featureTag);
321514
+ this.recordStats(tier, model, result.usage, options.featureTag, runGeneration);
321025
321515
  this.failover.recordSuccess(model.provider);
321026
321516
  return result;
321027
321517
  } catch (err) {
321028
- if (err instanceof Error && err.name === "AbortError" || this.runSignal?.aborted || options.signal?.aborted) {
321518
+ if (err instanceof _CascadeRouter.BudgetExceededError) throw err;
321519
+ if (err instanceof Error && err.name === "AbortError" || runSignal?.aborted || options.signal?.aborted) {
321029
321520
  throw new CascadeCancelledError("Run cancelled");
321030
321521
  }
321031
321522
  const errMsg = err instanceof Error ? err.message : String(err);
@@ -321043,6 +321534,8 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
321043
321534
  });
321044
321535
  releaseLocalSlot?.();
321045
321536
  releaseLocalSlot = void 0;
321537
+ releaseReservation?.();
321538
+ releaseReservation = void 0;
321046
321539
  const retryOpts = options.model && options.model.id === model.id ? { ...options, model: void 0 } : options;
321047
321540
  return this.generate(tier, retryOpts, onChunk, requireVision);
321048
321541
  }
@@ -321064,6 +321557,8 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
321064
321557
  });
321065
321558
  releaseLocalSlot?.();
321066
321559
  releaseLocalSlot = void 0;
321560
+ releaseReservation?.();
321561
+ releaseReservation = void 0;
321067
321562
  const base = options.model && options.model.id === model.id ? { ...options, model: void 0 } : options;
321068
321563
  const retryOpts = { ...base, _notFoundDepth: depth };
321069
321564
  return this.generate(tier, retryOpts, onChunk, requireVision);
@@ -321072,6 +321567,8 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
321072
321567
  throw err;
321073
321568
  } finally {
321074
321569
  releaseLocalSlot?.();
321570
+ releaseReservation?.();
321571
+ releaseReservation = void 0;
321075
321572
  }
321076
321573
  }
321077
321574
  getModelForTier(tier) {
@@ -321334,12 +321831,28 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
321334
321831
  return this.selector?.getAllAvailableModels() ?? [];
321335
321832
  }
321336
321833
  // ── Private ──────────────────────────────────
321834
+ /**
321835
+ * Why each configured provider failed its availability probe, kept so the
321836
+ * surfaces can say something better than "add an API key" to a user who
321837
+ * already has one. Cleared and refilled by each detectAvailableProviders().
321838
+ */
321839
+ probeFailures = /* @__PURE__ */ new Map();
321840
+ /**
321841
+ * What stopped the configured providers from being usable, if anything.
321842
+ * Empty when every configured provider passed — or when none was configured,
321843
+ * which is a different problem with a different answer.
321844
+ */
321845
+ providerProbeFailures() {
321846
+ return [...this.probeFailures].map(([provider, reason]) => ({ provider, reason }));
321847
+ }
321337
321848
  /** Logs why a configured provider failed its availability probe. */
321338
321849
  emitProbeFailure(type, reason) {
321850
+ this.probeFailures.set(type, reason);
321339
321851
  console.warn(`[router] provider "${type}" is not available: ${reason}`);
321340
321852
  }
321341
321853
  async detectAvailableProviders(configs) {
321342
321854
  const available = /* @__PURE__ */ new Set();
321855
+ this.probeFailures.clear();
321343
321856
  const checks = configs.map(async (cfg) => {
321344
321857
  try {
321345
321858
  const testModel = this.getAnyModelForProvider(cfg.type);
@@ -321349,7 +321862,13 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
321349
321862
  if (ok) available.add(cfg.type);
321350
321863
  else this.emitProbeFailure(cfg.type, "availability check returned false (bad key, wrong endpoint/deployment, or unreachable)");
321351
321864
  } catch (err) {
321352
- this.emitProbeFailure(cfg.type, err instanceof Error ? err.message : String(err));
321865
+ const reason = err instanceof Error ? err.message : String(err);
321866
+ if (err instanceof ProviderUnreachableError) {
321867
+ available.add(cfg.type);
321868
+ console.warn(`[router] provider "${cfg.type}" probe did not complete: ${reason} \u2014 continuing with it enabled`);
321869
+ return;
321870
+ }
321871
+ this.emitProbeFailure(cfg.type, reason);
321353
321872
  }
321354
321873
  });
321355
321874
  await Promise.allSettled(checks);
@@ -321442,7 +321961,14 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
321442
321961
  }
321443
321962
  return void 0;
321444
321963
  }
321445
- recordStats(tier, model, usage, featureTag) {
321964
+ /**
321965
+ * `runGeneration` is the generation the call was ADMITTED in — see
321966
+ * `generate()`. Session-wide accounting always applies (the spend is real
321967
+ * whenever it lands), but the per-RUN ceiling is only meaningful for the run
321968
+ * the call belongs to, so a straggler from a finished run is left out of it.
321969
+ * Omitted by the direct callers in tests, which have no run to straddle.
321970
+ */
321971
+ recordStats(tier, model, usage, featureTag, runGeneration) {
321446
321972
  this.stats.totalTokens += usage.totalTokens;
321447
321973
  this.stats.totalCostUsd += usage.estimatedCostUsd;
321448
321974
  this.sessionCostUsd += usage.estimatedCostUsd;
@@ -321467,10 +321993,13 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
321467
321993
  });
321468
321994
  }
321469
321995
  }
321470
- this.runTokens += usage.totalTokens;
321471
- this.runCostUsd += usage.estimatedCostUsd;
321996
+ const currentRun = runGeneration === void 0 || runGeneration === this.runGeneration;
321997
+ if (currentRun) {
321998
+ this.runTokens += usage.totalTokens;
321999
+ this.runCostUsd += usage.estimatedCostUsd;
322000
+ }
321472
322001
  this.updateBudgetState();
321473
- this.enforceRunBudget();
322002
+ if (currentRun) this.enforceRunBudget();
321474
322003
  }
321475
322004
  /**
321476
322005
  * Resets per-run accounting at the start of each `cascade run`. Session
@@ -321480,9 +322009,158 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
321480
322009
  beginRun() {
321481
322010
  this.runTokens = 0;
321482
322011
  this.runCostUsd = 0;
322012
+ this.reservedTokens = 0;
322013
+ this.reservedCostUsd = 0;
322014
+ this.runAbort = newRunAbort();
322015
+ this.runGeneration++;
321483
322016
  this.runBudgetExceeded = false;
321484
322017
  this.runBudgetExceededReason = void 0;
321485
322018
  }
322019
+ /**
322020
+ * Refuse a call whose input alone cannot fit what is left of the run budget.
322021
+ *
322022
+ * `enforceRunBudget()` below runs AFTER a call returns, which makes the caps
322023
+ * a stop rather than a pre-authorisation: the tokens are already bought by
322024
+ * the time it looks. That was tolerable while a prompt could not exceed
322025
+ * 20,000 characters, and stopped being so when that cap was removed in
322026
+ * 0.72.0 — a multi-megabyte prompt is billed in full on the very first call
322027
+ * (the complexity classifier sends the whole thing), and no ceiling
322028
+ * configured downstream can give that money back.
322029
+ *
322030
+ * Deliberately narrow, because a false refusal is worse than a late stop:
322031
+ *
322032
+ * • INPUT ONLY. What a call returns is not knowable in advance, and
322033
+ * charging a worst-case `maxTokens` of output against the budget would
322034
+ * decline runs that would have finished comfortably. Input is the part
322035
+ * that is already determined, and it is the part the removed cap used to
322036
+ * bound.
322037
+ * • Compared against what REMAINS (`cap - spent`), not the whole cap, so a
322038
+ * call is judged on the budget it can actually draw from.
322039
+ * • Skipped when the model has no usable price. An estimate cannot be made,
322040
+ * and refusing on ignorance would break every self-hosted and local model
322041
+ * the moment a cost cap is set. The post-hoc stop still applies there.
322042
+ * • Skipped entirely when no cap is configured — there is nothing to
322043
+ * pre-authorise against.
322044
+ *
322045
+ * The message names the cap AND the estimate, because "too expensive" with
322046
+ * no numbers gives the user nothing to change.
322047
+ */
322048
+ enforcePreflightBudget(model, options) {
322049
+ const budget = this.config?.budget;
322050
+ const maxCost = budget?.maxCostPerRunUsd;
322051
+ const maxTokens = budget?.maxTokensPerRun;
322052
+ if (maxCost == null && maxTokens == null) return void 0;
322053
+ const inputTokens = this.estimateInputTokens(options, model);
322054
+ if (maxTokens != null) {
322055
+ const remaining = maxTokens - this.runTokens - this.reservedTokens;
322056
+ if (inputTokens > remaining) {
322057
+ this.failPreflight(
322058
+ `This request's input is about ${inputTokens.toLocaleString()} tokens, but only ${Math.max(0, remaining).toLocaleString()} of the per-task cap of ${maxTokens.toLocaleString()} remain. Shorten the input, or raise budget.maxTokensPerRun.`
322059
+ );
322060
+ }
322061
+ }
322062
+ let estimatedUsd = 0;
322063
+ if (maxCost != null) {
322064
+ const { input: inputPer1k, unknown: unknown2 } = resolveModelPricing(model, { inputTokens });
322065
+ if (!unknown2 && inputPer1k > 0) {
322066
+ estimatedUsd = inputTokens / 1e3 * inputPer1k;
322067
+ const remaining = maxCost - this.runCostUsd - this.reservedCostUsd;
322068
+ if (estimatedUsd > remaining) {
322069
+ this.failPreflight(
322070
+ `Sending this request to ${model.provider}:${model.id} would cost about $${estimatedUsd.toFixed(4)} in input alone (~${inputTokens.toLocaleString()} tokens), and only $${Math.max(0, remaining).toFixed(4)} of the per-task cap of $${maxCost.toFixed(4)} remains. Shorten the input, choose a cheaper model, or raise budget.maxCostPerRunUsd.`
322071
+ );
322072
+ }
322073
+ }
322074
+ }
322075
+ this.reservedTokens += inputTokens;
322076
+ this.reservedCostUsd += estimatedUsd;
322077
+ const generation = this.runGeneration;
322078
+ let released = false;
322079
+ return () => {
322080
+ if (released || generation !== this.runGeneration) return;
322081
+ released = true;
322082
+ this.reservedTokens -= inputTokens;
322083
+ this.reservedCostUsd -= estimatedUsd;
322084
+ };
322085
+ }
322086
+ /**
322087
+ * What this call will send, in tokens.
322088
+ *
322089
+ * Text is the bulk of it, but not all of it: providers bill the serialized
322090
+ * TOOL DEFINITIONS on every native-tool call (Anthropic sends each name,
322091
+ * description and input schema), and an image is billed by its own size
322092
+ * rather than as the `[image]` placeholder `contentToText` reduces it to. A
322093
+ * tool-heavy or vision request could therefore pass either cap while its real
322094
+ * input already exceeded the allowance.
322095
+ */
322096
+ estimateInputTokens(options, model) {
322097
+ let tokens = guardTokens(options.systemPrompt ?? "");
322098
+ let images = 0;
322099
+ const wire = wireProfile(model.provider);
322100
+ const countsImage = (img) => wire.sendsUrlImages || img?.type === "base64";
322101
+ for (const message of options.messages) {
322102
+ if (wire.dropsMessage(message)) continue;
322103
+ tokens += TOKENS_PER_MESSAGE_FRAMING;
322104
+ if (typeof message.content === "string") {
322105
+ tokens += guardTokens(message.content);
322106
+ } else {
322107
+ switch (wire.blockHandling(message)) {
322108
+ case "blocks":
322109
+ for (const block of message.content) {
322110
+ if (!wire.sendsBlock(block)) continue;
322111
+ if (block.type === "image") {
322112
+ if (countsImage(block.image)) images++;
322113
+ continue;
322114
+ }
322115
+ tokens += guardTokens(contentToText([block]));
322116
+ }
322117
+ break;
322118
+ case "stringified":
322119
+ tokens += guardTokens(safeJson(message.content));
322120
+ break;
322121
+ }
322122
+ }
322123
+ if (message.toolCalls?.length && wire.sendsToolCalls(message)) {
322124
+ tokens += guardTokens(safeJson(wire.sizeToolCalls(message.toolCalls)));
322125
+ }
322126
+ if (message.toolCallId && message.role === "tool" && wire.sendsToolCallId) {
322127
+ tokens += guardTokens(message.toolCallId);
322128
+ }
322129
+ }
322130
+ if (options.tools?.length) {
322131
+ tokens += guardTokens(safeJson(wire.sizeTools(options.tools)));
322132
+ }
322133
+ if (wire.readsTopLevelImages) {
322134
+ const topLevel = (options.images ?? []).filter(countsImage).length;
322135
+ images += topLevel * geminiImageCopies(options.messages);
322136
+ }
322137
+ tokens += images * IMAGE_TOKENS_EACH;
322138
+ return tokens;
322139
+ }
322140
+ /**
322141
+ * Report a wait that ended because something asked it to stop, the same way
322142
+ * the provider call below reports one.
322143
+ *
322144
+ * The queue waits sit OUTSIDE the try/catch that maps aborts, so without
322145
+ * this a cancellation surfaced as whatever the queue happened to throw and
322146
+ * was retried or failed over like an ordinary error. A budget abort keeps
322147
+ * saying it ran out of budget — otherwise the reason the run stopped is lost
322148
+ * on the way up — and anything else that was aborted becomes a cancellation.
322149
+ */
322150
+ asAbortFailure(err, callSignal, runSignal) {
322151
+ if (err instanceof _CascadeRouter.BudgetExceededError) return err;
322152
+ if (err instanceof CascadeCancelledError) return err;
322153
+ if (callSignal?.aborted || runSignal?.aborted) return new CascadeCancelledError("Run cancelled");
322154
+ return err;
322155
+ }
322156
+ /** Trips the same run-level flag a post-hoc overrun does, and reports it the same way. */
322157
+ failPreflight(reason) {
322158
+ this.runBudgetExceeded = true;
322159
+ this.runBudgetExceededReason = reason;
322160
+ this.runAbort.abort(new _CascadeRouter.BudgetExceededError(reason));
322161
+ this.emit("budget:exceeded", { reason, spentUsd: this.sessionCostUsd });
322162
+ throw new _CascadeRouter.BudgetExceededError(reason);
322163
+ }
321486
322164
  /**
321487
322165
  * Enforce the hard per-task ceiling. Once tripped, the flag makes every
321488
322166
  * subsequent (and concurrent) generate() call in this run fail fast.
@@ -321498,6 +322176,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
321498
322176
  const reason = overTokens ? `Per-task token cap of ${maxTokens.toLocaleString()} reached (used ${this.runTokens.toLocaleString()}). Stopping this run to avoid runaway cost \u2014 raise budget.maxTokensPerRun for larger jobs.` : `Per-task cost cap of $${maxCost.toFixed(4)} reached (spent $${this.runCostUsd.toFixed(4)}). Stopping this run to avoid runaway cost.`;
321499
322177
  this.runBudgetExceeded = true;
321500
322178
  this.runBudgetExceededReason = reason;
322179
+ this.runAbort.abort(new _CascadeRouter.BudgetExceededError(reason));
321501
322180
  this.emit("budget:exceeded", { reason, spentUsd: this.sessionCostUsd });
321502
322181
  throw new _CascadeRouter.BudgetExceededError(reason);
321503
322182
  }
@@ -322734,6 +323413,12 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
322734
323413
  maxTokens: 4096,
322735
323414
  ...subtaskModel ? { model: subtaskModel } : {},
322736
323415
  featureTag: this.assignment?.sectionTitle,
323416
+ // The wave signal, so cancel-and-respawn reaches the request that is
323417
+ // RUNNING rather than only stopping the worker at its next checkpoint.
323418
+ // It is a superset of the run signal the router injects when this is
323419
+ // absent (T2 composes it from that signal plus its own per-wave one),
323420
+ // so wiring it here loses no cancellation and adds the wave's.
323421
+ ...this.signal ? { signal: this.signal } : {},
322737
323422
  ...this.localOnlyMatch ? { forceLocal: true } : {}
322738
323423
  };
322739
323424
  const result = await this.router.generate(
@@ -323212,6 +323897,7 @@ ${current}`
323212
323897
  HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
323213
323898
  maxTokens: 4096,
323214
323899
  featureTag: assignment.sectionTitle,
323900
+ ...this.signal ? { signal: this.signal } : {},
323215
323901
  ...this.localOnlyMatch ? { forceLocal: true } : {}
323216
323902
  });
323217
323903
  const next = (improved.content ?? "").trim();
@@ -323273,6 +323959,7 @@ Reply with JSON: { "completeness": "pass"|"fail", "correctness": "pass"|"fail",
323273
323959
 
323274
323960
  HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
323275
323961
  featureTag: assignment.sectionTitle,
323962
+ ...this.signal ? { signal: this.signal } : {},
323276
323963
  ...this.localOnlyMatch ? { forceLocal: true } : {}
323277
323964
  });
323278
323965
  try {
@@ -323312,6 +323999,7 @@ ${output.slice(0, 4e3)}`;
323312
323999
  messages: [{ role: "user", content: prompt }],
323313
324000
  maxTokens: 300,
323314
324001
  temperature: 0,
324002
+ ...this.signal ? { signal: this.signal } : {},
323315
324003
  ...this.localOnlyMatch ? { forceLocal: true } : {}
323316
324004
  });
323317
324005
  const match = /\[[\s\S]*\]/.exec(result.content);
@@ -323423,6 +324111,10 @@ Begin execution now.`;
323423
324111
  }
323424
324112
  };
323425
324113
  var PeerBus = class extends EventEmitter__default.default {
324114
+ constructor() {
324115
+ super();
324116
+ this.setMaxListeners(64);
324117
+ }
323426
324118
  outputs = /* @__PURE__ */ new Map();
323427
324119
  waiters = /* @__PURE__ */ new Map();
323428
324120
  members = /* @__PURE__ */ new Set();
@@ -324670,6 +325362,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
324670
325362
  const waveSignal = AbortSignal.any(
324671
325363
  [this.signal, this.waveAbortController.signal].filter(Boolean)
324672
325364
  );
325365
+ EventEmitter.setMaxListeners(64, waveSignal);
324673
325366
  const runOne = async (id) => {
324674
325367
  const assignment = sanitizedAssignments.find((a2) => a2.subtaskId === id);
324675
325368
  const worker = workerMap.get(id);
@@ -372617,115 +373310,6 @@ var GraphSearchTool = class extends BaseTool {
372617
373310
  }
372618
373311
  };
372619
373312
 
372620
- // src/core/context/compaction.ts
372621
- function estimateTokens(text) {
372622
- if (!text) return 0;
372623
- return Math.max(1, Math.ceil(text.length / 4));
372624
- }
372625
- function contentToText(content) {
372626
- if (typeof content === "string") return content;
372627
- return content.map((c4) => c4.type === "text" ? c4.text : c4.type === "tool_result" ? c4.content : `[${c4.type}]`).join(" ");
372628
- }
372629
- function messagesTokens(messages) {
372630
- return messages.reduce((n8, m3) => n8 + estimateTokens(contentToText(m3.content)), 0);
372631
- }
372632
- function atomize(text, maxChars) {
372633
- const seps = ["\n\n", "\n", ". ", " "];
372634
- let units = [text];
372635
- for (const sep3 of seps) {
372636
- if (units.every((u2) => u2.length <= maxChars)) break;
372637
- const next = [];
372638
- for (const u2 of units) {
372639
- if (u2.length <= maxChars) {
372640
- next.push(u2);
372641
- continue;
372642
- }
372643
- const parts = u2.split(sep3);
372644
- parts.forEach((p3, i4) => next.push(i4 < parts.length - 1 ? p3 + sep3 : p3));
372645
- }
372646
- units = next;
372647
- }
372648
- const out = [];
372649
- for (const u2 of units) {
372650
- if (u2.length <= maxChars) out.push(u2);
372651
- else for (let i4 = 0; i4 < u2.length; i4 += maxChars) out.push(u2.slice(i4, i4 + maxChars));
372652
- }
372653
- return out;
372654
- }
372655
- function chunkText(text, opts) {
372656
- const maxChars = Math.max(1, Math.floor(opts.maxTokens * 4));
372657
- const overlap = Math.min(maxChars - 1, Math.max(0, Math.floor(maxChars * (opts.overlapRatio ?? 0.1))));
372658
- const trimmed2 = text.trim();
372659
- if (!trimmed2) return [];
372660
- if (trimmed2.length <= maxChars) return [trimmed2];
372661
- const units = atomize(trimmed2, maxChars);
372662
- const chunks = [];
372663
- let cur = "";
372664
- for (const u2 of units) {
372665
- if (cur && cur.length + u2.length > maxChars) {
372666
- chunks.push(cur.trim());
372667
- cur = overlap > 0 ? cur.slice(-overlap) : "";
372668
- }
372669
- cur += u2;
372670
- }
372671
- if (cur.trim()) chunks.push(cur.trim());
372672
- return chunks;
372673
- }
372674
- function needsCompaction(totalTokens, contextWindow, reserve = 0.2) {
372675
- if (!contextWindow || contextWindow <= 0) return false;
372676
- return totalTokens > contextWindow * (1 - reserve);
372677
- }
372678
- async function mapReduceCompact(text, opts) {
372679
- const capChars = opts.capTokens * 4;
372680
- let working = text;
372681
- let truncated = false;
372682
- if (working.length > capChars) {
372683
- working = working.slice(0, capChars);
372684
- truncated = true;
372685
- }
372686
- const instruction = opts.instruction ?? "Summarize this content, preserving key facts, names, numbers, and decisions.";
372687
- let chunks = chunkText(working, { maxTokens: opts.chunkTokens });
372688
- if (chunks.length <= 1) return { text: working, chunks: chunks.length, calls: 0, truncated };
372689
- let calls = 0;
372690
- let parts = await Promise.all(
372691
- chunks.map((c4, i4) => {
372692
- calls++;
372693
- return opts.summarize(c4, `${instruction} (part ${i4 + 1} of ${chunks.length})`);
372694
- })
372695
- );
372696
- let combined = parts.join("\n\n");
372697
- const maxPasses = opts.maxReducePasses ?? 3;
372698
- for (let pass = 0; pass < maxPasses && estimateTokens(combined) > opts.targetTokens; pass++) {
372699
- const groups = chunkText(combined, { maxTokens: opts.chunkTokens });
372700
- if (groups.length <= 1) break;
372701
- parts = await Promise.all(
372702
- groups.map((g2) => {
372703
- calls++;
372704
- return opts.summarize(g2, "Merge these partial summaries into one cohesive summary; keep all distinct facts.");
372705
- })
372706
- );
372707
- combined = parts.join("\n\n");
372708
- }
372709
- const note = truncated ? "\n\n[Note: input exceeded the extended-context cap and was truncated.]" : "";
372710
- return { text: combined + note, chunks: chunks.length, calls, truncated };
372711
- }
372712
- async function rollingSummary(messages, opts) {
372713
- if (messages.length <= opts.keepRecent) return messages;
372714
- const older = messages.slice(0, messages.length - opts.keepRecent);
372715
- const recent = messages.slice(messages.length - opts.keepRecent);
372716
- const transcript = older.map((m3) => `${m3.role}: ${contentToText(m3.content)}`).join("\n");
372717
- const summary = await opts.summarize(
372718
- transcript,
372719
- "Summarize this earlier conversation. Preserve facts, decisions, names, numbers, and any open threads or todos."
372720
- );
372721
- const summaryMessage = {
372722
- role: "system",
372723
- content: `Summary of earlier conversation (folded to fit the context window):
372724
- ${summary}`
372725
- };
372726
- return [summaryMessage, ...recent];
372727
- }
372728
-
372729
373313
  // src/core/steering/guidance.ts
372730
373314
  var GuidanceQueue = class {
372731
373315
  entries = [];
@@ -374277,7 +374861,16 @@ ${finalOutput}` : ""
374277
374861
  const selector = this.router.getSelector();
374278
374862
  const model = options.fastAnswerModel ? selector.selectForTier(tier, options.fastAnswerModel) : selector.getCandidatesForTier(tier)[0] ?? selector.selectForTier(tier);
374279
374863
  if (!model) {
374280
- throw new Error("No model is available for a fast answer \u2014 add a provider API key first.");
374864
+ const failures2 = this.router.providerProbeFailures();
374865
+ const context = failures2.length ? ` Configured providers: ${failures2.map((f4) => `${f4.provider}: ${f4.reason}`).join("; ")}` : "";
374866
+ if (options.fastAnswerModel) {
374867
+ throw new Error(
374868
+ `No model is available for a fast answer: the pinned model "${options.fastAnswerModel}" could not be resolved \u2014 check it is spelled correctly and served by a configured provider.${context}`
374869
+ );
374870
+ }
374871
+ throw new Error(
374872
+ failures2.length ? `No model is available for a fast answer.${context}` : "No model is available for a fast answer \u2014 add a provider API key first."
374873
+ );
374281
374874
  }
374282
374875
  this.recordDecision(
374283
374876
  "model",
@@ -378652,10 +379245,10 @@ ${cur}`;
378652
379245
  }
378653
379246
 
378654
379247
  // src/retrieval/plan.ts
378655
- var CHARS_PER_TOKEN = 4;
379248
+ var CHARS_PER_TOKEN2 = 4;
378656
379249
  function cagCharBudget(contextWindowTokens, opts = {}) {
378657
379250
  const docFraction = opts.docFraction ?? 0.5;
378658
- const charsPerToken = opts.charsPerToken ?? CHARS_PER_TOKEN;
379251
+ const charsPerToken = opts.charsPerToken ?? CHARS_PER_TOKEN2;
378659
379252
  return Math.max(0, Math.floor(contextWindowTokens * docFraction * charsPerToken));
378660
379253
  }
378661
379254
  function planRetrieval(ctx) {
@@ -379659,7 +380252,7 @@ exports.CASCADE_IGNORE_FILE = CASCADE_IGNORE_FILE;
379659
380252
  exports.CASCADE_KEYSTORE_FILE = CASCADE_KEYSTORE_FILE;
379660
380253
  exports.CASCADE_MD_FILE = CASCADE_MD_FILE;
379661
380254
  exports.CASCADE_VERSION = CASCADE_VERSION;
379662
- exports.CHARS_PER_TOKEN = CHARS_PER_TOKEN;
380255
+ exports.CHARS_PER_TOKEN = CHARS_PER_TOKEN2;
379663
380256
  exports.COMPLEXITY_T2_COUNT = COMPLEXITY_T2_COUNT;
379664
380257
  exports.Cascade = Cascade;
379665
380258
  exports.CascadeCancelledError = CascadeCancelledError;