@opencode/ai 2.0.14 → 2.0.16

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 (208) hide show
  1. package/README.md +399 -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 +83 -0
  9. package/dist/generation.js +113 -0
  10. package/dist/image-client.d.ts +16 -7
  11. package/dist/image-client.js +29 -13
  12. package/dist/image.d.ts +1410 -81
  13. package/dist/image.js +97 -67
  14. package/dist/index.d.ts +17 -2
  15. package/dist/index.js +12 -1
  16. package/dist/llm.d.ts +9 -1
  17. package/dist/media-model.d.ts +44 -0
  18. package/dist/media-model.js +49 -0
  19. package/dist/media.d.ts +213 -0
  20. package/dist/media.js +227 -0
  21. package/dist/promise.d.ts +974 -0
  22. package/dist/promise.js +81 -0
  23. package/dist/protocols/alibaba-chat.d.ts +12 -0
  24. package/dist/protocols/alibaba-responses.d.ts +2 -2
  25. package/dist/protocols/anthropic-messages.js +8 -19
  26. package/dist/protocols/assemblyai-transcription.d.ts +40 -0
  27. package/dist/protocols/assemblyai-transcription.js +138 -0
  28. package/dist/protocols/bedrock-converse.d.ts +4 -4
  29. package/dist/protocols/bedrock-converse.js +6 -17
  30. package/dist/protocols/bfl-images.d.ts +32 -0
  31. package/dist/protocols/bfl-images.js +153 -0
  32. package/dist/protocols/cartesia-speech.d.ts +127 -0
  33. package/dist/protocols/cartesia-speech.js +126 -0
  34. package/dist/protocols/deepgram-speech.d.ts +119 -0
  35. package/dist/protocols/deepgram-speech.js +92 -0
  36. package/dist/protocols/deepgram-transcription.d.ts +25 -0
  37. package/dist/protocols/deepgram-transcription.js +129 -0
  38. package/dist/protocols/elevenlabs-speech.d.ts +122 -0
  39. package/dist/protocols/elevenlabs-speech.js +115 -0
  40. package/dist/protocols/fal-images.d.ts +24 -0
  41. package/dist/protocols/fal-images.js +114 -0
  42. package/dist/protocols/fal-video.d.ts +29 -0
  43. package/dist/protocols/fal-video.js +88 -0
  44. package/dist/protocols/gemini.d.ts +30 -9
  45. package/dist/protocols/gemini.js +45 -35
  46. package/dist/protocols/google-images.d.ts +9 -21
  47. package/dist/protocols/google-images.js +158 -133
  48. package/dist/protocols/google-speech.d.ts +130 -0
  49. package/dist/protocols/google-speech.js +84 -0
  50. package/dist/protocols/google-transcription.d.ts +173 -0
  51. package/dist/protocols/google-transcription.js +138 -0
  52. package/dist/protocols/google-video.d.ts +26 -0
  53. package/dist/protocols/google-video.js +158 -0
  54. package/dist/protocols/meta-images.d.ts +7 -12
  55. package/dist/protocols/meta-images.js +85 -66
  56. package/dist/protocols/meta-responses.d.ts +4 -4
  57. package/dist/protocols/meta-responses.js +1 -1
  58. package/dist/protocols/mistral-chat.js +7 -6
  59. package/dist/protocols/open-responses.d.ts +17 -9
  60. package/dist/protocols/open-responses.js +24 -14
  61. package/dist/protocols/openai-chat.d.ts +118 -1
  62. package/dist/protocols/openai-chat.js +125 -44
  63. package/dist/protocols/openai-compatible-chat.d.ts +12 -0
  64. package/dist/protocols/openai-compatible-responses.d.ts +2 -2
  65. package/dist/protocols/openai-images.d.ts +128 -18
  66. package/dist/protocols/openai-images.js +177 -154
  67. package/dist/protocols/openai-responses.d.ts +15 -15
  68. package/dist/protocols/openai-responses.js +5 -6
  69. package/dist/protocols/openai-speech.d.ts +116 -0
  70. package/dist/protocols/openai-speech.js +98 -0
  71. package/dist/protocols/openai-transcription.d.ts +207 -0
  72. package/dist/protocols/openai-transcription.js +190 -0
  73. package/dist/protocols/replicate-images.d.ts +28 -0
  74. package/dist/protocols/replicate-images.js +133 -0
  75. package/dist/protocols/runway-video.d.ts +38 -0
  76. package/dist/protocols/runway-video.js +146 -0
  77. package/dist/protocols/shared.d.ts +27 -17
  78. package/dist/protocols/shared.js +52 -35
  79. package/dist/protocols/stability-images.d.ts +38 -0
  80. package/dist/protocols/stability-images.js +148 -0
  81. package/dist/protocols/utils/bedrock-media.d.ts +2 -3
  82. package/dist/protocols/utils/bedrock-media.js +4 -4
  83. package/dist/protocols/utils/fal-queue.d.ts +28 -0
  84. package/dist/protocols/utils/fal-queue.js +69 -0
  85. package/dist/protocols/utils/gemini-generate-content.d.ts +65 -0
  86. package/dist/protocols/utils/gemini-generate-content.js +65 -0
  87. package/dist/protocols/utils/gemini-json-schema.d.ts +3 -0
  88. package/dist/protocols/utils/gemini-json-schema.js +76 -0
  89. package/dist/protocols/utils/media-input.d.ts +18 -0
  90. package/dist/protocols/utils/media-input.js +35 -0
  91. package/dist/protocols/utils/responses-compaction.js +6 -5
  92. package/dist/protocols/utils/speech-stream.d.ts +49 -0
  93. package/dist/protocols/utils/speech-stream.js +67 -0
  94. package/dist/protocols/utils/tool-schema.d.ts +2 -2
  95. package/dist/protocols/utils/tool-schema.js +40 -17
  96. package/dist/protocols/utils/tool-stream.d.ts +27 -3
  97. package/dist/protocols/xai-images.d.ts +9 -15
  98. package/dist/protocols/xai-images.js +75 -84
  99. package/dist/protocols/xai-responses.d.ts +2 -2
  100. package/dist/protocols/xai-video.d.ts +34 -0
  101. package/dist/protocols/xai-video.js +147 -0
  102. package/dist/protocols/zai-chat.d.ts +13 -1
  103. package/dist/protocols/zai-images.d.ts +9 -13
  104. package/dist/protocols/zai-images.js +59 -57
  105. package/dist/provider-error.js +3 -0
  106. package/dist/providers/alibaba.d.ts +14 -2
  107. package/dist/providers/amazon-bedrock-mantle.d.ts +14 -2
  108. package/dist/providers/amazon-bedrock.d.ts +2 -2
  109. package/dist/providers/assemblyai.d.ts +25 -0
  110. package/dist/providers/assemblyai.js +29 -0
  111. package/dist/providers/azure.d.ts +18 -6
  112. package/dist/providers/baseten.d.ts +24 -0
  113. package/dist/providers/black-forest-labs.d.ts +25 -0
  114. package/dist/providers/black-forest-labs.js +28 -0
  115. package/dist/providers/cartesia.d.ts +24 -0
  116. package/dist/providers/cartesia.js +22 -0
  117. package/dist/providers/cerebras.d.ts +24 -0
  118. package/dist/providers/cerebras.js +6 -1
  119. package/dist/providers/cloudflare-ai-gateway.d.ts +30 -6
  120. package/dist/providers/cloudflare-workers-ai.d.ts +24 -0
  121. package/dist/providers/deepgram.d.ts +29 -0
  122. package/dist/providers/deepgram.js +31 -0
  123. package/dist/providers/deepinfra.d.ts +24 -0
  124. package/dist/providers/deepinfra.js +6 -1
  125. package/dist/providers/deepseek.d.ts +24 -0
  126. package/dist/providers/elevenlabs.d.ts +24 -0
  127. package/dist/providers/elevenlabs.js +28 -0
  128. package/dist/providers/fal.d.ts +29 -0
  129. package/dist/providers/fal.js +33 -0
  130. package/dist/providers/fireworks.d.ts +24 -0
  131. package/dist/providers/google-vertex-chat.d.ts +12 -0
  132. package/dist/providers/google-vertex-responses.d.ts +2 -2
  133. package/dist/providers/google-vertex.d.ts +10 -3
  134. package/dist/providers/google.d.ts +25 -3
  135. package/dist/providers/google.js +11 -2
  136. package/dist/providers/groq.d.ts +24 -0
  137. package/dist/providers/index.d.ts +10 -0
  138. package/dist/providers/index.js +10 -0
  139. package/dist/providers/meta.d.ts +14 -2
  140. package/dist/providers/minimax.d.ts +14 -2
  141. package/dist/providers/moonshot.d.ts +14 -2
  142. package/dist/providers/moonshot.js +3 -3
  143. package/dist/providers/openai-compatible-responses.d.ts +2 -2
  144. package/dist/providers/openai-compatible.d.ts +12 -0
  145. package/dist/providers/openai.d.ts +25 -3
  146. package/dist/providers/openai.js +10 -1
  147. package/dist/providers/openrouter.d.ts +67 -0
  148. package/dist/providers/openrouter.js +13 -1
  149. package/dist/providers/replicate.d.ts +25 -0
  150. package/dist/providers/replicate.js +22 -0
  151. package/dist/providers/runway.d.ts +24 -0
  152. package/dist/providers/runway.js +22 -0
  153. package/dist/providers/stability.d.ts +28 -0
  154. package/dist/providers/stability.js +23 -0
  155. package/dist/providers/togetherai.d.ts +24 -0
  156. package/dist/providers/vercel-ai-gateway.d.ts +41 -0
  157. package/dist/providers/vercel-ai-gateway.js +85 -0
  158. package/dist/providers/xai.d.ts +17 -0
  159. package/dist/providers/xai.js +5 -2
  160. package/dist/providers/zai-coding-plan.d.ts +15 -3
  161. package/dist/providers/zai.d.ts +13 -1
  162. package/dist/route/auth.d.ts +4 -1
  163. package/dist/route/auth.js +6 -0
  164. package/dist/route/client.d.ts +9 -1
  165. package/dist/route/endpoint.d.ts +10 -10
  166. package/dist/route/executor-service.d.ts +12 -0
  167. package/dist/route/executor-service.js +3 -0
  168. package/dist/route/executor.d.ts +4 -9
  169. package/dist/route/executor.js +3 -3
  170. package/dist/route/framing.d.ts +5 -1
  171. package/dist/route/framing.js +9 -0
  172. package/dist/route/index.d.ts +2 -0
  173. package/dist/route/index.js +2 -0
  174. package/dist/route/media-protocol.d.ts +158 -0
  175. package/dist/route/media-protocol.js +96 -0
  176. package/dist/route/media.d.ts +97 -0
  177. package/dist/route/media.js +236 -0
  178. package/dist/schema/errors.d.ts +13 -3
  179. package/dist/schema/errors.js +7 -0
  180. package/dist/schema/events.d.ts +557 -40
  181. package/dist/schema/events.js +35 -2
  182. package/dist/schema/messages.d.ts +95 -8
  183. package/dist/schema/messages.js +8 -6
  184. package/dist/schema/options.d.ts +6 -3
  185. package/dist/schema/options.js +6 -2
  186. package/dist/speech-client.d.ts +21 -0
  187. package/dist/speech-client.js +25 -0
  188. package/dist/speech.d.ts +1150 -0
  189. package/dist/speech.js +119 -0
  190. package/dist/testing.d.ts +72 -8
  191. package/dist/transcription-client.d.ts +28 -0
  192. package/dist/transcription-client.js +44 -0
  193. package/dist/transcription.d.ts +1504 -0
  194. package/dist/transcription.js +133 -0
  195. package/dist/utils/bytes.d.ts +1 -0
  196. package/dist/utils/bytes.js +10 -0
  197. package/dist/utils/media-type.d.ts +7 -0
  198. package/dist/utils/media-type.js +70 -0
  199. package/dist/utils/sanitize.js +3 -1
  200. package/dist/video-client.d.ts +28 -0
  201. package/dist/video-client.js +40 -0
  202. package/dist/video.d.ts +1359 -0
  203. package/dist/video.js +119 -0
  204. package/package.json +7 -3
  205. package/dist/protocols/utils/gemini-tool-schema.d.ts +0 -2
  206. package/dist/protocols/utils/gemini-tool-schema.js +0 -103
  207. package/dist/protocols/utils/image-input.d.ts +0 -21
  208. package/dist/protocols/utils/image-input.js +0 -20
