@opencode-ai/ai 0.0.0-next-15910 → 0.0.0-next-15911

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -29,7 +29,7 @@ Run `LLMClient.stream(request)` instead of `generate` when you want incremental
29
29
  Use `Image.generate` with an image model for direct asset generation:
30
30
 
31
31
  ```ts
32
- import { Image } from "@opencode-ai/ai"
32
+ import { Image, ImageInput } from "@opencode-ai/ai"
33
33
  import { OpenAI } from "@opencode-ai/ai/providers"
34
34
 
35
35
  const program = Effect.gen(function* () {
@@ -49,6 +49,48 @@ const program = Effect.gen(function* () {
49
49
  })
50
50
  ```
51
51
 
52
+ Pass ordered image inputs to the same method for editing, composition, or image-conditioned generation:
53
+
54
+ ```ts
55
+ const response =
56
+ yield *
57
+ Image.generate({
58
+ model,
59
+ prompt: "Combine these product photos into one studio scene",
60
+ images: [
61
+ ImageInput.bytes(firstBytes, "image/png"),
62
+ ImageInput.url("https://example.com/second.webp"),
63
+ ImageInput.file("file_123"),
64
+ ],
65
+ options,
66
+ http,
67
+ })
68
+ ```
69
+
70
+ `ImageInput.fileUri(uri, mediaType)` represents provider file URIs such as Gemini Files. Raw strings are not
71
+ accepted as image inputs, avoiding ambiguity between base64, URLs, and provider IDs. Empty or omitted `images`
72
+ uses text-to-image generation; a non-empty array selects the provider's edit behavior without enforcing provider
73
+ image-count limits locally. `images` is the only common image-editing field. OpenAI uses multipart for byte/data-URL
74
+ edits and its JSON reference body for URL or file-ID edits. Its provider-specific `options.mask` accepts an
75
+ `ImageInput` for inpainting:
76
+
77
+ ```ts
78
+ yield *
79
+ Image.generate({
80
+ model: OpenAI.configure({ apiKey }).image("gpt-image-2"),
81
+ prompt,
82
+ images: [ImageInput.bytes(sourceBytes, "image/png")],
83
+ options: { mask: ImageInput.bytes(maskBytes, "image/png") },
84
+ })
85
+ ```
86
+
87
+ The OpenAI adapter extracts this helper value into the edit request's native `mask` field rather than passing the
88
+ tagged `ImageInput` object through as an ordinary option. On multipart requests, `http.body` can override option
89
+ fields but not structural `model`, `prompt`, `image[]`, or `mask` fields, and the transport owns the multipart
90
+ `Content-Type` boundary. For JSON requests, `http.body` remains the final raw-native overlay. Gemini does not fetch
91
+ public HTTP URLs, and hosted Z.ai image generation does not accept image inputs. These cases fail with
92
+ `InvalidRequest` before network I/O.
93
+
52
94
  Provider-native image options belong to each request. Raw `http.body` fields have final precedence over them:
53
95
 
