@prestyj/ai 4.6.3 → 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/README.md CHANGED
@@ -43,7 +43,7 @@ Tool parameters are Zod schemas. Converted to JSON Schema at the provider bounda
43
43
  | `anthropic` | Claude Fable 5, Opus 4.8, Sonnet 4.6, Haiku 4.5 | Extended thinking, prompt caching, server-side compaction |
44
44
  | `openai` | GPT-4.1, o3, o4-mini | Supports OAuth (codex endpoint) and API keys |
45
45
  | `glm` | GLM-5.1, GLM-4.7 | Z.AI platform, OpenAI-compatible |
46
- | `moonshot` | Kimi K2.6 | Moonshot platform, OpenAI-compatible |
46
+ | `moonshot` | Kimi K2.7 | Moonshot platform, OpenAI-compatible |
47
47
 
48
48
  ---
49
49
 
package/dist/index.cjs CHANGED
@@ -34,6 +34,7 @@ __export(index_exports, {
34
34
  EventStream: () => EventStream,
35
35
  ProviderError: () => ProviderError,
36
36
  StreamResult: () => StreamResult,
37
+ classifyProviderError: () => classifyProviderError,
37
38
  formatError: () => formatError,
38
39
  formatErrorForDisplay: () => formatErrorForDisplay,
39
40
  isHardBillingMessage: () => isHardBillingMessage,
@@ -137,10 +138,25 @@ function formatResetTime(resetsAt) {
137
138
  minute: "2-digit"
138
139
  });
139
140
  }
