@prestyj/ai 4.6.4 → 4.10.1

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/index.d.cts CHANGED
@@ -255,8 +255,9 @@ declare class StreamResult implements AsyncIterable<StreamEvent> {
255
255
  private resolveResponse;
256
256
  private rejectResponse;
257
257
  private resolveWait;
258
- constructor(generator: AsyncGenerator<StreamEvent, StreamResponse>);
258
+ constructor(generator: AsyncGenerator<StreamEvent, StreamResponse>, signal?: AbortSignal);
259
259
  private pump;
260
+ private _nextWithAbort;
260
261
  [Symbol.asyncIterator](): AsyncIterator<StreamEvent>;
261
262
  then<TResult1 = StreamResponse, TResult2 = never>(onfulfilled?: ((value: StreamResponse) => TResult1 | PromiseLike<TResult1>) | null, onrejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null): Promise<TResult1 | TResult2>;
262
263
  }
@@ -414,6 +415,19 @@ declare function formatError(err: unknown): FormattedError;
414
415
  */
415
416
  declare function formatErrorForDisplay(err: unknown): string;
416
417
 
418
+ /**
419
+ * Inspect a raw provider error message and tag it with a clearer, actionable
420
+ * prefix so a worker orchestrator can route on intent instead of regexing JSON.
421
+ * Preserves the original message verbatim after the prefix — helpful for
422
+ * debugging.
423
+ *
424
+ * Order matters: context-overflow is checked first because some providers wrap
425
+ * overflow errors in HTTP 429 envelopes; we want the structural meaning, not
426
+ * the transport status. Billing comes before auth/rate-limit because "402
427
+ * Payment Required" must not be mis-routed as a rate-limit retry.
428
+ */
429
+ declare function classifyProviderError(message: string): string;
430
+
417
431
  /**
418
432
  * Provider-level diagnostic hook. Mirrors the pattern used by gg-agent's
419
433
  * setStreamDiagnostic — the host app wires a callback (typically writing to
@@ -506,4 +520,4 @@ interface PalsuProviderConfig {
506
520
  */
507
521
  declare function registerPalsuProvider(config?: PalsuProviderConfig): PalsuProviderHandle;
508
522
 