@@ -1,32 +1,142 @@
1
- import { ImageModel, type ImageInput } from "../image.js";
2
- import { type Definition as AuthDefinition } from "../route/auth.js";
3
- import { type HttpOptions } from "../schema/index.js";
1
+ import { ImageModel, type ImageRequestFor } from "../image.js";
2
+ import { Media } from "../media.js";
3
+ import { MediaProtocol } from "../route/media-protocol.js";
4
+ import { MediaRoute } from "../route/media.js";
5
+ import { type MediaUsage } from "../schema/index.js";
4
6
  export declare const DEFAULT_BASE_URL = "https://api.openai.com/v1";
5
7
  export declare const PATH = "/images/generations";
6
8
  export declare const EDIT_PATH = "/images/edits";
7
9
  export type OpenAIImageString<Known extends string> = Known | (string & {});
10
+ /** Provider-native options. Common fields (`n`, `size`, `format`, `images`, `mask`) live on the request. */
8
11
  export type OpenAIImageOptions = {
9
- readonly mask?: ImageInput;
10
- readonly n?: number;
11
- readonly size?: OpenAIImageString<"auto" | "256x256" | "512x512" | "1024x1024" | "1536x1024" | "1024x1536" | "1792x1024" | "1024x1792">;
12
12
  readonly quality?: OpenAIImageString<"auto" | "low" | "medium" | "high" | "standard" | "hd">;
13
13
  readonly background?: OpenAIImageString<"auto" | "opaque" | "transparent">;
14
14
  readonly moderation?: OpenAIImageString<"auto" | "low">;
15
- readonly outputFormat?: OpenAIImageString<"png" | "jpeg" | "webp">;
16
15
  readonly outputCompression?: number;
16
+ /** Previews sent before the final image when streaming (default 2); ignored by `Image.generate`. */
17
+ readonly partialImages?: number;
17
18
  } & Record<string, unknown>;
