veryfront 0.1.438 → 0.1.440

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.
Files changed (42) hide show
  1. package/esm/deno.js +1 -1
  2. package/esm/extensions/ext-anthropic/src/anthropic-provider.d.ts.map +1 -1
  3. package/esm/extensions/ext-anthropic/src/anthropic-provider.js +23 -5
  4. package/esm/extensions/ext-google/src/google-provider.d.ts.map +1 -1
  5. package/esm/extensions/ext-google/src/google-provider.js +22 -2
  6. package/esm/extensions/ext-openai/src/openai-provider.d.ts.map +1 -1
  7. package/esm/extensions/ext-openai/src/openai-provider.js +23 -2
  8. package/esm/src/agent/hosted-durable-chat-run-start.d.ts +46 -0
  9. package/esm/src/agent/hosted-durable-chat-run-start.d.ts.map +1 -0
  10. package/esm/src/agent/hosted-durable-chat-run-start.js +148 -0
  11. package/esm/src/agent/index.d.ts +2 -0
  12. package/esm/src/agent/index.d.ts.map +1 -1
  13. package/esm/src/agent/index.js +1 -0
  14. package/esm/src/agent/runtime/text-generation-runtime-message-converter.d.ts.map +1 -1
  15. package/esm/src/agent/runtime/text-generation-runtime-message-converter.js +38 -2
  16. package/esm/src/agent/runtime/text-generation-runtime-message-types.d.ts +7 -1
  17. package/esm/src/agent/runtime/text-generation-runtime-message-types.d.ts.map +1 -1
  18. package/esm/src/agent/runtime-message-file-url-refresh.d.ts.map +1 -1
  19. package/esm/src/agent/runtime-message-file-url-refresh.js +64 -10
  20. package/esm/src/chat/conversation.d.ts.map +1 -1
  21. package/esm/src/chat/conversation.js +8 -5
  22. package/esm/src/provider/runtime-loader.d.ts +14 -1
  23. package/esm/src/provider/runtime-loader.d.ts.map +1 -1
  24. package/esm/src/provider/runtime-loader.js +19 -1
  25. package/esm/src/runtime/runtime-bridge.d.ts.map +1 -1
  26. package/esm/src/runtime/runtime-bridge.js +3 -1
  27. package/esm/src/utils/version-constant.d.ts +1 -1
  28. package/esm/src/utils/version-constant.js +1 -1
  29. package/package.json +1 -1
  30. package/src/deno.js +1 -1
  31. package/src/extensions/ext-anthropic/src/anthropic-provider.ts +28 -5
  32. package/src/extensions/ext-google/src/google-provider.ts +27 -2
  33. package/src/extensions/ext-openai/src/openai-provider.ts +29 -2
  34. package/src/src/agent/hosted-durable-chat-run-start.ts +246 -0
  35. package/src/src/agent/index.ts +12 -0
  36. package/src/src/agent/runtime/text-generation-runtime-message-converter.ts +41 -2
  37. package/src/src/agent/runtime/text-generation-runtime-message-types.ts +8 -1
  38. package/src/src/agent/runtime-message-file-url-refresh.ts +78 -12
  39. package/src/src/chat/conversation.ts +15 -9
  40. package/src/src/provider/runtime-loader.ts +46 -3
  41. package/src/src/runtime/runtime-bridge.ts +10 -2
  42. package/src/src/utils/version-constant.ts +1 -1