509
- export { type AssistantMessage, type CacheRetention, type ContentPart, type DoneEvent, EZCoderAIError, type ErrorEvent, type ErrorSource, EventStream, type FormattedError, type ImageContent, type Message, type PalsuModelConfig, type PalsuModelHandle, type PalsuProviderConfig, type PalsuProviderHandle, type PalsuProviderState, type PalsuResponse, type PalsuResponseFactory, type Provider, type ProviderDiagnosticFn, type ProviderEntry, ProviderError, type ProviderStreamFn, type RawContent, type ServerToolCall, type ServerToolCallEvent, type ServerToolDefinition, type ServerToolResult, type ServerToolResultEvent, type StopReason, type StreamEvent, type StreamOptions, type StreamResponse, StreamResult, type SystemMessage, type TextContent, type TextDeltaEvent, type ThinkingContent, type ThinkingDeltaEvent, type ThinkingLevel, type Tool, type ToolCall, type ToolCallDeltaEvent, type ToolCallDoneEvent, type ToolChoice, type ToolResult, type ToolResultContent, type ToolResultMessage, type Usage, type UserMessage, type VideoContent, formatError, formatErrorForDisplay, isHardBillingMessage, isUsageLimitError, palsuAssistantMessage, palsuText, palsuThinking, palsuToolCall, providerRegistry, registerPalsuProvider, setProviderDiagnostic, stream, toAnthropicMessages, toOpenAIMessages };
523
+ export { type AssistantMessage, type CacheRetention, type ContentPart, type DoneEvent, EZCoderAIError, type ErrorEvent, type ErrorSource, EventStream, type FormattedError, type ImageContent, type Message, type PalsuModelConfig, type PalsuModelHandle, type PalsuProviderConfig, type PalsuProviderHandle, type PalsuProviderState, type PalsuResponse, type PalsuResponseFactory, type Provider, type ProviderDiagnosticFn, type ProviderEntry, ProviderError, type ProviderStreamFn, type RawContent, type ServerToolCall, type ServerToolCallEvent, type ServerToolDefinition, type ServerToolResult, type ServerToolResultEvent, type StopReason, type StreamEvent, type StreamOptions, type StreamResponse, StreamResult, type SystemMessage, type TextContent, type TextDeltaEvent, type ThinkingContent, type ThinkingDeltaEvent, type ThinkingLevel, type Tool, type ToolCall, type ToolCallDeltaEvent, type ToolCallDoneEvent, type ToolChoice, type ToolResult, type ToolResultContent, type ToolResultMessage, type Usage, type UserMessage, type VideoContent, classifyProviderError, formatError, formatErrorForDisplay, isHardBillingMessage, isUsageLimitError, palsuAssistantMessage, palsuText, palsuThinking, palsuToolCall, providerRegistry, registerPalsuProvider, setProviderDiagnostic, stream, toAnthropicMessages, toOpenAIMessages };
package/dist/index.d.ts CHANGED
@@ -255,8 +255,9 @@ declare class StreamResult implements AsyncIterable<StreamEvent> {
255
255
  private resolveResponse;
256
256
  private rejectResponse;
257
257
  private resolveWait;
258
- constructor(generator: AsyncGenerator<StreamEvent, StreamResponse>);
258
+ constructor(generator: AsyncGenerator<StreamEvent, StreamResponse>, signal?: AbortSignal);
259
259
  private pump;
260
+ private _nextWithAbort;
260
261
  [Symbol.asyncIterator](): AsyncIterator<StreamEvent>;
261
262
  then<TResult1 = StreamResponse, TResult2 = never>(onfulfilled?: ((value: StreamResponse) => TResult1 | PromiseLike<TResult1>) | null, onrejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null): Promise<TResult1 | TResult2>;
262
263
  }
@@ -414,6 +415,19 @@ declare function formatError(err: unknown): FormattedError;
414
415
  */
415
416
  declare function formatErrorForDisplay(err: unknown): string;
416
417
 
418
+ /**
419
+ * Inspect a raw provider error message and tag it with a clearer, actionable
420
+ * prefix so a worker orchestrator can route on intent instead of regexing JSON.
421
+ * Preserves the original message verbatim after the prefix — helpful for
422
+ * debugging.
423
+ *
424
+ * Order matters: context-overflow is checked first because some providers wrap
425
+ * overflow errors in HTTP 429 envelopes; we want the structural meaning, not
426
+ * the transport status. Billing comes before auth/rate-limit because "402
427
+ * Payment Required" must not be mis-routed as a rate-limit retry.
428
+ */
429
+ declare function classifyProviderError(message: string): string;
430
+
417
431
  /**
418
432
  * Provider-level diagnostic hook. Mirrors the pattern used by gg-agent's
419
433
  * setStreamDiagnostic — the host app wires a callback (typically writing to
@@ -506,4 +520,4 @@ interface PalsuProviderConfig {
506
520
  */
507
521
  declare function registerPalsuProvider(config?: PalsuProviderConfig): PalsuProviderHandle;
508
522
 