54
96
  ```ts
package/dist/image.d.ts CHANGED
@@ -28,9 +28,46 @@ export declare namespace ImageModel {
28
28
  }
29
29
  }
30
30
  export declare const ImageModelSchema: Schema.declare<ImageModel<ImageOptions>, ImageModel<ImageOptions>>;
31
+ export declare const ImageInputSchema: Schema.toTaggedUnion<"type", readonly [Schema.Struct<{
32
+ readonly type: Schema.Literal<"bytes">;
33
+ readonly data: Schema.Uint8Array;
34
+ readonly mediaType: Schema.String;
35
+ }>, Schema.Struct<{
36
+ readonly type: Schema.Literal<"url">;
37
+ readonly url: Schema.String;
38
+ }>, Schema.Struct<{
39
+ readonly type: Schema.Literal<"file-id">;
40
+ readonly id: Schema.String;
41
+ }>, Schema.Struct<{
42
+ readonly type: Schema.Literal<"file-uri">;
43
+ readonly uri: Schema.String;
44
+ readonly mediaType: Schema.String;
45
+ }>]>;
46
+ export type ImageInput = Schema.Schema.Type<typeof ImageInputSchema>;
47
+ export declare const ImageInput: {
48
+ readonly bytes: (data: Uint8Array, mediaType: string) => ImageInput;
49
+ readonly url: (url: string) => ImageInput;
50
+ readonly file: (id: string) => ImageInput;
51
+ readonly fileUri: (uri: string, mediaType: string) => ImageInput;
52
+ };
31
53
  declare const ImageRequest_base: Schema.Class<ImageRequest, Schema.Struct<{
32
54
  readonly model: Schema.declare<ImageModel<ImageOptions>, ImageModel<ImageOptions>>;
33
55
  readonly prompt: Schema.String;
56
+ readonly images: Schema.optional<Schema.$Array<Schema.toTaggedUnion<"type", readonly [Schema.Struct<{
57
+ readonly type: Schema.Literal<"bytes">;
58
+ readonly data: Schema.Uint8Array;
59
+ readonly mediaType: Schema.String;
60
+ }>, Schema.Struct<{
61
+ readonly type: Schema.Literal<"url">;
62
+ readonly url: Schema.String;
63
+ }>, Schema.Struct<{
64
+ readonly type: Schema.Literal<"file-id">;
65
+ readonly id: Schema.String;
66
+ }>, Schema.Struct<{
67
+ readonly type: Schema.Literal<"file-uri">;
68
+ readonly uri: Schema.String;
69
+ readonly mediaType: Schema.String;
70
+ }>]>>>;
34
71
  readonly options: Schema.optional<Schema.$Record<Schema.String, Schema.Unknown>>;
35
72
  readonly http: Schema.optional<typeof HttpOptions>;
36
73
  }>, {}>;
package/dist/image.js CHANGED
@@ -24,9 +24,40 @@ export class ImageModel {
24
24
  export const ImageModelSchema = Schema.declare((value) => value instanceof ImageModel, {
25
25
  expected: "Image.Model",
26
26
  });
27
+ const ImageBytesInput = Schema.Struct({
28
+ type: Schema.Literal("bytes"),
29
+ data: Schema.Uint8Array,
30
+ mediaType: Schema.String,
31
+ });
32
+ const ImageUrlInput = Schema.Struct({
33
+ type: Schema.Literal("url"),
34
+ url: Schema.String,
35
+ });
36
+ const ImageFileIDInput = Schema.Struct({
37
+ type: Schema.Literal("file-id"),
38
+ id: Schema.String,
39
+ });
40
+ const ImageFileURIInput = Schema.Struct({
41
+ type: Schema.Literal("file-uri"),
42
+ uri: Schema.String,
43
+ mediaType: Schema.String,
44
+ });
45
+ export const ImageInputSchema = Schema.Union([
46
+ ImageBytesInput,
47
+ ImageUrlInput,
48
+ ImageFileIDInput,
49
+ ImageFileURIInput,
50
+ ]).pipe(Schema.toTaggedUnion("type"));
51
+ export const ImageInput = {
52
+ bytes: (data, mediaType) => ({ type: "bytes", data, mediaType }),
53
+ url: (url) => ({ type: "url", url }),
54
+ file: (id) => ({ type: "file-id", id }),
55
+ fileUri: (uri, mediaType) => ({ type: "file-uri", uri, mediaType }),
56
+ };
27
57
  export class ImageRequest extends Schema.Class("Image.Request")({
28
58
  model: ImageModelSchema,
29
59
  prompt: Schema.String,
60
+ images: Schema.optional(Schema.Array(ImageInputSchema)),
30
61
  options: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
31
62
  http: Schema.optional(HttpOptions),
32
63
  }) {
package/dist/index.d.ts CHANGED
@@ -6,7 +6,7 @@ export { ProviderPackage } from "./provider-package";
6
6
  export { isContextOverflow, isContextOverflowFailure } from "./provider-error";
7
7
  export type { RouteModelInput, RouteRoutedModelInput, Interface as LLMClientShape, Service as LLMClientService, } from "./route/client";
8
8
  export * from "./schema";
9
- export { GeneratedImage, ImageModel, ImageRequest, ImageResponse } from "./image";
9
+ export { GeneratedImage, ImageInput, ImageInputSchema, ImageModel, ImageRequest, ImageResponse } from "./image";
10
10
  export type { ImageModelOptions, ImageOptions, ImageRequestFor, ImageRequestInput, ImageRoute } from "./image";
11
11
  export { Image } from "./image";
12
12
  export { Tool, ToolFailure, toDefinitions } from "./tool";
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ export { Provider } from "./provider";
5
5
  export { ProviderPackage } from "./provider-package";
6
6
  export { isContextOverflow, isContextOverflowFailure } from "./provider-error";
7
7
  export * from "./schema";
8
- export { GeneratedImage, ImageModel, ImageRequest, ImageResponse } from "./image";
8
+ export { GeneratedImage, ImageInput, ImageInputSchema, ImageModel, ImageRequest, ImageResponse } from "./image";
9
9
  export { Image } from "./image";
10
10
  export { Tool, ToolFailure, toDefinitions } from "./tool";
11
11
  export { ToolRuntime } from "./tool-runtime";
@@ -13,9 +13,7 @@ export type GoogleImageOptions = {
13
13
  export type GoogleImageBody = Record<string, unknown> & {
14
14
  readonly contents: ReadonlyArray<{
15
15
  readonly role: "user";
16
- readonly parts: ReadonlyArray<{
17
- readonly text: string;
18
- }>;
16
+ readonly parts: ReadonlyArray<Record<string, unknown>>;
19
17
  }>;
20
18
  readonly generationConfig: Record<string, unknown>;
21
19
  };
@@ -1,9 +1,10 @@
1
1
  import { Effect, Encoding, Schema } from "effect";
2
2
  import { Headers, HttpClientRequest } from "effect/unstable/http";
3
- import { GeneratedImage, ImageModel, ImageResponse } from "../image";
3
+ import { GeneratedImage, ImageModel, ImageResponse, } from "../image";
4
4
  import { Auth } from "../route/auth";
5
5
  import { InvalidProviderOutputReason, LLMError, Usage, mergeHttpOptions, mergeJsonRecords, } from "../schema";
6
6
  import { ProviderShared } from "./shared";
7
+ import { ImageInputs } from "./utils/image-input";
7
8
  const ADAPTER = "google-images";
8
9
  export const DEFAULT_BASE_URL = "https://generativelanguage.googleapis.com/v1beta";
9
10
  const GoogleUsage = Schema.StructWithRest(Schema.Struct({
@@ -57,10 +58,6 @@ const nativeOptions = (options) => {
57
58
  thinkingConfig: Object.values(thinkingConfig).some((value) => value !== undefined) ? thinkingConfig : undefined,
58
59
  }, native) ?? { responseModalities: ["IMAGE"] });
59
60
  };
60
- const body = (request, overlay) => mergeJsonRecords({
61
- contents: [{ role: "user", parts: [{ text: request.prompt }] }],
62
- generationConfig: nativeOptions(request.options),
63
- }, overlay);
64
61
  const invalidOutput = (message, providerMetadata) => new LLMError({
65
62
  module: ADAPTER,
66
63
  method: "generate",
@@ -77,8 +74,13 @@ export const model = (input) => {
77
74
  const route = {
78
75
  id: ADAPTER,
79
76
  generate: Effect.fn("GoogleImages.generate")(function* (request, execute) {
77
+ const imageParts = yield* Effect.forEach(request.images ?? [], googleImagePart);
80
78
  const http = mergeHttpOptions(request.model.http, request.http);
81
- const text = ProviderShared.encodeJson(body(request, http?.body));
79
+ const requestBody = mergeJsonRecords({
80
+ contents: [{ role: "user", parts: [{ text: request.prompt }, ...imageParts] }],
81
+ generationConfig: nativeOptions(request.options),
82
+ }, http?.body);
83
+ const text = ProviderShared.encodeJson(requestBody);
82
84
  const url = applyQuery(`${(input.baseURL ?? DEFAULT_BASE_URL).replace(/\/$/, "")}/models/${request.model.id}:generateContent`, http?.query);
83
85
  const headers = yield* Auth.toEffect(input.auth)({
84
86
  request,
@@ -169,6 +171,21 @@ export const model = (input) => {
169
171
  };
170
172
  return ImageModel.make({ id: input.id, provider: "google", route, http: input.http });
171
173
  };
174
+ const googleImagePart = (image) => {
175
+ if (image.type === "bytes")
176
+ return Effect.succeed({ inlineData: { mimeType: image.mediaType, data: Encoding.encodeBase64(image.data) } });
177
+ if (image.type === "file-uri")
178
+ return Effect.succeed({ fileData: { mimeType: image.mediaType, fileUri: image.uri } });
179
+ if (image.type === "url")
180
+ return ImageInputs.decodeDataUrl(image.url, ADAPTER).pipe(Effect.flatMap((decoded) => {
181
+ if (decoded === undefined)
182
+ return Effect.fail(ImageInputs.invalid(ADAPTER, "Google generateContent does not fetch public image URLs; use bytes, a data URL, or a Gemini file URI"));
183
+ return Effect.succeed({
184
+ inlineData: { mimeType: decoded.mediaType, data: Encoding.encodeBase64(decoded.data) },
185
+ });
186
+ }));
187
+ return Effect.fail(ImageInputs.invalid(ADAPTER, "Google generateContent requires Gemini file URIs rather than provider file IDs"));
188
+ };
172
189
  export const GoogleImages = {
173
190
  model,
174
191
  };
@@ -71,13 +71,13 @@ export declare const route: Route<{
71
71
  } | {
72
72
  readonly type: "image_generation";
73
73
  readonly output_format?: "png" | "jpeg" | "webp" | undefined;
74
+ readonly size?: string | undefined;
75
+ readonly quality?: "low" | "medium" | "high" | "auto" | undefined;
76
+ readonly background?: "auto" | "opaque" | "transparent" | undefined;
74
77
  readonly output_compression?: number | undefined;
75
78
  readonly action?: "generate" | "auto" | "edit" | undefined;
76
- readonly background?: "auto" | "opaque" | "transparent" | undefined;
77
79
  readonly input_fidelity?: "low" | "high" | undefined;
78
80
  readonly partial_images?: number | undefined;
79
- readonly quality?: "low" | "medium" | "high" | "auto" | undefined;
80
- readonly size?: string | undefined;
81
81
  })[] | undefined;
82
82
  readonly temperature?: number | undefined;
83
83
  readonly tool_choice?: "required" | "none" | "auto" | import("effect/Schema").Struct.ReadonlySide<{
@@ -1,10 +1,12 @@
1
- import { ImageModel } from "../image";
1
+ import { ImageModel, type ImageInput } from "../image";
2
2
  import { type Definition as AuthDefinition } from "../route/auth";
3
3
  import { type HttpOptions } from "../schema";
4
4
  export declare const DEFAULT_BASE_URL = "https://api.openai.com/v1";
5
5
  export declare const PATH = "/images/generations";
6
+ export declare const EDIT_PATH = "/images/edits";
6
7
  export type OpenAIImageString<Known extends string> = Known | (string & {});
7
8
  export type OpenAIImageOptions = {
9
+ readonly mask?: ImageInput;
8
10
  readonly n?: number;
9
11
  readonly size?: OpenAIImageString<"auto" | "256x256" | "512x512" | "1024x1024" | "1536x1024" | "1024x1536" | "1792x1024" | "1024x1792">;
10
12
  readonly quality?: OpenAIImageString<"auto" | "low" | "medium" | "high" | "standard" | "hd">;
@@ -1,13 +1,15 @@
1
1
  import { Effect, Encoding, Schema } from "effect";
2
- import { Headers, HttpClientRequest } from "effect/unstable/http";
3
- import { ImageModel, GeneratedImage, ImageResponse } from "../image";
2
+ import { Headers, HttpClientRequest, HttpClientResponse } from "effect/unstable/http";
3
+ import { ImageModel, GeneratedImage, ImageResponse, } from "../image";
4
4
  import { Auth } from "../route/auth";
5
5
  import { InvalidProviderOutputReason, LLMError, Usage, mergeHttpOptions, mergeJsonRecords, } from "../schema";
6
6
  import { ProviderShared } from "./shared";
7
+ import { ImageInputs } from "./utils/image-input";
7
8
  import { OpenAIImage } from "./utils/openai-image";
8
9
  const ADAPTER = "openai-images";
9
10
  export const DEFAULT_BASE_URL = "https://api.openai.com/v1";
10
11
  export const PATH = "/images/generations";
12
+ export const EDIT_PATH = "/images/edits";
11
13
  const OpenAIImageResponse = Schema.Struct({
12
14
  data: Schema.Array(Schema.Struct({
13
15
  b64_json: Schema.optional(Schema.String),
@@ -26,7 +28,7 @@ const OpenAIImageResponse = Schema.Struct({
26
28
  const nativeOptions = (options) => {
27
29
  if (!options)
28
30
  return undefined;
29
- const { outputFormat, outputCompression, ...native } = options;
31
+ const { mask: _, outputFormat, outputCompression, ...native } = options;
30
32
  return {
31
33
  output_format: outputFormat,
32
34
  output_compression: outputCompression,
@@ -49,10 +51,85 @@ export const model = (input) => {
49
51
  const route = {
50
52
  id: ADAPTER,
51
53
  generate: Effect.fn("OpenAIImages.generate")(function* (request, execute) {
54
+ const mask = request.options?.mask;
55
+ if (mask !== undefined && (request.images?.length ?? 0) === 0)
56
+ return yield* ImageInputs.invalid(ADAPTER, "An OpenAI image mask requires at least one input image");
52
57
  const http = mergeHttpOptions(request.model.http, request.http);
53
- const requestBody = mergeJsonRecords({ model: request.model.id, prompt: request.prompt }, nativeOptions(request.options), http?.body);
58
+ const sourceImages = request.images ?? [];
59
+ const multipartImages = yield* Effect.forEach(sourceImages, (image) => {
60
+ if (image.type === "bytes")
61
+ return Effect.succeed({ data: image.data, mediaType: image.mediaType });
62
+ if (image.type === "url")
63
+ return ImageInputs.decodeDataUrl(image.url, ADAPTER);
64
+ return Effect.succeed(undefined);
65
+ });
66
+ const multipartMask = mask === undefined
67
+ ? undefined
68
+ : mask.type === "bytes"
69
+ ? { data: mask.data, mediaType: mask.mediaType }
70
+ : mask.type === "url"
71
+ ? yield* ImageInputs.decodeDataUrl(mask.url, ADAPTER)
72
+ : undefined;
73
+ const useMultipart = sourceImages.length > 0 &&
74
+ multipartImages.every((image) => image !== undefined) &&
75
+ (mask === undefined || multipartMask !== undefined);
76
+ const path = sourceImages.length === 0 ? PATH : EDIT_PATH;
77
+ const url = applyQuery(`${(input.baseURL ?? DEFAULT_BASE_URL).replace(/\/$/, "")}${path}`, http?.query);
78
+ if (useMultipart) {
79
+ const form = new FormData();
80
+ form.append("model", request.model.id);
81
+ form.append("prompt", request.prompt);
82
+ Object.entries(mergeJsonRecords(nativeOptions(request.options), http?.body) ?? {}).forEach(([key, value]) => {
83
+ if (["model", "prompt", "image", "image[]", "images", "mask"].includes(key))
84
+ return;
85
+ form.append(key, typeof value === "string" ? value : ProviderShared.encodeJson(value));
86
+ });
87
+ multipartImages.forEach((image, index) => {
88
+ if (image === undefined)
89
+ return;
90
+ form.append("image[]", imageBlob(image.data, image.mediaType), `image-${index}`);
91
+ });
92
+ if (multipartMask !== undefined)
93
+ form.append("mask", imageBlob(multipartMask.data, multipartMask.mediaType), "mask");
94
+ const headers = yield* Auth.toEffect(input.auth)({
95
+ request,
96
+ method: "POST",
97
+ url,
98
+ body: "[multipart/form-data]",
99
+ headers: Headers.remove(Headers.fromInput({ ...input.headers, ...http?.headers }), "content-type"),
100
+ });
101
+ const response = yield* execute(HttpClientRequest.post(url).pipe(HttpClientRequest.setHeaders(headers), HttpClientRequest.bodyFormData(form)));
102
+ return yield* parseResponse(response, request.options, http?.body);
103
+ }
104
+ const references = sourceImages.map((image) => {
105
+ if (image.type === "bytes")
106
+ return { image_url: ImageInputs.dataUrl(image) };
107
+ if (image.type === "url")
108
+ return { image_url: image.url };
109
+ if (image.type === "file-id")
110
+ return { file_id: image.id };
111
+ return undefined;
112
+ });
113
+ if (references.some((image) => image === undefined))
114
+ return yield* ImageInputs.invalid(ADAPTER, "OpenAI Images accepts image URLs, data URLs, bytes, and file IDs");
115
+ const maskReference = mask === undefined
116
+ ? undefined
117
+ : mask.type === "bytes"
118
+ ? { image_url: ImageInputs.dataUrl(mask) }
119
+ : mask.type === "url"
120
+ ? { image_url: mask.url }
121
+ : mask.type === "file-id"
122
+ ? { file_id: mask.id }
123
+ : undefined;
124
+ if (mask !== undefined && maskReference === undefined)
125
+ return yield* ImageInputs.invalid(ADAPTER, "OpenAI Images accepts masks as URLs, data URLs, bytes, or file IDs");
126
+ const requestBody = mergeJsonRecords({
127
+ model: request.model.id,
128
+ prompt: request.prompt,
129
+ images: references.length === 0 ? undefined : references,
130
+ mask: maskReference,
131
+ }, nativeOptions(request.options), http?.body);
54
132
  const text = ProviderShared.encodeJson(requestBody);
55
- const url = applyQuery(`${(input.baseURL ?? DEFAULT_BASE_URL).replace(/\/$/, "")}${PATH}`, http?.query);
56
133
  const headers = yield* Auth.toEffect(input.auth)({
57
134
  request,
58
135
  method: "POST",
@@ -61,42 +138,51 @@ export const model = (input) => {
61
138
  headers: Headers.fromInput({ ...input.headers, ...http?.headers }),
62
139
  });
63
140
  const response = yield* execute(HttpClientRequest.post(url).pipe(HttpClientRequest.setHeaders(headers), HttpClientRequest.bodyText(text, "application/json")));
64
- const payload = yield* response.json.pipe(Effect.mapError(() => invalidOutput("Failed to read the OpenAI Images response")));
65
- const decoded = yield* Schema.decodeUnknownEffect(OpenAIImageResponse)(payload).pipe(Effect.mapError(() => invalidOutput("OpenAI Images returned an invalid response")));
66
- const format = decoded.output_format ?? (typeof requestBody.output_format === "string" ? requestBody.output_format : "png");
67
- const images = yield* Effect.forEach(decoded.data, (item, index) => {
68
- if (item.b64_json)
69
- return Effect.fromResult(Encoding.decodeBase64(item.b64_json)).pipe(Effect.mapError(() => invalidOutput(`OpenAI Images result ${index} contains invalid base64 data`)), Effect.map((data) => new GeneratedImage({
70
- mediaType: `image/${format}`,
71
- data,
72
- providerMetadata: item.revised_prompt === undefined ? undefined : { openai: { revisedPrompt: item.revised_prompt } },
73
- })));
74
- if (item.url)
75
- return Effect.succeed(new GeneratedImage({
76
- mediaType: `image/${format}`,
77
- data: item.url,
78
- providerMetadata: item.revised_prompt === undefined ? undefined : { openai: { revisedPrompt: item.revised_prompt } },
79
- }));
80
- return Effect.fail(invalidOutput(`OpenAI Images result ${index} has neither image data nor a URL`));
81
- });
82
- if (images.length === 0)
83
- return yield* invalidOutput("OpenAI Images returned no images");
84
- return new ImageResponse({
85
- images,
86
- usage: decoded.usage === undefined
87
- ? undefined
88
- : new Usage({
89
- inputTokens: decoded.usage.input_tokens,
90
- outputTokens: decoded.usage.output_tokens,
91
- totalTokens: decoded.usage.total_tokens,
92
- providerMetadata: { openai: decoded.usage },
93
- }),
94
- providerMetadata: { openai: { outputFormat: format } },
95
- });
141
+ return yield* parseResponse(response, request.options, http?.body);
96
142
  }),
97
143
  };
98
144
  return ImageModel.make({ id: input.id, provider: "openai", route, http: input.http });
99
145
  };
146
+ const parseResponse = Effect.fn("OpenAIImages.parseResponse")(function* (response, options, overlay) {
147
+ const payload = yield* response.json.pipe(Effect.mapError(() => invalidOutput("Failed to read the OpenAI Images response")));
148
+ const decoded = yield* Schema.decodeUnknownEffect(OpenAIImageResponse)(payload).pipe(Effect.mapError(() => invalidOutput("OpenAI Images returned an invalid response")));
149
+ const requestBody = mergeJsonRecords(nativeOptions(options), overlay);
150
+ const format = decoded.output_format ?? (typeof requestBody?.output_format === "string" ? requestBody.output_format : "png");
151
+ const images = yield* Effect.forEach(decoded.data, (item, index) => {
152
+ if (item.b64_json)
153
+ return Effect.fromResult(Encoding.decodeBase64(item.b64_json)).pipe(Effect.mapError(() => invalidOutput(`OpenAI Images result ${index} contains invalid base64 data`)), Effect.map((data) => new GeneratedImage({
154
+ mediaType: `image/${format}`,
155
+ data,
156
+ providerMetadata: item.revised_prompt === undefined ? undefined : { openai: { revisedPrompt: item.revised_prompt } },
157
+ })));
158
+ if (item.url)
159
+ return Effect.succeed(new GeneratedImage({
160
+ mediaType: `image/${format}`,
161
+ data: item.url,
162
+ providerMetadata: item.revised_prompt === undefined ? undefined : { openai: { revisedPrompt: item.revised_prompt } },
163
+ }));
164
+ return Effect.fail(invalidOutput(`OpenAI Images result ${index} has neither image data nor a URL`));
165
+ });
166
+ if (images.length === 0)
167
+ return yield* invalidOutput("OpenAI Images returned no images");
168
+ return new ImageResponse({
169
+ images,
170
+ usage: decoded.usage === undefined
171
+ ? undefined
172
+ : new Usage({
173
+ inputTokens: decoded.usage.input_tokens,
174
+ outputTokens: decoded.usage.output_tokens,
175
+ totalTokens: decoded.usage.total_tokens,
176
+ providerMetadata: { openai: decoded.usage },
177
+ }),
178
+ providerMetadata: { openai: { outputFormat: format } },
179
+ });
180
+ });
181
+ const imageBlob = (data, mediaType) => {
182
+ const buffer = new ArrayBuffer(data.byteLength);
183
+ new Uint8Array(buffer).set(data);
184
+ return new Blob([buffer], { type: mediaType });
185
+ };
100
186
  export const OpenAIImages = {
101
187
  model,
102
188
  };
@@ -165,13 +165,13 @@ export declare const protocol: Protocol<{
165
165
  } | {
166
166
  readonly type: "image_generation";
167
167
  readonly output_format?: "png" | "jpeg" | "webp" | undefined;
168
+ readonly size?: string | undefined;
169
+ readonly quality?: "low" | "medium" | "high" | "auto" | undefined;
170
+ readonly background?: "auto" | "opaque" | "transparent" | undefined;
168
171
  readonly output_compression?: number | undefined;
169
172
  readonly action?: "generate" | "auto" | "edit" | undefined;
170
- readonly background?: "auto" | "opaque" | "transparent" | undefined;
171
173
  readonly input_fidelity?: "low" | "high" | undefined;
172
174
  readonly partial_images?: number | undefined;
173
- readonly quality?: "low" | "medium" | "high" | "auto" | undefined;
174
- readonly size?: string | undefined;
175
175
  })[] | undefined;
176
176
  readonly temperature?: number | undefined;
177
177
  readonly tool_choice?: "required" | "none" | "auto" | Schema.Struct.ReadonlySide<{
@@ -317,13 +317,13 @@ export declare const httpTransport: HttpTransport.HttpJsonTransport<{
317
317
  } | {
318
318
  readonly type: "image_generation";
319
319
  readonly output_format?: "png" | "jpeg" | "webp" | undefined;
320
+ readonly size?: string | undefined;
321
+ readonly quality?: "low" | "medium" | "high" | "auto" | undefined;
322
+ readonly background?: "auto" | "opaque" | "transparent" | undefined;
320
323
  readonly output_compression?: number | undefined;
321
324
  readonly action?: "generate" | "auto" | "edit" | undefined;
322
- readonly background?: "auto" | "opaque" | "transparent" | undefined;
323
325
  readonly input_fidelity?: "low" | "high" | undefined;
324
326
  readonly partial_images?: number | undefined;
325
- readonly quality?: "low" | "medium" | "high" | "auto" | undefined;
326
- readonly size?: string | undefined;
327
327
  })[] | undefined;
328
328
  readonly temperature?: number | undefined;
329
329
  readonly tool_choice?: "required" | "none" | "auto" | Schema.Struct.ReadonlySide<{
@@ -406,13 +406,13 @@ export declare const route: Route<{
406
406
  } | {
407
407
  readonly type: "image_generation";
408
408
  readonly output_format?: "png" | "jpeg" | "webp" | undefined;
409
+ readonly size?: string | undefined;
410
+ readonly quality?: "low" | "medium" | "high" | "auto" | undefined;
411
+ readonly background?: "auto" | "opaque" | "transparent" | undefined;
409
412
  readonly output_compression?: number | undefined;
410
413
  readonly action?: "generate" | "auto" | "edit" | undefined;
411
- readonly background?: "auto" | "opaque" | "transparent" | undefined;
412
414
  readonly input_fidelity?: "low" | "high" | undefined;
413
415
  readonly partial_images?: number | undefined;
414
- readonly quality?: "low" | "medium" | "high" | "auto" | undefined;
415
- readonly size?: string | undefined;
416
416
  })[] | undefined;
417
417
  readonly temperature?: number | undefined;
418
418
  readonly tool_choice?: "required" | "none" | "auto" | Schema.Struct.ReadonlySide<{
@@ -495,13 +495,13 @@ export declare const webSocketTransport: import("../route/transport/websocket").
495
495
  } | {
496
496
  readonly type: "image_generation";
497
497
  readonly output_format?: "png" | "jpeg" | "webp" | undefined;
498
+ readonly size?: string | undefined;
499
+ readonly quality?: "low" | "medium" | "high" | "auto" | undefined;
500
+ readonly background?: "auto" | "opaque" | "transparent" | undefined;
498
501
  readonly output_compression?: number | undefined;
499
502
  readonly action?: "generate" | "auto" | "edit" | undefined;
500
- readonly background?: "auto" | "opaque" | "transparent" | undefined;
501
503
  readonly input_fidelity?: "low" | "high" | undefined;
502
504
  readonly partial_images?: number | undefined;
503
- readonly quality?: "low" | "medium" | "high" | "auto" | undefined;
504
- readonly size?: string | undefined;
505
505
  })[] | undefined;
506
506
  readonly temperature?: number | undefined;
507
507
  readonly tool_choice?: "required" | "none" | "auto" | Schema.Struct.ReadonlySide<{
@@ -584,13 +584,13 @@ export declare const webSocketTransport: import("../route/transport/websocket").
584
584
  } | {
585
585
  readonly type: "image_generation";
586
586
  readonly output_format?: "png" | "jpeg" | "webp" | undefined;
587
+ readonly size?: string | undefined;
588
+ readonly quality?: "low" | "medium" | "high" | "auto" | undefined;
589
+ readonly background?: "auto" | "opaque" | "transparent" | undefined;
587
590
  readonly output_compression?: number | undefined;
588
591
  readonly action?: "generate" | "auto" | "edit" | undefined;
589
- readonly background?: "auto" | "opaque" | "transparent" | undefined;
590
592
  readonly input_fidelity?: "low" | "high" | undefined;
591
593
  readonly partial_images?: number | undefined;
592
- readonly quality?: "low" | "medium" | "high" | "auto" | undefined;
593
- readonly size?: string | undefined;
594
594
  })[] | undefined;
595
595
  readonly temperature?: number | undefined;
596
596
  readonly tool_choice?: "required" | "none" | "auto" | Schema.Struct.ReadonlySide<{
@@ -673,13 +673,13 @@ export declare const webSocketRoute: Route<{
673
673
  } | {
674
674
  readonly type: "image_generation";
675
675
  readonly output_format?: "png" | "jpeg" | "webp" | undefined;
676
+ readonly size?: string | undefined;
677
+ readonly quality?: "low" | "medium" | "high" | "auto" | undefined;
678
+ readonly background?: "auto" | "opaque" | "transparent" | undefined;
676
679
  readonly output_compression?: number | undefined;
677
680
  readonly action?: "generate" | "auto" | "edit" | undefined;
678
- readonly background?: "auto" | "opaque" | "transparent" | undefined;
679
681
  readonly input_fidelity?: "low" | "high" | undefined;
680
682
  readonly partial_images?: number | undefined;
681
- readonly quality?: "low" | "medium" | "high" | "auto" | undefined;
682
- readonly size?: string | undefined;
683
683
  })[] | undefined;
684
684
  readonly temperature?: number | undefined;
685
685
  readonly tool_choice?: "required" | "none" | "auto" | Schema.Struct.ReadonlySide<{
@@ -0,0 +1,21 @@
1
+ import { Effect } from "effect";
2
+ import type { ImageInput } from "../../image";
3
+ import { LLMError } from "../../schema";
4
+ export declare const dataUrl: (input: Extract<ImageInput, {
5
+ readonly type: "bytes";
6
+ }>) => string;
7
+ export declare const decodeDataUrl: (url: string, module: string) => Effect.Effect<{
8
+ readonly mediaType: string;
9
+ readonly data: Uint8Array;
10
+ } | undefined, LLMError>;
11
+ export declare const invalidImageInput: (module: string, message: string) => LLMError;
12
+ export declare const ImageInputs: {
13
+ readonly dataUrl: (input: Extract<ImageInput, {
14
+ readonly type: "bytes";
15
+ }>) => string;
16
+ readonly decodeDataUrl: (url: string, module: string) => Effect.Effect<{
17
+ readonly mediaType: string;
18
+ readonly data: Uint8Array;
19
+ } | undefined, LLMError>;
20
+ readonly invalid: (module: string, message: string) => LLMError;
21
+ };
@@ -0,0 +1,22 @@
1
+ import { Effect, Encoding } from "effect";
2
+ import { InvalidRequestReason, LLMError } from "../../schema";
3
+ const invalid = (module, message) => new LLMError({
4
+ module,
5
+ method: "generate",
6
+ reason: new InvalidRequestReason({ message }),
7
+ });
8
+ export const dataUrl = (input) => `data:${input.mediaType};base64,${Encoding.encodeBase64(input.data)}`;
9
+ export const decodeDataUrl = (url, module) => {
10
+ if (!url.startsWith("data:"))
11
+ return Effect.succeed(undefined);
12
+ const match = /^data:([^;,]+);base64,(.*)$/s.exec(url);
13
+ if (!match)
14
+ return Effect.fail(invalid(module, "Image data URLs must contain a MIME type and base64 data"));
15
+ return Effect.fromResult(Encoding.decodeBase64(match[2])).pipe(Effect.mapError(() => invalid(module, "Image data URL contains invalid base64 data")), Effect.map((data) => ({ mediaType: match[1], data })));
16
+ };
17
+ export const invalidImageInput = invalid;
18
+ export const ImageInputs = {
19
+ dataUrl,
20
+ decodeDataUrl,
21
+ invalid: invalidImageInput,
22
+ };
@@ -3,6 +3,7 @@ import { type Definition as AuthDefinition } from "../route/auth";
3
3
  import { type HttpOptions } from "../schema";
4
4
  export declare const DEFAULT_BASE_URL = "https://api.x.ai/v1";
5
5
  export declare const PATH = "/images/generations";
6
+ export declare const EDIT_PATH = "/images/edits";
6
7
  export type XAIImageString<Known extends string> = Known | (string & {});
7
8
  export type XAIImageOptions = {
8
9
  readonly n?: number;
@@ -4,9 +4,11 @@ import { GeneratedImage, ImageModel, ImageResponse } from "../image";
4
4
  import { Auth } from "../route/auth";
5
5
  import { InvalidProviderOutputReason, LLMError, Usage, mergeHttpOptions, mergeJsonRecords, } from "../schema";
6
6
  import { ProviderShared, optionalNull } from "./shared";
7
+ import { ImageInputs } from "./utils/image-input";
7
8
  const ADAPTER = "xai-images";
8
9
  export const DEFAULT_BASE_URL = "https://api.x.ai/v1";
9
10
  export const PATH = "/images/generations";
11
+ export const EDIT_PATH = "/images/edits";
10
12
  const XAIImageResponse = Schema.Struct({
11
13
  data: Schema.Array(Schema.Struct({
12
14
  b64_json: optionalNull(Schema.String),
@@ -43,9 +45,25 @@ export const model = (input) => {
43
45
  id: ADAPTER,
44
46
  generate: Effect.fn("XAIImages.generate")(function* (request, execute) {
45
47
  const http = mergeHttpOptions(request.model.http, request.http);
46
- const requestBody = mergeJsonRecords({ model: request.model.id, prompt: request.prompt }, nativeOptions(request.options), http?.body);
48
+ const imageReferences = (request.images ?? []).map((image) => {
49
+ if (image.type === "bytes")
50
+ return { url: ImageInputs.dataUrl(image), type: "image_url" };
51
+ if (image.type === "url")
52
+ return { url: image.url, type: "image_url" };
53
+ if (image.type === "file-id")
54
+ return { file_id: image.id };
55
+ return undefined;
56
+ });
57
+ if (imageReferences.some((image) => image === undefined))
58
+ return yield* ImageInputs.invalid(ADAPTER, "xAI Images accepts image URLs, data URLs, bytes, and file IDs");
59
+ const requestBody = mergeJsonRecords({
60
+ model: request.model.id,
61
+ prompt: request.prompt,
62
+ image: imageReferences.length === 1 ? imageReferences[0] : undefined,
63
+ images: imageReferences.length > 1 ? imageReferences : undefined,
64
+ }, nativeOptions(request.options), http?.body);
47
65
  const text = ProviderShared.encodeJson(requestBody);
48
- const url = applyQuery(`${(input.baseURL ?? DEFAULT_BASE_URL).replace(/\/$/, "")}${PATH}`, http?.query);
66
+ const url = applyQuery(`${(input.baseURL ?? DEFAULT_BASE_URL).replace(/\/$/, "")}${imageReferences.length === 0 ? PATH : EDIT_PATH}`, http?.query);
49
67
  const headers = yield* Auth.toEffect(input.auth)({
50
68
  request,
51
69
  method: "POST",
@@ -4,6 +4,7 @@ import { GeneratedImage, ImageModel, ImageResponse } from "../image";
4
4
  import { Auth } from "../route/auth";
5
5
  import { InvalidProviderOutputReason, LLMError, mergeHttpOptions, mergeJsonRecords } from "../schema";
6
6
  import { ProviderShared } from "./shared";
7
+ import { ImageInputs } from "./utils/image-input";
7
8
  const ADAPTER = "zai-images";
8
9
  export const DEFAULT_BASE_URL = "https://api.z.ai/api/paas/v4";
9
10
  export const PATH = "/images/generations";
@@ -42,6 +43,8 @@ export const model = (input) => {
42
43
  const route = {
43
44
  id: ADAPTER,
44
45
  generate: Effect.fn("ZAIImages.generate")(function* (request, execute) {
46
+ if ((request.images?.length ?? 0) > 0)
47
+ return yield* ImageInputs.invalid(ADAPTER, "Z.ai hosted image generation does not support image inputs");
45
48
  const http = mergeHttpOptions(request.model.http, request.http);
46
49
  const requestBody = mergeJsonRecords({ model: request.model.id, prompt: request.prompt }, nativeOptions(request.options), http?.body);
47
50
  const text = ProviderShared.encodeJson(requestBody);
@@ -150,13 +150,13 @@ export declare const routes: (RouteDef<{
150
150
  } | {
151
151
  readonly type: "image_generation";
152
152
  readonly output_format?: "png" | "jpeg" | "webp" | undefined;
153
+ readonly size?: string | undefined;
154
+ readonly quality?: "low" | "medium" | "high" | "auto" | undefined;
155
+ readonly background?: "auto" | "opaque" | "transparent" | undefined;
153
156
  readonly output_compression?: number | undefined;
154
157
  readonly action?: "generate" | "auto" | "edit" | undefined;
155
- readonly background?: "auto" | "opaque" | "transparent" | undefined;
156
158
  readonly input_fidelity?: "low" | "high" | undefined;
157
159
  readonly partial_images?: number | undefined;
158
- readonly quality?: "low" | "medium" | "high" | "auto" | undefined;
159
- readonly size?: string | undefined;
160
160
  })[] | undefined;
161
161
  readonly temperature?: number | undefined;
162
162
  readonly tool_choice?: "required" | "none" | "auto" | import("effect/Schema").Struct.ReadonlySide<{
@@ -138,13 +138,13 @@ export declare const routes: (import("../route").Route<{
138
138
  } | {
139
139
  readonly type: "image_generation";
140
140
  readonly output_format?: "png" | "jpeg" | "webp" | undefined;
141
+ readonly size?: string | undefined;
142
+ readonly quality?: "low" | "medium" | "high" | "auto" | undefined;
143
+ readonly background?: "auto" | "opaque" | "transparent" | undefined;
141
144
  readonly output_compression?: number | undefined;
142
145
  readonly action?: "generate" | "auto" | "edit" | undefined;
143
- readonly background?: "auto" | "opaque" | "transparent" | undefined;
144
146
  readonly input_fidelity?: "low" | "high" | undefined;
145
147
  readonly partial_images?: number | undefined;
146
- readonly quality?: "low" | "medium" | "high" | "auto" | undefined;
147
- readonly size?: string | undefined;
148
148
  })[] | undefined;
149
149
  readonly temperature?: number | undefined;
150
150
  readonly tool_choice?: "required" | "none" | "auto" | import("effect/Schema").Struct.ReadonlySide<{
@@ -82,13 +82,13 @@ export declare const routes: import("../route").Route<{
82
82
  } | {
83
83
  readonly type: "image_generation";
84
84
  readonly output_format?: "png" | "jpeg" | "webp" | undefined;
85
+ readonly size?: string | undefined;
86
+ readonly quality?: "low" | "medium" | "high" | "auto" | undefined;
87
+ readonly background?: "auto" | "opaque" | "transparent" | undefined;
85
88
  readonly output_compression?: number | undefined;
86
89
  readonly action?: "generate" | "auto" | "edit" | undefined;
87
- readonly background?: "auto" | "opaque" | "transparent" | undefined;
88
90
  readonly input_fidelity?: "low" | "high" | undefined;
89
91
  readonly partial_images?: number | undefined;
90
- readonly quality?: "low" | "medium" | "high" | "auto" | undefined;
91
- readonly size?: string | undefined;
92
92
  })[] | undefined;
93
93
  readonly temperature?: number | undefined;
94
94
  readonly tool_choice?: "required" | "none" | "auto" | import("effect/Schema").Struct.ReadonlySide<{
@@ -80,13 +80,13 @@ export declare const routes: import("../route").Route<{
80
80
  } | {
81
81
  readonly type: "image_generation";
82
82
  readonly output_format?: "png" | "jpeg" | "webp" | undefined;
83
+ readonly size?: string | undefined;
84
+ readonly quality?: "low" | "medium" | "high" | "auto" | undefined;
85
+ readonly background?: "auto" | "opaque" | "transparent" | undefined;
83
86
  readonly output_compression?: number | undefined;
84
87
  readonly action?: "generate" | "auto" | "edit" | undefined;
85
- readonly background?: "auto" | "opaque" | "transparent" | undefined;
86
88
  readonly input_fidelity?: "low" | "high" | undefined;
87
89
  readonly partial_images?: number | undefined;
88
- readonly quality?: "low" | "medium" | "high" | "auto" | undefined;
89
- readonly size?: string | undefined;
90
90
  })[] | undefined;
91
91
  readonly temperature?: number | undefined;
92
92
  readonly tool_choice?: "required" | "none" | "auto" | import("effect/Schema").Struct.ReadonlySide<{
@@ -136,13 +136,13 @@ export declare const routes: (Route<{
136
136
  } | {
137
137
  readonly type: "image_generation";
138
138
  readonly output_format?: "png" | "jpeg" | "webp" | undefined;
139
+ readonly size?: string | undefined;
140
+ readonly quality?: "low" | "medium" | "high" | "auto" | undefined;
141
+ readonly background?: "auto" | "opaque" | "transparent" | undefined;
139
142
  readonly output_compression?: number | undefined;
140
143
  readonly action?: "generate" | "auto" | "edit" | undefined;
141
- readonly background?: "auto" | "opaque" | "transparent" | undefined;
142
144
  readonly input_fidelity?: "low" | "high" | undefined;
143
145
  readonly partial_images?: number | undefined;
144
- readonly quality?: "low" | "medium" | "high" | "auto" | undefined;
145
- readonly size?: string | undefined;
146
146
  })[] | undefined;
147
147
  readonly temperature?: number | undefined;
148
148
  readonly tool_choice?: "required" | "none" | "auto" | import("effect/Schema").Struct.ReadonlySide<{
@@ -224,13 +224,13 @@ export declare const routes: (Route<{
224
224
  } | {
225
225
  readonly type: "image_generation";
226
226
  readonly output_format?: "png" | "jpeg" | "webp" | undefined;
227
+ readonly size?: string | undefined;
228
+ readonly quality?: "low" | "medium" | "high" | "auto" | undefined;
229
+ readonly background?: "auto" | "opaque" | "transparent" | undefined;
227
230
  readonly output_compression?: number | undefined;
228
231
  readonly action?: "generate" | "auto" | "edit" | undefined;
229
- readonly background?: "auto" | "opaque" | "transparent" | undefined;
230
232
  readonly input_fidelity?: "low" | "high" | undefined;
231
233
  readonly partial_images?: number | undefined;
232
- readonly quality?: "low" | "medium" | "high" | "auto" | undefined;
233
- readonly size?: string | undefined;
234
234
  })[] | undefined;
235
235
  readonly temperature?: number | undefined;
236
236
  readonly tool_choice?: "required" | "none" | "auto" | import("effect/Schema").Struct.ReadonlySide<{
@@ -135,13 +135,13 @@ export declare const routes: (import("../route").Route<{
135
135
  } | {
136
136
  readonly type: "image_generation";
137
137
  readonly output_format?: "png" | "jpeg" | "webp" | undefined;
138
+ readonly size?: string | undefined;
139
+ readonly quality?: "low" | "medium" | "high" | "auto" | undefined;
140
+ readonly background?: "auto" | "opaque" | "transparent" | undefined;
138
141
  readonly output_compression?: number | undefined;
139
142
  readonly action?: "generate" | "auto" | "edit" | undefined;
140
- readonly background?: "auto" | "opaque" | "transparent" | undefined;
141
143
  readonly input_fidelity?: "low" | "high" | undefined;
142
144
  readonly partial_images?: number | undefined;
143
- readonly quality?: "low" | "medium" | "high" | "auto" | undefined;
144
- readonly size?: string | undefined;
145
145
  })[] | undefined;
146
146
  readonly temperature?: number | undefined;
147
147
  readonly tool_choice?: "required" | "none" | "auto" | import("effect/Schema").Struct.ReadonlySide<{
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
- "version": "0.0.0-next-15910",
3
+ "version": "0.0.0-next-15911",
4
4
  "name": "@opencode-ai/ai",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -26,7 +26,7 @@
26
26
  "devDependencies": {
27
27
  "@clack/prompts": "1.0.0-alpha.1",
28
28
  "@effect/platform-node": "4.0.0-beta.98",
29
- "@opencode-ai/http-recorder": "0.0.0-next-15910",
29
+ "@opencode-ai/http-recorder": "0.0.0-next-15911",
30
30
  "@tsconfig/bun": "1.0.9",
31
31
  "@types/bun": "1.3.13",
32
32
  "@typescript/native-preview": "7.0.0-dev.20251207.1",
@@ -35,7 +35,7 @@
35
35
  "dependencies": {
36
36
  "@smithy/eventstream-codec": "4.2.14",
37
37
  "@smithy/util-utf8": "4.2.2",
38
- "@opencode-ai/schema": "0.0.0-next-15910",
38
+ "@opencode-ai/schema": "0.0.0-next-15911",
39
39
  "aws4fetch": "1.0.20",
40
40
  "effect": "4.0.0-beta.98",
41
41
  "google-auth-library": "10.5.0"