@opencode/ai 2.0.14 → 2.0.15

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 (86) hide show
  1. package/README.md +115 -56
  2. package/dist/experimental/evaluation-client.d.ts +1 -1
  3. package/dist/experimental/evaluation-client.js +39 -3
  4. package/dist/experimental/evaluation.d.ts +4 -4
  5. package/dist/experimental/evaluation.js +2 -2
  6. package/dist/experimental/system-one.d.ts +3 -3
  7. package/dist/experimental/system-one.js +40 -51
  8. package/dist/generation.d.ts +69 -0
  9. package/dist/generation.js +84 -0
  10. package/dist/image-client.d.ts +6 -4
  11. package/dist/image-client.js +11 -11
  12. package/dist/image.d.ts +1040 -62
  13. package/dist/image.js +80 -52
  14. package/dist/index.d.ts +5 -2
  15. package/dist/index.js +3 -1
  16. package/dist/llm.d.ts +9 -1
  17. package/dist/media.d.ts +212 -0
  18. package/dist/media.js +228 -0
  19. package/dist/promise.d.ts +554 -0
  20. package/dist/promise.js +44 -0
  21. package/dist/protocols/anthropic-messages.js +7 -17
  22. package/dist/protocols/bedrock-converse.d.ts +4 -4
  23. package/dist/protocols/bedrock-converse.js +1 -6
  24. package/dist/protocols/gemini.d.ts +21 -0
  25. package/dist/protocols/gemini.js +42 -6
  26. package/dist/protocols/google-images.d.ts +9 -21
  27. package/dist/protocols/google-images.js +169 -132
  28. package/dist/protocols/meta-images.d.ts +7 -12
  29. package/dist/protocols/meta-images.js +92 -66
  30. package/dist/protocols/mistral-chat.js +7 -6
  31. package/dist/protocols/open-responses.d.ts +11 -3
  32. package/dist/protocols/open-responses.js +23 -12
  33. package/dist/protocols/openai-chat.d.ts +34 -1
  34. package/dist/protocols/openai-chat.js +100 -31
  35. package/dist/protocols/openai-images.d.ts +9 -20
  36. package/dist/protocols/openai-images.js +112 -142
  37. package/dist/protocols/shared.d.ts +17 -17
  38. package/dist/protocols/shared.js +32 -35
  39. package/dist/protocols/utils/bedrock-media.d.ts +2 -3
  40. package/dist/protocols/utils/bedrock-media.js +4 -4
  41. package/dist/protocols/utils/media-input.d.ts +10 -0
  42. package/dist/protocols/utils/media-input.js +17 -0
  43. package/dist/protocols/utils/responses-compaction.js +6 -5
  44. package/dist/protocols/utils/tool-stream.d.ts +27 -3
  45. package/dist/protocols/xai-images.d.ts +9 -15
  46. package/dist/protocols/xai-images.js +85 -83
  47. package/dist/protocols/zai-images.d.ts +9 -13
  48. package/dist/protocols/zai-images.js +59 -57
  49. package/dist/providers/amazon-bedrock.d.ts +2 -2
  50. package/dist/providers/cerebras.js +6 -1
  51. package/dist/providers/deepinfra.js +6 -1
  52. package/dist/providers/google-vertex.d.ts +7 -0
  53. package/dist/providers/google.d.ts +7 -0
  54. package/dist/providers/index.d.ts +1 -0
  55. package/dist/providers/index.js +1 -0
  56. package/dist/providers/openrouter.d.ts +19 -0
  57. package/dist/providers/openrouter.js +13 -1
  58. package/dist/providers/vercel-ai-gateway.d.ts +41 -0
  59. package/dist/providers/vercel-ai-gateway.js +85 -0
  60. package/dist/route/client.d.ts +9 -1
  61. package/dist/route/endpoint.d.ts +10 -10
  62. package/dist/route/executor-service.d.ts +12 -0
  63. package/dist/route/executor-service.js +3 -0
  64. package/dist/route/executor.d.ts +4 -9
  65. package/dist/route/executor.js +3 -3
  66. package/dist/route/index.d.ts +2 -0
  67. package/dist/route/index.js +2 -0
  68. package/dist/route/media-protocol.d.ts +45 -0
  69. package/dist/route/media-protocol.js +40 -0
  70. package/dist/route/media.d.ts +46 -0
  71. package/dist/route/media.js +64 -0
  72. package/dist/schema/errors.d.ts +13 -3
  73. package/dist/schema/errors.js +7 -0
  74. package/dist/schema/events.d.ts +563 -40
  75. package/dist/schema/events.js +35 -2
  76. package/dist/schema/messages.d.ts +98 -8
  77. package/dist/schema/messages.js +8 -6
  78. package/dist/schema/options.d.ts +2 -0
  79. package/dist/schema/options.js +3 -0
  80. package/dist/testing.d.ts +72 -8
  81. package/dist/utils/media-type.d.ts +6 -0
  82. package/dist/utils/media-type.js +49 -0
  83. package/dist/utils/sanitize.js +3 -1
  84. package/package.json +7 -3
  85. package/dist/protocols/utils/image-input.d.ts +0 -21
  86. package/dist/protocols/utils/image-input.js +0 -20