@@ -0,0 +1,246 @@
1
+ import { parseProviderError } from "../chat/provider-errors.js";
2
+ import {
3
+ AgUiDetachedStartAcceptedSchema,
4
+ buildDetachedAgUiStartRequest,
5
+ executeAgUiDetachedStart,
6
+ } from "./ag-ui-detached-start.js";
7
+ import type { AgUiResumeValue } from "./ag-ui-tool-shared.js";
8
+ import type { DetachedRunTracker } from "./detached-run-tracker.js";
9
+ import type { ParsedHostedChatRequest } from "./hosted-chat-request-parser.js";
10
+
11
+ export type HostedDurableRunSetupErrorStatusCode = 400 | 402 | 413 | 429 | 500 | 503;
12
+
13
+ export type HostedDurableRunAccepted = {
14
+ accepted: boolean;
15
+ duplicate: boolean;
16
+ };
17
+
18
+ export type HostedDurableRunAuthErrorResponse = {
19
+ errorCode: string;
20
+ statusCode: number;
21
+ metadata?: Record<string, unknown>;
22
+ };
23
+
24
+ export type HostedDurableRunLogger = {
25
+ error(message: string, metadata?: Record<string, unknown>): void;
26
+ };
27
+
28
+ export type HostedDurableRunStartExecutionInput<TExecution> = {
29
+ execution: TExecution;
30
+ abortSignal: AbortSignal;
31
+ };
32
+
33
+ export type HostedDurableRunStartCleanupInput<TExecution> = {
34
+ execution: TExecution;
35
+ runId: string;
36
+ conversationId: string;
37
+ };
38
+
39
+ export type ExecuteHostedDurableChatRunInput<TExecution> = {
40
+ req: ParsedHostedChatRequest;
41
+ rawRequest: Request;
42
+ requestOrCtx?: unknown;
43
+ tracker: DetachedRunTracker<AgUiResumeValue>;
44
+ prepareExecution: (req: ParsedHostedChatRequest) => Promise<TExecution>;
45
+ startDetachedExecution: (
46
+ input: HostedDurableRunStartExecutionInput<TExecution>,
47
+ ) => Promise<void>;
48
+ cleanupExecution?: (input: HostedDurableRunStartCleanupInput<TExecution>) => Promise<void>;
49
+ resolveAuthError?: (error: unknown) => HostedDurableRunAuthErrorResponse | null | undefined;
50
+ logger?: HostedDurableRunLogger;
51
+ };
52
+
53
+ function readBooleanProperty(input: object | null, propertyName: string): boolean {
54
+ if (!input) {
55
+ return false;
56
+ }
57
+
58
+ return Object.getOwnPropertyDescriptor(input, propertyName)?.value === true;
59
+ }
60
+
61
+ function isDurableRunSetupErrorStatusCode(
62
+ status: number | undefined,
63
+ ): status is HostedDurableRunSetupErrorStatusCode {
64
+ return status === 400 || status === 402 || status === 413 || status === 429 ||
65
+ status === 500 || status === 503;
66
+ }
67
+
68
+ function fallbackDurableRunSetupErrorStatusCode(
69
+ code: string,
70
+ ): HostedDurableRunSetupErrorStatusCode {
71
+ if (code === "OVERLOADED_ERROR") return 503;
72
+ if (code === "CONTEXT_LENGTH_EXCEEDED") return 413;
73
+
74
+ return 500;
75
+ }
76
+
77
+ export function resolveHostedDurableRunSetupErrorResponse(input: {
78
+ code: string;
79
+ status?: number;
80
+ originalError: unknown;
81
+ }): {
82
+ errorCode: string;
83
+ statusCode: HostedDurableRunSetupErrorStatusCode;
84
+ } {
85
+ if (
86
+ input.originalError instanceof Error &&
87
+ input.originalError.message === "DURABLE_CHAT_ROOT_REQUIRES_CONVERSATION"
88
+ ) {
89
+ return {
90
+ errorCode: "DURABLE_CHAT_ROOT_REQUIRES_CONVERSATION",
91
+ statusCode: 400,
92
+ };
93
+ }
94
+
95
+ return {
96
+ errorCode: input.code,
97
+ statusCode: isDurableRunSetupErrorStatusCode(input.status)
98
+ ? input.status
99
+ : fallbackDurableRunSetupErrorStatusCode(input.code),
100
+ };
101
+ }
102
+
103
+ async function parseAcceptedDetachedStartResponse(
104
+ response: Response,
105
+ ): Promise<HostedDurableRunAccepted> {
106
+ if (response.status !== 202) {
107
+ return { accepted: false, duplicate: false };
108
+ }
109
+
110
+ const payload = await response.json().catch((): null => null);
111
+ const parsed = AgUiDetachedStartAcceptedSchema.safeParse(payload);
112
+ if (parsed.success) {
113
+ return {
114
+ accepted: parsed.data.accepted,
115
+ duplicate: parsed.data.duplicate,
116
+ };
117
+ }
118
+
119
+ const payloadObject = typeof payload === "object" ? payload : null;
120
+ return {
121
+ accepted: readBooleanProperty(payloadObject, "accepted"),
122
+ duplicate: readBooleanProperty(payloadObject, "duplicate"),
123
+ };
124
+ }
125
+
126
+ async function executeHostedDurableChatRunStart<TExecution>(
127
+ input: ExecuteHostedDurableChatRunInput<TExecution>,
128
+ ): Promise<Response | HostedDurableRunAccepted> {
129
+ const { durableRootRun, conversationId } = input.req;
130
+ if (!durableRootRun || !conversationId) {
131
+ throw new Error("DURABLE_CHAT_ROOT_REQUIRES_CONVERSATION");
132
+ }
133
+
134
+ const execution = await input.prepareExecution(input.req);
135
+ const detachedStartRequest = buildDetachedAgUiStartRequest({
136
+ runId: durableRootRun.runId,
137
+ threadId: conversationId,
138
+ messages: input.req.messages,
139
+ model: input.req.model,
140
+ forwardedProps: input.req.forwardedProps,
141
+ });
142
+ const detachedStartResponse = await executeAgUiDetachedStart(
143
+ {
144
+ sessionManager: input.tracker.sessionManager,
145
+ startDetachedExecution: async ({ abortSignal }) => {
146
+ const detachedExecution = input.startDetachedExecution({
147
+ execution,
148
+ abortSignal,
149
+ });
150
+ input.tracker.registerExecution(durableRootRun.runId, detachedExecution);
151
+ await detachedExecution;
152
+ },
153
+ onDuplicate: async () => {
154
+ await input.cleanupExecution?.({
155
+ execution,
156
+ runId: durableRootRun.runId,
157
+ conversationId,
158
+ });
159
+ },
160
+ onAccepted: async () => {
161
+ input.tracker.trackRun(durableRootRun.runId);
162
+ },
163
+ onError: async ({ error }) => {
164
+ input.tracker.untrackRun(durableRootRun.runId);
165
+ input.logger?.error("Detached durable run execution failed", {
166
+ runId: durableRootRun.runId,
167
+ conversationId,
168
+ error: error instanceof Error ? error.message : String(error),
169
+ });
170
+ },
171
+ },
172
+ {
173
+ request: detachedStartRequest,
174
+ rawRequest: input.rawRequest,
175
+ requestOrCtx: input.requestOrCtx ?? input.rawRequest,
176
+ },
177
+ );
178
+
179
+ if (detachedStartResponse.status !== 202) {
180
+ return detachedStartResponse;
181
+ }
182
+
183
+ return await parseAcceptedDetachedStartResponse(detachedStartResponse);
184
+ }
185
+
186
+ export async function executeHostedDurableChatRun<TExecution>(
187
+ input: ExecuteHostedDurableChatRunInput<TExecution>,
188
+ ): Promise<Response> {
189
+ const { durableRootRun, conversationId, projectId, userId } = input.req;
190
+ if (!durableRootRun || !conversationId) {
191
+ return Response.json(
192
+ { errorCode: "DURABLE_CHAT_ROOT_REQUIRES_CONVERSATION" },
193
+ { status: 400 },
194
+ );
195
+ }
196
+
197
+ const existingRunStatus = input.tracker.sessionManager.getRunStatus(durableRootRun.runId);
198
+ if (existingRunStatus === "running" || existingRunStatus === "waiting") {
199
+ return Response.json({ accepted: true, duplicate: true }, { status: 202 });
200
+ }
201
+
202
+ try {
203
+ const startResult = await executeHostedDurableChatRunStart(input);
204
+
205
+ if (startResult instanceof Response) {
206
+ return startResult;
207
+ }
208
+
209
+ return Response.json(startResult, { status: 202 });
210
+ } catch (error) {
211
+ const authError = input.resolveAuthError?.(error);
212
+ if (authError) {
213
+ input.logger?.error("Durable chat auth error from API", {
214
+ errorCode: authError.errorCode,
215
+ statusCode: authError.statusCode,
216
+ projectId,
217
+ userId,
218
+ runId: durableRootRun.runId,
219
+ ...authError.metadata,
220
+ });
221
+ return Response.json(
222
+ { errorCode: authError.errorCode },
223
+ { status: authError.statusCode },
224
+ );
225
+ }
226
+
227
+ const { code, status } = parseProviderError(error);
228
+ const response = resolveHostedDurableRunSetupErrorResponse({
229
+ code,
230
+ status,
231
+ originalError: error,
232
+ });
233
+ input.logger?.error("Durable chat execute failed during setup", {
234
+ errorCode: code,
235
+ originalError: error instanceof Error ? error.message : String(error),
236
+ projectId,
237
+ userId,
238
+ runId: durableRootRun.runId,
239
+ });
240
+
241
+ return Response.json(
242
+ { errorCode: response.errorCode },
243
+ { status: response.statusCode },
244
+ );
245
+ }
246
+ }
@@ -282,6 +282,17 @@ export type {
282
282
  HostedChatRuntimeToUiMessageStreamOptions,
283
283
  } from "./hosted-chat-runtime-contract.js";