141
+ function isMythosAccessError(message) {
142
+ const lower = message.toLowerCase();
143
+ return lower.includes("mythos") && (lower.includes("not_found") || lower.includes("not found") || lower.includes("no access"));
144
+ }
140
145
  function formatError(err) {
141
146
  if (err instanceof ProviderError) {
142
147
  const name = providerDisplayName(err.provider);
143
148
  const cleanMessage = cleanProviderMessage(err.message);
149
+ if (isMythosAccessError(cleanMessage)) {
150
+ return {
151
+ headline: "Claude Mythos 5 is invitation-only.",
152
+ source: "provider",
153
+ message: "Your Anthropic account isn't approved for Project Glasswing, so the API reports the model as not found.",
154
+ provider: err.provider,
155
+ statusCode: err.statusCode,
156
+ ...err.requestId ? { requestId: err.requestId } : {},
157
+ 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."
158
+ };
159
+ }
144
160
  if (isUsageLimitError(err)) {
145
161
  const resetClause = err.resetsAt ? ` It resets at ${formatResetTime(err.resetsAt)}.` : "";
146
162
  return {
@@ -327,21 +343,21 @@ var StreamResult = class {
327
343
  resolveResponse;
328
344
  rejectResponse;
329
345
  resolveWait = null;
330
- constructor(generator) {
346
+ constructor(generator, signal) {
331
347
  this.response = new Promise((resolve, reject) => {
332
348
  this.resolveResponse = resolve;
333
349
  this.rejectResponse = reject;
334
350
  });
335
- this.pump(generator);
351
+ this.pump(generator, signal);
336
352
  }
337
- async pump(generator) {
353
+ async pump(generator, signal) {
338
354
  try {
339
- let next = await generator.next();
355
+ let next = await this._nextWithAbort(generator, signal);
340
356
  while (!next.done) {
341
357
  this.buffer.push(next.value);
342
358
  this.resolveWait?.();
343
359
  this.resolveWait = null;
344
- next = await generator.next();
360
+ next = await this._nextWithAbort(generator, signal);
345
361
  }
346
362
  this.done = true;
347
363
  this.resolveResponse(next.value);
@@ -356,6 +372,28 @@ var StreamResult = class {
356
372
  this.resolveWait = null;
357
373
  }
358
374
  }
375
+ async _nextWithAbort(generator, signal) {
376
+ if (!signal) {
377
+ return generator.next();
378
+ }
379
+ if (signal.aborted) {
380
+ return Promise.reject(new DOMException("Aborted", "AbortError"));
381
+ }
382
+ let onAbort;
383
+ const abortPromise = new Promise((_, reject) => {
384
+ onAbort = () => {
385
+ generator.return?.(void 0).catch(() => {
386
+ });
387
+ reject(new DOMException("Aborted", "AbortError"));
388
+ };
389
+ signal.addEventListener("abort", onAbort, { once: true });
390
+ });
391
+ try {
392
+ return await Promise.race([generator.next(), abortPromise]);
393
+ } finally {
394
+ if (onAbort) signal.removeEventListener("abort", onAbort);
395
+ }
396
+ }
359
397
  async *[Symbol.asyncIterator]() {
360
398
  let index = 0;
361
399
  while (true) {
@@ -452,6 +490,29 @@ function isRawThinking(part) {
452
490
  const t = part.data.type;
453
491
  return t === "thinking" || t === "redacted_thinking";
454
492
  }
493
+ var ANTHROPIC_INPUT_BLOCK_TYPES = /* @__PURE__ */ new Set([
494
+ "bash_code_execution_tool_result",
495
+ "code_execution_tool_result",
496
+ "connector_text",
497
+ "container_upload",
498
+ "document",
499
+ "image",
500
+ "mid_conv_system",
501
+ "redacted_thinking",
502
+ "search_result",
503
+ "server_tool_use",
504
+ "text",
505
+ "text_editor_code_execution_tool_result",
506
+ "thinking",
507
+ "tool_result",
508
+ "tool_search_tool_result",
509
+ "tool_use",
510
+ "web_fetch_tool_result",
511
+ "web_search_tool_result"
512
+ ]);
513
+ function isAnthropicCompatibleRaw(part) {
514
+ return ANTHROPIC_INPUT_BLOCK_TYPES.has(part.data.type);
515
+ }
455
516
  function isPositionSensitiveThinking(part) {
456
517
  if (part.type === "thinking") return hasValidThinkingSignature(part);
457
518
  return isRawThinking(part);
@@ -478,7 +539,8 @@ function toAnthropicAssistantPart(part, idMap) {
478
539
  };
479
540
  if (part.type === "server_tool_result")
480
541
  return part.data;
481
- if (part.type === "raw") return part.data;
542
+ if (part.type === "raw")
543
+ return isAnthropicCompatibleRaw(part) ? part.data : null;
482
544
  return null;
483
545
  }
484
546
  function toAnthropicAssistantContent(content, preserveThinking, idMap) {
@@ -737,12 +799,12 @@ function toAnthropicToolChoice(choice) {
737
799
  return { type: "tool", name: choice.name };
738
800
  }
739
801
  function isAdaptiveThinkingModel(model) {
740
- return /fable-5|opus-4[-.]8|opus-4[-.]7|opus-4[-.]6|sonnet-4[-.]6/.test(model);
802
+ return /opus-4[-.]8|opus-4[-.]7|opus-4[-.]6|sonnet-4[-.]6|fable-5|mythos-5/.test(model);
741
803
  }
742
804
  function toAnthropicThinking(level, maxTokens, model) {
743
805
  if (isAdaptiveThinkingModel(model)) {
744
806
  let effort = level;
745
- if (effort === "xhigh" && !/opus-4-8|opus-4-7|fable-5/.test(model)) {
807
+ if (effort === "xhigh" && !/opus-4-8|opus-4-7/.test(model)) {
746
808
  effort = "high";
747
809
  }
748
810
  return {
@@ -972,10 +1034,10 @@ function createClient(options) {
972
1034
  ...isOAuth ? { apiKey: null, authToken: options.apiKey } : { apiKey: options.apiKey },
973
1035
  ...options.baseUrl ? { baseURL: options.baseUrl } : {},
974
1036
  ...options.fetch ? { fetch: options.fetch } : {},
975
- // Allow SDK retries for connection-level failures (socket hang up, 500s,
976
- // connection refused). Our stall detection handles abort-initiated retries
977
- // separately SDK retries only fire on genuine transport errors.
978
- maxRetries: 2,
1037
+ // Disable SDK retries the agent loop has its own stall/overload retry
1038
+ // logic that surfaces errors properly. SDK retries on 429s can cause
1039
+ // multi-minute hangs when the provider stops responding mid-retry.
1040
+ maxRetries: 0,
979
1041
  ...isOAuth ? {
980
1042
  defaultHeaders: {
981
1043
  // Anthropic's OAuth edge validates the claude-cli version. Callers
@@ -988,7 +1050,7 @@ function createClient(options) {
988
1050
  });
989
1051
  }
990
1052
  function streamAnthropic(options) {
991
- return new StreamResult(runStream(options));
1053
+ return new StreamResult(runStream(options), options.signal);
992
1054
  }
993
1055
  async function* runStream(options) {
994
1056
  const client = createClient(options);
@@ -1083,7 +1145,6 @@ async function* runStream(options) {
1083
1145
  throw toError(err);
1084
1146
  }
1085
1147
  }
1086
- const stream2 = client.messages.stream(params, requestOptions);
1087
1148
  const contentParts = [];
1088
1149
  const blocks = /* @__PURE__ */ new Map();
1089
1150
  let inputTokens = 0;
@@ -1092,8 +1153,14 @@ async function* runStream(options) {
1092
1153
  let cacheWrite;
1093
1154
  let stopReason = null;
1094
1155
  const keepalive = { type: "keepalive" };
1156
+ let receivedAnyEvent = false;
1095
1157
  try {
1158
+ const stream2 = await client.messages.create(
1159
+ params,
1160
+ requestOptions
1161
+ );
1096
1162
  for await (const event of stream2) {
1163
+ receivedAnyEvent = true;
1097
1164
  switch (event.type) {
1098
1165
  case "message_start": {
1099
1166
  const usage = event.message.usage;
@@ -1130,7 +1197,7 @@ async function* runStream(options) {
1130
1197
  accum.toolId = block.id;
1131
1198
  accum.toolName = block.name;
1132
1199
  accum.input = block.input;
1133
- } else if (block.type === "redacted_thinking") {
1200
+ } else if (block.type !== "text" && block.type !== "thinking") {
1134
1201
  accum.raw = block;
1135
1202
  }
1136
1203
  blocks.set(idx, accum);
@@ -1227,8 +1294,7 @@ async function* runStream(options) {
1227
1294
  contentParts.push({ type: "raw", data: accum.raw });
1228
1295
  yield keepalive;
1229
1296
  } else {
1230
- const msg = stream2.currentMessage;
1231
- const rawBlock = msg?.content[event.index];
1297
+ const rawBlock = accum.raw;
1232
1298
  if (rawBlock) {
1233
1299
  const blockType = rawBlock.type;
1234
1300
  if (blockType === "web_search_tool_result") {
@@ -1274,6 +1340,11 @@ async function* runStream(options) {
1274
1340
  } catch (err) {
1275
1341
  throw toError(err);
1276
1342
  }
1343
+ if (!receivedAnyEvent) {
1344
+ throw new ProviderError("anthropic", "Stream ended without producing any events.", {
1345
+ statusCode: 504
1346
+ });
1347
+ }
1277
1348
  const normalizedStop = normalizeAnthropicStopReason(stopReason);
1278
1349
  const response = {
1279
1350
  message: {
@@ -1565,7 +1636,7 @@ function createClient2(options) {
1565
1636
  });
1566
1637
  }
1567
1638
  function streamOpenAI(options) {
1568
- return new StreamResult(runStream2(options));
1639
+ return new StreamResult(runStream2(options), options.signal);
1569
1640
  }
1570
1641
  async function* runStream2(options) {
1571
1642
  const providerName = options.provider ?? "openai";
@@ -1657,51 +1728,62 @@ async function* runStream2(options) {
1657
1728
  let outputTokens = 0;
1658
1729
  let cacheRead = 0;
1659
1730
  let finishReason = null;
1660
- for await (const chunk of stream2) {
1661
- const choice = chunk.choices?.[0];
1662
- if (chunk.usage) {
1663
- ({ inputTokens, outputTokens, cacheRead } = extractOpenAIUsage(chunk.usage));
1664
- }
1665
- if (!choice) continue;
1666
- if (choice.finish_reason) {
1667
- finishReason = choice.finish_reason;
1668
- }
1669
- const delta = choice.delta;
1670
- const reasoningContent = delta.reasoning_content;
1671
- if (typeof reasoningContent === "string" && reasoningContent) {
1672
- thinkingAccum += reasoningContent;
1673
- if (options.thinking) {
1674
- yield { type: "thinking_delta", text: reasoningContent };
1731
+ let receivedAnyChunk = false;
1732
+ try {
1733
+ for await (const chunk of stream2) {
1734
+ receivedAnyChunk = true;
1735
+ const choice = chunk.choices?.[0];
1736
+ if (chunk.usage) {
1737
+ ({ inputTokens, outputTokens, cacheRead } = extractOpenAIUsage(chunk.usage));
1675
1738
  }
1676
- }
1677
- if (delta.content) {
1678
- textAccum += delta.content;
1679
- yield { type: "text_delta", text: delta.content };
1680
- }
1681
- if (delta.tool_calls) {
1682
- for (const tc of delta.tool_calls) {
1683
- let accum = toolCallAccum.get(tc.index);
1684
- if (!accum) {
1685
- accum = {
1686
- id: tc.id ?? "",
1687
- name: tc.function?.name ?? "",
1688
- argsJson: ""
1689
- };
1690
- toolCallAccum.set(tc.index, accum);
1739
+ if (!choice) continue;
1740
+ if (choice.finish_reason) {
1741
+ finishReason = choice.finish_reason;
1742
+ }
1743
+ const delta = choice.delta;
1744
+ const reasoningContent = delta.reasoning_content;
1745
+ if (typeof reasoningContent === "string" && reasoningContent) {
1746
+ thinkingAccum += reasoningContent;
1747
+ if (options.thinking) {
1748
+ yield { type: "thinking_delta", text: reasoningContent };
1691
1749
  }
1692
- if (tc.id) accum.id = tc.id;
1693
- if (tc.function?.name) accum.name = tc.function.name;
1694
- if (tc.function?.arguments) {
1695
- accum.argsJson += tc.function.arguments;
1696
- yield {
1697
- type: "toolcall_delta",
1698
- id: accum.id,
1699
- name: accum.name,
1700
- argsJson: tc.function.arguments
1701
- };
1750
+ }
1751
+ if (delta.content) {
1752
+ textAccum += delta.content;
1753
+ yield { type: "text_delta", text: delta.content };
1754
+ }
1755
+ if (delta.tool_calls) {
1756
+ for (const tc of delta.tool_calls) {
1757
+ let accum = toolCallAccum.get(tc.index);
1758
+ if (!accum) {
1759
+ accum = {
1760
+ id: tc.id ?? "",
1761
+ name: tc.function?.name ?? "",
1762
+ argsJson: ""
1763
+ };
1764
+ toolCallAccum.set(tc.index, accum);
1765
+ }
1766
+ if (tc.id) accum.id = tc.id;
1767
+ if (tc.function?.name) accum.name = tc.function.name;
1768
+ if (tc.function?.arguments) {
1769
+ accum.argsJson += tc.function.arguments;
1770
+ yield {
1771
+ type: "toolcall_delta",
1772
+ id: accum.id,
1773
+ name: accum.name,
1774
+ argsJson: tc.function.arguments
1775
+ };
1776
+ }
1702
1777
  }
1703
1778
  }
1704
1779
  }
1780
+ } catch (err) {
1781
+ throw toError2(err, providerName);
1782
+ }
1783
+ if (!receivedAnyChunk) {
1784
+ throw new ProviderError(providerName, "Stream ended without producing any chunks.", {
1785
+ statusCode: 504
1786
+ });
1705
1787
  }
1706
1788
  if (thinkingAccum) {
1707
1789
  contentParts.push({ type: "thinking", text: thinkingAccum });
@@ -1944,7 +2026,7 @@ function isVisibleOutputItem(itemType) {
1944
2026
  return itemType === "message";
1945
2027
  }
1946
2028
  function streamOpenAICodex(options) {
1947
- return new StreamResult(runStream3(options));
2029
+ return new StreamResult(runStream3(options), options.signal);
1948
2030
  }
1949
2031
  async function* runStream3(options) {
1950
2032
  const baseUrl = (options.baseUrl || DEFAULT_BASE_URL).replace(/\/+$/, "");
@@ -2274,10 +2356,16 @@ async function* parseSSE(body) {
2274
2356
  }
2275
2357
  }
2276
2358
  function remapCodexId(id, idMap) {
2277
- if (id.startsWith("fc_") || id.startsWith("fc-")) return id;
2278
2359
  const existing = idMap.get(id);
2279
2360
  if (existing) return existing;
2280
- const mapped = `fc_${id.replace(/^toolu_/, "")}`;
2361
+ const withPrefix = id.startsWith("fc_") || id.startsWith("fc-") ? id : `fc_${id.replace(/^toolu_/, "")}`;
2362
+ const sanitized = withPrefix.replace(/[^A-Za-z0-9_-]/g, "_");
2363
+ let mapped = sanitized;
2364
+ let suffix = 2;
2365
+ const used = new Set(idMap.values());
2366
+ while (used.has(mapped)) {
2367
+ mapped = `${sanitized}_${suffix++}`;
2368
+ }
2281
2369
  idMap.set(id, mapped);
2282
2370
  return mapped;
2283
2371
  }
@@ -2800,7 +2888,7 @@ async function fetchCodeAssistWithRetry(plan, options) {
2800
2888
  throw lastError ?? new ProviderError("gemini", "Gemini Code Assist request failed.");
2801
2889
  }
2802
2890
  function streamGemini(options) {
2803
- return new StreamResult(runStream4(options));
2891
+ return new StreamResult(runStream4(options), options.signal);
2804
2892
  }
2805
2893
  async function* runStream4(options) {
2806
2894
  const useStreaming = options.streaming !== false;
@@ -3020,6 +3108,96 @@ function messagesContainVideo(messages) {
3020
3108
  return false;
3021
3109
  }
3022
3110
 
3111
+ // src/error-classification.ts
3112
+ var CONTEXT_OVERFLOW_PATTERNS = [
3113
+ /context_length_exceeded/i,
3114
+ /context length exceeded/i,
3115
+ /context window/i,
3116
+ // OpenAI Codex / Responses
3117
+ /maximum context length/i,
3118
+ // OpenAI / OpenRouter / Mistral
3119
+ /prompt is too long/i,
3120
+ // Anthropic
3121
+ /request_too_large/i,
3122
+ // Anthropic HTTP 413
3123
+ /input is too long/i,
3124
+ // Bedrock
3125
+ /input token count.*exceeds the maximum/i,
3126
+ // Gemini
3127
+ /maximum prompt length/i,
3128
+ // xAI / Grok
3129
+ /reduce the length of the messages/i,
3130
+ // Groq
3131
+ /too large for model/i,
3132
+ // Mistral
3133
+ /token limit/i
3134
+ // generic
3135
+ ];
3136
+ var RATE_LIMIT_PATTERNS = [
3137
+ /rate[ _-]?limit/i,
3138
+ /\b429\b/,
3139
+ /too many requests/i,
3140
+ /tokens per minute/i,
3141
+ /requests per minute/i
3142
+ ];
3143
+ var PROVIDER_TRANSIENT_PATTERNS = [
3144
+ /\b5\d\d\b/,
3145
+ /api_error/i,
3146
+ /server_error/i,
3147
+ /internal server error/i,
3148
+ /bad gateway/i,
3149
+ /service unavailable/i,
3150
+ /gateway timeout/i,
3151
+ /overloaded/i,
3152
+ /\b529\b/
3153
+ ];
3154
+ var BILLING_PATTERNS = [
3155
+ /payment required/i,
3156
+ /\b402\b/,
3157
+ /quota_exceeded/i,
3158
+ // underscore variant not in isHardBillingMessage
3159
+ /credit balance/i
3160
+ ];
3161
+ var AUTH_PATTERNS = [
3162
+ /invalid[ _]api[ _]key/i,
3163
+ /unauthorized/i,
3164
+ /\b401\b/,
3165
+ /authentication[ _]failed/i,
3166
+ /please run \/login/i
3167
+ // Anthropic Claude Code-style hint
3168
+ ];
3169
+ function matchesAny(message, patterns) {
3170
+ return patterns.some((p) => p.test(message));
3171
+ }
3172
+ function classifyProviderError(message) {
3173
+ if (matchesAny(message, CONTEXT_OVERFLOW_PATTERNS)) {
3174
+ 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.
3175
+
3176
+ Original: ${message}`;
3177
+ }
3178
+ if (isHardBillingMessage(message) || matchesAny(message, BILLING_PATTERNS)) {
3179
+ return `[billing] Provider billing/quota issue. Recovery: surface to the user \u2014 they need to top up or switch providers. Do NOT retry.
3180
+
3181
+ Original: ${message}`;
3182
+ }
3183
+ if (matchesAny(message, AUTH_PATTERNS)) {
3184
+ return `[auth] Provider authentication failed. Recovery: surface to the user \u2014 they need to re-login. Do NOT retry.
3185
+
3186
+ Original: ${message}`;
3187
+ }
3188
+ if (matchesAny(message, RATE_LIMIT_PATTERNS)) {
3189
+ return `[rate_limited] Provider rate limit hit. Recovery: wait ~30s, then re-prompt the same worker (no reset needed).
3190
+
3191
+ Original: ${message}`;
3192
+ }
3193
+ if (matchesAny(message, PROVIDER_TRANSIENT_PATTERNS)) {
3194
+ 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.
3195
+
3196
+ Original: ${message}`;
3197
+ }
3198
+ return message;
3199
+ }
3200
+
3023
3201
  // src/providers/palsu.ts
3024
3202
  function palsuText(text) {
3025
3203
  return { role: "assistant", content: text ? [{ type: "text", text }] : [] };
@@ -3166,7 +3344,7 @@ function registerPalsuProvider(config) {
3166
3344
  const stopReason = explicitStop ?? (hasToolCalls ? "tool_use" : "end_turn");
3167
3345
  return yield* simulateStream(message, stopReason, options.signal, cacheUsage);
3168
3346
  })();
3169
- return new StreamResult(gen);
3347
+ return new StreamResult(gen, options.signal);
3170
3348
  }
3171
3349
  });
3172
3350
  return handle;
@@ -3177,6 +3355,7 @@ function registerPalsuProvider(config) {
3177
3355
  EventStream,
3178
3356
  ProviderError,
3179
3357
  StreamResult,
3358
+ classifyProviderError,
3180
3359
  formatError,
3181
3360
  formatErrorForDisplay,
3182
3361
  isHardBillingMessage,