@@ -157,7 +157,7 @@ export declare const protocol: Protocol<{
157
157
  };
158
158
  } | {
159
159
  readonly document: {
160
- readonly format: "pdf" | "csv" | "doc" | "docx" | "xls" | "xlsx" | "html" | "txt" | "md";
160
+ readonly format: "pdf" | "txt" | "md" | "csv" | "doc" | "docx" | "xls" | "xlsx" | "html";
161
161
  readonly name: string;
162
162
  readonly source: {
163
163
  readonly bytes: string;
@@ -176,7 +176,7 @@ export declare const protocol: Protocol<{
176
176
  };
177
177
  } | {
178
178
  readonly document: {
179
- readonly format: "pdf" | "csv" | "doc" | "docx" | "xls" | "xlsx" | "html" | "txt" | "md";
179
+ readonly format: "pdf" | "txt" | "md" | "csv" | "doc" | "docx" | "xls" | "xlsx" | "html";
180
180
  readonly name: string;
181
181
  readonly source: {
182
182
  readonly bytes: string;
@@ -386,7 +386,7 @@ export declare const route: Route<{
386
386
  };
387
387
  } | {
388
388
  readonly document: {
389
- readonly format: "pdf" | "csv" | "doc" | "docx" | "xls" | "xlsx" | "html" | "txt" | "md";
389
+ readonly format: "pdf" | "txt" | "md" | "csv" | "doc" | "docx" | "xls" | "xlsx" | "html";
390
390
  readonly name: string;
391
391
  readonly source: {
392
392
  readonly bytes: string;
@@ -405,7 +405,7 @@ export declare const route: Route<{
405
405
  };
406
406
  } | {
407
407
  readonly document: {
408
- readonly format: "pdf" | "csv" | "doc" | "docx" | "xls" | "xlsx" | "html" | "txt" | "md";
408
+ readonly format: "pdf" | "txt" | "md" | "csv" | "doc" | "docx" | "xls" | "xlsx" | "html";
409
409
  readonly name: string;
410
410
  readonly source: {
411
411
  readonly bytes: string;
@@ -221,12 +221,7 @@ const lowerToolResultContent = Effect.fn("BedrockConverse.lowerToolResultContent
221
221
  content.push({ text: item.text });
222
222
  continue;
223
223
  }
224
- const media = yield* BedrockMedia.lower({
225
- type: "media",
226
- mediaType: item.mime,
227
- data: item.uri,
228
- filename: item.name,
229
- }, documentNames);
224
+ const media = yield* BedrockMedia.lower(ProviderShared.toolFileMedia(item), documentNames);
230
225
  content.push(...media);
231
226
  }
232
227
  return content;
@@ -33,6 +33,12 @@ declare const GeminiBody: Schema.Struct<{
33
33
  readonly mimeType: Schema.String;
34
34
  readonly data: Schema.String;
35
35
  }>;
36
+ readonly thoughtSignature: Schema.optional<Schema.NullOr<Schema.String>>;
37
+ }>, Schema.Struct<{
38
+ readonly fileData: Schema.Struct<{
39
+ readonly mimeType: Schema.String;
40
+ readonly fileUri: Schema.String;
41
+ }>;
36
42
  }>, Schema.Struct<{
37
43
  readonly functionCall: Schema.Struct<{
38
44
  readonly id: Schema.optional<Schema.NullOr<Schema.String>>;
@@ -50,6 +56,7 @@ declare const GeminiBody: Schema.Struct<{
50
56
  readonly mimeType: Schema.String;
51
57
  readonly data: Schema.String;
52
58
  }>;
59
+ readonly thoughtSignature: Schema.optional<Schema.NullOr<Schema.String>>;
53
60
  }>>>;
54
61
  }>;
55
62
  }>]>>>>;
@@ -106,10 +113,16 @@ export declare const protocol: Protocol<{
106
113
  readonly mimeType: string;
107
114
  readonly data: string;
108
115
  };
116
+ readonly thoughtSignature?: string | null | undefined;
109
117
  } | {
110
118
  readonly text: string;
111
119
  readonly thought?: boolean | null | undefined;
112
120
  readonly thoughtSignature?: string | null | undefined;
121
+ } | {
122
+ readonly fileData: {
123
+ readonly mimeType: string;
124
+ readonly fileUri: string;
125
+ };
113
126
  } | {
114
127
  readonly functionCall: {
115
128
  readonly name: string;
@@ -127,6 +140,7 @@ export declare const protocol: Protocol<{
127
140
  readonly mimeType: string;
128
141
  readonly data: string;
129
142
  };
143
+ readonly thoughtSignature?: string | null | undefined;
130
144
  }[] | undefined;
131
145
  };
132
146
  })[] | null | undefined;
@@ -213,10 +227,16 @@ export declare const route: Route<{
213
227
  readonly mimeType: string;
214
228
  readonly data: string;
215
229
  };
230
+ readonly thoughtSignature?: string | null | undefined;
216
231
  } | {
217
232
  readonly text: string;
218
233
  readonly thought?: boolean | null | undefined;
219
234
  readonly thoughtSignature?: string | null | undefined;
235
+ } | {
236
+ readonly fileData: {
237
+ readonly mimeType: string;
238
+ readonly fileUri: string;
239
+ };
220
240
  } | {
221
241
  readonly functionCall: {
222
242
  readonly name: string;
@@ -234,6 +254,7 @@ export declare const route: Route<{
234
254
  readonly mimeType: string;
235
255
  readonly data: string;
236
256
  };
257
+ readonly thoughtSignature?: string | null | undefined;
237
258
  }[] | undefined;
238
259
  };
239
260
  })[] | null | undefined;
