@opencode-ai/ai 0.0.0-beta-17498 → 0.0.0-beta-17570

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: {
@@ -38,7 +38,7 @@ declare const GeminiBody: Schema.Struct<{
38
38
  readonly functionCall: Schema.Struct<{
39
39
  readonly id: Schema.optional<Schema.String>;
40
40
  readonly name: Schema.String;
41
- readonly args: Schema.Unknown;
41
+ readonly args: Schema.optional<Schema.Unknown>;
42
42
  }>;
43
43
  readonly thoughtSignature: Schema.optional<Schema.String>;
44
44
  }>, Schema.Struct<{
@@ -55,6 +55,7 @@ declare const GeminiBody: Schema.Struct<{
55
55
  }>;
56
56
  }>]>>;
57
57
  }>>;
58
+ labels: Schema.optional<Schema.$Record<Schema.String, Schema.String>>;
58
59
  safetySettings: Schema.optional<Schema.$Array<Schema.Struct<{
59
60
  readonly category: Schema.String;
60
61
  readonly threshold: Schema.String;
@@ -115,8 +116,8 @@ export declare const protocol: Protocol<{
115
116
  } | {
116
117
  readonly functionCall: {
117
118
  readonly name: string;
118
- readonly args: unknown;
119
119
  readonly id?: string | undefined;
120
+ readonly args?: unknown;
120
121
  };
121
122
  readonly thoughtSignature?: string | undefined;
122
123
  } | {
@@ -154,6 +155,9 @@ export declare const protocol: Protocol<{
154
155
  readonly threshold: string;
155
156
  }[] | undefined;
156
157
  readonly serviceTier?: string | undefined;
158
+ readonly labels?: {
159
+ readonly [x: string]: string;
160
+ } | undefined;
157
161
  readonly systemInstruction?: {
158
162
  readonly parts: readonly {
159
163
  readonly text: string;
@@ -190,8 +194,8 @@ export declare const protocol: Protocol<{
190
194
  } | {
191
195
  readonly functionCall: {
192
196
  readonly name: string;
193
- readonly args: unknown;
194
197
  readonly id?: string | undefined;
198
+ readonly args?: unknown;
195
199
  };
196
200
  readonly thoughtSignature?: string | undefined;
197
201
  } | {
@@ -210,6 +214,12 @@ export declare const protocol: Protocol<{
210
214
  } | undefined;
211
215
  readonly finishReason?: string | undefined;
212
216
  }[] | undefined;
217
+ readonly promptFeedback?: {
218
+ readonly [x: string]: unknown;
219
+ readonly blockReason?: string | undefined;
220
+ readonly blockReasonMessage?: string | undefined;
221
+ readonly safetyRatings?: unknown;
222
+ } | undefined;
213
223
  readonly usageMetadata?: {
214
224
  readonly cachedContentTokenCount?: number | undefined;
215
225
  readonly thoughtsTokenCount?: number | undefined;
@@ -237,8 +247,8 @@ export declare const route: Route<{
237
247
  } | {
238
248
  readonly functionCall: {
239
249
  readonly name: string;
240
- readonly args: unknown;
241
250
  readonly id?: string | undefined;
251
+ readonly args?: unknown;
242
252
  };
243
253
  readonly thoughtSignature?: string | undefined;
244
254
  } | {
@@ -276,6 +286,9 @@ export declare const route: Route<{
276
286
  readonly threshold: string;
277
287
  }[] | undefined;
278
288
  readonly serviceTier?: string | undefined;
289
+ readonly labels?: {
290
+ readonly [x: string]: string;
291
+ } | undefined;
279
292
  readonly systemInstruction?: {
280
293
  readonly parts: readonly {
281
294
  readonly text: string;
@@ -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";
@@ -45,7 +44,7 @@ const GeminiFunctionCallPart = Schema.Struct({
45
44
  functionCall: Schema.Struct({
46
45
  id: Schema.optional(Schema.String),
47
46
  name: Schema.String,
48
- args: Schema.Unknown,
47
+ args: Schema.optional(Schema.Unknown),
49
48
  }),
50
49
  thoughtSignature: Schema.optional(Schema.String),
51
50
  });
@@ -107,6 +106,7 @@ const GeminiGenerationConfig = Schema.Struct({
107
106
  const GeminiBodyFields = {
108
107
  cachedContent: Schema.optional(Schema.String),
109
108
  contents: Schema.Array(GeminiContent),
109
+ labels: Schema.optional(Schema.Record(Schema.String, Schema.String)),
110
110
  safetySettings: optionalArray(GeminiSafetySetting),
111
111
  serviceTier: Schema.optional(Schema.String),
112
112
  systemInstruction: Schema.optional(GeminiSystemInstruction),
@@ -126,8 +126,14 @@ const GeminiCandidate = Schema.Struct({
126
126
  content: Schema.optional(GeminiContent),
127
127
  finishReason: Schema.optional(Schema.String),
128
128
  });
129
+ const GeminiPromptFeedback = Schema.StructWithRest(Schema.Struct({
130
+ blockReason: Schema.optional(Schema.String),
131
+ blockReasonMessage: Schema.optional(Schema.String),
132
+ safetyRatings: Schema.optional(Schema.Unknown),
133
+ }), [Schema.Record(Schema.String, Schema.Unknown)]);
129
134
  const GeminiEvent = Schema.Struct({
130
135
  candidates: optionalArray(GeminiCandidate),
136
+ promptFeedback: Schema.optional(GeminiPromptFeedback),
131
137
  usageMetadata: Schema.optional(GeminiUsage),
132
138
  });
133
139
  // =============================================================================
@@ -169,7 +175,7 @@ const lowerToolConfig = (toolChoice) => ProviderShared.matchToolChoice("Gemini",
169
175
  const lowerUserPart = Effect.fn("Gemini.lowerUserPart")(function* (part) {
170
176
  if (part.type === "text")
171
177
  return { text: part.text };
172
- const media = yield* ProviderShared.validateMedia("Gemini", part, MEDIA_MIMES);
178
+ const media = ProviderShared.normalizeMedia(part);
173
179
  return { inlineData: { mimeType: media.mime, data: media.base64 } };
174
180
  });
175
181
  const googleMetadata = (metadata) => ({ google: metadata });
@@ -267,7 +273,7 @@ const lowerMessages = Effect.fn("Gemini.lowerMessages")(function* (request) {
267
273
  for (const item of content) {
268
274
  if (item.type === "text")
269
275
  continue;
270
- const value = yield* ProviderShared.validateToolFile("Gemini", item, MEDIA_MIMES);
276
+ const value = ProviderShared.normalizeToolFile(item);
271
277
  media.push({ inlineData: { mimeType: value.mime, data: value.base64 } });
272
278
  }
273
279
  parts.push({
@@ -402,25 +408,29 @@ const mapFinishReason = (finishReason, hasToolCalls) => {
402
408
  return "error";
403
409
  return "unknown";
404
410
  };
405
- const finish = (state) => state.finishReason || state.usage
406
- ? (() => {
407
- const events = [];
408
- const lifecycle = state.reasoningSignature
409
- ? Lifecycle.reasoningEnd(state.lifecycle, events, "reasoning-0", googleMetadata({ thoughtSignature: state.reasoningSignature }))
410
- : state.lifecycle;
411
- Lifecycle.finish(lifecycle, events, {
412
- reason: {
413
- normalized: mapFinishReason(state.finishReason, state.hasToolCalls),
414
- raw: state.finishReason,
415
- },
416
- usage: state.usage,
417
- });
418
- return events;
419
- })()
420
- : [];
411
+ const finish = (state) => {
412
+ const promptBlockReason = state.finishReason === undefined ? state.promptFeedback?.blockReason : undefined;
413
+ const finishReason = state.finishReason ?? promptBlockReason;
414
+ if (finishReason === undefined && state.usage === undefined)
415
+ return [];
416
+ const events = [];
417
+ const lifecycle = state.reasoningSignature
418
+ ? Lifecycle.reasoningEnd(state.lifecycle, events, "reasoning-0", googleMetadata({ thoughtSignature: state.reasoningSignature }))
419
+ : state.lifecycle;
420
+ Lifecycle.finish(lifecycle, events, {
421
+ reason: {
422
+ normalized: promptBlockReason === undefined ? mapFinishReason(finishReason, state.hasToolCalls) : "content-filter",
423
+ raw: finishReason,
424
+ },
425
+ usage: state.usage,
426
+ providerMetadata: state.promptFeedback === undefined ? undefined : googleMetadata({ promptFeedback: state.promptFeedback }),
427
+ });
428
+ return events;
429
+ };
421
430
  const step = (state, event) => {
422
431
  const nextState = {
423
432
  ...state,
433
+ promptFeedback: event.promptFeedback ?? state.promptFeedback,
424
434
  usage: event.usageMetadata ? (mapUsage(event.usageMetadata) ?? state.usage) : state.usage,
425
435
  };
426
436
  const candidate = event.candidates?.[0];
@@ -447,7 +457,7 @@ const step = (state, event) => {
447
457
  continue;
448
458
  }
449
459
  if ("functionCall" in part) {
450
- const input = part.functionCall.args;
460
+ const input = part.functionCall.args === undefined ? {} : part.functionCall.args;
451
461
  const id = `tool_${nextToolCallId++}`;
452
462
  const metadata = {
453
463
  ...(part.functionCall.id === undefined ? {} : { functionCallId: part.functionCall.id }),
@@ -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;
@@ -147,8 +120,8 @@ export declare const errorText: (error: unknown) => string;
147
120
  * decoder, and drops empty / `[DONE]` keep-alive events so the downstream
148
121
  * `decodeChunk` sees one JSON string per element. The SSE channel emits a
149
122
  * `Retry` control event on its error channel; we drop it here (we don't
150
- * implement client-driven retries) so the public error channel stays
151
- * `AIError`.
123
+ * implement client-driven retries). Decoder failures become provider output
124
+ * errors so the public error channel stays `AIError`.
152
125
  */
153
126
  export declare const sseFraming: (bytes: Stream.Stream<Uint8Array, AIError>) => Stream.Stream<string, AIError>;
154
127
  /**
@@ -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")
@@ -184,10 +154,10 @@ export const errorText = (error) => {
184
154
  * decoder, and drops empty / `[DONE]` keep-alive events so the downstream
185
155
  * `decodeChunk` sees one JSON string per element. The SSE channel emits a
186
156
  * `Retry` control event on its error channel; we drop it here (we don't
187
- * implement client-driven retries) so the public error channel stays
188
- * `AIError`.
157
+ * implement client-driven retries). Decoder failures become provider output
158
+ * errors so the public error channel stays `AIError`.
189
159
  */
190
- export const sseFraming = (bytes) => bytes.pipe(Stream.decodeText(), Stream.pipeThroughChannel(Sse.decode()), Stream.catchTag("Retry", () => Stream.empty), Stream.filter((event) => event.data.length > 0 && event.data !== "[DONE]"), Stream.map((event) => event.data));
160
+ export const sseFraming = (bytes) => bytes.pipe(Stream.decodeText(), Stream.pipeThroughChannel(Sse.decode()), Stream.catchTag("Retry", () => Stream.empty), Stream.catchTag("SseError", (error) => Stream.fail(eventError("sse", error.message))), Stream.filter((event) => event.data.length > 0 && event.data !== "[DONE]"), Stream.map((event) => event.data));
191
161
  /**
192
162
  * Canonical invalid-request constructor. Lift one-line `const invalid =
193
163
  * (message) => invalidRequest(message)` aliases out of every
@@ -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}`);
@@ -35,7 +35,7 @@ export const isPayloadTooLarge = (message) => payloadPatterns.some((pattern) =>
35
35
  export const isContextOverflowFailure = (failure) => failure instanceof AIError
36
36
  ? failure.reason._tag === "InvalidRequest" && failure.reason.classification === "context-overflow"
37
37
  : Schema.is(ProviderErrorEvent)(failure) && failure.classification === "context-overflow";
38
- const decodeJson = Schema.decodeUnknownOption(Schema.UnknownFromJsonString);
38
+ const decodeJson = Schema.decodeUnknownOption(Schema.fromJsonString(Schema.Unknown));
39
39
  const QUOTA_CODES = new Set(["insufficient_quota", "usage_not_included", "billing_error"]);
40
40
  const SERVER_CODES = new Set([
41
41
  "api_error",
@@ -1,16 +1,20 @@
1
1
  import type { ProviderPackage } from "../provider-package.js";
2
2
  import { Gemini } from "../protocols/gemini.js";
3
3
  import { Route, type RouteDefaultsInput } from "../route/client.js";
4
- import { type ModelID } from "../schema/index.js";
4
+ import { type ModelID, type ProviderOptions } from "../schema/index.js";
5
5
  import { GoogleVertexShared } from "./google-vertex-shared.js";
6
- export type GeminiOptionsInput = Gemini.OptionsInput;
7
- export type GeminiProviderOptionsInput = Gemini.ProviderOptionsInput;
6
+ export interface GeminiOptionsInput extends Gemini.OptionsInput {
7
+ readonly labels?: Readonly<Record<string, string>>;
8
+ }
9
+ export type GeminiProviderOptionsInput = ProviderOptions & {
10
+ readonly gemini?: GeminiOptionsInput;
11
+ };
8
12
  export declare const id: string & import("effect/Brand").Brand<"AI.ProviderID">;
9
13
  export type Config = RouteDefaultsInput & GoogleVertexShared.ApiKeyOptions & {
10
14
  readonly baseURL?: string;
11
15
  readonly location?: string;
12
16
  readonly project?: string;
13
- readonly providerOptions?: Gemini.ProviderOptionsInput;
17
+ readonly providerOptions?: GeminiProviderOptionsInput;
14
18
  };
15
19
  export type Settings = ProviderPackage.Settings & ({
16
20
  readonly accessToken?: string;
@@ -22,7 +26,7 @@ export type Settings = ProviderPackage.Settings & ({
22
26
  readonly baseURL?: string;
23
27
  readonly location?: string;
24
28
  readonly project?: string;
25
- readonly providerOptions?: Gemini.ProviderOptionsInput;
29
+ readonly providerOptions?: GeminiProviderOptionsInput;
26
30
  };
27
31
  export declare const routes: Route<{
28
32
  readonly contents: readonly {
@@ -39,8 +43,8 @@ export declare const routes: Route<{
39
43
  } | {
40
44
  readonly functionCall: {
41
45
  readonly name: string;
42
- readonly args: unknown;
43
46
  readonly id?: string | undefined;
47
+ readonly args?: unknown;
44
48
  };
45
49
  readonly thoughtSignature?: string | undefined;
46
50
  } | {
@@ -78,6 +82,9 @@ export declare const routes: Route<{
78
82
  readonly threshold: string;
79
83
  }[] | undefined;
80
84
  readonly serviceTier?: string | undefined;
85
+ readonly labels?: {
86
+ readonly [x: string]: string;
87
+ } | undefined;
81
88
  readonly systemInstruction?: {
82
89
  readonly parts: readonly {
83
90
  readonly text: string;
@@ -101,15 +108,15 @@ export declare const routes: Route<{
101
108
  }, import("../route/transport/http.js").HttpPrepared<string>>[];
102
109
  export declare const configure: (input?: Config) => {
103
110
  id: string & import("effect/Brand").Brand<"AI.ProviderID">;
104
- model: (modelID: string | ModelID) => import("../schema/options.js").LanguageModel<Gemini.ProviderOptionsInput>;
111
+ model: (modelID: string | ModelID) => import("../schema/options.js").LanguageModel<GeminiProviderOptionsInput>;
105
112
  configure: (input?: Config) => /*elided*/ any;
106
113
  };
107
114
  export declare const provider: {
108
115
  id: string & import("effect/Brand").Brand<"AI.ProviderID">;
109
116
  configure: (input?: Config) => {
110
117
  id: string & import("effect/Brand").Brand<"AI.ProviderID">;
111
- model: (modelID: string | ModelID) => import("../schema/options.js").LanguageModel<Gemini.ProviderOptionsInput>;
118
+ model: (modelID: string | ModelID) => import("../schema/options.js").LanguageModel<GeminiProviderOptionsInput>;
112
119
  configure: /*elided*/ any;
113
120
  };
114
121
  };
115
- export declare const model: ProviderPackage.Definition<Settings, Gemini.ProviderOptionsInput>["model"];
122
+ export declare const model: ProviderPackage.Definition<Settings, GeminiProviderOptionsInput>["model"];
@@ -1,4 +1,6 @@
1
+ import { Effect } from "effect";
1
2
  import { Gemini } from "../protocols/gemini.js";
3
+ import { ProviderShared } from "../protocols/shared.js";
2
4
  import { Auth } from "../route/auth.js";
3
5
  import { Route } from "../route/client.js";
4
6
  import { Endpoint } from "../route/endpoint.js";
@@ -6,11 +8,26 @@ import { Framing } from "../route/framing.js";
6
8
  import { ProviderID } from "../schema/index.js";
7
9
  import { GoogleVertexShared } from "./google-vertex-shared.js";
8
10
  export const id = ProviderID.make("google-vertex");
11
+ const fromRequest = Effect.fn("GoogleVertex.fromRequest")(function* (request) {
12
+ const body = yield* Gemini.protocol.body.from(request);
13
+ const value = request.providerOptions?.gemini?.labels;
14
+ const labels = ProviderShared.isRecord(value)
15
+ ? Object.fromEntries(Object.entries(value).filter((entry) => typeof entry[1] === "string"))
16
+ : undefined;
17
+ return { ...body, labels };
18
+ });
19
+ const protocol = {
20
+ ...Gemini.protocol,
21
+ body: {
22
+ ...Gemini.protocol.body,
23
+ from: fromRequest,
24
+ },
25
+ };
9
26
  const route = Route.make({
10
27
  id: "google-vertex-gemini",
11
28
  provider: id,
12
29
  providerMetadataKey: "google",
13
- protocol: Gemini.protocol,
30
+ protocol,
14
31
  endpoint: Endpoint.path(({ request }) => {
15
32
  const model = String(request.model.id);
16
33
  return `/${model.startsWith("endpoints/") ? model : `models/${model}`}:streamGenerateContent?alt=sse`;
@@ -22,8 +22,8 @@ export declare const routes: import("../route/client.js").Route<{
22
22
  } | {
23
23
  readonly functionCall: {
24
24
  readonly name: string;
25
- readonly args: unknown;
26
25
  readonly id?: string | undefined;
26
+ readonly args?: unknown;
27
27
  };
28
28
  readonly thoughtSignature?: string | undefined;
29
29
  } | {
@@ -61,6 +61,9 @@ export declare const routes: import("../route/client.js").Route<{
61
61
  readonly threshold: string;
62
62
  }[] | undefined;
63
63
  readonly serviceTier?: string | undefined;
64
+ readonly labels?: {
65
+ readonly [x: string]: string;
66
+ } | undefined;
64
67
  readonly systemInstruction?: {
65
68
  readonly parts: readonly {
66
69
  readonly text: string;
@@ -132,7 +132,7 @@ export const AIErrorReason = Schema.Union([
132
132
  InvalidProviderOutputReason,
133
133
  UnknownProviderReason,
134
134
  ]).pipe(Schema.toTaggedUnion("_tag"));
135
- export class AIError extends Schema.TaggedErrorClass()("AI.Error", {
135
+ export class AIError extends Schema.TaggedError()("AI.Error", {
136
136
  module: Schema.String,
137
137
  method: Schema.String,
138
138
  reason: AIErrorReason,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
- "version": "0.0.0-beta-17498",
3
+ "version": "0.0.0-beta-17570",
4
4
  "name": "@opencode-ai/ai",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -29,8 +29,8 @@
29
29
  },
30
30
  "devDependencies": {
31
31
  "@clack/prompts": "1.0.0-alpha.1",
32
- "@effect/platform-node": "4.0.0-beta.101",
33
- "@opencode-ai/http-recorder": "0.0.0-beta-17498",
32
+ "@effect/platform-node": "4.0.0-beta.107",
33
+ "@opencode-ai/http-recorder": "0.0.0-beta-17570",
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,9 +39,9 @@
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-beta-17498",
42
+ "@opencode-ai/schema": "0.0.0-beta-17570",
43
43
  "aws4fetch": "1.0.20",
44
- "effect": "4.0.0-beta.101",
44
+ "effect": "4.0.0-beta.107",
45
45
  "google-auth-library": "10.5.0"
46
46
  }
47
47
  }