@prestyj/ai 4.3.15 → 4.3.54

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
@@ -1,6 +1,6 @@
1
1
  import { z } from 'zod';
2
2
 
3
- type Provider = "anthropic" | "xiaomi" | "openai" | "glm" | "moonshot" | "minimax" | "palsu";
3
+ type Provider = "anthropic" | "xiaomi" | "openai" | "glm" | "moonshot" | "minimax" | "openrouter" | "palsu";
4
4
  type ThinkingLevel = "low" | "medium" | "high" | "max";
5
5
  type CacheRetention = "none" | "short" | "long";
6
6
  interface TextContent {
@@ -23,10 +23,11 @@ interface ToolCall {
23
23
  name: string;
24
24
  args: Record<string, unknown>;
25
25
  }
26
+ type ToolResultContent = string | (TextContent | ImageContent)[];
26
27
  interface ToolResult {
27
28
  type: "tool_result";
28
29
  toolCallId: string;
29
- content: string;
30
+ content: ToolResultContent;
30
31
  isError?: boolean;
31
32
  }
32
33
  interface ServerToolCall {
@@ -174,6 +175,16 @@ interface StreamOptions {
174
175
  * where the default `globalThis.fetch` doesn't support streaming properly.
175
176
  * Passed directly to the underlying provider SDK. */
176
177
  fetch?: typeof globalThis.fetch;
178
+ /** Whether the target model supports image input. When false, image content
179
+ * in user messages and tool_result messages is downgraded to a text placeholder
180
+ * before being sent to the provider. Default: true. */
181
+ supportsImages?: boolean;
182
+ /** Use streaming transport (default: true). When false, providers issue a
183
+ * single non-streaming request and synthesize events from the full response.
184
+ * The agent loop flips this to `false` as a fallback after repeated stream
185
+ * stalls — broken SSE connections (transient CDN / proxy issues) often
186
+ * recover when the same request is issued over a plain HTTP request/response. */
187
+ streaming?: boolean;
177
188
  }
178
189
 
179
190
  /**
@@ -362,4 +373,4 @@ interface PalsuProviderConfig {
362
373
  */
363
374
  declare function registerPalsuProvider(config?: PalsuProviderConfig): PalsuProviderHandle;
364
375
 
365
- export { type AssistantMessage, type CacheRetention, type ContentPart, type DoneEvent, EZCoderAIError, type ErrorEvent, EventStream, type ImageContent, type Message, type PalsuModelConfig, type PalsuModelHandle, type PalsuProviderConfig, type PalsuProviderHandle, type PalsuProviderState, type PalsuResponse, type PalsuResponseFactory, type Provider, 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 ToolResultMessage, type Usage, type UserMessage, palsuAssistantMessage, palsuText, palsuThinking, palsuToolCall, providerRegistry, registerPalsuProvider, stream };
376
+ export { type AssistantMessage, type CacheRetention, type ContentPart, type DoneEvent, EZCoderAIError, type ErrorEvent, EventStream, type ImageContent, type Message, type PalsuModelConfig, type PalsuModelHandle, type PalsuProviderConfig, type PalsuProviderHandle, type PalsuProviderState, type PalsuResponse, type PalsuResponseFactory, type Provider, 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, palsuAssistantMessage, palsuText, palsuThinking, palsuToolCall, providerRegistry, registerPalsuProvider, stream };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { z } from 'zod';
2
2
 
3
- type Provider = "anthropic" | "xiaomi" | "openai" | "glm" | "moonshot" | "minimax" | "palsu";
3
+ type Provider = "anthropic" | "xiaomi" | "openai" | "glm" | "moonshot" | "minimax" | "openrouter" | "palsu";
4
4
  type ThinkingLevel = "low" | "medium" | "high" | "max";
5
5
  type CacheRetention = "none" | "short" | "long";
6
6
  interface TextContent {
@@ -23,10 +23,11 @@ interface ToolCall {
23
23
  name: string;
24
24
  args: Record<string, unknown>;
25
25
  }
26
+ type ToolResultContent = string | (TextContent | ImageContent)[];
26
27
  interface ToolResult {
27
28
  type: "tool_result";
28
29
  toolCallId: string;
29
- content: string;
30
+ content: ToolResultContent;
30
31
  isError?: boolean;
31
32
  }
32
33
  interface ServerToolCall {
@@ -174,6 +175,16 @@ interface StreamOptions {
174
175
  * where the default `globalThis.fetch` doesn't support streaming properly.
175
176
  * Passed directly to the underlying provider SDK. */
176
177
  fetch?: typeof globalThis.fetch;
178
+ /** Whether the target model supports image input. When false, image content
179
+ * in user messages and tool_result messages is downgraded to a text placeholder
180
+ * before being sent to the provider. Default: true. */
181
+ supportsImages?: boolean;
182
+ /** Use streaming transport (default: true). When false, providers issue a
183
+ * single non-streaming request and synthesize events from the full response.
184
+ * The agent loop flips this to `false` as a fallback after repeated stream
185
+ * stalls — broken SSE connections (transient CDN / proxy issues) often
186
+ * recover when the same request is issued over a plain HTTP request/response. */
187
+ streaming?: boolean;
177
188
  }
178
189
 
179
190
  /**
@@ -362,4 +373,4 @@ interface PalsuProviderConfig {
362
373
  */
363
374
  declare function registerPalsuProvider(config?: PalsuProviderConfig): PalsuProviderHandle;
364
375
 
365
- export { type AssistantMessage, type CacheRetention, type ContentPart, type DoneEvent, EZCoderAIError, type ErrorEvent, EventStream, type ImageContent, type Message, type PalsuModelConfig, type PalsuModelHandle, type PalsuProviderConfig, type PalsuProviderHandle, type PalsuProviderState, type PalsuResponse, type PalsuResponseFactory, type Provider, 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 ToolResultMessage, type Usage, type UserMessage, palsuAssistantMessage, palsuText, palsuThinking, palsuToolCall, providerRegistry, registerPalsuProvider, stream };
376
+ export { type AssistantMessage, type CacheRetention, type ContentPart, type DoneEvent, EZCoderAIError, type ErrorEvent, EventStream, type ImageContent, type Message, type PalsuModelConfig, type PalsuModelHandle, type PalsuProviderConfig, type PalsuProviderHandle, type PalsuProviderState, type PalsuResponse, type PalsuResponseFactory, type Provider, 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, palsuAssistantMessage, palsuText, palsuThinking, palsuToolCall, providerRegistry, registerPalsuProvider, stream };
package/dist/index.js CHANGED
@@ -128,12 +128,70 @@ function zodToJsonSchema(schema) {
128
128
  }
129
129
 
130
130
  // src/providers/transform.ts
131
+ var NON_VISION_USER_IMAGE_PLACEHOLDER = "(image omitted: model does not support images)";
132
+ var NON_VISION_TOOL_IMAGE_PLACEHOLDER = "(tool image omitted: model does not support images)";
133
+ function stripImages(content, placeholder) {
134
+ const out = [];
135
+ let lastWasPlaceholder = false;
136
+ for (const block of content) {
137
+ if (block.type === "image") {
138
+ if (!lastWasPlaceholder) out.push({ type: "text", text: placeholder });
139
+ lastWasPlaceholder = true;
140
+ continue;
141
+ }
142
+ out.push(block);
143
+ lastWasPlaceholder = block.text === placeholder;
144
+ }
145
+ return out;
146
+ }
147
+ function downgradeUnsupportedImages(messages, supportsImages) {
148
+ if (supportsImages !== false) return messages;
149
+ return messages.map((msg) => {
150
+ if (msg.role === "user" && Array.isArray(msg.content)) {
151
+ return { ...msg, content: stripImages(msg.content, NON_VISION_USER_IMAGE_PLACEHOLDER) };
152
+ }
153
+ if (msg.role === "tool") {
154
+ return {
155
+ ...msg,
156
+ content: msg.content.map(
157
+ (tr) => Array.isArray(tr.content) ? {
158
+ ...tr,
159
+ content: stripImages(tr.content, NON_VISION_TOOL_IMAGE_PLACEHOLDER)
160
+ } : tr
161
+ )
162
+ };
163
+ }
164
+ return msg;
165
+ });
166
+ }
167
+ function toolResultText(content) {
168
+ if (typeof content === "string") return content;
169
+ return content.filter((b) => b.type === "text").map((b) => b.text).join("\n");
170
+ }
171
+ function toolResultImages(content) {
172
+ if (typeof content === "string") return [];
173
+ return content.filter((b) => b.type === "image");
174
+ }
131
175
  function toAnthropicCacheControl(retention, baseUrl) {
132
176
  const resolved = retention ?? "short";
133
177
  if (resolved === "none") return void 0;
134
178
  const ttl = resolved === "long" && (!baseUrl || baseUrl.includes("api.anthropic.com")) ? "1h" : void 0;
135
179
  return { type: "ephemeral", ...ttl && { ttl } };
136
180
  }
181
+ function toAnthropicToolResultContent(content) {
182
+ if (typeof content === "string") return content;
183
+ return content.map((block) => {
184
+ if (block.type === "text") return { type: "text", text: block.text };
185
+ return {
186
+ type: "image",
187
+ source: {
188
+ type: "base64",
189
+ media_type: block.mediaType,
190
+ data: block.data
191
+ }
192
+ };
193
+ });
194
+ }
137
195
  function toAnthropicMessages(messages, cacheControl) {
138
196
  let systemText;
139
197
  const out = [];
@@ -197,7 +255,7 @@ function toAnthropicMessages(messages, cacheControl) {
197
255
  content: msg.content.map((result) => ({
198
256
  type: "tool_result",
199
257
  tool_use_id: result.toolCallId,
200
- content: result.content,
258
+ content: toAnthropicToolResultContent(result.content),
201
259
  is_error: result.isError
202
260
  }))
203
261
  });
@@ -266,7 +324,7 @@ function toAnthropicToolChoice(choice) {
266
324
  return { type: "tool", name: choice.name };
267
325
  }
268
326
  function supportsAdaptiveThinking(model) {
269
- return /opus-4-6|sonnet-4-6/.test(model);
327
+ return /opus-4-7|opus-4-6|sonnet-4-6/.test(model);
270
328
  }
271
329
  function toAnthropicThinking(level, maxTokens, model) {
272
330
  if (supportsAdaptiveThinking(model)) {
@@ -293,10 +351,10 @@ function toAnthropicThinking(level, maxTokens, model) {
293
351
  };
294
352
  }
295
353
  function remapToolCallId(id, idMap) {
296
- if (id.startsWith("call_")) return id;
354
+ if (!id.startsWith("toolu_")) return id;
297
355
  const existing = idMap.get(id);
298
356
  if (existing) return existing;
299
- const mapped = `call_${id.replace(/^toolu_/, "")}`;
357
+ const mapped = `call_${id.slice(5)}`;
300
358
  idMap.set(id, mapped);
301
359
  return mapped;
302
360
  }
@@ -361,16 +419,36 @@ function toOpenAIMessages(messages, options) {
361
419
  };
362
420
  if (thinkingParts) {
363
421
  assistantMsg.reasoning_content = thinkingParts;
422
+ } else if (options?.thinking && hasToolCalls && options.provider !== "glm") {
423
+ assistantMsg.reasoning_content = " ";
364
424
  }
365
425
  out.push(assistantMsg);
366
426
  continue;
367
427
  }
368
428
  if (msg.role === "tool") {
429
+ const imageBlocks = [];
369
430
  for (const result of msg.content) {
431
+ const text = toolResultText(result.content);
432
+ const images = toolResultImages(result.content);
433
+ const hasText = text.length > 0;
370
434
  out.push({
371
435
  role: "tool",
372
436
  tool_call_id: remapToolCallId(result.toolCallId, idMap),
373
- content: result.content
437
+ content: hasText ? text : "(see attached image)"
438
+ });
439
+ if (images.length > 0 && options?.supportsImages !== false) {
440
+ for (const img of images) {
441
+ imageBlocks.push({
442
+ type: "image_url",
443
+ image_url: { url: `data:${img.mediaType};base64,${img.data}` }
444
+ });
445
+ }
446
+ }
447
+ }
448
+ if (imageBlocks.length > 0) {
449
+ out.push({
450
+ role: "user",
451
+ content: [{ type: "text", text: "Attached image(s) from tool result:" }, ...imageBlocks]
374
452
  });
375
453
  }
376
454
  }
@@ -450,8 +528,10 @@ function streamAnthropic(options) {
450
528
  async function* runStream(options) {
451
529
  const client = createClient(options);
452
530
  const isOAuth = options.apiKey?.startsWith("sk-ant-oat");
531
+ const useStreaming = options.streaming !== false;
453
532
  const cacheControl = toAnthropicCacheControl(options.cacheRetention, options.baseUrl);
454
- const { system: rawSystem, messages } = toAnthropicMessages(options.messages, cacheControl);
533
+ const downgradedMessages = downgradeUnsupportedImages(options.messages, options.supportsImages);
534
+ const { system: rawSystem, messages } = toAnthropicMessages(downgradedMessages, cacheControl);
455
535
  const system = isOAuth ? [
456
536
  {
457
537
  type: "text",
@@ -495,9 +575,9 @@ async function* runStream(options) {
495
575
  ];
496
576
  return contextEdits.length ? { context_management: { edits: contextEdits } } : {};
497
577
  })(),
498
- stream: true
578
+ stream: useStreaming
499
579
  };
500
- const hasAdaptiveThinking = options.model.includes("opus-4-6") || options.model.includes("opus-4.6") || options.model.includes("sonnet-4-6") || options.model.includes("sonnet-4.6");
580
+ const hasAdaptiveThinking = options.model.includes("opus-4-7") || options.model.includes("opus-4.7") || options.model.includes("opus-4-6") || options.model.includes("opus-4.6") || options.model.includes("sonnet-4-6") || options.model.includes("sonnet-4.6");
501
581
  const betaHeaders = [
502
582
  ...isOAuth ? ["claude-code-20250219", "oauth-2025-04-20"] : [],
503
583
  ...options.compaction ? ["compact-2026-01-12"] : [],
@@ -505,10 +585,23 @@ async function* runStream(options) {
505
585
  "fine-grained-tool-streaming-2025-05-14",
506
586
  ...!hasAdaptiveThinking ? ["interleaved-thinking-2025-05-14"] : []
507
587
  ];
508
- const stream2 = client.messages.stream(params, {
588
+ const requestOptions = {
509
589
  signal: options.signal ?? void 0,
510
590
  ...betaHeaders.length ? { headers: { "anthropic-beta": betaHeaders.join(",") } } : {}
511
- });
591
+ };
592
+ if (!useStreaming) {
593
+ try {
594
+ const message = await client.messages.create(
595
+ { ...params, stream: false },
596
+ requestOptions
597
+ );
598
+ yield* synthesizeEventsFromMessage(message);
599
+ return messageToResponse(message);
600
+ } catch (err) {
601
+ throw toError(err);
602
+ }
603
+ }
604
+ const stream2 = client.messages.stream(params, requestOptions);
512
605
  const contentParts = [];
513
606
  const blocks = /* @__PURE__ */ new Map();
514
607
  let inputTokens = 0;
@@ -701,6 +794,105 @@ async function* runStream(options) {
701
794
  yield { type: "done", stopReason: normalizedStop };
702
795
  return response;
703
796
  }
797
+ function* synthesizeEventsFromMessage(message) {
798
+ for (const block of message.content) {
799
+ const blk = block;
800
+ const type = blk.type;
801
+ if (type === "text") {
802
+ const text = blk.text;
803
+ if (text) yield { type: "text_delta", text };
804
+ } else if (type === "thinking") {
805
+ const text = blk.thinking;
806
+ if (text) yield { type: "thinking_delta", text };
807
+ } else if (type === "tool_use") {
808
+ const argsJson = JSON.stringify(blk.input ?? {});
809
+ yield {
810
+ type: "toolcall_delta",
811
+ id: blk.id,
812
+ name: blk.name,
813
+ argsJson
814
+ };
815
+ yield {
816
+ type: "toolcall_done",
817
+ id: blk.id,
818
+ name: blk.name,
819
+ args: blk.input ?? {}
820
+ };
821
+ } else if (type === "server_tool_use") {
822
+ yield {
823
+ type: "server_toolcall",
824
+ id: blk.id,
825
+ name: blk.name,
826
+ input: blk.input
827
+ };
828
+ } else if (type === "web_search_tool_result") {
829
+ yield {
830
+ type: "server_toolresult",
831
+ toolUseId: blk.tool_use_id,
832
+ resultType: type,
833
+ data: blk
834
+ };
835
+ }
836
+ }
837
+ yield { type: "done", stopReason: normalizeAnthropicStopReason(message.stop_reason) };
838
+ }
839
+ function messageToResponse(message) {
840
+ const contentParts = [];
841
+ for (const block of message.content) {
842
+ const blk = block;
843
+ const type = blk.type;
844
+ if (type === "text") {
845
+ contentParts.push({ type: "text", text: blk.text });
846
+ } else if (type === "thinking") {
847
+ contentParts.push({
848
+ type: "thinking",
849
+ text: blk.thinking,
850
+ signature: blk.signature ?? ""
851
+ });
852
+ } else if (type === "tool_use") {
853
+ contentParts.push({
854
+ type: "tool_call",
855
+ id: blk.id,
856
+ name: blk.name,
857
+ args: blk.input ?? {}
858
+ });
859
+ } else if (type === "server_tool_use") {
860
+ contentParts.push({
861
+ type: "server_tool_call",
862
+ id: blk.id,
863
+ name: blk.name,
864
+ input: blk.input
865
+ });
866
+ } else if (type === "web_search_tool_result") {
867
+ contentParts.push({
868
+ type: "server_tool_result",
869
+ toolUseId: blk.tool_use_id,
870
+ resultType: type,
871
+ data: blk
872
+ });
873
+ } else {
874
+ contentParts.push({ type: "raw", data: blk });
875
+ }
876
+ }
877
+ const usage = message.usage;
878
+ const inputTokens = usage.input_tokens ?? 0;
879
+ const outputTokens = usage.output_tokens ?? 0;
880
+ const cacheRead = usage.cache_read_input_tokens;
881
+ const cacheWrite = usage.cache_creation_input_tokens;
882
+ return {
883
+ message: {
884
+ role: "assistant",
885
+ content: contentParts.length > 0 ? contentParts : ""
886
+ },
887
+ stopReason: normalizeAnthropicStopReason(message.stop_reason),
888
+ usage: {
889
+ inputTokens,
890
+ outputTokens,
891
+ ...cacheRead != null && { cacheRead },
892
+ ...cacheWrite != null && { cacheWrite }
893
+ }
894
+ };
895
+ }
704
896
  function toError(err) {
705
897
  if (err instanceof Anthropic.APIError) {
706
898
  return new ProviderError("anthropic", err.message, {
@@ -728,15 +920,21 @@ function streamOpenAI(options) {
728
920
  }
729
921
  async function* runStream2(options) {
730
922
  const providerName = options.provider ?? "openai";
923
+ const useStreaming = options.streaming !== false;
731
924
  const client = createClient2(options);
732
925
  const usesThinkingParam = options.provider === "glm" || options.provider === "moonshot" || options.provider === "xiaomi";
733
- const messages = toOpenAIMessages(options.messages, { provider: options.provider });
926
+ const downgradedMessages = downgradeUnsupportedImages(options.messages, options.supportsImages);
927
+ const messages = toOpenAIMessages(downgradedMessages, {
928
+ provider: options.provider,
929
+ thinking: !!options.thinking,
930
+ supportsImages: options.supportsImages
931
+ });
734
932
  const defaultTemp = options.provider === "glm" ? 0.6 : void 0;
735
933
  const effectiveTemp = options.temperature ?? defaultTemp;
736
934
  const params = {
737
935
  model: options.model,
738
936
  messages,
739
- stream: true,
937
+ stream: useStreaming,
740
938
  ...options.maxTokens ? { max_completion_tokens: options.maxTokens } : {},
741
939
  ...effectiveTemp != null && !options.thinking ? { temperature: effectiveTemp } : {},
742
940
  ...options.topP != null ? { top_p: options.topP } : {},
@@ -744,14 +942,14 @@ async function* runStream2(options) {
744
942
  ...options.thinking && !usesThinkingParam ? { reasoning_effort: toOpenAIReasoningEffort(options.thinking) } : {},
745
943
  ...options.tools?.length ? { tools: toOpenAITools(options.tools) } : {},
746
944
  ...options.toolChoice && options.tools?.length ? { tool_choice: toOpenAIToolChoice(options.toolChoice) } : {},
747
- stream_options: { include_usage: true }
945
+ ...useStreaming ? { stream_options: { include_usage: true } } : {}
748
946
  };
749
- if (options.webSearch) {
750
- if (options.provider === "moonshot") {
751
- const raw = params;
752
- const tools = (raw.tools ?? []).slice();
753
- tools.push({ type: "builtin_function", function: { name: "$web_search" } });
754
- raw.tools = tools;
947
+ if (options.provider === "openai" || options.provider === "moonshot") {
948
+ const paramsAny = params;
949
+ paramsAny.prompt_cache_key = "ezcoder";
950
+ const retention = options.cacheRetention ?? "short";
951
+ if (retention === "long") {
952
+ paramsAny.prompt_cache_retention = "24h";
755
953
  }
756
954
  }
757
955
  if (usesThinkingParam) {
@@ -772,6 +970,17 @@ async function* runStream2(options) {
772
970
  `
773
971
  );
774
972
  }
973
+ if (!useStreaming) {
974
+ try {
975
+ const completion = await client.chat.completions.create(params, {
976
+ signal: options.signal ?? void 0
977
+ });
978
+ yield* synthesizeEventsFromCompletion(completion, !!options.thinking);
979
+ return completionToResponse(completion);
980
+ } catch (err) {
981
+ throw toError2(err, providerName);
982
+ }
983
+ }
775
984
  let stream2;
776
985
  try {
777
986
  stream2 = await client.chat.completions.create(params, {
@@ -796,6 +1005,10 @@ async function* runStream2(options) {
796
1005
  if (details?.cached_tokens) {
797
1006
  cacheRead = details.cached_tokens;
798
1007
  }
1008
+ const usageAny = chunk.usage;
1009
+ if (!cacheRead && typeof usageAny.cached_tokens === "number" && usageAny.cached_tokens > 0) {
1010
+ cacheRead = usageAny.cached_tokens;
1011
+ }
799
1012
  inputTokens = chunk.usage.prompt_tokens - cacheRead;
800
1013
  }
801
1014
  if (!choice) continue;
@@ -877,6 +1090,102 @@ async function* runStream2(options) {
877
1090
  yield { type: "done", stopReason };
878
1091
  return response;
879
1092
  }
1093
+ function* synthesizeEventsFromCompletion(completion, thinkingEnabled) {
1094
+ const choice = completion.choices?.[0];
1095
+ if (!choice) {
1096
+ yield { type: "done", stopReason: normalizeOpenAIStopReason(null) };
1097
+ return;
1098
+ }
1099
+ const msg = choice.message;
1100
+ const reasoning = msg.reasoning_content;
1101
+ if (typeof reasoning === "string" && reasoning && thinkingEnabled) {
1102
+ yield { type: "thinking_delta", text: reasoning };
1103
+ }
1104
+ if (typeof msg.content === "string" && msg.content) {
1105
+ yield { type: "text_delta", text: msg.content };
1106
+ }
1107
+ const toolCalls = msg.tool_calls;
1108
+ if (toolCalls) {
1109
+ for (const tc of toolCalls) {
1110
+ const argsJson = tc.function?.arguments ?? "";
1111
+ if (argsJson) {
1112
+ yield {
1113
+ type: "toolcall_delta",
1114
+ id: tc.id,
1115
+ name: tc.function?.name ?? "",
1116
+ argsJson
1117
+ };
1118
+ }
1119
+ let args = {};
1120
+ try {
1121
+ args = JSON.parse(argsJson);
1122
+ } catch {
1123
+ }
1124
+ yield {
1125
+ type: "toolcall_done",
1126
+ id: tc.id,
1127
+ name: tc.function?.name ?? "",
1128
+ args
1129
+ };
1130
+ }
1131
+ }
1132
+ yield { type: "done", stopReason: normalizeOpenAIStopReason(choice.finish_reason ?? null) };
1133
+ }
1134
+ function completionToResponse(completion) {
1135
+ const choice = completion.choices?.[0];
1136
+ const contentParts = [];
1137
+ let textAccum = "";
1138
+ if (choice) {
1139
+ const msg = choice.message;
1140
+ const reasoning = msg.reasoning_content;
1141
+ if (typeof reasoning === "string" && reasoning) {
1142
+ contentParts.push({ type: "thinking", text: reasoning });
1143
+ }
1144
+ if (typeof msg.content === "string" && msg.content) {
1145
+ textAccum = msg.content;
1146
+ contentParts.push({ type: "text", text: msg.content });
1147
+ }
1148
+ const toolCalls = msg.tool_calls;
1149
+ if (toolCalls) {
1150
+ for (const tc of toolCalls) {
1151
+ let args = {};
1152
+ try {
1153
+ args = JSON.parse(tc.function?.arguments ?? "{}");
1154
+ } catch {
1155
+ }
1156
+ const toolCall = {
1157
+ type: "tool_call",
1158
+ id: tc.id,
1159
+ name: tc.function?.name ?? "",
1160
+ args
1161
+ };
1162
+ contentParts.push(toolCall);
1163
+ }
1164
+ }
1165
+ }
1166
+ let inputTokens = 0;
1167
+ let outputTokens = 0;
1168
+ let cacheRead = 0;
1169
+ if (completion.usage) {
1170
+ outputTokens = completion.usage.completion_tokens;
1171
+ const details = completion.usage.prompt_tokens_details;
1172
+ if (details?.cached_tokens) cacheRead = details.cached_tokens;
1173
+ const usageAny = completion.usage;
1174
+ if (!cacheRead && typeof usageAny.cached_tokens === "number" && usageAny.cached_tokens > 0) {
1175
+ cacheRead = usageAny.cached_tokens;
1176
+ }
1177
+ inputTokens = completion.usage.prompt_tokens - cacheRead;
1178
+ }
1179
+ const stopReason = normalizeOpenAIStopReason(choice?.finish_reason ?? null);
1180
+ return {
1181
+ message: {
1182
+ role: "assistant",
1183
+ content: contentParts.length > 0 ? contentParts : textAccum
1184
+ },
1185
+ stopReason,
1186
+ usage: { inputTokens, outputTokens, ...cacheRead > 0 && { cacheRead } }
1187
+ };
1188
+ }
880
1189
  function toError2(err, provider = "openai") {
881
1190
  if (err instanceof OpenAI.APIError) {
882
1191
  let msg = err.message;
@@ -910,7 +1219,8 @@ function streamOpenAICodex(options) {
910
1219
  async function* runStream3(options) {
911
1220
  const baseUrl = (options.baseUrl || DEFAULT_BASE_URL).replace(/\/+$/, "");
912
1221
  const url = `${baseUrl}/codex/responses`;
913
- const { system, input } = toCodexInput(options.messages);
1222
+ const downgraded = downgradeUnsupportedImages(options.messages, options.supportsImages);
1223
+ const { system, input } = toCodexInput(downgraded, { supportsImages: options.supportsImages });
914
1224
  const body = {
915
1225
  model: options.model,
916
1226
  store: false,
@@ -1130,7 +1440,11 @@ function remapCodexId(id, idMap) {
1130
1440
  idMap.set(id, mapped);
1131
1441
  return mapped;
1132
1442
  }
1133
- function toCodexInput(messages) {
1443
+ function codexToolResultText(content) {
1444
+ if (typeof content === "string") return content;
1445
+ return content.filter((b) => b.type === "text").map((b) => b.text).join("\n");
1446
+ }
1447
+ function toCodexInput(messages, options) {
1134
1448
  let system;
1135
1449
  const input = [];
1136
1450
  const idMap = /* @__PURE__ */ new Map();
@@ -1183,12 +1497,33 @@ function toCodexInput(messages) {
1183
1497
  continue;
1184
1498
  }
1185
1499
  if (msg.role === "tool") {
1500
+ const toolImages = [];
1186
1501
  for (const result of msg.content) {
1187
1502
  const [callId] = result.toolCallId.includes("|") ? result.toolCallId.split("|", 2) : [result.toolCallId];
1503
+ const text = codexToolResultText(result.content);
1188
1504
  input.push({
1189
1505
  type: "function_call_output",
1190
1506
  call_id: remapCodexId(callId, idMap),
1191
- output: result.content
1507
+ output: text.length > 0 ? text : "(see attached image)"
1508
+ });
1509
+ if (options?.supportsImages !== false && Array.isArray(result.content)) {
1510
+ for (const block of result.content) {
1511
+ if (block.type === "image") toolImages.push(block);
1512
+ }
1513
+ }
1514
+ }
1515
+ if (toolImages.length > 0) {
1516
+ input.push({
1517
+ type: "message",
1518
+ role: "user",
1519
+ content: [
1520
+ { type: "input_text", text: "Attached image(s) from tool result:" },
1521
+ ...toolImages.map((img) => ({
1522
+ type: "input_image",
1523
+ detail: "auto",
1524
+ image_url: `data:${img.mediaType};base64,${img.data}`
1525
+ }))
1526
+ ]
1192
1527
  });
1193
1528
  }
1194
1529
  }
@@ -1273,6 +1608,12 @@ providerRegistry.register("moonshot", {
1273
1608
  baseUrl: options.baseUrl ?? "https://api.moonshot.ai/v1"
1274
1609
  })
1275
1610
  });
1611
+ providerRegistry.register("openrouter", {
1612
+ stream: (options) => streamOpenAI({
1613
+ ...options,
1614
+ baseUrl: options.baseUrl ?? "https://openrouter.ai/api/v1"
1615
+ })
1616
+ });
1276
1617
  providerRegistry.register("minimax", {
1277
1618
  stream: (options) => streamAnthropic({
1278
1619
  ...options,