509
- export { type AssistantMessage, type CacheRetention, type ContentPart, type DoneEvent, EZCoderAIError, type ErrorEvent, type ErrorSource, EventStream, type FormattedError, type ImageContent, type Message, type PalsuModelConfig, type PalsuModelHandle, type PalsuProviderConfig, type PalsuProviderHandle, type PalsuProviderState, type PalsuResponse, type PalsuResponseFactory, type Provider, type ProviderDiagnosticFn, type ProviderEntry, ProviderError, type ProviderStreamFn, type RawContent, type ServerToolCall, type ServerToolCallEvent, type ServerToolDefinition, type ServerToolResult, type ServerToolResultEvent, type StopReason, type StreamEvent, type StreamOptions, type StreamResponse, StreamResult, type SystemMessage, type TextContent, type TextDeltaEvent, type ThinkingContent, type ThinkingDeltaEvent, type ThinkingLevel, type Tool, type ToolCall, type ToolCallDeltaEvent, type ToolCallDoneEvent, type ToolChoice, type ToolResult, type ToolResultContent, type ToolResultMessage, type Usage, type UserMessage, type VideoContent, formatError, formatErrorForDisplay, isHardBillingMessage, isUsageLimitError, palsuAssistantMessage, palsuText, palsuThinking, palsuToolCall, providerRegistry, registerPalsuProvider, setProviderDiagnostic, stream, toAnthropicMessages, toOpenAIMessages };
523
+ export { type AssistantMessage, type CacheRetention, type ContentPart, type DoneEvent, EZCoderAIError, type ErrorEvent, type ErrorSource, EventStream, type FormattedError, type ImageContent, type Message, type PalsuModelConfig, type PalsuModelHandle, type PalsuProviderConfig, type PalsuProviderHandle, type PalsuProviderState, type PalsuResponse, type PalsuResponseFactory, type Provider, type ProviderDiagnosticFn, type ProviderEntry, ProviderError, type ProviderStreamFn, type RawContent, type ServerToolCall, type ServerToolCallEvent, type ServerToolDefinition, type ServerToolResult, type ServerToolResultEvent, type StopReason, type StreamEvent, type StreamOptions, type StreamResponse, StreamResult, type SystemMessage, type TextContent, type TextDeltaEvent, type ThinkingContent, type ThinkingDeltaEvent, type ThinkingLevel, type Tool, type ToolCall, type ToolCallDeltaEvent, type ToolCallDoneEvent, type ToolChoice, type ToolResult, type ToolResultContent, type ToolResultMessage, type Usage, type UserMessage, type VideoContent, classifyProviderError, formatError, formatErrorForDisplay, isHardBillingMessage, isUsageLimitError, palsuAssistantMessage, palsuText, palsuThinking, palsuToolCall, providerRegistry, registerPalsuProvider, setProviderDiagnostic, stream, toAnthropicMessages, toOpenAIMessages };
package/dist/index.js CHANGED
@@ -84,10 +84,25 @@ function formatResetTime(resetsAt) {
84
84
  minute: "2-digit"
85
85
  });
86
86
  }
