@opencode-ai/ai 0.0.0-dev-17491 → 0.0.0-dev-17503

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.
@@ -13,7 +13,6 @@ import { Lifecycle } from "./utils/lifecycle.js";
13
13
  import { ToolSchemaProjection } from "./utils/tool-schema.js";
14
14
  import { ToolStream } from "./utils/tool-stream.js";
15
15
  const ADAPTER = "anthropic-messages";
16
- const MEDIA_MIMES = new Set([...ProviderShared.IMAGE_MIMES, ...ProviderShared.PDF_MIMES]);
17
16
  export const DEFAULT_BASE_URL = "https://api.anthropic.com/v1";
18
17
  export const PATH = "/messages";
19
18
  // =============================================================================
@@ -293,7 +292,7 @@ const lowerServerToolResult = Effect.fn("AnthropicMessages.lowerServerToolResult
293
292
  return { type: wireType, tool_use_id: part.id, content: payload };
294
293
  });
295
294
  const lowerMedia = Effect.fn("AnthropicMessages.lowerMedia")(function* (part) {
296
- const media = yield* ProviderShared.validateMedia("Anthropic Messages", part, MEDIA_MIMES);
295
+ const media = ProviderShared.normalizeMedia(part);
297
296
  if (media.mime === "application/pdf")
298
297
  return {
299
298
  type: "document",
@@ -303,6 +302,8 @@ const lowerMedia = Effect.fn("AnthropicMessages.lowerMedia")(function* (part) {
303
302
  data: media.base64,
304
303
  },
305
304
  };
305
+ if (!media.mime.startsWith("image/"))
306
+ return yield* invalid(`Anthropic Messages does not support media type ${part.mediaType}`);
306
307
  return {
307
308
  type: "image",
308
309
  source: {
@@ -11,7 +11,6 @@ import { GeminiToolSchema } from "./utils/gemini-tool-schema.js";
11
11
  import { Lifecycle } from "./utils/lifecycle.js";
12
12
  import { ToolSchemaProjection } from "./utils/tool-schema.js";
13
13
  const ADAPTER = "gemini";
14
- const MEDIA_MIMES = new Set(ProviderShared.MEDIA_MIMES);
15
14
  // Google documents this sentinel for replaying Gemini 3 function calls after their original signature was lost.
16
15
  const SKIP_THOUGHT_SIGNATURE_VALIDATOR = "skip_thought_signature_validator";
17
16
  export const DEFAULT_BASE_URL = "https://generativelanguage.googleapis.com/v1beta";
@@ -169,7 +168,7 @@ const lowerToolConfig = (toolChoice) => ProviderShared.matchToolChoice("Gemini",
169
168
  const lowerUserPart = Effect.fn("Gemini.lowerUserPart")(function* (part) {
170
169
  if (part.type === "text")
171
170
  return { text: part.text };
172
- const media = yield* ProviderShared.validateMedia("Gemini", part, MEDIA_MIMES);
171
+ const media = ProviderShared.normalizeMedia(part);
173
172
  return { inlineData: { mimeType: media.mime, data: media.base64 } };
174
173
  });
175
174
  const googleMetadata = (metadata) => ({ google: metadata });
@@ -267,7 +266,7 @@ const lowerMessages = Effect.fn("Gemini.lowerMessages")(function* (request) {
267
266
  for (const item of content) {
268
267
  if (item.type === "text")
269
268
  continue;
270
- const value = yield* ProviderShared.validateToolFile("Gemini", item, MEDIA_MIMES);
269
+ const value = ProviderShared.normalizeToolFile(item);
271
270
  media.push({ inlineData: { mimeType: value.mime, data: value.base64 } });
272
271
  }
273
272
  parts.push({
@@ -366,7 +366,7 @@ export interface Extension {
366
366
  readonly name: string;
367
367
  readonly lowerMedia?: (input: {
368
368
  readonly part: MediaPart;
369
- readonly media: ProviderShared.ValidatedMedia;
369
+ readonly media: ProviderShared.NormalizedMedia;
370
370
  readonly request: LLMRequest;
371
371
  }) => MediaInput | undefined;
372
372
  readonly messagePhase?: (value: unknown) => MessagePhase | null | undefined;
@@ -10,7 +10,6 @@ import { ToolSchemaProjection } from "./utils/tool-schema.js";
10
10
  import { ToolStream } from "./utils/tool-stream.js";
11
11
  const ADAPTER = "open-responses";
12
12
  const NAME = "Open Responses";
13
- const MEDIA_MIMES = new Set([...ProviderShared.IMAGE_MIMES, ...ProviderShared.PDF_MIMES]);
14
13
  export const PATH = "/responses";
15
14
  // =============================================================================
16
15
  // Request Body Schema
@@ -243,14 +242,14 @@ const hostedToolItemID = (part, providerMetadataKey) => {
243
242
  : undefined;
244
243
  };
245
244
  const lowerMedia = Effect.fn("OpenResponses.lowerMedia")(function* (part, request, extension) {
246
- const media = yield* ProviderShared.validateMedia(extension.name, part, MEDIA_MIMES);
245
+ const media = ProviderShared.normalizeMedia(part);
247
246
  const extended = extension.lowerMedia?.({ part, media, request });
248
247
  if (extended)
249
248
  return extended;
250
- if (media.mime === "application/pdf") {
249
+ if (!media.mime.startsWith("image/")) {
251
250
  return {
252
251
  type: "input_file",
253
- filename: part.filename ?? "document.pdf",
252
+ filename: part.filename ?? (media.mime === "application/pdf" ? "document.pdf" : "file"),
254
253
  file_data: media.dataUrl,
255
254
  };
256
255
  }
@@ -13,7 +13,6 @@ import { Lifecycle } from "./utils/lifecycle.js";
13
13
  import { ToolSchemaProjection } from "./utils/tool-schema.js";
14
14
  import { ToolStream } from "./utils/tool-stream.js";
15
15
  const ADAPTER = "openai-chat";
16
- const IMAGE_MIMES = new Set(ProviderShared.IMAGE_MIMES);
17
16
  const RESERVED_REASONING_FIELDS = new Set(["role", "content", "tool_calls"]);
18
17
  export const DEFAULT_BASE_URL = "https://api.openai.com/v1";
19
18
  export const PATH = "/chat/completions";
@@ -189,7 +188,9 @@ const lowerToolCall = (part) => ({
189
188
  },
190
189
  });
191
190
  const lowerMedia = Effect.fn("OpenAIChat.lowerMedia")(function* (part) {
192
- const media = yield* ProviderShared.validateMedia("OpenAI Chat", part, IMAGE_MIMES);
191
+ const media = ProviderShared.normalizeMedia(part);
192
+ if (!media.mime.startsWith("image/"))
193
+ return yield* ProviderShared.invalidRequest(`OpenAI Chat does not support media type ${part.mediaType}`);
193
194
  return { type: "image_url", image_url: { url: media.dataUrl } };
194
195
  });
195
196
  const openAICompatibleReasoningContent = (native) => isRecord(native) && typeof native.reasoning_content === "string" ? native.reasoning_content : undefined;
@@ -1,8 +1,7 @@
1
- import { Buffer } from "node:buffer";
2
1
  import { Tool } from "@opencode-ai/schema/tool";
3
2
  import { Effect, Schema, Stream } from "effect";
4
3
  import { Headers, HttpClientRequest } from "effect/unstable/http";
5
- import { AIError, type ContentPart, type LLMRequest, type ToolResultPart } from "../schema/index.js";
4
+ import { AIError, type ContentPart, type LLMRequest, type MediaPart, type ToolResultPart } from "../schema/index.js";
6
5
  import { isRecord } from "../utils/record.js";
7
6
  export { isRecord };
8
7
  export declare const Json: Schema.fromJsonString<Schema.Unknown>;
@@ -106,39 +105,13 @@ export declare const wrappedSystemUpdate: (route: string, message: import("../sc
106
105
  * routes: `Invalid JSON input for <route> tool call <name>`.
107
106
  */
108
107
  export declare const parseToolInput: (route: string, name: string, raw: string) => Effect.Effect<unknown, AIError, never>;
109
- export declare const IMAGE_MIMES: readonly ["image/png", "image/jpeg", "image/gif", "image/webp"];
110
- export declare const VIDEO_MIMES: readonly ["video/mp4", "video/webm", "video/quicktime"];
111
- export declare const AUDIO_MIMES: readonly ["audio/wav", "audio/mp3", "audio/aiff", "audio/aac", "audio/ogg", "audio/flac"];
112
- export declare const PDF_MIMES: readonly ["application/pdf"];
113
- export declare const MEDIA_MIMES: readonly ["image/png", "image/jpeg", "image/gif", "image/webp", "video/mp4", "video/webm", "video/quicktime", "audio/wav", "audio/mp3", "audio/aiff", "audio/aac", "audio/ogg", "audio/flac", "application/pdf"];
114
- export declare const MAX_MEDIA_ENCODED_BYTES: number;
115
- export declare const MAX_MEDIA_DECODED_BYTES: number;
116
- export interface ValidatedMedia {
108
+ export interface NormalizedMedia {
117
109
  readonly mime: string;
118
110
  readonly base64: string;
119
111
  readonly dataUrl: string;
120
- readonly bytes: Uint8Array;
121
112
  }
122
- export declare const validateMedia: (route: string, part: {
123
- readonly data: string | Uint8Array<ArrayBufferLike>;
124
- readonly type: "media";
125
- readonly mediaType: string;
126
- readonly metadata?: {
127
- readonly [x: string]: unknown;
128
- } | undefined;
129
- readonly filename?: string | undefined;
130
- }, supportedMimes: ReadonlySet<string>) => Effect.Effect<{
131
- mime: string;
132
- base64: string;
133
- dataUrl: string;
134
- bytes: Buffer<ArrayBuffer>;
135
- }, AIError, never>;
136
- export declare const validateToolFile: (route: string, part: Tool.FileContent, supportedMimes: ReadonlySet<string>) => Effect.Effect<{
137
- mime: string;
138
- base64: string;
139
- dataUrl: string;
140
- bytes: Buffer<ArrayBuffer>;
141
- }, AIError, never>;
113
+ export declare const normalizeMedia: (part: MediaPart) => NormalizedMedia;
114
+ export declare const normalizeToolFile: (part: Tool.FileContent) => NormalizedMedia;
142
115
  export declare const trimBaseUrl: (value: string) => string;
143
116
  export declare const toolResultText: (part: ToolResultPart) => string;
144
117
  export declare const errorText: (error: unknown) => string;
@@ -113,47 +113,17 @@ export const wrappedSystemUpdate = Effect.fn("ProviderShared.wrappedSystemUpdate
113
113
  * routes: `Invalid JSON input for <route> tool call <name>`.
114
114
  */
115
115
  export const parseToolInput = (route, name, raw) => parseJson(route, raw || "{}", `Invalid JSON input for ${route} tool call ${name}`);
116
- export const IMAGE_MIMES = ["image/png", "image/jpeg", "image/gif", "image/webp"];
117
- export const VIDEO_MIMES = ["video/mp4", "video/webm", "video/quicktime"];
118
- export const AUDIO_MIMES = ["audio/wav", "audio/mp3", "audio/aiff", "audio/aac", "audio/ogg", "audio/flac"];
119
- export const PDF_MIMES = ["application/pdf"];
120
- export const MEDIA_MIMES = [...IMAGE_MIMES, ...VIDEO_MIMES, ...AUDIO_MIMES, ...PDF_MIMES];
121
- export const MAX_MEDIA_ENCODED_BYTES = 28 * 1024 * 1024;
122
- export const MAX_MEDIA_DECODED_BYTES = 20 * 1024 * 1024;
123
- const base64Pattern = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
124
- export const validateMedia = Effect.fn("ProviderShared.validateMedia")(function* (route, part, supportedMimes) {
116
+ export const normalizeMedia = (part) => {
125
117
  const mime = part.mediaType.toLowerCase();
126
- if (!supportedMimes.has(mime))
127
- return yield* invalidRequest(`${route} does not support media type ${part.mediaType}`);
128
- let base64;
129
118
  if (typeof part.data !== "string") {
130
- if (part.data.byteLength > MAX_MEDIA_DECODED_BYTES)
131
- return yield* invalidRequest(`${route} media exceeds the ${MAX_MEDIA_DECODED_BYTES} byte decoded limit`);
132
- base64 = Buffer.from(part.data).toString("base64");
119
+ const base64 = Buffer.from(part.data).toString("base64");
120
+ return { mime, base64, dataUrl: `data:${mime};base64,${base64}` };
133
121
  }
134
- else if (part.data.startsWith("data:")) {
135
- const match = /^data:([^;,]+);base64,([A-Za-z0-9+/]*={0,2})$/s.exec(part.data);
136
- if (!match)
137
- return yield* invalidRequest(`${route} media data URL must contain valid base64`);
138
- if (match[1].toLowerCase() !== mime)
139
- return yield* invalidRequest(`${route} media type ${part.mediaType} does not match data URL type ${match[1]}`);
140
- base64 = match[2];
141
- }
142
- else {
143
- base64 = part.data;
144
- }
145
- if (Buffer.byteLength(base64, "utf8") > MAX_MEDIA_ENCODED_BYTES)
146
- return yield* invalidRequest(`${route} media exceeds the ${MAX_MEDIA_ENCODED_BYTES} byte encoded limit`);
147
- if (!base64 || base64.length % 4 !== 0 || !base64Pattern.test(base64))
148
- return yield* invalidRequest(`${route} media must contain valid base64`);
149
- const bytes = Buffer.from(base64, "base64");
150
- if (bytes.byteLength > MAX_MEDIA_DECODED_BYTES)
151
- return yield* invalidRequest(`${route} media exceeds the ${MAX_MEDIA_DECODED_BYTES} byte decoded limit`);
152
- if (bytes.toString("base64") !== base64)
153
- return yield* invalidRequest(`${route} media must contain canonical base64`);
154
- return { mime, base64, dataUrl: `data:${mime};base64,${base64}`, bytes };
155
- });
156
- export const validateToolFile = (route, part, supportedMimes) => validateMedia(route, { type: "media", mediaType: part.mime, data: part.uri, filename: part.name }, supportedMimes);
122
+ if (!part.data.startsWith("data:"))
123
+ return { mime, base64: part.data, dataUrl: `data:${mime};base64,${part.data}` };
124
+ return { mime, base64: part.data.slice(part.data.indexOf(",") + 1), dataUrl: part.data };
125
+ };
126
+ export const normalizeToolFile = (part) => normalizeMedia({ type: "media", mediaType: part.mime, data: part.uri, filename: part.name });
157
127
  export const trimBaseUrl = (value) => value.replace(/\/+$/, "");
158
128
  export const toolResultText = (part) => {
159
129
  if (part.result.type === "text")
@@ -53,7 +53,7 @@ export const lower = Effect.fn("BedrockMedia.lower")(function* (part) {
53
53
  const mime = part.mediaType.toLowerCase();
54
54
  const imageFormat = IMAGE_FORMATS[mime];
55
55
  if (imageFormat) {
56
- const media = yield* ProviderShared.validateMedia("Bedrock Converse", part, new Set(Object.keys(IMAGE_FORMATS)));
56
+ const media = ProviderShared.normalizeMedia(part);
57
57
  return { image: { format: imageFormat, source: { bytes: media.base64 } } };
58
58
  }
59
59
  if (mime.startsWith("image/"))
@@ -62,7 +62,7 @@ export const lower = Effect.fn("BedrockMedia.lower")(function* (part) {
62
62
  if (documentFormat) {
63
63
  if (!part.filename)
64
64
  return yield* ProviderShared.invalidRequest("Bedrock Converse document media requires a filename");
65
- const media = yield* ProviderShared.validateMedia("Bedrock Converse", part, new Set(Object.keys(DOCUMENT_FORMATS)));
65
+ const media = ProviderShared.normalizeMedia(part);
66
66
  return documentBlock(part.filename, documentFormat, media.base64);
67
67
  }
68
68
  return yield* ProviderShared.invalidRequest(`Bedrock Converse does not support media type ${part.mediaType}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
- "version": "0.0.0-dev-17491",
3
+ "version": "0.0.0-dev-17503",
4
4
  "name": "@opencode-ai/ai",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -30,7 +30,7 @@
30
30
  "devDependencies": {
31
31
  "@clack/prompts": "1.0.0-alpha.1",
32
32
  "@effect/platform-node": "4.0.0-beta.101",
33
- "@opencode-ai/http-recorder": "0.0.0-dev-17491",
33
+ "@opencode-ai/http-recorder": "0.0.0-dev-17503",
34
34
  "@tsconfig/bun": "1.0.9",
35
35
  "@types/bun": "1.3.13",
36
36
  "@typescript/native-preview": "7.0.0-dev.20251207.1",
@@ -39,7 +39,7 @@
39
39
  "dependencies": {
40
40
  "@smithy/eventstream-codec": "4.2.14",
41
41
  "@smithy/util-utf8": "4.2.2",
42
- "@opencode-ai/schema": "0.0.0-dev-17491",
42
+ "@opencode-ai/schema": "0.0.0-dev-17503",
43
43
  "aws4fetch": "1.0.20",
44
44
  "effect": "4.0.0-beta.101",
45
45
  "google-auth-library": "10.5.0"