284
284
 
285
+ export {
286
+ executeHostedDurableChatRun,
287
+ type ExecuteHostedDurableChatRunInput,
288
+ type HostedDurableRunAccepted,
289
+ type HostedDurableRunAuthErrorResponse,
290
+ type HostedDurableRunLogger,
291
+ type HostedDurableRunSetupErrorStatusCode,
292
+ type HostedDurableRunStartCleanupInput,
293
+ type HostedDurableRunStartExecutionInput,
294
+ resolveHostedDurableRunSetupErrorResponse,
295
+ } from "./hosted-durable-chat-run-start.js";
285
296
  export {
286
297
  buildParsedHostedChatRequest,
287
298
  type HostedChatProjectAccessError,
@@ -1082,6 +1093,7 @@ export {
1082
1093
  executeAgUiDetachedStart,
1083
1094
  type ExecuteAgUiDetachedStartInput,
1084
1095
  } from "./ag-ui-detached-start.js";
1096
+ export type { AgUiResumeValue } from "./ag-ui-tool-shared.js";
1085
1097
  export {
1086
1098
  createDetachedRunTracker,
1087
1099
  type DetachedRunDrainResult,
@@ -9,6 +9,7 @@
9
9
 
10
10
  import type {
11
11
  TextGenerationRuntimeAssistantMessage,
12
+ TextGenerationRuntimeFilePart,
12
13
  TextGenerationRuntimeMessage,
13
14
  TextGenerationRuntimeTextPart,
14
15
  TextGenerationRuntimeToolCallPart,
@@ -75,6 +76,26 @@ function getUserTextWithAttachmentContext(parts: Message["parts"]): string {
75
76
  : appendReadableAttachmentContext(text, buildAttachmentContextFromParts(parts));
76
77
  }
77
78
 
79
+ function getUserFileParts(parts: Message["parts"]): TextGenerationRuntimeFilePart[] {
80
+ return parts.flatMap((part) => {
81
+ const type = getStringPartField(part, "type");
82
+ if (type !== "file" && type !== "image") return [];
83
+
84
+ const mediaType = getStringPartField(part, "mediaType");
85
+ const url = getStringPartField(part, "url");
86
+ if (!mediaType || !url || url.startsWith("data:")) return [];
87
+
88
+ return [{
89
+ type,
90
+ mediaType,
91
+ url,
92
+ ...(getStringPartField(part, "filename")
93
+ ? { filename: getStringPartField(part, "filename") }
94
+ : {}),
95
+ }];
96
+ });
97
+ }
98
+
78
99
  /**
79
100
  * Convert a veryfront Message to the current text-generation runtime message format.
80
101
  */
@@ -86,8 +107,26 @@ export function convertToTextGenerationRuntimeMessage(msg: Message): TextGenerat
86
107
  }
87
108
 
88
109
  case "user": {
89
- const text = getUserTextWithAttachmentContext(msg.parts);
90
- return { role: "user", content: text };
110
+ const fileParts = getUserFileParts(msg.parts);
111
+ if (fileParts.length === 0) {
112
+ const text = getUserTextWithAttachmentContext(msg.parts);
113
+ return { role: "user", content: text };
114
+ }
115
+
116
+ const text = getTextFromParts(msg.parts);
117
+ const attachmentContext = text.includes("<uploaded_files>")
118
+ ? ""
119
+ : buildAttachmentContextFromParts(msg.parts);
120
+ return {
121
+ role: "user",
122
+ content: [
123
+ ...(text.length > 0 ? [{ type: "text" as const, text }] : []),
124
+ ...fileParts,
125
+ ...(attachmentContext.length > 0
126
+ ? [{ type: "text" as const, text: attachmentContext.trimStart() }]
127
+ : []),
128
+ ],
129
+ };
91
130
  }
92
131
 
93
132
  case "assistant": {
@@ -11,6 +11,13 @@ export interface TextGenerationRuntimeTextPart {
11
11
  text: string;
12
12
  }
13
13
 
14
+ export interface TextGenerationRuntimeFilePart {
15
+ type: "file" | "image";
16
+ mediaType: string;
17
+ url: string;
18
+ filename?: string;
19
+ }
20
+
14
21
  export interface TextGenerationRuntimeToolCallPart {
15
22
  type: "tool-call";
16
23
  toolCallId: string;
@@ -35,7 +42,7 @@ export interface TextGenerationRuntimeSystemMessage {
35
42
 
36
43
  export interface TextGenerationRuntimeUserMessage {
37
44
  role: "user";
38
- content: string;
45
+ content: string | Array<TextGenerationRuntimeTextPart | TextGenerationRuntimeFilePart>;
39
46
  }
40
47
 
41
48
  export interface TextGenerationRuntimeAssistantMessage {
@@ -1,4 +1,4 @@
1
- import type { ChatFileUiPart, ChatUiMessage, FileUIPartWithUpload } from "../chat/types.js";
1
+ import type { ChatUiMessage, FileUIPartWithUpload } from "../chat/types.js";
2
2
 
3
3
  export type RuntimeFileUrlResolverInput = {
4
4
  uploadId: string;
@@ -18,25 +18,30 @@ export async function resolveRuntimeMessageFileUrls(
18
18
 
19
19
  return Promise.all(
20
20
  messages.map(async (message) => {
21
- if (!message.parts.some((part) => part.type === "file" && part.uploadId)) {
21
+ if (!message.parts.some((part) => getUploadId(part))) {
22
22
  return message;
23
23
  }
24
24
 
25
25
  const parts = await Promise.all(
26
26
  message.parts.map(async (part) => {
27
- if (!isFilePartWithUpload(part)) return part;
27
+ const uploadId = getUploadId(part);
28
+ if (!uploadId) return part;
28
29
 
29
- let urlPromise = urlByUploadId.get(part.uploadId);
30
+ let urlPromise = urlByUploadId.get(uploadId);
30
31
  if (!urlPromise) {
31
- urlPromise = resolveFileUrl({ uploadId: part.uploadId, part, message });
32
- urlByUploadId.set(part.uploadId, urlPromise);
32
+ urlPromise = resolveFileUrl({
33
+ uploadId,
34
+ part: toResolverPart(part, uploadId),
35
+ message,
36
+ });
37
+ urlByUploadId.set(uploadId, urlPromise);
33
38
  }
34
39
 
35
40
  const signedUrl = await urlPromise;
36
- if (!signedUrl || signedUrl === part.url) return part;
41
+ if (!signedUrl) return normalizeUploadedFilePart(part, uploadId);
37
42
 
38
43
  return {
39
- ...part,
44
+ ...normalizeUploadedFilePart(part, uploadId),
40
45
  url: signedUrl,
41
46
  };
42
47
  }),
@@ -47,8 +52,69 @@ export async function resolveRuntimeMessageFileUrls(
47
52
  );
48
53
  }
49
54
 
50
- function isFilePartWithUpload(part: ChatUiMessage["parts"][number]): part is ChatFileUiPart & {
51
- uploadId: string;
52
- } {
53
- return part.type === "file" && typeof part.uploadId === "string" && part.uploadId.length > 0;
55
+ function isRecord(value: unknown): value is Record<string, unknown> {
56
+ return typeof value === "object" && value !== null && !Array.isArray(value);
57
+ }
58
+
59
+ function getStringField(part: unknown, key: string): string | undefined {
60
+ if (!isRecord(part)) return undefined;
61
+
62
+ const value = part[key];
63
+ return typeof value === "string" && value.length > 0 ? value : undefined;
64
+ }
65
+
66
+ function getUploadId(part: unknown): string | undefined {
67
+ if (!isRecord(part) || (part.type !== "file" && part.type !== "image")) {
68
+ return undefined;
69
+ }
70
+
71
+ return getStringField(part, "uploadId") ?? getStringField(part, "upload_id");
72
+ }
73
+
74
+ function getMediaType(part: unknown): string | undefined {
75
+ return getStringField(part, "mediaType") ?? getStringField(part, "media_type");
76
+ }
77
+
78
+ function normalizeUploadedFilePart(
79
+ part: ChatUiMessage["parts"][number],
80
+ uploadId: string,
81
+ ): ChatUiMessage["parts"][number] {
82
+ if (!isRecord(part)) return part;
83
+
84
+ const partRecord: Record<string, unknown> = part;
85
+ const partType = partRecord.type;
86
+ if (partType !== "file" && partType !== "image") return part;
87
+
88
+ const mediaType = getMediaType(part);
89
+ const url = getStringField(part, "url");
90
+ if (!mediaType || !url) return part;
91
+
92
+ const filename = getStringField(part, "filename");
93
+ const uploadPath = getStringField(part, "uploadPath") ?? getStringField(part, "upload_path");
94
+
95
+ return {
96
+ type: partType === "image" ? "image" : "file",
97
+ mediaType,
98
+ url,
99
+ ...(filename ? { filename } : {}),
100
+ uploadId,
101
+ ...(uploadPath ? { uploadPath } : {}),
102
+ } as ChatUiMessage["parts"][number];
103
+ }
104
+
105
+ function toResolverPart(
106
+ part: ChatUiMessage["parts"][number],
107
+ uploadId: string,
108
+ ): FileUIPartWithUpload {
109
+ const normalized = normalizeUploadedFilePart(part, uploadId);
110
+ if (isRecord(normalized) && normalized.type === "file") {
111
+ return normalized as FileUIPartWithUpload;
112
+ }
113
+
114
+ return {
115
+ type: "file",
116
+ mediaType: getMediaType(part) ?? "application/octet-stream",
117
+ url: getStringField(part, "url") ?? "",
118
+ uploadId,
119
+ };
54
120
  }
@@ -515,7 +515,7 @@ function toJsonValue(value: unknown): JsonValue {
515
515
  }
516
516
 
517
517
  function getFilePart(part: unknown): {
518
- type: "file";
518
+ type: "file" | "image";
519
519
  mediaType: string;
520
520
  data: string;
521
521
  url: string;
@@ -523,22 +523,25 @@ function getFilePart(part: unknown): {
523
523
  uploadId?: string;
524
524
  uploadPath?: string;
525
525
  } | null {
526
- if (!isRecord(part) || part.type !== "file") {
526
+ if (!isRecord(part) || (part.type !== "file" && part.type !== "image")) {
527
527
  return null;
528
528
  }
529
529
 
530
- const mediaType = getNonEmptyStringField(part, "mediaType");
530
+ const mediaType = getNonEmptyStringField(part, "mediaType") ??
531
+ getNonEmptyStringField(part, "media_type");
531
532
  const data = getNonEmptyStringField(part, "url");
532
533
  if (!mediaType || !data) {
533
534
  return null;
534
535
  }
535
536
 
536
537
  const filename = getNonEmptyStringField(part, "filename");
537
- const uploadId = getNonEmptyStringField(part, "uploadId");
538
- const uploadPath = getNonEmptyStringField(part, "uploadPath");
538
+ const uploadId = getNonEmptyStringField(part, "uploadId") ??
539
+ getNonEmptyStringField(part, "upload_id");
540
+ const uploadPath = getNonEmptyStringField(part, "uploadPath") ??
541
+ getNonEmptyStringField(part, "upload_path");
539
542
 
540
543
  return {
541
- type: "file",
544
+ type: part.type === "image" ? "image" : "file",
542
545
  mediaType,
543
546
  data,
544
547
  url: data,
@@ -721,10 +724,13 @@ function convertSystemMessage(message: ChatUiMessage): ProviderModelMessage[] {
721
724
  function convertUserMessage(message: ChatUiMessage): ProviderModelMessage[] {
722
725
  const content: Array<
723
726
  { type: "text"; text: string } | {
724
- type: "file";
727
+ type: "file" | "image";
725
728
  mediaType: string;
726
729
  data: string;
730
+ url: string;
727
731
  filename?: string;
732
+ uploadId?: string;
733
+ uploadPath?: string;
728
734
  }
729
735
  > = [];
730
736
 
@@ -757,7 +763,7 @@ function convertAssistantMessage(message: ChatUiMessage): ProviderModelMessage[]
757
763
  const assistantContent: Array<
758
764
  | { type: "text"; text: string }
759
765
  | { type: "reasoning"; text: string }
760
- | { type: "file"; mediaType: string; data: string; filename?: string }
766
+ | { type: "file" | "image"; mediaType: string; data: string; filename?: string }
761
767
  | { type: "tool-call"; toolCallId: string; toolName: string; input: Record<string, unknown> }
762
768
  > = [];
763
769
  const deferredAssistantContent: typeof assistantContent = [];
@@ -806,7 +812,7 @@ function convertAssistantMessage(message: ChatUiMessage): ProviderModelMessage[]
806
812
  part:
807
813
  | { type: "text"; text: string }
808
814
  | { type: "reasoning"; text: string }
809
- | { type: "file"; mediaType: string; data: string; filename?: string }
815
+ | { type: "file" | "image"; mediaType: string; data: string; filename?: string }
810
816
  | { type: "tool-call"; toolCallId: string; toolName: string; input: Record<string, unknown> },
811
817
  ) => {
812
818
  if (toolResults.length > 0) {
@@ -34,7 +34,13 @@ export type { RuntimeUsage };
34
34
 
35
35
  export type RuntimePromptMessage =
36
36
  | { role: "system"; content: string }
37
- | { role: "user"; content: Array<{ type: "text"; text: string }> }
37
+ | {
38
+ role: "user";
39
+ content: Array<
40
+ | { type: "text"; text: string }
41
+ | { type: "image" | "file"; mediaType: string; url: string; filename?: string }
42
+ >;
43
+ }
38
44
  | {
39
45
  role: "assistant";
40
46
  content: Array<
@@ -261,7 +267,15 @@ type OpenAICompatibleLanguageOptions = {
261
267
  };
262
268
  export type OpenAICompatibleChatMessage =
263
269
  | { role: "system"; content: string }
264
- | { role: "user"; content: string }
270
+ | {
271
+ role: "user";
272
+ content:
273
+ | string
274
+ | Array<
275
+ | { type: "text"; text: string }
276
+ | { type: "image_url"; image_url: { url: string } }
277
+ >;
278
+ }
265
279
  | {
266
280
  role: "assistant";
267
281
  content: string | null;
@@ -279,6 +293,11 @@ export type OpenAICompatibleChatMessage =
279
293
  tool_call_id: string;
280
294
  content: string;
281
295
  };
296
+ type RuntimePromptUserContent = Extract<RuntimePromptMessage, { role: "user" }>["content"];
297
+ type OpenAICompatibleUserContent = Extract<
298
+ OpenAICompatibleChatMessage,
299
+ { role: "user" }
300
+ >["content"];
282
301
  export type OpenAICompatibleChatRequest = {
283
302
  model: string;
284
303
  messages: OpenAICompatibleChatMessage[];
@@ -359,6 +378,30 @@ export function readTextParts(parts: Array<{ type: string; text?: string }>): st
359
378
  return text;
360
379
  }
361
380
 
381
+ function toOpenAICompatibleUserContent(
382
+ parts: RuntimePromptUserContent,
383
+ ): OpenAICompatibleUserContent {
384
+ if (!parts.some((part) => part.type !== "text" && part.mediaType.startsWith("image/"))) {
385
+ return readTextParts(parts);
386
+ }
387
+
388
+ const content: Exclude<OpenAICompatibleUserContent, string> = [];
389
+
390
+ for (const part of parts) {
391
+ if (part.type === "text") {
392
+ if (part.text.length > 0) {
393
+ content.push({ type: "text", text: part.text });
394
+ }
395
+ continue;
396
+ }
397
+ if (part.type === "image" || part.mediaType.startsWith("image/")) {
398
+ content.push({ type: "image_url", image_url: { url: part.url } });
399
+ }
400
+ }
401
+
402
+ return content.length > 0 ? content : readTextParts(parts);
403
+ }
404
+
362
405
  export function toOpenAICompatibleMessages(
363
406
  prompt: RuntimePromptMessage[],
364
407
  ): OpenAICompatibleChatMessage[] {
@@ -370,7 +413,7 @@ export function toOpenAICompatibleMessages(
370
413
  messages.push({ role: "system", content: message.content });
371
414
  break;
372
415
  case "user":
373
- messages.push({ role: "user", content: readTextParts(message.content) });
416
+ messages.push({ role: "user", content: toOpenAICompatibleUserContent(message.content) });
374
417
  break;
375
418
  case "assistant": {
376
419
  let text = "";
@@ -73,7 +73,13 @@ type EmbedManyOptions = {
73
73
 
74
74
  type RuntimePromptMessage =
75
75
  | { role: "system"; content: string }
76
- | { role: "user"; content: Array<{ type: "text"; text: string }> }
76
+ | {
77
+ role: "user";
78
+ content: Array<
79
+ | { type: "text"; text: string }
80
+ | { type: "image" | "file"; mediaType: string; url: string; filename?: string }
81
+ >;
82
+ }
77
83
  | {
78
84
  role: "assistant";
79
85
  content: Array<
@@ -181,7 +187,9 @@ function toRuntimePrompt(
181
187
  case "user":
182
188
  prompt.push({
183
189
  role: "user",
184
- content: [{ type: "text", text: message.content }],
190
+ content: typeof message.content === "string"
191
+ ? [{ type: "text", text: message.content }]
192
+ : message.content,
185
193
  });
186
194
  break;
187
195
  case "assistant":
@@ -1,3 +1,3 @@
1
1
  // Keep in sync with deno.json version.
2
2
  // scripts/release.ts updates this constant during releases.
3
- export const VERSION = "0.1.438";
3
+ export const VERSION = "0.1.440";