87
+ function isMythosAccessError(message) {
88
+ const lower = message.toLowerCase();
89
+ return lower.includes("mythos") && (lower.includes("not_found") || lower.includes("not found") || lower.includes("no access"));
90
+ }
87
91
  function formatError(err) {
88
92
  if (err instanceof ProviderError) {
89
93
  const name = providerDisplayName(err.provider);
90
94
  const cleanMessage = cleanProviderMessage(err.message);
95
+ if (isMythosAccessError(cleanMessage)) {
96
+ return {
97
+ headline: "Claude Mythos 5 is invitation-only.",
98
+ source: "provider",
99
+ message: "Your Anthropic account isn't approved for Project Glasswing, so the API reports the model as not found.",
100
+ provider: err.provider,
101
+ statusCode: err.statusCode,
102
+ ...err.requestId ? { requestId: err.requestId } : {},
103
+ guidance: "Request access via your Anthropic account team (see platform.claude.com/docs/en/about-claude/models/overview), or switch to claude-fable-5 with /model \u2014 same underlying model, generally available."
104
+ };
105
+ }
91
106
  if (isUsageLimitError(err)) {
92
107
  const resetClause = err.resetsAt ? ` It resets at ${formatResetTime(err.resetsAt)}.` : "";
93
108
  return {
@@ -274,21 +289,21 @@ var StreamResult = class {
274
289
  resolveResponse;
275
290
  rejectResponse;
276
291
  resolveWait = null;
277
- constructor(generator) {
292
+ constructor(generator, signal) {
278
293
  this.response = new Promise((resolve, reject) => {
279
294
  this.resolveResponse = resolve;
280
295
  this.rejectResponse = reject;
281
296
  });
282
- this.pump(generator);
297
+ this.pump(generator, signal);
283
298
  }
284
- async pump(generator) {
299
+ async pump(generator, signal) {
285
300
  try {
286
- let next = await generator.next();
301
+ let next = await this._nextWithAbort(generator, signal);
287
302
  while (!next.done) {
288
303
  this.buffer.push(next.value);
289
304
  this.resolveWait?.();
290
305
  this.resolveWait = null;
291
- next = await generator.next();
306
+ next = await this._nextWithAbort(generator, signal);
292
307
  }
293
308
  this.done = true;
294
309
  this.resolveResponse(next.value);
@@ -303,6 +318,28 @@ var StreamResult = class {
303
318
  this.resolveWait = null;
304
319
  }
305
320
  }
321
+ async _nextWithAbort(generator, signal) {
322
+ if (!signal) {
323
+ return generator.next();
324
+ }
325
+ if (signal.aborted) {
326
+ return Promise.reject(new DOMException("Aborted", "AbortError"));
327
+ }
328
+ let onAbort;
329
+ const abortPromise = new Promise((_, reject) => {
330
+ onAbort = () => {
331
+ generator.return?.(void 0).catch(() => {
332
+ });
333
+ reject(new DOMException("Aborted", "AbortError"));
334
+ };
335
+ signal.addEventListener("abort", onAbort, { once: true });
336
+ });
337
+ try {
338
+ return await Promise.race([generator.next(), abortPromise]);
339
+ } finally {
340
+ if (onAbort) signal.removeEventListener("abort", onAbort);
341
+ }
342
+ }
306
343
  async *[Symbol.asyncIterator]() {
307
344
  let index = 0;
308
345
  while (true) {
@@ -399,6 +436,29 @@ function isRawThinking(part) {
399
436
  const t = part.data.type;
400
437
  return t === "thinking" || t === "redacted_thinking";
401
438
  }
439
+ var ANTHROPIC_INPUT_BLOCK_TYPES = /* @__PURE__ */ new Set([
440
+ "bash_code_execution_tool_result",
441
+ "code_execution_tool_result",
442
+ "connector_text",
443
+ "container_upload",
444
+ "document",
445
+ "image",
446
+ "mid_conv_system",
447
+ "redacted_thinking",
448
+ "search_result",
449
+ "server_tool_use",
450
+ "text",
451
+ "text_editor_code_execution_tool_result",
452
+ "thinking",
453
+ "tool_result",
454
+ "tool_search_tool_result",
455
+ "tool_use",
456
+ "web_fetch_tool_result",
457
+ "web_search_tool_result"
458
+ ]);
459
+ function isAnthropicCompatibleRaw(part) {
460
+ return ANTHROPIC_INPUT_BLOCK_TYPES.has(part.data.type);
461
+ }
402
462
  function isPositionSensitiveThinking(part) {
403
463
  if (part.type === "thinking") return hasValidThinkingSignature(part);
404
464
  return isRawThinking(part);
@@ -425,7 +485,8 @@ function toAnthropicAssistantPart(part, idMap) {
425
485
  };
426
486
  if (part.type === "server_tool_result")
427
487
  return part.data;
428
- if (part.type === "raw") return part.data;
488
+ if (part.type === "raw")
489
+ return isAnthropicCompatibleRaw(part) ? part.data : null;
429
490
  return null;
430
491
  }
431
492
  function toAnthropicAssistantContent(content, preserveThinking, idMap) {
@@ -684,12 +745,12 @@ function toAnthropicToolChoice(choice) {
684
745
  return { type: "tool", name: choice.name };
685
746
  }
686
747
  function isAdaptiveThinkingModel(model) {
687
- return /fable-5|opus-4[-.]8|opus-4[-.]7|opus-4[-.]6|sonnet-4[-.]6/.test(model);
748
+ return /opus-4[-.]8|opus-4[-.]7|opus-4[-.]6|sonnet-4[-.]6|fable-5|mythos-5/.test(model);
688
749
  }
689
750
  function toAnthropicThinking(level, maxTokens, model) {
690
751
  if (isAdaptiveThinkingModel(model)) {
691
752
  let effort = level;
692
- if (effort === "xhigh" && !/opus-4-8|opus-4-7|fable-5/.test(model)) {
753
+ if (effort === "xhigh" && !/opus-4-8|opus-4-7/.test(model)) {
693
754
  effort = "high";
694
755
  }
695
756
  return {
@@ -919,10 +980,10 @@ function createClient(options) {
919
980
  ...isOAuth ? { apiKey: null, authToken: options.apiKey } : { apiKey: options.apiKey },
920
981
  ...options.baseUrl ? { baseURL: options.baseUrl } : {},
921
982
  ...options.fetch ? { fetch: options.fetch } : {},
922
- // Allow SDK retries for connection-level failures (socket hang up, 500s,
923
- // connection refused). Our stall detection handles abort-initiated retries
924
- // separately SDK retries only fire on genuine transport errors.
925
- maxRetries: 2,
983
+ // Disable SDK retries the agent loop has its own stall/overload retry
984
+ // logic that surfaces errors properly. SDK retries on 429s can cause
985
+ // multi-minute hangs when the provider stops responding mid-retry.
986
+ maxRetries: 0,
926
987
  ...isOAuth ? {
927
988
  defaultHeaders: {
928
989
  // Anthropic's OAuth edge validates the claude-cli version. Callers
@@ -935,7 +996,7 @@ function createClient(options) {
935
996
  });
936
997
  }
937
998
  function streamAnthropic(options) {
938
- return new StreamResult(runStream(options));
999
+ return new StreamResult(runStream(options), options.signal);
939
1000
  }
940
1001
  async function* runStream(options) {
941
1002
  const client = createClient(options);
@@ -1030,7 +1091,6 @@ async function* runStream(options) {
1030
1091
  throw toError(err);
1031
1092
  }
1032
1093
  }
1033
- const stream2 = client.messages.stream(params, requestOptions);
1034
1094
  const contentParts = [];
1035
1095
  const blocks = /* @__PURE__ */ new Map();
1036
1096
  let inputTokens = 0;
@@ -1039,8 +1099,14 @@ async function* runStream(options) {
1039
1099
  let cacheWrite;
1040
1100
  let stopReason = null;
1041
1101
  const keepalive = { type: "keepalive" };
1102
+ let receivedAnyEvent = false;
1042
1103
  try {
1104
+ const stream2 = await client.messages.create(
1105
+ params,
1106
+ requestOptions
1107
+ );
1043
1108
  for await (const event of stream2) {
1109
+ receivedAnyEvent = true;
1044
1110
  switch (event.type) {
1045
1111
  case "message_start": {
1046
1112
  const usage = event.message.usage;
@@ -1077,7 +1143,7 @@ async function* runStream(options) {
1077
1143
  accum.toolId = block.id;
1078
1144
  accum.toolName = block.name;
1079
1145
  accum.input = block.input;
1080
- } else if (block.type === "redacted_thinking") {
1146
+ } else if (block.type !== "text" && block.type !== "thinking") {
1081
1147
  accum.raw = block;
1082
1148
  }
1083
1149
  blocks.set(idx, accum);
@@ -1174,8 +1240,7 @@ async function* runStream(options) {
1174
1240
  contentParts.push({ type: "raw", data: accum.raw });
1175
1241
  yield keepalive;
1176
1242
  } else {
1177
- const msg = stream2.currentMessage;
1178
- const rawBlock = msg?.content[event.index];
1243
+ const rawBlock = accum.raw;
1179
1244
  if (rawBlock) {
1180
1245
  const blockType = rawBlock.type;
1181
1246
  if (blockType === "web_search_tool_result") {
@@ -1221,6 +1286,11 @@ async function* runStream(options) {
1221
1286
  } catch (err) {
1222
1287
  throw toError(err);
1223
1288
  }
1289
+ if (!receivedAnyEvent) {
1290
+ throw new ProviderError("anthropic", "Stream ended without producing any events.", {
1291
+ statusCode: 504
1292
+ });
1293
+ }
1224
1294
  const normalizedStop = normalizeAnthropicStopReason(stopReason);
1225
1295
  const response = {
1226
1296
  message: {
@@ -1512,7 +1582,7 @@ function createClient2(options) {
1512
1582
  });
1513
1583
  }
1514
1584
  function streamOpenAI(options) {
1515
- return new StreamResult(runStream2(options));
1585
+ return new StreamResult(runStream2(options), options.signal);
1516
1586
  }
1517
1587
  async function* runStream2(options) {
1518
1588
  const providerName = options.provider ?? "openai";
@@ -1604,51 +1674,62 @@ async function* runStream2(options) {
1604
1674
  let outputTokens = 0;
1605
1675
  let cacheRead = 0;
1606
1676
  let finishReason = null;
1607
- for await (const chunk of stream2) {
1608
- const choice = chunk.choices?.[0];
1609
- if (chunk.usage) {
1610
- ({ inputTokens, outputTokens, cacheRead } = extractOpenAIUsage(chunk.usage));
1611
- }
1612
- if (!choice) continue;
1613
- if (choice.finish_reason) {
1614
- finishReason = choice.finish_reason;
1615
- }
1616
- const delta = choice.delta;
1617
- const reasoningContent = delta.reasoning_content;
1618
- if (typeof reasoningContent === "string" && reasoningContent) {
1619
- thinkingAccum += reasoningContent;
1620
- if (options.thinking) {
1621
- yield { type: "thinking_delta", text: reasoningContent };
1677
+ let receivedAnyChunk = false;
1678
+ try {
1679
+ for await (const chunk of stream2) {
1680
+ receivedAnyChunk = true;
1681
+ const choice = chunk.choices?.[0];
1682
+ if (chunk.usage) {
1683
+ ({ inputTokens, outputTokens, cacheRead } = extractOpenAIUsage(chunk.usage));
1622
1684
  }
1623
- }
1624
- if (delta.content) {
1625
- textAccum += delta.content;
1626
- yield { type: "text_delta", text: delta.content };
1627
- }
1628
- if (delta.tool_calls) {
1629
- for (const tc of delta.tool_calls) {
1630
- let accum = toolCallAccum.get(tc.index);
1631
- if (!accum) {
1632
- accum = {
1633
- id: tc.id ?? "",
1634
- name: tc.function?.name ?? "",
1635
- argsJson: ""
1636
- };
1637
- toolCallAccum.set(tc.index, accum);
1685
+ if (!choice) continue;
1686
+ if (choice.finish_reason) {
1687
+ finishReason = choice.finish_reason;
1688
+ }
1689
+ const delta = choice.delta;
1690
+ const reasoningContent = delta.reasoning_content;
1691
+ if (typeof reasoningContent === "string" && reasoningContent) {
1692
+ thinkingAccum += reasoningContent;
1693
+ if (options.thinking) {
1694
+ yield { type: "thinking_delta", text: reasoningContent };
1638
1695
  }
1639
- if (tc.id) accum.id = tc.id;
1640
- if (tc.function?.name) accum.name = tc.function.name;
1641
- if (tc.function?.arguments) {
1642
- accum.argsJson += tc.function.arguments;
1643
- yield {
1644
- type: "toolcall_delta",
1645
- id: accum.id,
1646
- name: accum.name,
1647
- argsJson: tc.function.arguments
1648
- };
1696
+ }
1697
+ if (delta.content) {
1698
+ textAccum += delta.content;
1699
+ yield { type: "text_delta", text: delta.content };
1700
+ }
1701
+ if (delta.tool_calls) {
1702
+ for (const tc of delta.tool_calls) {
1703
+ let accum = toolCallAccum.get(tc.index);
1704
+ if (!accum) {
1705
+ accum = {
1706
+ id: tc.id ?? "",
1707
+ name: tc.function?.name ?? "",
1708
+ argsJson: ""
1709
+ };
1710
+ toolCallAccum.set(tc.index, accum);
1711
+ }
1712
+ if (tc.id) accum.id = tc.id;
1713
+ if (tc.function?.name) accum.name = tc.function.name;
1714
+ if (tc.function?.arguments) {
1715
+ accum.argsJson += tc.function.arguments;
1716
+ yield {
1717
+ type: "toolcall_delta",
1718
+ id: accum.id,
1719
+ name: accum.name,
1720
+ argsJson: tc.function.arguments
1721
+ };
1722
+ }
1649
1723
  }
1650
1724
  }
1651
1725
  }
1726
+ } catch (err) {
1727
+ throw toError2(err, providerName);
1728
+ }
1729
+ if (!receivedAnyChunk) {
1730
+ throw new ProviderError(providerName, "Stream ended without producing any chunks.", {
1731
+ statusCode: 504
1732
+ });
1652
1733
  }
1653
1734
  if (thinkingAccum) {
1654
1735
  contentParts.push({ type: "thinking", text: thinkingAccum });
@@ -1891,7 +1972,7 @@ function isVisibleOutputItem(itemType) {
1891
1972
  return itemType === "message";
1892
1973
  }
1893
1974
  function streamOpenAICodex(options) {
1894
- return new StreamResult(runStream3(options));
1975
+ return new StreamResult(runStream3(options), options.signal);
1895
1976
  }
1896
1977
  async function* runStream3(options) {
1897
1978
  const baseUrl = (options.baseUrl || DEFAULT_BASE_URL).replace(/\/+$/, "");
@@ -2221,10 +2302,16 @@ async function* parseSSE(body) {
2221
2302
  }
2222
2303
  }
2223
2304
  function remapCodexId(id, idMap) {
2224
- if (id.startsWith("fc_") || id.startsWith("fc-")) return id;
2225
2305
  const existing = idMap.get(id);
2226
2306
  if (existing) return existing;
2227
- const mapped = `fc_${id.replace(/^toolu_/, "")}`;
2307
+ const withPrefix = id.startsWith("fc_") || id.startsWith("fc-") ? id : `fc_${id.replace(/^toolu_/, "")}`;
2308
+ const sanitized = withPrefix.replace(/[^A-Za-z0-9_-]/g, "_");
2309
+ let mapped = sanitized;
2310
+ let suffix = 2;
2311
+ const used = new Set(idMap.values());
2312
+ while (used.has(mapped)) {
2313
+ mapped = `${sanitized}_${suffix++}`;
2314
+ }
2228
2315
  idMap.set(id, mapped);
2229
2316
  return mapped;
2230
2317
  }
@@ -2747,7 +2834,7 @@ async function fetchCodeAssistWithRetry(plan, options) {
2747
2834
  throw lastError ?? new ProviderError("gemini", "Gemini Code Assist request failed.");
2748
2835
  }
2749
2836
  function streamGemini(options) {
2750
- return new StreamResult(runStream4(options));
2837
+ return new StreamResult(runStream4(options), options.signal);
2751
2838
  }
2752
2839
  async function* runStream4(options) {
2753
2840
  const useStreaming = options.streaming !== false;
@@ -2967,6 +3054,96 @@ function messagesContainVideo(messages) {
2967
3054
  return false;
2968
3055
  }
2969
3056
 
3057
+ // src/error-classification.ts
3058
+ var CONTEXT_OVERFLOW_PATTERNS = [
3059
+ /context_length_exceeded/i,
3060
+ /context length exceeded/i,
3061
+ /context window/i,
3062
+ // OpenAI Codex / Responses
3063
+ /maximum context length/i,
3064
+ // OpenAI / OpenRouter / Mistral
3065
+ /prompt is too long/i,
3066
+ // Anthropic
3067
+ /request_too_large/i,
3068
+ // Anthropic HTTP 413
3069
+ /input is too long/i,
3070
+ // Bedrock
3071
+ /input token count.*exceeds the maximum/i,
3072
+ // Gemini
3073
+ /maximum prompt length/i,
3074
+ // xAI / Grok
3075
+ /reduce the length of the messages/i,
3076
+ // Groq
3077
+ /too large for model/i,
3078
+ // Mistral
3079
+ /token limit/i
3080
+ // generic
3081
+ ];
3082
+ var RATE_LIMIT_PATTERNS = [
3083
+ /rate[ _-]?limit/i,
3084
+ /\b429\b/,
3085
+ /too many requests/i,
3086
+ /tokens per minute/i,
3087
+ /requests per minute/i
3088
+ ];
3089
+ var PROVIDER_TRANSIENT_PATTERNS = [
3090
+ /\b5\d\d\b/,
3091
+ /api_error/i,
3092
+ /server_error/i,
3093
+ /internal server error/i,
3094
+ /bad gateway/i,
3095
+ /service unavailable/i,
3096
+ /gateway timeout/i,
3097
+ /overloaded/i,
3098
+ /\b529\b/
3099
+ ];
3100
+ var BILLING_PATTERNS = [
3101
+ /payment required/i,
3102
+ /\b402\b/,
3103
+ /quota_exceeded/i,
3104
+ // underscore variant not in isHardBillingMessage
3105
+ /credit balance/i
3106
+ ];
3107
+ var AUTH_PATTERNS = [
3108
+ /invalid[ _]api[ _]key/i,
3109
+ /unauthorized/i,
3110
+ /\b401\b/,
3111
+ /authentication[ _]failed/i,
3112
+ /please run \/login/i
3113
+ // Anthropic Claude Code-style hint
3114
+ ];
3115
+ function matchesAny(message, patterns) {
3116
+ return patterns.some((p) => p.test(message));
3117
+ }
3118
+ function classifyProviderError(message) {
3119
+ if (matchesAny(message, CONTEXT_OVERFLOW_PATTERNS)) {
3120
+ return `[context_overflow] Worker context window exceeded \u2014 the conversation is too large to continue. Recovery: call reset_worker(project) to wipe history, then re-prompt with the task. Re-prompting WITHOUT reset will fail the same way.
3121
+
3122
+ Original: ${message}`;
3123
+ }
3124
+ if (isHardBillingMessage(message) || matchesAny(message, BILLING_PATTERNS)) {
3125
+ return `[billing] Provider billing/quota issue. Recovery: surface to the user \u2014 they need to top up or switch providers. Do NOT retry.
3126
+
3127
+ Original: ${message}`;
3128
+ }
3129
+ if (matchesAny(message, AUTH_PATTERNS)) {
3130
+ return `[auth] Provider authentication failed. Recovery: surface to the user \u2014 they need to re-login. Do NOT retry.
3131
+
3132
+ Original: ${message}`;
3133
+ }
3134
+ if (matchesAny(message, RATE_LIMIT_PATTERNS)) {
3135
+ return `[rate_limited] Provider rate limit hit. Recovery: wait ~30s, then re-prompt the same worker (no reset needed).
3136
+
3137
+ Original: ${message}`;
3138
+ }
3139
+ if (matchesAny(message, PROVIDER_TRANSIENT_PATTERNS)) {
3140
+ return `[provider_transient] Provider server-side/transient error. Recovery: wait briefly, then re-prompt the same worker (no reset needed). If it keeps happening, switch models/providers or check provider status.
3141
+
3142
+ Original: ${message}`;
3143
+ }
3144
+ return message;
3145
+ }
3146
+
2970
3147
  // src/providers/palsu.ts
2971
3148
  function palsuText(text) {
2972
3149
  return { role: "assistant", content: text ? [{ type: "text", text }] : [] };
@@ -3113,7 +3290,7 @@ function registerPalsuProvider(config) {
3113
3290
  const stopReason = explicitStop ?? (hasToolCalls ? "tool_use" : "end_turn");
3114
3291
  return yield* simulateStream(message, stopReason, options.signal, cacheUsage);
3115
3292
  })();
3116
- return new StreamResult(gen);
3293
+ return new StreamResult(gen, options.signal);
3117
3294
  }
3118
3295
  });
3119
3296
  return handle;
@@ -3123,6 +3300,7 @@ export {
3123
3300
  EventStream,
3124
3301
  ProviderError,
3125
3302
  StreamResult,
3303
+ classifyProviderError,
3126
3304
  formatError,
3127
3305
  formatErrorForDisplay,
3128
3306
  isHardBillingMessage,