@prestyj/ai 4.3.239 → 4.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -25,6 +25,12 @@ var EZCoderAIError = class extends Error {
25
25
  this.hint = options?.hint;
26
26
  }
27
27
  };
28
+ var VideoUnsupportedError = class extends EZCoderAIError {
29
+ constructor() {
30
+ super("This model can't analyze video.", { source: "capability" });
31
+ this.name = "VideoUnsupportedError";
32
+ }
33
+ };
28
34
  var ProviderError = class extends EZCoderAIError {
29
35
  provider;
30
36
  statusCode;
@@ -65,6 +71,10 @@ function isUsageLimitError(err) {
65
71
  if (!(err instanceof Error)) return false;
66
72
  return /usage limit reached/i.test(err.message);
67
73
  }
74
+ function isHardBillingMessage(message) {
75
+ const lower = message.toLowerCase();
76
+ return lower.includes("insufficient balance") || lower.includes("insufficient credits") || lower.includes("more credits") || lower.includes("insufficient_quota") || lower.includes("exceeded your current quota") || lower.includes("quota exceeded") || lower.includes("no resource package") || lower.includes("recharge") || lower.includes("balance is too low") || lower.includes("out of credits") || lower.includes("arrears") || lower.includes("arrearage") || lower.includes("token quota") || lower.includes("exceeded_current_quota_error") || lower.includes("check your account balance") || lower.includes("does not yet include access") || lower.includes("subscription plan") || lower.includes("billing");
77
+ }
68
78
  function formatResetTime(resetsAt) {
69
79
  const when = new Date(resetsAt * 1e3);
70
80
  const sameDay = when.toDateString() === (/* @__PURE__ */ new Date()).toDateString();
@@ -136,6 +146,14 @@ function finaliseBySource(source, message, requestId, hint) {
136
146
  guidance: hint ?? providerGuidance(void 0, message, void 0),
137
147
  ...requestId ? { requestId } : {}
138
148
  };
149
+ case "capability":
150
+ return {
151
+ headline: message,
152
+ source,
153
+ message: "",
154
+ guidance: hint ?? "Only Kimi, Gemini, MiniMax, and MiMo-V2.5 can analyze video. Switch with /model.",
155
+ ...requestId ? { requestId } : {}
156
+ };
139
157
  case "ezcoder":
140
158
  return {
141
159
  headline: "ezcoder hit an unexpected error.",
@@ -410,8 +428,8 @@ function toAnthropicAssistantPart(part, idMap) {
410
428
  if (part.type === "raw") return part.data;
411
429
  return null;
412
430
  }
413
- function toAnthropicAssistantContent(content, isLatest, idMap) {
414
- if (!isLatest) {
431
+ function toAnthropicAssistantContent(content, preserveThinking, idMap) {
432
+ if (!preserveThinking) {
415
433
  return content.filter((part) => {
416
434
  if (part.type === "thinking" || isRawThinking(part)) return false;
417
435
  if (part.type === "text" && !part.text) return false;
@@ -430,6 +448,7 @@ function toAnthropicAssistantContent(content, isLatest, idMap) {
430
448
  }
431
449
  var NON_VISION_USER_IMAGE_PLACEHOLDER = "(image omitted: model does not support images)";
432
450
  var NON_VISION_TOOL_IMAGE_PLACEHOLDER = "(tool image omitted: model does not support images)";
451
+ var NON_VIDEO_USER_PLACEHOLDER = "(video omitted: model does not support video)";
433
452
  function stripImages(content, placeholder) {
434
453
  const out = [];
435
454
  let lastWasPlaceholder = false;
@@ -440,10 +459,33 @@ function stripImages(content, placeholder) {
440
459
  continue;
441
460
  }
442
461
  out.push(block);
443
- lastWasPlaceholder = block.text === placeholder;
462
+ lastWasPlaceholder = block.type === "text" && block.text === placeholder;
444
463
  }
445
464
  return out;
446
465
  }
466
+ function stripVideos(content, placeholder) {
467
+ const out = [];
468
+ let lastWasPlaceholder = false;
469
+ for (const block of content) {
470
+ if (block.type === "video") {
471
+ if (!lastWasPlaceholder) out.push({ type: "text", text: placeholder });
472
+ lastWasPlaceholder = true;
473
+ continue;
474
+ }
475
+ out.push(block);
476
+ lastWasPlaceholder = block.type === "text" && block.text === placeholder;
477
+ }
478
+ return out;
479
+ }
480
+ function downgradeUnsupportedVideos(messages, supportsVideo) {
481
+ if (supportsVideo === true) return messages;
482
+ return messages.map((msg) => {
483
+ if (msg.role === "user" && Array.isArray(msg.content)) {
484
+ return { ...msg, content: stripVideos(msg.content, NON_VIDEO_USER_PLACEHOLDER) };
485
+ }
486
+ return msg;
487
+ });
488
+ }
447
489
  function downgradeUnsupportedImages(messages, supportsImages) {
448
490
  if (supportsImages !== false) return messages;
449
491
  return messages.map((msg) => {
@@ -472,6 +514,10 @@ function toolResultImages(content) {
472
514
  if (typeof content === "string") return [];
473
515
  return content.filter((b) => b.type === "image");
474
516
  }
517
+ function toolResultVideos(content) {
518
+ if (typeof content === "string") return [];
519
+ return content.filter((b) => b.type === "video");
520
+ }
475
521
  function toAnthropicCacheControl(retention, baseUrl) {
476
522
  const resolved = retention ?? "short";
477
523
  if (resolved === "none") return void 0;
@@ -482,6 +528,12 @@ function toAnthropicToolResultContent(content) {
482
528
  if (typeof content === "string") return content;
483
529
  return content.map((block) => {
484
530
  if (block.type === "text") return { type: "text", text: block.text };
531
+ if (block.type === "video") {
532
+ return {
533
+ type: "video",
534
+ source: { type: "base64", media_type: block.mediaType, data: block.data }
535
+ };
536
+ }
485
537
  return {
486
538
  type: "image",
487
539
  source: {
@@ -504,8 +556,8 @@ function toAnthropicMessages(messages, cacheControl) {
504
556
  let systemText;
505
557
  const out = [];
506
558
  const idMap = /* @__PURE__ */ new Map();
507
- const lastAssistantIdx = messages.reduce(
508
- (last, m, i) => m.role === "assistant" ? i : last,
559
+ const trajectoryStartIdx = messages.reduce(
560
+ (last, m, i) => m.role === "user" ? i : last,
509
561
  -1
510
562
  );
511
563
  let msgIdx = -1;
@@ -520,6 +572,16 @@ function toAnthropicMessages(messages, cacheControl) {
520
572
  role: "user",
521
573
  content: typeof msg.content === "string" ? msg.content : msg.content.map((part) => {
522
574
  if (part.type === "text") return { type: "text", text: part.text };
575
+ if (part.type === "video") {
576
+ return {
577
+ type: "video",
578
+ source: {
579
+ type: "base64",
580
+ media_type: part.mediaType,
581
+ data: part.data
582
+ }
583
+ };
584
+ }
523
585
  return {
524
586
  type: "image",
525
587
  source: {
@@ -533,7 +595,7 @@ function toAnthropicMessages(messages, cacheControl) {
533
595
  continue;
534
596
  }
535
597
  if (msg.role === "assistant") {
536
- const content = typeof msg.content === "string" ? msg.content : toAnthropicAssistantContent(msg.content, msgIdx === lastAssistantIdx, idMap);
598
+ const content = typeof msg.content === "string" ? msg.content : toAnthropicAssistantContent(msg.content, msgIdx > trajectoryStartIdx, idMap);
537
599
  if (Array.isArray(content) && content.length === 0) continue;
538
600
  out.push({ role: "assistant", content });
539
601
  continue;
@@ -541,6 +603,8 @@ function toAnthropicMessages(messages, cacheControl) {
541
603
  if (msg.role === "tool") {
542
604
  out.push({
543
605
  role: "user",
606
+ // Cast covers the video block (used by the Anthropic-compatible MiniMax
607
+ // API), which isn't in the first-party Anthropic tool_result types.
544
608
  content: msg.content.map((result) => ({
545
609
  type: "tool_result",
546
610
  tool_use_id: remapAnthropicToolCallId(result.toolCallId, idMap),
@@ -680,6 +744,13 @@ function toOpenAIMessages(messages, options) {
680
744
  content: msg.content.map(
681
745
  (part) => {
682
746
  if (part.type === "text") return { type: "text", text: part.text };
747
+ if (part.type === "video") {
748
+ const videoUrl = part.fileId ? { url: `ms://${part.fileId}`, id: part.fileId } : { url: `data:${part.mediaType};base64,${part.data}` };
749
+ return {
750
+ type: "video_url",
751
+ video_url: videoUrl
752
+ };
753
+ }
683
754
  return {
684
755
  type: "image_url",
685
756
  image_url: {
@@ -722,29 +793,56 @@ function toOpenAIMessages(messages, options) {
722
793
  continue;
723
794
  }
724
795
  if (msg.role === "tool") {
725
- const imageBlocks = [];
796
+ const isMoonshot = options?.provider === "moonshot";
797
+ const followUpMediaBlocks = [];
798
+ let followUpHasVideo = false;
726
799
  for (const result of msg.content) {
727
800
  const text = toolResultText(result.content);
728
801
  const images = toolResultImages(result.content);
802
+ const videos = toolResultVideos(result.content);
729
803
  const hasText = text.length > 0;
804
+ if (isMoonshot && videos.length > 0) {
805
+ const parts = [];
806
+ if (hasText) parts.push({ type: "text", text });
807
+ const videoParts = videos.map((v) => {
808
+ const videoUrl = v.fileId ? { url: `ms://${v.fileId}`, id: v.fileId } : { url: `data:${v.mediaType};base64,${v.data}` };
809
+ return { type: "video_url", video_url: videoUrl };
810
+ });
811
+ out.push({
812
+ role: "tool",
813
+ tool_call_id: remapToolCallId(result.toolCallId, idMap),
814
+ content: [...parts, ...videoParts]
815
+ });
816
+ continue;
817
+ }
730
818
  out.push({
731
819
  role: "tool",
732
820
  tool_call_id: remapToolCallId(result.toolCallId, idMap),
733
- content: hasText ? text : "(see attached image)"
821
+ content: hasText ? text : "(see attached media)"
734
822
  });
735
823
  if (images.length > 0 && options?.supportsImages !== false) {
736
824
  for (const img of images) {
737
- imageBlocks.push({
825
+ followUpMediaBlocks.push({
738
826
  type: "image_url",
739
827
  image_url: { url: `data:${img.mediaType};base64,${img.data}` }
740
828
  });
741
829
  }
742
830
  }
831
+ if (!isMoonshot && videos.length > 0) {
832
+ for (const v of videos) {
833
+ followUpMediaBlocks.push({
834
+ type: "video_url",
835
+ video_url: { url: `data:${v.mediaType};base64,${v.data}` }
836
+ });
837
+ followUpHasVideo = true;
838
+ }
839
+ }
743
840
  }
744
- if (imageBlocks.length > 0) {
841
+ if (followUpMediaBlocks.length > 0) {
842
+ const label = followUpHasVideo ? "Attached media from tool result:" : "Attached image(s) from tool result:";
745
843
  out.push({
746
844
  role: "user",
747
- content: [{ type: "text", text: "Attached image(s) from tool result:" }, ...imageBlocks]
845
+ content: [{ type: "text", text: label }, ...followUpMediaBlocks]
748
846
  });
749
847
  }
750
848
  }
@@ -845,7 +943,8 @@ async function* runStream(options) {
845
943
  const useStreaming = options.streaming !== false;
846
944
  const cacheControl = toAnthropicCacheControl(options.cacheRetention, options.baseUrl);
847
945
  const supportsFirstPartyToolExtras = !options.baseUrl || options.baseUrl.includes("api.anthropic.com");
848
- const downgradedMessages = downgradeUnsupportedImages(options.messages, options.supportsImages);
946
+ const downgradedImages = downgradeUnsupportedImages(options.messages, options.supportsImages);
947
+ const downgradedMessages = downgradeUnsupportedVideos(downgradedImages, options.supportsVideo);
849
948
  const { system: rawSystem, messages } = toAnthropicMessages(downgradedMessages, cacheControl);
850
949
  const system = isOAuth ? [
851
950
  {
@@ -1051,11 +1150,18 @@ async function* runStream(options) {
1051
1150
  args: tc.args
1052
1151
  };
1053
1152
  } else if (accum.type === "server_tool_use") {
1153
+ let input = accum.input;
1154
+ if (accum.argsJson) {
1155
+ try {
1156
+ input = JSON.parse(accum.argsJson);
1157
+ } catch {
1158
+ }
1159
+ }
1054
1160
  const stc = {
1055
1161
  type: "server_tool_call",
1056
1162
  id: accum.toolId,
1057
1163
  name: accum.toolName,
1058
- input: accum.input
1164
+ input
1059
1165
  };
1060
1166
  contentParts.push(stc);
1061
1167
  yield {
@@ -1263,6 +1369,14 @@ function toError(err) {
1263
1369
  });
1264
1370
  }
1265
1371
  }
1372
+ if (isHardBillingMessage(message)) {
1373
+ const usageMessage = /usage limit reached/i.test(message) ? message : `usage limit reached: ${message}`;
1374
+ return new ProviderError("anthropic", usageMessage, {
1375
+ statusCode: err.status,
1376
+ ...requestId ? { requestId } : {},
1377
+ cause: err
1378
+ });
1379
+ }
1266
1380
  return new ProviderError("anthropic", message, {
1267
1381
  statusCode: err.status,
1268
1382
  ...requestId ? { requestId } : {},
@@ -1295,6 +1409,75 @@ function fnv1aHash(value) {
1295
1409
  return (hash >>> 0).toString(16).padStart(8, "0");
1296
1410
  }
1297
1411
 
1412
+ // src/utils/diag.ts
1413
+ var _diagFn = null;
1414
+ function setProviderDiagnostic(fn) {
1415
+ _diagFn = fn;
1416
+ }
1417
+ function providerDiag(phase, data) {
1418
+ _diagFn?.(phase, data);
1419
+ }
1420
+
1421
+ // src/providers/moonshot-video.ts
1422
+ async function uploadMoonshotVideos(client, messages, signal) {
1423
+ for (const msg of messages) {
1424
+ if (typeof msg.content === "string") continue;
1425
+ for (const part of msg.content) {
1426
+ if (part.type === "video") {
1427
+ await ensureUploaded(client, part, signal);
1428
+ continue;
1429
+ }
1430
+ if (part.type === "tool_result" && Array.isArray(part.content)) {
1431
+ for (const inner of part.content) {
1432
+ if (inner.type === "video") {
1433
+ await ensureUploaded(client, inner, signal);
1434
+ }
1435
+ }
1436
+ }
1437
+ }
1438
+ }
1439
+ }
1440
+ async function ensureUploaded(client, video, signal) {
1441
+ if (video.fileId) {
1442
+ providerDiag("moonshot_video_cached", { fileId: video.fileId });
1443
+ return;
1444
+ }
1445
+ if (!video.data) {
1446
+ providerDiag("moonshot_video_skipped_no_data", {});
1447
+ return;
1448
+ }
1449
+ providerDiag("moonshot_video_upload_start", {
1450
+ mediaType: video.mediaType,
1451
+ bytes: Math.floor(video.data.length * 3 / 4)
1452
+ });
1453
+ video.fileId = await uploadOne(client, video, signal);
1454
+ providerDiag("moonshot_video_upload_done", { fileId: video.fileId });
1455
+ }
1456
+ async function uploadOne(client, video, signal) {
1457
+ const bytes = Buffer.from(video.data, "base64");
1458
+ const mediaType = video.mediaType || "video/mp4";
1459
+ const filename = `upload.${extForMime(mediaType)}`;
1460
+ const file = new File([new Uint8Array(bytes)], filename, { type: mediaType });
1461
+ const uploaded = await client.files.create(
1462
+ { file, purpose: "video" },
1463
+ signal ? { signal } : void 0
1464
+ );
1465
+ return uploaded.id;
1466
+ }
1467
+ var MIME_TO_EXT = {
1468
+ "video/mp4": "mp4",
1469
+ "video/mpeg": "mpeg",
1470
+ "video/quicktime": "mov",
1471
+ "video/webm": "webm",
1472
+ "video/x-matroska": "mkv",
1473
+ "video/x-msvideo": "avi",
1474
+ "video/x-flv": "flv",
1475
+ "video/3gpp": "3gp"
1476
+ };
1477
+ function extForMime(mediaType) {
1478
+ return MIME_TO_EXT[mediaType.toLowerCase()] ?? "mp4";
1479
+ }
1480
+
1298
1481
  // src/utils/env.ts
1299
1482
  function getEnvironment() {
1300
1483
  return globalThis.process?.env;
@@ -1324,7 +1507,8 @@ function createClient2(options) {
1324
1507
  return new OpenAI({
1325
1508
  apiKey: options.apiKey,
1326
1509
  ...options.baseUrl ? { baseURL: options.baseUrl } : {},
1327
- ...options.fetch ? { fetch: options.fetch } : {}
1510
+ ...options.fetch ? { fetch: options.fetch } : {},
1511
+ ...options.defaultHeaders ? { defaultHeaders: options.defaultHeaders } : {}
1328
1512
  });
1329
1513
  }
1330
1514
  function streamOpenAI(options) {
@@ -1335,7 +1519,15 @@ async function* runStream2(options) {
1335
1519
  const useStreaming = options.streaming !== false;
1336
1520
  const client = createClient2(options);
1337
1521
  const usesThinkingParam = options.provider === "glm" || options.provider === "moonshot" || options.provider === "xiaomi";
1338
- const downgradedMessages = downgradeUnsupportedImages(options.messages, options.supportsImages);
1522
+ const downgradedImages = downgradeUnsupportedImages(options.messages, options.supportsImages);
1523
+ const downgradedMessages = downgradeUnsupportedVideos(downgradedImages, options.supportsVideo);
1524
+ if (options.provider === "moonshot") {
1525
+ try {
1526
+ await uploadMoonshotVideos(client, downgradedMessages, options.signal);
1527
+ } catch (err) {
1528
+ throw toError2(err, providerName);
1529
+ }
1530
+ }
1339
1531
  const messages = toOpenAIMessages(downgradedMessages, {
1340
1532
  provider: options.provider,
1341
1533
  thinking: !!options.thinking,
@@ -1573,6 +1765,16 @@ function completionToResponse(completion) {
1573
1765
  usage: { inputTokens, outputTokens, ...cacheRead > 0 && { cacheRead } }
1574
1766
  };
1575
1767
  }
1768
+ function classifyOpenAICompatLimit(args) {
1769
+ const { status, code, type, message } = args;
1770
+ const codeType = `${code ?? ""} ${type ?? ""}`.toLowerCase();
1771
+ const isHard = status === 402 || codeType.includes("insufficient_quota") || isHardBillingMessage(message);
1772
+ if (isHard) return "hard";
1773
+ if (status === 429 || codeType.includes("rate_limit_exceeded") || codeType.includes("too_many_requests")) {
1774
+ return "transient";
1775
+ }
1776
+ return null;
1777
+ }
1576
1778
  function toError2(err, provider = "openai") {
1577
1779
  if (err instanceof OpenAI.APIError) {
1578
1780
  const body = err.error;
@@ -1584,6 +1786,35 @@ function toError2(err, provider = "openai") {
1584
1786
  hint = "codex-mini-latest requires an OpenAI Pro or Max subscription. Your account currently has access to GPT-5.4 and GPT-5.4 Mini.";
1585
1787
  }
1586
1788
  const requestId = err.request_id ?? (typeof body?.request_id === "string" ? body.request_id : void 0);
1789
+ const code = typeof err.code === "string" ? err.code : void 0;
1790
+ const type = typeof err.type === "string" ? err.type : void 0;
1791
+ const limit = classifyOpenAICompatLimit({
1792
+ status: err.status,
1793
+ code,
1794
+ type,
1795
+ message: cleanMessage
1796
+ });
1797
+ if (limit === "hard") {
1798
+ const message = /usage limit reached/i.test(cleanMessage) ? cleanMessage : `usage limit reached: ${cleanMessage}`;
1799
+ return new ProviderError(provider, message, {
1800
+ statusCode: err.status,
1801
+ ...requestId ? { requestId } : {},
1802
+ ...hint ? { hint } : {},
1803
+ cause: err
1804
+ });
1805
+ }
1806
+ if (limit === "transient") {
1807
+ const retryAfterRaw = readHeader(err.headers, "retry-after");
1808
+ const retryAfterSec = retryAfterRaw != null ? Number(retryAfterRaw) : Number.NaN;
1809
+ const resetsAt = Number.isFinite(retryAfterSec) && retryAfterSec > 0 ? Math.floor(Date.now() / 1e3) + retryAfterSec : void 0;
1810
+ return new ProviderError(provider, cleanMessage, {
1811
+ statusCode: err.status,
1812
+ ...requestId ? { requestId } : {},
1813
+ ...hint ? { hint } : {},
1814
+ ...resetsAt ? { resetsAt } : {},
1815
+ cause: err
1816
+ });
1817
+ }
1587
1818
  return new ProviderError(provider, cleanMessage, {
1588
1819
  statusCode: err.status,
1589
1820
  ...requestId ? { requestId } : {},
@@ -1600,15 +1831,6 @@ function toError2(err, provider = "openai") {
1600
1831
  // src/providers/openai-codex.ts
1601
1832
  import os from "os";
1602
1833
 
1603
- // src/utils/diag.ts
1604
- var _diagFn = null;
1605
- function setProviderDiagnostic(fn) {
1606
- _diagFn = fn;
1607
- }
1608
- function providerDiag(phase, data) {
1609
- _diagFn?.(phase, data);
1610
- }
1611
-
1612
1834
  // src/utils/sse.ts
1613
1835
  function parseSseBuffer(buffer) {
1614
1836
  const events = [];
@@ -1674,7 +1896,8 @@ function streamOpenAICodex(options) {
1674
1896
  async function* runStream3(options) {
1675
1897
  const baseUrl = (options.baseUrl || DEFAULT_BASE_URL).replace(/\/+$/, "");
1676
1898
  const url = `${baseUrl}/codex/responses`;
1677
- const downgraded = downgradeUnsupportedImages(options.messages, options.supportsImages);
1899
+ const downgradedImages = downgradeUnsupportedImages(options.messages, options.supportsImages);
1900
+ const downgraded = downgradeUnsupportedVideos(downgradedImages, options.supportsVideo);
1678
1901
  const { system, input } = toCodexInput(downgraded, { supportsImages: options.supportsImages });
1679
1902
  const body = {
1680
1903
  model: options.model,
@@ -1751,6 +1974,7 @@ async function* runStream3(options) {
1751
1974
  const contentParts = [];
1752
1975
  let textAccum = "";
1753
1976
  const toolCalls = /* @__PURE__ */ new Map();
1977
+ const orderedItems = [];
1754
1978
  const outputItemTypes = /* @__PURE__ */ new Map();
1755
1979
  const outputTextByPart = /* @__PURE__ */ new Map();
1756
1980
  const pendingOutputTextByPart = /* @__PURE__ */ new Map();
@@ -1898,12 +2122,26 @@ async function* runStream3(options) {
1898
2122
  }
1899
2123
  if (type === "response.output_item.done") {
1900
2124
  const item = event.item;
2125
+ if (item?.type === "reasoning") {
2126
+ const encrypted = item.encrypted_content;
2127
+ const reasoningId = item.id;
2128
+ if (encrypted && reasoningId) {
2129
+ orderedItems.push({
2130
+ kind: "reasoning",
2131
+ part: {
2132
+ type: "raw",
2133
+ data: { ...item, summary: Array.isArray(item.summary) ? item.summary : [] }
2134
+ }
2135
+ });
2136
+ }
2137
+ }
1901
2138
  if (item?.type === "function_call") {
1902
2139
  const callId = item.call_id;
1903
2140
  const itemId = item.id;
1904
2141
  const id = `${callId}|${itemId}`;
1905
2142
  const tc = toolCalls.get(id);
1906
2143
  if (tc) {
2144
+ orderedItems.push({ kind: "tool", id });
1907
2145
  const args = parseToolArguments(tc.argsJson);
1908
2146
  yield {
1909
2147
  type: "toolcall_done",
@@ -1924,19 +2162,41 @@ async function* runStream3(options) {
1924
2162
  }
1925
2163
  }
1926
2164
  }
1927
- if (textAccum) {
1928
- contentParts.push({ type: "text", text: textAccum });
1929
- }
1930
- for (const [, tc] of toolCalls) {
1931
- const args = parseToolArguments(tc.argsJson);
2165
+ const seenTool = /* @__PURE__ */ new Set();
2166
+ let textInserted = false;
2167
+ for (const entry of orderedItems) {
2168
+ if (entry.kind === "reasoning") {
2169
+ contentParts.push(entry.part);
2170
+ continue;
2171
+ }
2172
+ if (textAccum && !textInserted) {
2173
+ contentParts.push({ type: "text", text: textAccum });
2174
+ textInserted = true;
2175
+ }
2176
+ const tc = toolCalls.get(entry.id);
2177
+ if (!tc || seenTool.has(entry.id)) continue;
2178
+ seenTool.add(entry.id);
1932
2179
  const toolCall = {
1933
2180
  type: "tool_call",
1934
2181
  id: tc.id,
1935
2182
  name: tc.name,
1936
- args
2183
+ args: parseToolArguments(tc.argsJson)
1937
2184
  };
1938
2185
  contentParts.push(toolCall);
1939
2186
  }
2187
+ if (textAccum && !textInserted) {
2188
+ contentParts.push({ type: "text", text: textAccum });
2189
+ }
2190
+ for (const [id, tc] of toolCalls) {
2191
+ if (seenTool.has(id)) continue;
2192
+ seenTool.add(id);
2193
+ contentParts.push({
2194
+ type: "tool_call",
2195
+ id: tc.id,
2196
+ name: tc.name,
2197
+ args: parseToolArguments(tc.argsJson)
2198
+ });
2199
+ }
1940
2200
  const hasToolCalls = contentParts.some((p) => p.type === "tool_call");
1941
2201
  const stopReason = hasToolCalls ? "tool_use" : "end_turn";
1942
2202
  const streamResponse = {
@@ -1968,6 +2228,9 @@ function remapCodexId(id, idMap) {
1968
2228
  idMap.set(id, mapped);
1969
2229
  return mapped;
1970
2230
  }
2231
+ function isEncryptedReasoning(data) {
2232
+ return data.type === "reasoning" && typeof data.id === "string" && typeof data.encrypted_content === "string";
2233
+ }
1971
2234
  function toCodexInput(messages, options) {
1972
2235
  let system;
1973
2236
  const input = [];
@@ -2000,7 +2263,9 @@ function toCodexInput(messages, options) {
2000
2263
  continue;
2001
2264
  }
2002
2265
  for (const part of msg.content) {
2003
- if (part.type === "text") {
2266
+ if (part.type === "raw" && isEncryptedReasoning(part.data)) {
2267
+ input.push(part.data);
2268
+ } else if (part.type === "text") {
2004
2269
  input.push({
2005
2270
  type: "message",
2006
2271
  role: "assistant",
@@ -2141,6 +2406,20 @@ ${formatUnsupportedModelMessage(model)}`;
2141
2406
  }
2142
2407
  return `Gemini API error (${status}): ${body}`;
2143
2408
  }
2409
+ function parseRetryDelaySeconds(body) {
2410
+ const match = body.match(/"retryDelay"\s*:\s*"(\d+(?:\.\d+)?)s"/);
2411
+ if (!match) return void 0;
2412
+ const seconds = Number(match[1]);
2413
+ return Number.isFinite(seconds) ? seconds : void 0;
2414
+ }
2415
+ function parseGeminiQuota(status, body) {
2416
+ if (status !== 429) return null;
2417
+ const lower = body.toLowerCase();
2418
+ if (!lower.includes("resource_exhausted") && !lower.includes("quota")) return null;
2419
+ const retryDelaySeconds = parseRetryDelaySeconds(body);
2420
+ const exhausted = retryDelaySeconds === void 0;
2421
+ return { exhausted, retryDelaySeconds };
2422
+ }
2144
2423
  function toSystemAndContents(messages) {
2145
2424
  let systemText = "";
2146
2425
  const contents = [];
@@ -2200,6 +2479,13 @@ ${msg.content}` : msg.content;
2200
2479
  }
2201
2480
  }
2202
2481
  });
2482
+ if (typeof result.content !== "string") {
2483
+ for (const block of result.content) {
2484
+ if (block.type === "video") {
2485
+ parts.push({ inlineData: { mimeType: block.mediaType, data: block.data } });
2486
+ }
2487
+ }
2488
+ }
2203
2489
  }
2204
2490
  if (parts.length > 0) contents.push({ role: "user", parts });
2205
2491
  }
@@ -2292,7 +2578,8 @@ function toThinkingConfig(model, level) {
2292
2578
  };
2293
2579
  }
2294
2580
  function buildGenerateRequest(options) {
2295
- const downgradedMessages = downgradeUnsupportedImages(options.messages, options.supportsImages);
2581
+ const downgradedImages = downgradeUnsupportedImages(options.messages, options.supportsImages);
2582
+ const downgradedMessages = downgradeUnsupportedVideos(downgradedImages, options.supportsVideo);
2296
2583
  const { systemInstruction, contents } = toSystemAndContents(downgradedMessages);
2297
2584
  const tools = toGeminiTools(options.tools);
2298
2585
  const toolConfig = toGeminiToolConfig(options.toolChoice, options.tools);
@@ -2420,8 +2707,17 @@ async function fetchCodeAssist(plan, options) {
2420
2707
  });
2421
2708
  if (!response.ok) {
2422
2709
  const text = await response.text().catch(() => "");
2423
- throw new ProviderError("gemini", formatErrorMessage(response.status, text, options.model), {
2424
- statusCode: response.status
2710
+ const quota = parseGeminiQuota(response.status, text);
2711
+ let message = formatErrorMessage(response.status, text, options.model);
2712
+ let resetsAt;
2713
+ if (quota?.exhausted) {
2714
+ message = `Gemini quota exhausted \u2014 usage limit reached. ${message}`;
2715
+ } else if (quota?.retryDelaySeconds !== void 0) {
2716
+ resetsAt = Math.floor(Date.now() / 1e3) + Math.ceil(quota.retryDelaySeconds);
2717
+ }
2718
+ throw new ProviderError("gemini", message, {
2719
+ statusCode: response.status,
2720
+ ...resetsAt !== void 0 ? { resetsAt } : {}
2425
2721
  });
2426
2722
  }
2427
2723
  return response;
@@ -2587,6 +2883,7 @@ var providerRegistry = new ProviderRegistryImpl();
2587
2883
 
2588
2884
  // src/stream.ts
2589
2885
  var GLM_CODING_BASE_URL = "https://api.z.ai/api/coding/paas/v4";
2886
+ var KIMI_CODE_USER_AGENT = `kimi-code-cli/${process.env.KIMI_CODE_VERSION ?? "1.0.11"}`;
2590
2887
  providerRegistry.register("anthropic", {
2591
2888
  stream: (options) => streamAnthropic(options)
2592
2889
  });
@@ -2615,10 +2912,11 @@ providerRegistry.register("glm", {
2615
2912
  })
2616
2913
  });
2617
2914
  providerRegistry.register("moonshot", {
2618
- stream: (options) => streamOpenAI({
2619
- ...options,
2620
- baseUrl: options.baseUrl ?? "https://api.moonshot.ai/v1"
2621
- })
2915
+ stream: (options) => {
2916
+ const baseUrl = options.baseUrl ?? "https://api.moonshot.ai/v1";
2917
+ const defaultHeaders = baseUrl.includes("api.kimi.com") ? { "User-Agent": KIMI_CODE_USER_AGENT, ...options.defaultHeaders } : options.defaultHeaders;
2918
+ return streamOpenAI({ ...options, baseUrl, defaultHeaders });
2919
+ }
2622
2920
  });
2623
2921
  providerRegistry.register("deepseek", {
2624
2922
  stream: (options) => streamOpenAI({
@@ -2651,8 +2949,23 @@ function stream(options) {
2651
2949
  `Unknown provider: "${options.provider}". Registered: ${providerRegistry.list().join(", ")}`
2652
2950
  );
2653
2951
  }
2952
+ if (options.supportsVideo !== true && messagesContainVideo(options.messages)) {
2953
+ throw new VideoUnsupportedError();
2954
+ }
2654
2955
  return entry.stream(options);
2655
2956
  }
2957
+ function messagesContainVideo(messages) {
2958
+ for (const msg of messages) {
2959
+ if (typeof msg.content === "string" || !Array.isArray(msg.content)) continue;
2960
+ for (const part of msg.content) {
2961
+ if (part.type === "video") return true;
2962
+ if (part.type === "tool_result" && Array.isArray(part.content)) {
2963
+ if (part.content.some((block) => block.type === "video")) return true;
2964
+ }
2965
+ }
2966
+ }
2967
+ return false;
2968
+ }
2656
2969
 
2657
2970
  // src/providers/palsu.ts
2658
2971
  function palsuText(text) {
@@ -2812,6 +3125,7 @@ export {
2812
3125
  StreamResult,
2813
3126
  formatError,
2814
3127
  formatErrorForDisplay,
3128
+ isHardBillingMessage,
2815
3129
  isUsageLimitError,
2816
3130
  palsuAssistantMessage,
2817
3131
  palsuText,