@@ -7,6 +7,7 @@ import { Framing } from "../route/framing.js";
7
7
  import { Protocol } from "../route/protocol.js";
8
8
  import { AIError, LLMEvent, Usage, } from "../schema/index.js";
9
9
  import { classifyProviderFailure } from "../provider-error.js";
10
+ import { Media } from "../media.js";
10
11
  import { JsonObject, knownString, lenient, optionalArray, optionalNull, ProviderShared } from "./shared.js";
11
12
  import { GeminiToolSchema } from "./utils/gemini-tool-schema.js";
12
13
  import { Lifecycle } from "./utils/lifecycle.js";
@@ -54,6 +55,14 @@ const GeminiInlineDataPart = Schema.Struct({
54
55
  mimeType: Schema.String,
55
56
  data: Schema.String,
56
57
  }),
58
+ thoughtSignature: optionalNull(Schema.String),
59
+ });
60
+ /** Gemini Files API reference; the only remote input Gemini accepts. */
61
+ const GeminiFileDataPart = Schema.Struct({
62
+ fileData: Schema.Struct({
63
+ mimeType: Schema.String,
64
+ fileUri: Schema.String,
65
+ }),
57
66
  });
58
67
  const GeminiFunctionCallPart = Schema.Struct({
59
68
  functionCall: Schema.Struct({
@@ -74,6 +83,7 @@ const GeminiFunctionResponsePart = Schema.Struct({
74
83
  const GeminiContentPart = Schema.Union([
75
84
  GeminiTextPart,
76
85
  GeminiInlineDataPart,
86
+ GeminiFileDataPart,
77
87
  GeminiFunctionCallPart,
78
88
  GeminiFunctionResponsePart,
79
89
  ]);
@@ -211,10 +221,14 @@ const lowerToolConfig = (toolChoice) => ProviderShared.matchToolChoice("Gemini",
211
221
  required: () => ({ functionCallingConfig: { mode: "ANY" } }),
212
222
  tool: (name) => ({ functionCallingConfig: { mode: "ANY", allowedFunctionNames: [name] } }),
213
223
  });
214
- const lowerUserPart = Effect.fn("Gemini.lowerUserPart")(function* (part) {
224
+ // Gemini does not fetch public URLs; inline payloads and Gemini Files references are the accepted inputs.
225
+ const lowerContentPart = Effect.fn("Gemini.lowerContentPart")(function* (part) {
215
226
  if (part.type === "text")
216
227
  return { text: part.text };
217
- const media = ProviderShared.normalizeMedia(part);
228
+ const source = part.media.source;
229
+ if (source.type === "ref" && source.provider === "google")
230
+ return { fileData: { mimeType: part.media.mediaType, fileUri: source.id } };
231
+ const media = yield* ProviderShared.requireInlineMedia("Gemini", part.media);
218
232
  return { inlineData: { mimeType: media.mime, data: media.base64 } };
219
233
  });
220
234
  const providerMetadata = (key, metadata) => ({ [key]: metadata });
@@ -259,7 +273,7 @@ const lowerMessages = Effect.fn("Gemini.lowerMessages")(function* (request) {
259
273
  for (const part of message.content) {
260
274
  if (!ProviderShared.supportsContent(part, ["text", "media"]))
261
275
  return yield* ProviderShared.unsupportedContent("Gemini", "user", ["text", "media"]);
262
- parts.push(yield* lowerUserPart(part));
276
+ parts.push(yield* lowerContentPart(part));
263
277
  }
264
278
  contents.push({ role: "user", parts });
265
279
  continue;
@@ -269,12 +283,23 @@ const lowerMessages = Effect.fn("Gemini.lowerMessages")(function* (request) {
269
283
  // Parallel Gemini 3 calls may carry one signature on the first call; unsigned sibling calls are valid.
270
284
  let hasSignedToolCall = false;
271
285
  for (const part of message.content) {
272
- if (!ProviderShared.supportsContent(part, ["text", "reasoning", "tool-call"]))
273
- return yield* ProviderShared.unsupportedContent("Gemini", "assistant", ["text", "reasoning", "tool-call"]);
286
+ if (!ProviderShared.supportsContent(part, ["text", "reasoning", "tool-call", "media"]))
287
+ return yield* ProviderShared.unsupportedContent("Gemini", "assistant", [
288
+ "text",
289
+ "reasoning",
290
+ "tool-call",
291
+ "media",
292
+ ]);
274
293
  if (part.type === "text") {
275
294
  parts.push({ text: part.text, thoughtSignature: thoughtSignature(part.providerMetadata, metadataKey) });
276
295
  continue;
277
296
  }
297
+ // Generated images replay as model-role inline data so multi-turn image editing keeps the prior output.
298
+ if (part.type === "media") {
299
+ const lowered = yield* lowerContentPart(part);
300
+ parts.push({ ...lowered, thoughtSignature: thoughtSignature(part.providerMetadata, metadataKey) });
301
+ continue;
302
+ }
278
303
  if (part.type === "reasoning") {
279
304
  parts.push({
280
305
  text: part.text,
@@ -324,7 +349,7 @@ const lowerMessages = Effect.fn("Gemini.lowerMessages")(function* (request) {
324
349
  for (const item of content) {
325
350
  if (item.type === "text")
326
351
  continue;
327
- const value = ProviderShared.normalizeToolFile(item);
352
+ const value = yield* ProviderShared.requireInlineMedia("Gemini", ProviderShared.toolFileMedia(item).media);
328
353
  media.push({ inlineData: { mimeType: value.mime, data: value.base64 } });
329
354
  }
330
355
  if (legacyToolMedia && media.length > 0)
@@ -539,6 +564,17 @@ const step = (state, event) => {
539
564
  reasoningSignature = signature;
540
565
  else if (signature !== undefined && "text" in part)
541
566
  textSignature = signature;
567
+ // Image-capable Gemini models return generated images as inline data parts; surface them as first-class output.
568
+ if ("inlineData" in part) {
569
+ lifecycle = Lifecycle.stepStart(lifecycle, events);
570
+ events.push(LLMEvent.media({
571
+ media: Media.base64(part.inlineData.data, part.inlineData.mimeType),
572
+ providerMetadata: signature
573
+ ? providerMetadata(state.providerMetadataKey, { thoughtSignature: signature })
574
+ : undefined,
575
+ }));
576
+ continue;
577
+ }
542
578
  if ("text" in part && part.text.length > 0) {
543
579
  if (part.thought) {
544
580
  if (textId !== undefined) {
@@ -1,30 +1,18 @@
1
- import { ImageModel } from "../image.js";
2
- import { type Definition as AuthDefinition } from "../route/auth.js";
3
- import { type HttpOptions } from "../schema/index.js";
1
+ import { ImageModel, ImageResponse, type ImageRequestFor } from "../image.js";
2
+ import { MediaProtocol } from "../route/media-protocol.js";
3
+ import { MediaRoute } from "../route/media.js";
4
4
  export declare const DEFAULT_BASE_URL = "https://generativelanguage.googleapis.com/v1beta";
5
5
  export type GoogleImageString<Known extends string> = Known | (string & {});
6
+ /** Provider-native options. Common fields (`aspectRatio`, `seed`, `images`) live on the request. */
6
7
  export type GoogleImageOptions = {
7
- readonly aspectRatio?: GoogleImageString<"1:1" | "2:3" | "3:2" | "3:4" | "4:3" | "4:5" | "5:4" | "9:16" | "16:9" | "21:9">;
8
8
  readonly imageSize?: GoogleImageString<"1K" | "2K" | "4K">;
9
- readonly seed?: number;
10
9
  readonly thinkingLevel?: GoogleImageString<"MINIMAL" | "LOW" | "MEDIUM" | "HIGH">;
11
10
  readonly includeThoughts?: boolean;
12
11
  } & Record<string, unknown>;
13
- export type GoogleImageBody = Record<string, unknown> & {
14
- readonly contents: ReadonlyArray<{
15
- readonly role: "user";
16
- readonly parts: ReadonlyArray<Record<string, unknown>>;
17
- }>;
18
- readonly generationConfig: Record<string, unknown>;
19
- };
20
- export interface ModelInput {
21
- readonly id: string;
22
- readonly auth: AuthDefinition;
23
- readonly baseURL?: string;
24
- readonly headers?: Record<string, string>;
25
- readonly http?: HttpOptions;
26
- }
27
- export declare const model: (input: ModelInput) => ImageModel<GoogleImageOptions>;
12
+ export type Request = ImageRequestFor<GoogleImageOptions>;
13
+ export declare const protocol: MediaProtocol.Inline<Request, ImageResponse>;
14
+ export declare const model: (input: MediaRoute.ModelInput) => ImageModel<GoogleImageOptions>;
28
15
  export declare const GoogleImages: {
29
- readonly model: (input: ModelInput) => ImageModel<GoogleImageOptions>;
16
+ readonly protocol: MediaProtocol.Inline<Request, ImageResponse>;
17
+ readonly model: (input: MediaRoute.ModelInput) => ImageModel<GoogleImageOptions>;
30
18
  };
@@ -1,12 +1,18 @@
1
- import { Effect, Encoding, Schema } from "effect";
2
- import { Headers, HttpClientRequest } from "effect/unstable/http";
3
- import { GeneratedImage, ImageModel, ImageResponse, } from "../image.js";
4
- import { Auth } from "../route/auth.js";
5
- import { AIError, Usage, mergeHttpOptions, mergeJsonRecords } from "../schema/index.js";
1
+ import { Effect, Schema } from "effect";
2
+ import { ImageModel, ImageResponse } from "../image.js";
3
+ import { Media } from "../media.js";
4
+ import { MediaProtocol } from "../route/media-protocol.js";
5
+ import { MediaRoute } from "../route/media.js";
6
+ import { ProviderID, mergeJsonRecords } from "../schema/index.js";
6
7
  import { ProviderShared } from "./shared.js";
7
- import { ImageInputs } from "./utils/image-input.js";
8
+ import { MediaInput } from "./utils/media-input.js";
8
9
  const ADAPTER = "google-images";
10
+ const NAME = "Google Images";
11
+ const PROVIDER = ProviderID.make("google");
9
12
  export const DEFAULT_BASE_URL = "https://generativelanguage.googleapis.com/v1beta";
13
+ // ---------------------------------------------------------------------------
14
+ // 2. Response schema
15
+ // ---------------------------------------------------------------------------
10
16
  const GoogleUsage = Schema.StructWithRest(Schema.Struct({
11
17
  cachedContentTokenCount: Schema.optional(Schema.Number),
12
18
  thoughtsTokenCount: Schema.optional(Schema.Number),
@@ -41,141 +47,172 @@ const GoogleImageResponse = Schema.Struct({
41
47
  responseId: Schema.optional(Schema.String),
42
48
  promptFeedback: Schema.optional(Schema.Unknown),
43
49
  });
44
- const nativeOptions = (options) => {
45
- const { aspectRatio, imageSize, seed, thinkingLevel, includeThoughts, ...native } = options ?? {};
46
- const image = {
47
- aspectRatio,
48
- imageSize,
49
- };
50
- const thinkingConfig = {
51
- thinkingLevel,
52
- includeThoughts,
53
- };
50
+ // ---------------------------------------------------------------------------
51
+ // 5. Request body construction
52
+ // ---------------------------------------------------------------------------
53
+ const generationConfig = (request) => {
54
+ const { imageSize, thinkingLevel, includeThoughts, ...native } = request.providerOptions ?? {};
55
+ const imageConfig = { aspectRatio: request.aspectRatio, imageSize };
56
+ const thinkingConfig = { thinkingLevel, includeThoughts };
54
57
  return (mergeJsonRecords({
55
58
  responseModalities: ["IMAGE"],
56
- imageConfig: Object.values(image).some((value) => value !== undefined) ? image : undefined,
57
- seed,
59
+ imageConfig: Object.values(imageConfig).some((value) => value !== undefined) ? imageConfig : undefined,
60
+ seed: request.seed,
58
61
  thinkingConfig: Object.values(thinkingConfig).some((value) => value !== undefined) ? thinkingConfig : undefined,
59
62
  }, native) ?? { responseModalities: ["IMAGE"] });
60
63
  };
61
- const applyQuery = (url, query) => {
62
- if (!query)
63
- return url;
64
- const next = new URL(url);
65
- Object.entries(query).forEach(([key, value]) => next.searchParams.set(key, value));
66
- return next.toString();
64
+ // Gemini does not fetch public URLs; inline payloads or Gemini Files references are the only accepted inputs.
65
+ const imagePart = (asset) => {
66
+ const inline = asset.inline();
67
+ if (inline)
68
+ return Effect.succeed({ inlineData: { mimeType: inline.mime, data: inline.base64 } });
69
+ const id = MediaInput.refID(asset, PROVIDER);
70
+ if (id)
71
+ return Effect.succeed({ fileData: { mimeType: asset.mediaType, fileUri: id } });
72
+ if (asset.source.type === "ref")
73
+ return Effect.fail(ProviderShared.invalidRequest("Google generateContent requires Gemini file references rather than other providers' file IDs"));
74
+ return Effect.fail(ProviderShared.invalidRequest("Google generateContent does not fetch public image URLs; use bytes, a data URL, or a Gemini file reference"));
67
75
  };
68
- export const model = (input) => {
69
- const route = {
70
- id: ADAPTER,
71
- generate: Effect.fn("GoogleImages.generate")(function* (request, execute) {
72
- const imageParts = yield* Effect.forEach(request.images ?? [], googleImagePart);
73
- const http = mergeHttpOptions(request.model.http, request.http);
74
- const requestBody = mergeJsonRecords({
75
- contents: [{ role: "user", parts: [{ text: request.prompt }, ...imageParts] }],
76
- generationConfig: nativeOptions(request.options),
77
- }, http?.body);
78
- const text = ProviderShared.encodeJson(requestBody);
79
- const url = applyQuery(`${(input.baseURL ?? DEFAULT_BASE_URL).replace(/\/$/, "")}/models/${request.model.id}:generateContent`, http?.query);
80
- const headers = yield* Auth.toEffect(input.auth)({
81
- request,
82
- method: "POST",
83
- url,
84
- body: text,
85
- headers: Headers.fromInput({ ...input.headers, ...http?.headers }),
86
- });
87
- const response = yield* execute(HttpClientRequest.post(url).pipe(HttpClientRequest.setHeaders(headers), HttpClientRequest.bodyText(text, "application/json")));
88
- const output = yield* ProviderShared.imageResponse(ADAPTER, "Google Images", response);
89
- const decoded = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(GoogleImageResponse))(output.body).pipe(Effect.mapError((cause) => output.invalid("Google Images returned an invalid response", cause)));
90
- const candidates = decoded.candidates ?? [];
91
- const candidateMetadata = candidates.map((candidate, candidateIndex) => ({
92
- index: candidate.index ?? candidateIndex,
93
- finishReason: candidate.finishReason,
94
- finishMessage: candidate.finishMessage,
95
- safetyRatings: candidate.safetyRatings,
96
- citationMetadata: candidate.citationMetadata,
97
- groundingMetadata: candidate.groundingMetadata,
98
- parts: (candidate.content?.parts ?? []).map((part) => part.inlineData === undefined
99
- ? {
100
- type: "text",
101
- text: part.text,
102
- thought: part.thought,
103
- thoughtSignature: part.thoughtSignature,
104
- }
105
- : {
106
- type: "inlineData",
107
- mediaType: part.inlineData.mimeType,
108
- thought: part.thought,
109
- thoughtSignature: part.thoughtSignature,
110
- }),
111
- }));
112
- const encoded = candidates.flatMap((candidate, candidateIndex) => (candidate.content?.parts ?? []).flatMap((part, partIndex) => part.inlineData === undefined || part.thought === true
113
- ? []
114
- : [{ candidate, candidateIndex, partIndex, inlineData: part.inlineData }]));
115
- const images = yield* Effect.forEach(encoded, (item) => Effect.fromResult(Encoding.decodeBase64(item.inlineData.data)).pipe(Effect.mapError((cause) => output.invalid(`Google Images candidate ${item.candidateIndex} part ${item.partIndex} contains invalid base64 data`, cause)), Effect.map((data) => new GeneratedImage({
116
- mediaType: item.inlineData.mimeType,
117
- data,
118
- providerMetadata: {
119
- google: {
120
- candidateIndex: item.candidate.index ?? item.candidateIndex,
121
- partIndex: item.partIndex,
122
- finishReason: item.candidate.finishReason,
123
- safetyRatings: item.candidate.safetyRatings,
124
- citationMetadata: item.candidate.citationMetadata,
125
- groundingMetadata: item.candidate.groundingMetadata,
126
- thoughtSignature: item.candidate.content?.parts[item.partIndex]?.thoughtSignature,
127
- },
76
+ const fromRequest = Effect.fn("GoogleImages.fromRequest")(function* (request) {
77
+ if (request.n !== undefined && request.n > 1)
78
+ return yield* ProviderShared.unsupportedOperation({
79
+ operation: "image.n",
80
+ provider: PROVIDER,
81
+ route: ADAPTER,
82
+ message: `${NAME} generates one image per request; call it once per image instead of n=${request.n}`,
83
+ });
84
+ const parts = yield* Effect.forEach(request.images ?? [], imagePart);
85
+ return MediaProtocol.json(mergeJsonRecords({
86
+ contents: [{ role: "user", parts: [{ text: request.prompt }, ...parts] }],
87
+ generationConfig: generationConfig(request),
88
+ }, request.http?.body) ?? {});
89
+ });
90
+ // ---------------------------------------------------------------------------
91
+ // 6. Response decoding
92
+ // ---------------------------------------------------------------------------
93
+ const decodeResponse = Effect.fn("GoogleImages.decodeResponse")(function* (response) {
94
+ const output = yield* MediaProtocol.decodeJson(ADAPTER, NAME, GoogleImageResponse)(response);
95
+ const decoded = output.value;
96
+ const candidates = decoded.candidates ?? [];
97
+ const candidateMetadata = candidates.map((candidate, candidateIndex) => ({
98
+ index: candidate.index ?? candidateIndex,
99
+ finishReason: candidate.finishReason,
100
+ finishMessage: candidate.finishMessage,
101
+ safetyRatings: candidate.safetyRatings,
102
+ citationMetadata: candidate.citationMetadata,
103
+ groundingMetadata: candidate.groundingMetadata,
104
+ parts: (candidate.content?.parts ?? []).map((part) => part.inlineData === undefined
105
+ ? { type: "text", text: part.text, thought: part.thought, thoughtSignature: part.thoughtSignature }
106
+ : {
107
+ type: "inlineData",
108
+ mediaType: part.inlineData.mimeType,
109
+ thought: part.thought,
110
+ thoughtSignature: part.thoughtSignature,
111
+ }),
112
+ }));
113
+ // Thought parts are drafts; only non-thought inline data is a final image.
114
+ const encoded = candidates.flatMap((candidate, candidateIndex) => (candidate.content?.parts ?? []).flatMap((part, partIndex) => part.inlineData === undefined || part.thought === true
115
+ ? []
116
+ : [
117
+ {
118
+ candidate,
119
+ candidateIndex,
120
+ partIndex,
121
+ inlineData: part.inlineData,
122
+ thoughtSignature: part.thoughtSignature,
123
+ },
124
+ ]));
125
+ const images = yield* Effect.forEach(encoded, (item) => MediaInput.decodedAsset(output.invalid, `${NAME} candidate ${item.candidateIndex} part ${item.partIndex}`, item.inlineData.data, item.inlineData.mimeType, {
126
+ providerMetadata: {
127
+ google: {
128
+ candidateIndex: item.candidate.index ?? item.candidateIndex,
129
+ partIndex: item.partIndex,
130
+ finishReason: item.candidate.finishReason,
131
+ safetyRatings: item.candidate.safetyRatings,
132
+ citationMetadata: item.candidate.citationMetadata,
133
+ groundingMetadata: item.candidate.groundingMetadata,
134
+ thoughtSignature: item.thoughtSignature,
135
+ },
136
+ },
137
+ }));
138
+ if (images.length === 0) {
139
+ const finishReasons = candidates.flatMap((candidate) => candidate.finishReason === undefined ? [] : [candidate.finishReason]);
140
+ return yield* output.invalid(`${NAME} returned no final images${finishReasons.length === 0 ? "" : ` (finish reasons: ${finishReasons.join(", ")})`}; inspect body for prompt feedback and candidate details`);
141
+ }
142
+ // Candidates that stopped for a safety or policy reason are partial results, not a silent drop.
143
+ const notices = [
144
+ ...(decoded.promptFeedback === undefined
145
+ ? []
146
+ : [
147
+ {
148
+ type: "filtered",
149
+ message: `${NAME} reported prompt feedback`,
150
+ providerMetadata: { google: { promptFeedback: decoded.promptFeedback } },
128
151
  },
129
- }))));
130
- if (images.length === 0) {
131
- const finishReasons = candidates.flatMap((candidate) => candidate.finishReason === undefined ? [] : [candidate.finishReason]);
132
- return yield* output.invalid(`Google Images returned no final images${finishReasons.length === 0 ? "" : ` (finish reasons: ${finishReasons.join(", ")})`}; inspect body for prompt feedback and candidate details`);
133
- }
134
- const usage = decoded.usageMetadata;
135
- const outputTokens = usage?.candidatesTokenCount === undefined
136
- ? undefined
137
- : usage.candidatesTokenCount + (usage.thoughtsTokenCount ?? 0);
138
- return new ImageResponse({
139
- images,
140
- usage: usage === undefined
141
- ? undefined
142
- : new Usage({
143
- inputTokens: usage.promptTokenCount,
144
- outputTokens,
145
- nonCachedInputTokens: ProviderShared.subtractTokens(usage.promptTokenCount, usage.cachedContentTokenCount),
146
- cacheReadInputTokens: usage.cachedContentTokenCount,
147
- reasoningTokens: usage.thoughtsTokenCount,
148
- totalTokens: ProviderShared.totalTokens(usage.promptTokenCount, outputTokens, usage.totalTokenCount),
149
- providerMetadata: { google: usage },
150
- }),
151
- providerMetadata: {
152
- google: {
153
- modelVersion: decoded.modelVersion,
154
- responseId: decoded.responseId,
155
- promptFeedback: decoded.promptFeedback,
156
- candidates: candidateMetadata,
152
+ ]),
153
+ ...candidates.flatMap((candidate, index) => candidate.finishReason === undefined || candidate.finishReason === "STOP"
154
+ ? []
155
+ : [
156
+ {
157
+ type: "filtered",
158
+ message: `${NAME} candidate ${candidate.index ?? index} finished with ${candidate.finishReason}${candidate.finishMessage === undefined ? "" : `: ${candidate.finishMessage}`}`,
159
+ providerMetadata: {
160
+ google: {
161
+ candidateIndex: candidate.index ?? index,
162
+ finishReason: candidate.finishReason,
163
+ finishMessage: candidate.finishMessage,
164
+ safetyRatings: candidate.safetyRatings,
165
+ },
157
166
  },
158
167
  },
159
- });
160
- }),
161
- };
162
- return ImageModel.make({ id: input.id, provider: "google", route, http: input.http });
163
- };
164
- const googleImagePart = (image) => {
165
- if (image.type === "bytes")
166
- return Effect.succeed({ inlineData: { mimeType: image.mediaType, data: Encoding.encodeBase64(image.data) } });
167
- if (image.type === "file-uri")
168
- return Effect.succeed({ fileData: { mimeType: image.mediaType, fileUri: image.uri } });
169
- if (image.type === "url")
170
- return ImageInputs.decodeDataUrl(image.url).pipe(Effect.flatMap((decoded) => {
171
- if (decoded === undefined)
172
- return Effect.fail(ImageInputs.invalid("Google generateContent does not fetch public image URLs; use bytes, a data URL, or a Gemini file URI"));
173
- return Effect.succeed({
174
- inlineData: { mimeType: decoded.mediaType, data: Encoding.encodeBase64(decoded.data) },
175
- });
176
- }));
177
- return Effect.fail(ImageInputs.invalid("Google generateContent requires Gemini file URIs rather than provider file IDs"));
178
- };
168
+ ]),
169
+ ];
170
+ const usage = decoded.usageMetadata;
171
+ const outputTokens = usage?.candidatesTokenCount === undefined ? undefined : usage.candidatesTokenCount + (usage.thoughtsTokenCount ?? 0);
172
+ return new ImageResponse({
173
+ images,
174
+ notices: notices.length === 0 ? undefined : notices,
175
+ usage: usage === undefined
176
+ ? undefined
177
+ : {
178
+ type: "tokens",
179
+ input: usage.promptTokenCount,
180
+ output: outputTokens,
181
+ total: ProviderShared.totalTokens(usage.promptTokenCount, outputTokens, usage.totalTokenCount),
182
+ details: {
183
+ reasoningTokens: usage.thoughtsTokenCount,
184
+ cacheReadInputTokens: usage.cachedContentTokenCount,
185
+ google: usage,
186
+ },
187
+ },
188
+ providerMetadata: {
189
+ google: {
190
+ modelVersion: decoded.modelVersion,
191
+ responseId: decoded.responseId,
192
+ promptFeedback: decoded.promptFeedback,
193
+ candidates: candidateMetadata,
194
+ },
195
+ },
196
+ });
197
+ });
198
+ // ---------------------------------------------------------------------------
199
+ // 7. Protocol and route
200
+ // ---------------------------------------------------------------------------
201
+ export const protocol = MediaProtocol.inline({
202
+ id: ADAPTER,
203
+ name: NAME,
204
+ unsupported: ["mask", "size", "format"],
205
+ body: { from: fromRequest },
206
+ response: { decode: decodeResponse },
207
+ });
208
+ export const model = (input) => ImageModel.fromRoute({
209
+ id: ADAPTER,
210
+ provider: PROVIDER,
211
+ protocol,
212
+ baseURL: DEFAULT_BASE_URL,
213
+ path: ({ request }) => `/models/${request.model.id}:generateContent`,
214
+ }, input);
179
215
  export const GoogleImages = {
216
+ protocol,
180
217
  model,
181
218
  };
@@ -1,12 +1,9 @@
1
- import { ImageModel } from "../image.js";
2
- import { Auth } from "../route/auth.js";
3
- import { type HttpOptions } from "../schema/index.js";
1
+ import { ImageModel, ImageResponse, type ImageRequestFor } from "../image.js";
2
+ import { MediaProtocol } from "../route/media-protocol.js";
3
+ import { MediaRoute } from "../route/media.js";
4
4
  type OpenString<Known extends string> = Known | (string & {});
5
+ /** Provider-native options. Common fields (`n`, `size`, `format`, `images`) live on the request. */
5
6
  export type ImageOptions = {
6
- readonly n?: number;
7
- /** Aspect ratio hint, not an exact output resolution. */
8
- readonly size?: string;
9
- readonly outputFormat?: OpenString<"webp" | "png" | "jpeg">;
10
7
  readonly responseFormat?: OpenString<"b64_json" | "url">;
11
8
  readonly reasoningStrength?: OpenString<"low" | "high">;
12
9
  readonly toolEnablement?: {
@@ -16,11 +13,9 @@ export type ImageOptions = {
16
13
  };
17
14
  readonly [key: string]: unknown;
18
15
  };
19
- export declare const model: (input: {
20
- readonly id: string;
21
- readonly auth: Auth.Definition;
16
+ export type Request = ImageRequestFor<ImageOptions>;
17
+ export declare const protocol: MediaProtocol.Inline<Request, ImageResponse>;
18
+ export declare const model: (input: MediaRoute.ModelInput & {
22
19
  readonly baseURL: string;
23
- readonly headers?: Record<string, string>;
24
- readonly http?: HttpOptions;
25
20
  }) => ImageModel<ImageOptions>;
26
21
  export * as MetaImages from "./meta-images.js";