18
- export type OpenAIImageBody = Record<string, unknown> & {
19
- readonly model: string;
20
- readonly prompt: string;
19
+ export type Request = ImageRequestFor<OpenAIImageOptions>;
20
+ /** `generate` reads the whole JSON response as one frame, with the requested format for responses that omit it. */
21
+ type Frame = string | {
22
+ readonly document: string;
23
+ readonly requested: string | undefined;
21
24
  };
22
- export interface ModelInput {
23
- readonly id: string;
24
- readonly auth: AuthDefinition;
25
- readonly baseURL?: string;
26
- readonly headers?: Record<string, string>;
27
- readonly http?: HttpOptions;
25
+ interface State {
26
+ readonly completed: number;
27
+ readonly format?: string;
28
+ readonly usage?: MediaUsage;
28
29
  }
29
- export declare const model: (input: ModelInput) => ImageModel<OpenAIImageOptions>;
30
+ export declare const protocol: MediaProtocol.Streamed<Request, {
31
+ readonly id: string;
32
+ readonly type: "generation-queued";
33
+ readonly position?: number | undefined;
34
+ } | {
35
+ readonly id: string;
36
+ readonly type: "generation-progress";
37
+ readonly progress?: number | undefined;
38
+ } | {
39
+ readonly type: "image-partial";
40
+ readonly index: number;
41
+ readonly image: Media.Asset;
42
+ } | {
43
+ readonly type: "image";
44
+ readonly index: number;
45
+ readonly image: Media.Asset;
46
+ } | {
47
+ readonly type: "finish";
48
+ readonly providerMetadata?: {
49
+ readonly [x: string]: {
50
+ readonly [x: string]: unknown;
51
+ };
52
+ } | undefined;
53
+ readonly usage?: {
54
+ readonly type: "tokens";
55
+ readonly input?: number | undefined;
56
+ readonly output?: number | undefined;
57
+ readonly total?: number | undefined;
58
+ readonly details?: {
59
+ readonly [x: string]: unknown;
60
+ } | undefined;
61
+ } | {
62
+ readonly type: "seconds";
63
+ readonly seconds: number;
64
+ } | {
65
+ readonly type: "characters";
66
+ readonly characters: number;
67
+ } | {
68
+ readonly type: "credits";
69
+ readonly credits: number;
70
+ } | {
71
+ readonly type: "compute";
72
+ readonly seconds: number;
73
+ } | undefined;
74
+ readonly notices?: readonly {
75
+ readonly type: "other" | "moderated" | "filtered";
76
+ readonly message: string;
77
+ readonly providerMetadata?: {
78
+ readonly [x: string]: {
79
+ readonly [x: string]: unknown;
80
+ };
81
+ } | undefined;
82
+ }[] | undefined;
83
+ }, Frame, State>;
84
+ export declare const model: (input: MediaRoute.ModelInput) => ImageModel<OpenAIImageOptions>;
30
85
  export declare const OpenAIImages: {
31
- readonly model: (input: ModelInput) => ImageModel<OpenAIImageOptions>;
86
+ readonly protocol: MediaProtocol.Streamed<Request, {
87
+ readonly id: string;
88
+ readonly type: "generation-queued";
89
+ readonly position?: number | undefined;
90
+ } | {
91
+ readonly id: string;
92
+ readonly type: "generation-progress";
93
+ readonly progress?: number | undefined;
94
+ } | {
95
+ readonly type: "image-partial";
96
+ readonly index: number;
97
+ readonly image: Media.Asset;
98
+ } | {
99
+ readonly type: "image";
100
+ readonly index: number;
101
+ readonly image: Media.Asset;
102
+ } | {
103
+ readonly type: "finish";
104
+ readonly providerMetadata?: {
105
+ readonly [x: string]: {
106
+ readonly [x: string]: unknown;
107
+ };
108
+ } | undefined;
109
+ readonly usage?: {
110
+ readonly type: "tokens";
111
+ readonly input?: number | undefined;
112
+ readonly output?: number | undefined;
113
+ readonly total?: number | undefined;
114
+ readonly details?: {
115
+ readonly [x: string]: unknown;
116
+ } | undefined;
117
+ } | {
118
+ readonly type: "seconds";
119
+ readonly seconds: number;
120
+ } | {
121
+ readonly type: "characters";
122
+ readonly characters: number;
123
+ } | {
124
+ readonly type: "credits";
125
+ readonly credits: number;
126
+ } | {
127
+ readonly type: "compute";
128
+ readonly seconds: number;
129
+ } | undefined;
130
+ readonly notices?: readonly {
131
+ readonly type: "other" | "moderated" | "filtered";
132
+ readonly message: string;
133
+ readonly providerMetadata?: {
134
+ readonly [x: string]: {
135
+ readonly [x: string]: unknown;
136
+ };
137
+ } | undefined;
138
+ }[] | undefined;
139
+ }, Frame, State>;
140
+ readonly model: (input: MediaRoute.ModelInput) => ImageModel<OpenAIImageOptions>;
32
141
  };
142
+ export {};
@@ -1,15 +1,28 @@
1
- import { Effect, Encoding, Schema } from "effect";
2
- import { Headers, HttpClientRequest, HttpClientResponse } from "effect/unstable/http";
3
- import { ImageModel, GeneratedImage, ImageResponse, } from "../image.js";
4
- import { Auth } from "../route/auth.js";
5
- import { Usage, mergeHttpOptions, mergeJsonRecords } from "../schema/index.js";
1
+ import { Effect, Schema, Stream } from "effect";
2
+ import { ImageFinishEvent, ImageModel, ImageOutputEvent, ImagePartialEvent, } from "../image.js";
3
+ import { Media } from "../media.js";
4
+ import { Framing } from "../route/framing.js";
5
+ import { MediaProtocol } from "../route/media-protocol.js";
6
+ import { MediaRoute } from "../route/media.js";
7
+ import { ProviderID, mergeJsonRecords } from "../schema/index.js";
6
8
  import { ProviderShared } from "./shared.js";
7
- import { ImageInputs } from "./utils/image-input.js";
8
- import { OpenAIImage } from "./utils/openai-image.js";
9
+ import { MediaInput } from "./utils/media-input.js";
9
10
  const ADAPTER = "openai-images";
11
+ const NAME = "OpenAI Images";
12
+ const PROVIDER = ProviderID.make("openai");
10
13
  export const DEFAULT_BASE_URL = "https://api.openai.com/v1";
11
14
  export const PATH = "/images/generations";
12
15
  export const EDIT_PATH = "/images/edits";
16
+ // ---------------------------------------------------------------------------
17
+ // 2. Response schema
18
+ // ---------------------------------------------------------------------------
19
+ const Usage = Schema.Struct({
20
+ input_tokens: Schema.optional(Schema.Number),
21
+ output_tokens: Schema.optional(Schema.Number),
22
+ total_tokens: Schema.optional(Schema.Number),
23
+ input_tokens_details: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
24
+ output_tokens_details: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
25
+ });
13
26
  const OpenAIImageResponse = Schema.Struct({
14
27
  data: Schema.Array(Schema.Struct({
15
28
  b64_json: Schema.optional(Schema.String),
@@ -17,167 +30,177 @@ const OpenAIImageResponse = Schema.Struct({
17
30
  revised_prompt: Schema.optional(Schema.String),
18
31
  })),
19
32
  output_format: Schema.optional(Schema.String),
20
- usage: Schema.optional(Schema.Struct({
21
- input_tokens: Schema.optional(Schema.Number),
22
- output_tokens: Schema.optional(Schema.Number),
23
- total_tokens: Schema.optional(Schema.Number),
24
- input_tokens_details: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
25
- output_tokens_details: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
26
- })),
33
+ usage: Schema.optional(Usage),
27
34
  });
35
+ // ---------------------------------------------------------------------------
36
+ // 3. Streaming event schema
37
+ // ---------------------------------------------------------------------------
38
+ const StreamEvent = Schema.Union([
39
+ Schema.Struct({
40
+ type: Schema.Literals(["image_generation.partial_image", "image_edit.partial_image"]),
41
+ b64_json: Schema.String,
42
+ partial_image_index: Schema.Number,
43
+ output_format: Schema.String,
44
+ }),
45
+ Schema.Struct({
46
+ type: Schema.Literals(["image_generation.completed", "image_edit.completed"]),
47
+ b64_json: Schema.String,
48
+ output_format: Schema.String,
49
+ usage: Schema.optional(Usage),
50
+ }),
51
+ ]);
52
+ const decodeEvent = MediaProtocol.decodeFrame(ADAPTER, NAME, StreamEvent);
53
+ const decodeDocument = Schema.decodeUnknownEffect(Schema.fromJsonString(OpenAIImageResponse));
54
+ // ---------------------------------------------------------------------------
55
+ // 5. Request body construction
56
+ // ---------------------------------------------------------------------------
57
+ /** Multipart field names the route owns; `http.body` overlays cannot smuggle replacements for them. */
58
+ const RESERVED_FORM_FIELDS = new Set(["model", "prompt", "image", "image[]", "images", "mask"]);
28
59
  const nativeOptions = (options) => {
29
60
  if (!options)
30
61
  return undefined;
31
- const { mask: _, outputFormat, outputCompression, ...native } = options;
32
- return {
33
- output_format: outputFormat,
34
- output_compression: outputCompression,
35
- ...native,
36
- };
62
+ const { outputCompression, partialImages: _, ...native } = options;
63
+ return { output_compression: outputCompression, ...native };
37
64
  };
38
- const applyQuery = (url, query) => {
39
- if (!query)
40
- return url;
41
- const next = new URL(url);
42
- Object.entries(query).forEach(([key, value]) => next.searchParams.set(key, value));
43
- return next.toString();
65
+ const streamOptions = (request) => {
66
+ if (request.mode !== "stream")
67
+ return Effect.succeed(undefined);
68
+ if (request.model.id.startsWith("dall-e"))
69
+ return Effect.fail(ProviderShared.unsupportedOperation({
70
+ operation: "media.stream",
71
+ provider: PROVIDER,
72
+ route: ADAPTER,
73
+ message: `${request.model.id} does not stream; use Image.generate or a GPT image model`,
74
+ }));
75
+ if (request.n !== undefined && request.n > 1)
76
+ return Effect.fail(ProviderShared.unsupportedOperation({
77
+ operation: "media.n",
78
+ provider: PROVIDER,
79
+ route: ADAPTER,
80
+ message: `${NAME} streams one image; use Image.generate for n=${request.n}`,
81
+ }));
82
+ return Effect.succeed({ stream: true, partial_images: request.providerOptions?.partialImages ?? 2 });
44
83
  };
45
- export const model = (input) => {
46
- const route = {
47
- id: ADAPTER,
48
- generate: Effect.fn("OpenAIImages.generate")(function* (request, execute) {
49
- const mask = request.options?.mask;
50
- if (mask !== undefined && (request.images?.length ?? 0) === 0)
51
- return yield* ImageInputs.invalid("An OpenAI image mask requires at least one input image");
52
- const http = mergeHttpOptions(request.model.http, request.http);
53
- const sourceImages = request.images ?? [];
54
- const multipartImages = yield* Effect.forEach(sourceImages, (image) => {
55
- if (image.type === "bytes")
56
- return Effect.succeed({ data: image.data, mediaType: image.mediaType });
57
- if (image.type === "url")
58
- return ImageInputs.decodeDataUrl(image.url);
59
- return Effect.undefined;
60
- });
61
- const multipartMask = mask === undefined
62
- ? undefined
63
- : mask.type === "bytes"
64
- ? { data: mask.data, mediaType: mask.mediaType }
65
- : mask.type === "url"
66
- ? yield* ImageInputs.decodeDataUrl(mask.url)
67
- : undefined;
68
- const useMultipart = sourceImages.length > 0 &&
69
- multipartImages.every((image) => image !== undefined) &&
70
- (mask === undefined || multipartMask !== undefined);
71
- const path = sourceImages.length === 0 ? PATH : EDIT_PATH;
72
- const url = applyQuery(`${(input.baseURL ?? DEFAULT_BASE_URL).replace(/\/$/, "")}${path}`, http?.query);
73
- if (useMultipart) {
74
- const form = new FormData();
75
- form.append("model", request.model.id);
76
- form.append("prompt", request.prompt);
77
- Object.entries(mergeJsonRecords(nativeOptions(request.options), http?.body) ?? {}).forEach(([key, value]) => {
78
- if (["model", "prompt", "image", "image[]", "images", "mask"].includes(key))
79
- return;
80
- form.append(key, typeof value === "string" ? value : ProviderShared.encodeJson(value));
81
- });
82
- multipartImages.forEach((image, index) => {
83
- if (image === undefined)
84
- return;
85
- form.append("image[]", imageBlob(image.data, image.mediaType), `image-${index}`);
86
- });
87
- if (multipartMask !== undefined)
88
- form.append("mask", imageBlob(multipartMask.data, multipartMask.mediaType), "mask");
89
- const headers = yield* Auth.toEffect(input.auth)({
90
- request,
91
- method: "POST",
92
- url,
93
- body: "[multipart/form-data]",
94
- headers: Headers.remove(Headers.fromInput({ ...input.headers, ...http?.headers }), "content-type"),
95
- });
96
- const response = yield* execute(HttpClientRequest.post(url).pipe(HttpClientRequest.setHeaders(headers), HttpClientRequest.bodyFormData(form)));
97
- return yield* parseResponse(response, request.options, http?.body);
98
- }
99
- const references = sourceImages.map((image) => {
100
- if (image.type === "bytes")
101
- return { image_url: ImageInputs.dataUrl(image) };
102
- if (image.type === "url")
103
- return { image_url: image.url };
104
- if (image.type === "file-id")
105
- return { file_id: image.id };
106
- return undefined;
107
- });
108
- if (references.some((image) => image === undefined))
109
- return yield* ImageInputs.invalid("OpenAI Images accepts image URLs, data URLs, bytes, and file IDs");
110
- const maskReference = mask === undefined
111
- ? undefined
112
- : mask.type === "bytes"
113
- ? { image_url: ImageInputs.dataUrl(mask) }
114
- : mask.type === "url"
115
- ? { image_url: mask.url }
116
- : mask.type === "file-id"
117
- ? { file_id: mask.id }
118
- : undefined;
119
- if (mask !== undefined && maskReference === undefined)
120
- return yield* ImageInputs.invalid("OpenAI Images accepts masks as URLs, data URLs, bytes, or file IDs");
121
- const requestBody = mergeJsonRecords({
122
- model: request.model.id,
123
- prompt: request.prompt,
124
- images: references.length === 0 ? undefined : references,
125
- mask: maskReference,
126
- }, nativeOptions(request.options), http?.body);
127
- const text = ProviderShared.encodeJson(requestBody);
128
- const headers = yield* Auth.toEffect(input.auth)({
129
- request,
130
- method: "POST",
131
- url,
132
- body: text,
133
- headers: Headers.fromInput({ ...input.headers, ...http?.headers }),
134
- });
135
- const response = yield* execute(HttpClientRequest.post(url).pipe(HttpClientRequest.setHeaders(headers), HttpClientRequest.bodyText(text, "application/json")));
136
- return yield* parseResponse(response, request.options, http?.body);
137
- }),
138
- };
139
- return ImageModel.make({ id: input.id, provider: "openai", route, http: input.http });
84
+ const isEdit = (request) => (request.images?.length ?? 0) > 0;
85
+ const isInline = (asset) => asset.source.type === "bytes" || asset.source.type === "base64";
86
+ const reference = (asset) => ProviderShared.mediaReference(asset, PROVIDER, NAME).pipe(Effect.map((item) => (item.type === "ref" ? { file_id: item.value } : { image_url: item.value })));
87
+ const fromRequest = Effect.fn("OpenAIImages.fromRequest")(function* (request) {
88
+ const images = request.images ?? [];
89
+ const mask = request.mask;
90
+ if (mask !== undefined && images.length === 0)
91
+ return yield* ProviderShared.invalidRequest("An OpenAI image mask requires at least one input image");
92
+ const fields = mergeJsonRecords({ n: request.n, size: request.size, output_format: request.format, ...(yield* streamOptions(request)) }, nativeOptions(request.providerOptions), request.http?.body);
93
+ // Owned bytes go through multipart edits; remote URLs and file IDs use the JSON edits body instead.
94
+ if (images.length > 0 && images.every(isInline) && (mask === undefined || isInline(mask))) {
95
+ const form = new FormData();
96
+ form.append("model", request.model.id);
97
+ form.append("prompt", request.prompt);
98
+ Object.entries(fields ?? {}).forEach(([key, value]) => {
99
+ if (RESERVED_FORM_FIELDS.has(key))
100
+ return;
101
+ form.append(key, typeof value === "string" ? value : ProviderShared.encodeJson(value));
102
+ });
103
+ const uploads = yield* Effect.forEach(images, (image) => MediaInput.inlineBytes(ADAPTER, image));
104
+ uploads.forEach((data, index) => form.append("image[]", MediaInput.blob(data, images[index].mediaType), `image-${index}`));
105
+ if (mask !== undefined)
106
+ form.append("mask", MediaInput.blob(yield* MediaInput.inlineBytes(ADAPTER, mask), mask.mediaType), "mask");
107
+ return MediaProtocol.multipart(form);
108
+ }
109
+ const references = yield* Effect.forEach(images, reference);
110
+ const maskReference = mask === undefined ? undefined : yield* reference(mask);
111
+ return MediaProtocol.json(mergeJsonRecords({
112
+ model: request.model.id,
113
+ prompt: request.prompt,
114
+ images: references.length === 0 ? undefined : references,
115
+ mask: maskReference,
116
+ }, fields) ?? {});
117
+ });
118
+ // ---------------------------------------------------------------------------
119
+ // 6. Stream parsing
120
+ // ---------------------------------------------------------------------------
121
+ const requestedFormat = (body) => {
122
+ if (body.type === "binary")
123
+ return undefined;
124
+ const value = body.type === "json" ? body.value.output_format : body.value.get("output_format");
125
+ return typeof value === "string" ? value : undefined;
140
126
  };
141
- const parseResponse = Effect.fn("OpenAIImages.parseResponse")(function* (response, options, overlay) {
142
- const output = yield* ProviderShared.imageResponse(ADAPTER, "OpenAI Images", response);
143
- const decoded = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(OpenAIImageResponse))(output.body).pipe(Effect.mapError((cause) => output.invalid("OpenAI Images returned an invalid response", cause)));
144
- const requestBody = mergeJsonRecords(nativeOptions(options), overlay);
145
- const format = decoded.output_format ?? (typeof requestBody?.output_format === "string" ? requestBody.output_format : "png");
127
+ const usage = (value) => value === undefined
128
+ ? undefined
129
+ : {
130
+ type: "tokens",
131
+ input: value.input_tokens,
132
+ output: value.output_tokens,
133
+ total: value.total_tokens,
134
+ details: { openai: value },
135
+ };
136
+ const eventImage = (frame, label, data, format) => MediaInput.decodedAsset((message, cause) => MediaProtocol.frameError(ADAPTER, message, frame, cause), label, data, `image/${format}`, { info: { format } });
137
+ const onEvent = Effect.fn("OpenAIImages.onEvent")(function* (state, frame) {
138
+ const event = yield* decodeEvent(frame);
139
+ const format = event.output_format;
140
+ if ("partial_image_index" in event) {
141
+ const image = yield* eventImage(frame, `${NAME} partial image`, event.b64_json, format);
142
+ return [state, [ImagePartialEvent.make({ index: event.partial_image_index, image })]];
143
+ }
144
+ const image = yield* eventImage(frame, `${NAME} result ${state.completed}`, event.b64_json, format);
145
+ return [
146
+ { ...state, completed: state.completed + 1, format, usage: usage(event.usage) },
147
+ [ImageOutputEvent.make({ index: state.completed, image })],
148
+ ];
149
+ });
150
+ const onDocument = Effect.fn("OpenAIImages.onDocument")(function* (frame) {
151
+ const invalid = (message, cause) => MediaProtocol.frameError(ADAPTER, message, frame.document, cause);
152
+ const decoded = yield* decodeDocument(frame.document).pipe(Effect.mapError((cause) => invalid(`${NAME} returned an invalid response`, cause)));
153
+ const format = decoded.output_format ?? frame.requested ?? "png";
154
+ const mediaType = `image/${format}`;
146
155
  const images = yield* Effect.forEach(decoded.data, (item, index) => {
156
+ const providerMetadata = item.revised_prompt === undefined ? undefined : { openai: { revisedPrompt: item.revised_prompt } };
147
157
  if (item.b64_json)
148
- return Effect.fromResult(Encoding.decodeBase64(item.b64_json)).pipe(Effect.mapError((cause) => output.invalid(`OpenAI Images result ${index} contains invalid base64 data`, cause)), Effect.map((data) => new GeneratedImage({
149
- mediaType: `image/${format}`,
150
- data,
151
- providerMetadata: item.revised_prompt === undefined ? undefined : { openai: { revisedPrompt: item.revised_prompt } },
152
- })));
158
+ return MediaInput.decodedAsset(invalid, `${NAME} result ${index}`, item.b64_json, mediaType, {
159
+ info: { format },
160
+ providerMetadata,
161
+ });
153
162
  if (item.url)
154
- return Effect.succeed(new GeneratedImage({
155
- mediaType: `image/${format}`,
156
- data: item.url,
157
- providerMetadata: item.revised_prompt === undefined ? undefined : { openai: { revisedPrompt: item.revised_prompt } },
158
- }));
159
- return Effect.fail(output.invalid(`OpenAI Images result ${index} has neither image data nor a URL`));
163
+ return Effect.succeed(Media.url(item.url, { mediaType, info: { format }, providerMetadata }));
164
+ return Effect.fail(invalid(`${NAME} result ${index} has neither image data nor a URL`));
160
165
  });
161
166
  if (images.length === 0)
162
- return yield* output.invalid("OpenAI Images returned no images");
163
- return new ImageResponse({
164
- images,
165
- usage: decoded.usage === undefined
166
- ? undefined
167
- : new Usage({
168
- inputTokens: decoded.usage.input_tokens,
169
- outputTokens: decoded.usage.output_tokens,
170
- totalTokens: decoded.usage.total_tokens,
171
- providerMetadata: { openai: decoded.usage },
172
- }),
173
- providerMetadata: { openai: { outputFormat: format } },
174
- });
167
+ return yield* invalid(`${NAME} returned no images`);
168
+ const state = { completed: images.length, format, usage: usage(decoded.usage) };
169
+ return [state, images.map((image, index) => ImageOutputEvent.make({ index, image }))];
175
170
  });
176
- const imageBlob = (data, mediaType) => {
177
- const buffer = new ArrayBuffer(data.byteLength);
178
- new Uint8Array(buffer).set(data);
179
- return new Blob([buffer], { type: mediaType });
171
+ const step = (state, frame) => (typeof frame === "string" ? onEvent(state, frame) : onDocument(frame));
172
+ const finish = (state) => {
173
+ if (state.completed === 0)
174
+ return Effect.fail(MediaProtocol.incomplete(ADAPTER));
175
+ return Effect.succeed([
176
+ ImageFinishEvent.make({ usage: state.usage, providerMetadata: { openai: { outputFormat: state.format } } }),
177
+ ]);
180
178
  };
179
+ // ---------------------------------------------------------------------------
180
+ // 7. Protocol and route
181
+ // ---------------------------------------------------------------------------
182
+ export const protocol = MediaProtocol.stream({
183
+ id: ADAPTER,
184
+ name: NAME,
185
+ unsupported: ["aspectRatio", "seed"],
186
+ body: { from: fromRequest },
187
+ frames: (bytes, context) => context.request.mode === "stream"
188
+ ? Framing.sse.frame(bytes)
189
+ : Framing.document
190
+ .frame(bytes)
191
+ .pipe(Stream.map((document) => ({ document, requested: requestedFormat(context.body) }))),
192
+ initial: () => ({ completed: 0 }),
193
+ step,
194
+ finish,
195
+ });
196
+ export const model = (input) => ImageModel.fromRoute({
197
+ id: ADAPTER,
198
+ provider: PROVIDER,
199
+ protocol,
200
+ baseURL: DEFAULT_BASE_URL,
201
+ path: ({ request }) => (isEdit(request) ? EDIT_PATH : PATH),
202
+ }, input);
181
203
  export const OpenAIImages = {
204
+ protocol,
182
205
  model,
183
206
  };