@opencode/ai 0.0.0-dev-20061 → 0.0.0-dev-20065

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 (48) hide show
  1. package/README.md +56 -1
  2. package/dist/image-client.d.ts +14 -7
  3. package/dist/image-client.js +24 -8
  4. package/dist/image.d.ts +392 -12
  5. package/dist/image.js +41 -9
  6. package/dist/media-model.d.ts +3 -1
  7. package/dist/media-model.js +10 -0
  8. package/dist/promise.d.ts +33 -5
  9. package/dist/promise.js +4 -2
  10. package/dist/protocols/bfl-images.d.ts +32 -0
  11. package/dist/protocols/bfl-images.js +153 -0
  12. package/dist/protocols/fal-images.d.ts +24 -0
  13. package/dist/protocols/fal-images.js +114 -0
  14. package/dist/protocols/fal-video.d.ts +0 -10
  15. package/dist/protocols/fal-video.js +8 -59
  16. package/dist/protocols/openai-images.d.ts +124 -3
  17. package/dist/protocols/openai-images.js +102 -35
  18. package/dist/protocols/openai-responses.d.ts +5 -5
  19. package/dist/protocols/replicate-images.d.ts +28 -0
  20. package/dist/protocols/replicate-images.js +133 -0
  21. package/dist/protocols/stability-images.d.ts +38 -0
  22. package/dist/protocols/stability-images.js +148 -0
  23. package/dist/protocols/utils/fal-queue.d.ts +28 -0
  24. package/dist/protocols/utils/fal-queue.js +69 -0
  25. package/dist/protocols/utils/media-input.d.ts +4 -0
  26. package/dist/protocols/utils/media-input.js +4 -0
  27. package/dist/provider-error.js +3 -0
  28. package/dist/providers/azure.d.ts +2 -2
  29. package/dist/providers/black-forest-labs.d.ts +25 -0
  30. package/dist/providers/black-forest-labs.js +28 -0
  31. package/dist/providers/cloudflare-ai-gateway.d.ts +2 -2
  32. package/dist/providers/fal.d.ts +5 -0
  33. package/dist/providers/fal.js +8 -4
  34. package/dist/providers/index.d.ts +3 -0
  35. package/dist/providers/index.js +3 -0
  36. package/dist/providers/openai.d.ts +1 -1
  37. package/dist/providers/replicate.d.ts +25 -0
  38. package/dist/providers/replicate.js +22 -0
  39. package/dist/providers/stability.d.ts +28 -0
  40. package/dist/providers/stability.js +23 -0
  41. package/dist/route/media-protocol.d.ts +3 -1
  42. package/dist/route/media.d.ts +13 -2
  43. package/dist/route/media.js +37 -2
  44. package/dist/transcription-client.d.ts +1 -1
  45. package/dist/transcription-client.js +10 -35
  46. package/dist/transcription.d.ts +2 -2
  47. package/dist/transcription.js +2 -9
  48. package/package.json +3 -3
@@ -1,6 +1,8 @@
1
- import { ImageModel, ImageResponse, type ImageRequestFor } from "../image.js";
1
+ import { ImageModel, type ImageRequestFor } from "../image.js";
2
+ import { Media } from "../media.js";
2
3
  import { MediaProtocol } from "../route/media-protocol.js";
3
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";
@@ -11,11 +13,130 @@ export type OpenAIImageOptions = {
11
13
  readonly background?: OpenAIImageString<"auto" | "opaque" | "transparent">;
12
14
  readonly moderation?: OpenAIImageString<"auto" | "low">;
13
15
  readonly outputCompression?: number;
16
+ /** Previews sent before the final image when streaming (default 2); ignored by `Image.generate`. */
17
+ readonly partialImages?: number;
14
18
  } & Record<string, unknown>;
15
19
  export type Request = ImageRequestFor<OpenAIImageOptions>;
16
- export declare const protocol: MediaProtocol.Inline<Request, ImageResponse>;
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;
24
+ };
25
+ interface State {
26
+ readonly completed: number;
27
+ readonly format?: string;
28
+ readonly usage?: MediaUsage;
29
+ }
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>;
17
84
  export declare const model: (input: MediaRoute.ModelInput) => ImageModel<OpenAIImageOptions>;
18
85
  export declare const OpenAIImages: {
19
- readonly protocol: MediaProtocol.Inline<Request, ImageResponse>;
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>;
20
140
  readonly model: (input: MediaRoute.ModelInput) => ImageModel<OpenAIImageOptions>;
21
141
  };
142
+ export {};
@@ -1,6 +1,7 @@
1
- import { Effect, Schema } from "effect";
2
- import { ImageModel, ImageResponse } from "../image.js";
1
+ import { Effect, Schema, Stream } from "effect";
2
+ import { ImageFinishEvent, ImageModel, ImageOutputEvent, ImagePartialEvent, } from "../image.js";
3
3
  import { Media } from "../media.js";
4
+ import { Framing } from "../route/framing.js";
4
5
  import { MediaProtocol } from "../route/media-protocol.js";
5
6
  import { MediaRoute } from "../route/media.js";
6
7
  import { ProviderID, mergeJsonRecords } from "../schema/index.js";
@@ -15,6 +16,13 @@ export const EDIT_PATH = "/images/edits";
15
16
  // ---------------------------------------------------------------------------
16
17
  // 2. Response schema
17
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
+ });
18
26
  const OpenAIImageResponse = Schema.Struct({
19
27
  data: Schema.Array(Schema.Struct({
20
28
  b64_json: Schema.optional(Schema.String),
@@ -22,15 +30,28 @@ const OpenAIImageResponse = Schema.Struct({
22
30
  revised_prompt: Schema.optional(Schema.String),
23
31
  })),
24
32
  output_format: Schema.optional(Schema.String),
25
- usage: Schema.optional(Schema.Struct({
26
- input_tokens: Schema.optional(Schema.Number),
27
- output_tokens: Schema.optional(Schema.Number),
28
- total_tokens: Schema.optional(Schema.Number),
29
- input_tokens_details: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
30
- output_tokens_details: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
31
- })),
33
+ usage: Schema.optional(Usage),
32
34
  });
33
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
+ // ---------------------------------------------------------------------------
34
55
  // 5. Request body construction
35
56
  // ---------------------------------------------------------------------------
36
57
  /** Multipart field names the route owns; `http.body` overlays cannot smuggle replacements for them. */
@@ -38,18 +59,37 @@ const RESERVED_FORM_FIELDS = new Set(["model", "prompt", "image", "image[]", "im
38
59
  const nativeOptions = (options) => {
39
60
  if (!options)
40
61
  return undefined;
41
- const { outputCompression, ...native } = options;
62
+ const { outputCompression, partialImages: _, ...native } = options;
42
63
  return { output_compression: outputCompression, ...native };
43
64
  };
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 });
83
+ };
44
84
  const isEdit = (request) => (request.images?.length ?? 0) > 0;
45
- const isInline = (asset) => asset.inline() !== undefined;
85
+ const isInline = (asset) => asset.source.type === "bytes" || asset.source.type === "base64";
46
86
  const reference = (asset) => ProviderShared.mediaReference(asset, PROVIDER, NAME).pipe(Effect.map((item) => (item.type === "ref" ? { file_id: item.value } : { image_url: item.value })));
47
87
  const fromRequest = Effect.fn("OpenAIImages.fromRequest")(function* (request) {
48
88
  const images = request.images ?? [];
49
89
  const mask = request.mask;
50
90
  if (mask !== undefined && images.length === 0)
51
91
  return yield* ProviderShared.invalidRequest("An OpenAI image mask requires at least one input image");
52
- const fields = mergeJsonRecords({ n: request.n, size: request.size, output_format: request.format }, nativeOptions(request.providerOptions), request.http?.body);
92
+ const fields = mergeJsonRecords({ n: request.n, size: request.size, output_format: request.format, ...(yield* streamOptions(request)) }, nativeOptions(request.providerOptions), request.http?.body);
53
93
  // Owned bytes go through multipart edits; remote URLs and file IDs use the JSON edits body instead.
54
94
  if (images.length > 0 && images.every(isInline) && (mask === undefined || isInline(mask))) {
55
95
  const form = new FormData();
@@ -76,7 +116,7 @@ const fromRequest = Effect.fn("OpenAIImages.fromRequest")(function* (request) {
76
116
  }, fields) ?? {});
77
117
  });
78
118
  // ---------------------------------------------------------------------------
79
- // 6. Response decoding
119
+ // 6. Stream parsing
80
120
  // ---------------------------------------------------------------------------
81
121
  const requestedFormat = (body) => {
82
122
  if (body.type === "binary")
@@ -84,47 +124,74 @@ const requestedFormat = (body) => {
84
124
  const value = body.type === "json" ? body.value.output_format : body.value.get("output_format");
85
125
  return typeof value === "string" ? value : undefined;
86
126
  };
87
- const decodeResponse = Effect.fn("OpenAIImages.decodeResponse")(function* (response, context) {
88
- const output = yield* MediaProtocol.decodeJson(ADAPTER, NAME, OpenAIImageResponse)(response);
89
- const decoded = output.value;
90
- const format = decoded.output_format ?? requestedFormat(context.body) ?? "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";
91
154
  const mediaType = `image/${format}`;
92
155
  const images = yield* Effect.forEach(decoded.data, (item, index) => {
93
156
  const providerMetadata = item.revised_prompt === undefined ? undefined : { openai: { revisedPrompt: item.revised_prompt } };
94
157
  if (item.b64_json)
95
- return MediaInput.decodedAsset(output.invalid, `${NAME} result ${index}`, item.b64_json, mediaType, {
158
+ return MediaInput.decodedAsset(invalid, `${NAME} result ${index}`, item.b64_json, mediaType, {
96
159
  info: { format },
97
160
  providerMetadata,
98
161
  });
99
162
  if (item.url)
100
163
  return Effect.succeed(Media.url(item.url, { mediaType, info: { format }, providerMetadata }));
101
- return Effect.fail(output.invalid(`${NAME} result ${index} has neither image data nor a URL`));
164
+ return Effect.fail(invalid(`${NAME} result ${index} has neither image data nor a URL`));
102
165
  });
103
166
  if (images.length === 0)
104
- return yield* output.invalid(`${NAME} returned no images`);
105
- return new ImageResponse({
106
- images,
107
- usage: decoded.usage === undefined
108
- ? undefined
109
- : {
110
- type: "tokens",
111
- input: decoded.usage.input_tokens,
112
- output: decoded.usage.output_tokens,
113
- total: decoded.usage.total_tokens,
114
- details: { openai: decoded.usage },
115
- },
116
- providerMetadata: { openai: { outputFormat: format } },
117
- });
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 }))];
118
170
  });
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
+ ]);
178
+ };
119
179
  // ---------------------------------------------------------------------------
120
180
  // 7. Protocol and route
121
181
  // ---------------------------------------------------------------------------
122
- export const protocol = MediaProtocol.inline({
182
+ export const protocol = MediaProtocol.stream({
123
183
  id: ADAPTER,
124
184
  name: NAME,
125
185
  unsupported: ["aspectRatio", "seed"],
126
186
  body: { from: fromRequest },
127
- response: { decode: decodeResponse },
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,
128
195
  });
129
196
  export const model = (input) => ImageModel.fromRoute({
130
197
  id: ADAPTER,
@@ -390,11 +390,11 @@ export declare const protocol: Protocol<{
390
390
  readonly size?: string | undefined;
391
391
  readonly action?: "generate" | "auto" | "edit" | undefined;
392
392
  readonly output_format?: "png" | "jpeg" | "webp" | undefined;
393
+ readonly partial_images?: number | undefined;
393
394
  readonly quality?: "auto" | "low" | "medium" | "high" | undefined;
394
395
  readonly background?: "auto" | "opaque" | "transparent" | undefined;
395
396
  readonly output_compression?: number | undefined;
396
397
  readonly input_fidelity?: "low" | "high" | undefined;
397
- readonly partial_images?: number | undefined;
398
398
  } | {
399
399
  readonly type: "namespace";
400
400
  readonly name: string;
@@ -673,11 +673,11 @@ export declare const httpTransport: HttpTransport.HttpJsonTransport<{
673
673
  readonly size?: string | undefined;
674
674
  readonly action?: "generate" | "auto" | "edit" | undefined;
675
675
  readonly output_format?: "png" | "jpeg" | "webp" | undefined;
676
+ readonly partial_images?: number | undefined;
676
677
  readonly quality?: "auto" | "low" | "medium" | "high" | undefined;
677
678
  readonly background?: "auto" | "opaque" | "transparent" | undefined;
678
679
  readonly output_compression?: number | undefined;
679
680
  readonly input_fidelity?: "low" | "high" | undefined;
680
- readonly partial_images?: number | undefined;
681
681
  } | {
682
682
  readonly type: "namespace";
683
683
  readonly name: string;
@@ -901,11 +901,11 @@ export declare const channelTransport: (options: import("./open-responses-channe
901
901
  readonly size?: string | undefined;
902
902
  readonly action?: "generate" | "auto" | "edit" | undefined;
903
903
  readonly output_format?: "png" | "jpeg" | "webp" | undefined;
904
+ readonly partial_images?: number | undefined;
904
905
  readonly quality?: "auto" | "low" | "medium" | "high" | undefined;
905
906
  readonly background?: "auto" | "opaque" | "transparent" | undefined;
906
907
  readonly output_compression?: number | undefined;
907
908
  readonly input_fidelity?: "low" | "high" | undefined;
908
- readonly partial_images?: number | undefined;
909
909
  } | {
910
910
  readonly type: "namespace";
911
911
  readonly name: string;
@@ -1129,11 +1129,11 @@ export declare const transport: import("../route/transport/index.js").Transport<
1129
1129
  readonly size?: string | undefined;
1130
1130
  readonly action?: "generate" | "auto" | "edit" | undefined;
1131
1131
  readonly output_format?: "png" | "jpeg" | "webp" | undefined;
1132
+ readonly partial_images?: number | undefined;
1132
1133
  readonly quality?: "auto" | "low" | "medium" | "high" | undefined;
1133
1134
  readonly background?: "auto" | "opaque" | "transparent" | undefined;
1134
1135
  readonly output_compression?: number | undefined;
1135
1136
  readonly input_fidelity?: "low" | "high" | undefined;
1136
- readonly partial_images?: number | undefined;
1137
1137
  } | {
1138
1138
  readonly type: "namespace";
1139
1139
  readonly name: string;
@@ -1357,11 +1357,11 @@ export declare const route: Route<{
1357
1357
  readonly size?: string | undefined;
1358
1358
  readonly action?: "generate" | "auto" | "edit" | undefined;
1359
1359
  readonly output_format?: "png" | "jpeg" | "webp" | undefined;
1360
+ readonly partial_images?: number | undefined;
1360
1361
  readonly quality?: "auto" | "low" | "medium" | "high" | undefined;
1361
1362
  readonly background?: "auto" | "opaque" | "transparent" | undefined;
1362
1363
  readonly output_compression?: number | undefined;
1363
1364
  readonly input_fidelity?: "low" | "high" | undefined;
1364
- readonly partial_images?: number | undefined;
1365
1365
  } | {
1366
1366
  readonly type: "namespace";
1367
1367
  readonly name: string;
@@ -0,0 +1,28 @@
1
+ import { Schema } from "effect";
2
+ import { ImageModel, ImageResponse, type ImageRequestFor } from "../image.js";
3
+ import { MediaProtocol } from "../route/media-protocol.js";
4
+ import { MediaRoute } from "../route/media.js";
5
+ export declare const DEFAULT_BASE_URL = "https://api.replicate.com";
6
+ /** Model-defined `input` fields under the model's own names; `Media.Asset` values lower to URLs or data URLs. */
7
+ export type ReplicateImageOptions = Record<string, unknown>;
8
+ export type Request = ImageRequestFor<ReplicateImageOptions>;
9
+ export declare const Token: Schema.Struct<{
10
+ readonly id: Schema.String;
11
+ readonly getURL: Schema.String;
12
+ readonly cancelURL: Schema.String;
13
+ }>;
14
+ export type Token = Schema.Schema.Type<typeof Token>;
15
+ export declare const protocol: MediaProtocol.Queued<Request, ImageResponse, {
16
+ readonly id: string;
17
+ readonly getURL: string;
18
+ readonly cancelURL: string;
19
+ }>;
20
+ export declare const model: (input: MediaRoute.ModelInput) => ImageModel<ReplicateImageOptions>;
21
+ export declare const ReplicateImages: {
22
+ readonly protocol: MediaProtocol.Queued<Request, ImageResponse, {
23
+ readonly id: string;
24
+ readonly getURL: string;
25
+ readonly cancelURL: string;
26
+ }>;
27
+ readonly model: (input: MediaRoute.ModelInput) => ImageModel<ReplicateImageOptions>;
28
+ };
@@ -0,0 +1,133 @@
1
+ import { Duration, 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";
7
+ import { ProviderShared, optionalNull } from "./shared.js";
8
+ const ADAPTER = "replicate-images";
9
+ const NAME = "Replicate";
10
+ const PROVIDER = ProviderID.make("replicate");
11
+ export const DEFAULT_BASE_URL = "https://api.replicate.com";
12
+ const OUTPUT_RETENTION = Duration.hours(1);
13
+ const MAX_DATA_URL_BYTES = 256 * 1024;
14
+ // ---------------------------------------------------------------------------
15
+ // 2. Token and response schemas
16
+ // ---------------------------------------------------------------------------
17
+ export const Token = Schema.Struct({ id: Schema.String, getURL: Schema.String, cancelURL: Schema.String });
18
+ const Prediction = Schema.Struct({
19
+ id: Schema.String,
20
+ status: Schema.String,
21
+ output: optionalNull(Schema.Unknown),
22
+ error: optionalNull(Schema.Unknown),
23
+ data_removed: optionalNull(Schema.Boolean),
24
+ completed_at: optionalNull(Schema.String),
25
+ metrics: optionalNull(Schema.Struct({ predict_time: optionalNull(Schema.Number) })),
26
+ urls: Schema.Struct({ get: Schema.String, cancel: Schema.String }),
27
+ });
28
+ const Output = Schema.Union([Schema.String, Schema.Array(Schema.String)]);
29
+ const isOutput = Schema.is(Output);
30
+ const STATUS = {
31
+ starting: "queued",
32
+ processing: "running",
33
+ succeeded: "completed",
34
+ failed: "failed",
35
+ canceled: "cancelled",
36
+ // The prediction hit its `Cancel-After` deadline before it started running.
37
+ aborted: "expired",
38
+ };
39
+ // ---------------------------------------------------------------------------
40
+ // 5. Request body construction
41
+ // ---------------------------------------------------------------------------
42
+ // Official models run by `owner/name`; anything else (`owner/name:version` or a bare version id) is a pinned version.
43
+ const isOfficial = (model) => /^[^/:]+\/[^/:]+$/.test(model);
44
+ const inlineSize = (source) => {
45
+ if (source.type === "bytes")
46
+ return source.data.byteLength;
47
+ if (source.type === "base64")
48
+ return source.data.length * 0.75;
49
+ return 0;
50
+ };
51
+ const fileInput = (asset) => {
52
+ if (inlineSize(asset.source) > MAX_DATA_URL_BYTES)
53
+ return Effect.fail(ProviderShared.invalidRequest(`${NAME} data URL inputs are limited to 256 KB; pass a larger file by https URL`));
54
+ return ProviderShared.mediaReference(asset, undefined, NAME).pipe(Effect.map((reference) => reference.value));
55
+ };
56
+ const inputValue = (value) => {
57
+ if (value instanceof Media.Asset)
58
+ return fileInput(value);
59
+ if (Array.isArray(value) && value.some((item) => item instanceof Media.Asset))
60
+ return Effect.forEach(value, (item) => (item instanceof Media.Asset ? fileInput(item) : Effect.succeed(item)));
61
+ return Effect.succeed(value);
62
+ };
63
+ const fromRequest = Effect.fn("ReplicateImages.fromRequest")(function* (request) {
64
+ const native = yield* Effect.forEach(Object.entries(request.providerOptions ?? {}), ([key, value]) => inputValue(value).pipe(Effect.map((lowered) => [key, lowered])));
65
+ const input = mergeJsonRecords({ prompt: request.prompt }, Object.fromEntries(native));
66
+ const model = request.model.id;
67
+ return MediaProtocol.json(mergeJsonRecords(isOfficial(model) ? { input } : { version: model, input }, request.http?.body) ?? {});
68
+ });
69
+ // ---------------------------------------------------------------------------
70
+ // 6. Response decoding
71
+ // ---------------------------------------------------------------------------
72
+ const decodePrediction = MediaProtocol.decodeJson(ADAPTER, NAME, Prediction);
73
+ const decodeStart = Effect.fn("ReplicateImages.decodeStart")(function* (response) {
74
+ const output = yield* decodePrediction(response);
75
+ const prediction = output.value;
76
+ return {
77
+ token: { id: prediction.id, getURL: prediction.urls.get, cancelURL: prediction.urls.cancel },
78
+ // `Prefer: wait` can return an already-finished prediction, so `await` skips straight to the result.
79
+ snapshot: { id: prediction.id, status: yield* MediaProtocol.status(STATUS, prediction.status, output) },
80
+ };
81
+ });
82
+ const decodeStatus = Effect.fn("ReplicateImages.decodeStatus")(function* (response, context) {
83
+ const output = yield* decodePrediction(response);
84
+ return { id: context.token.id, status: yield* MediaProtocol.status(STATUS, output.value.status, output) };
85
+ });
86
+ const decodeResult = Effect.fn("ReplicateImages.decodeResult")(function* (response, context) {
87
+ const output = yield* decodePrediction(response);
88
+ const prediction = output.value;
89
+ const status = yield* MediaProtocol.status(STATUS, prediction.status, output);
90
+ if (status === "failed" || status === "cancelled" || status === "expired")
91
+ return yield* output.ended(status, `${NAME} prediction ${context.token.id} ${prediction.status}${typeof prediction.error === "string" ? `: ${prediction.error}` : ""}`);
92
+ if (status !== "completed")
93
+ return yield* output.invalid(`${NAME} prediction ${context.token.id} has not finished`);
94
+ if (prediction.data_removed === true)
95
+ return yield* output.ended("expired", `${NAME} removed the output of prediction ${context.token.id}`);
96
+ if (!isOutput(prediction.output))
97
+ return yield* output.invalid(`${NAME} prediction ${context.token.id} returned output that is not image URLs`);
98
+ const urls = typeof prediction.output === "string" ? [prediction.output] : prediction.output;
99
+ if (urls.length === 0)
100
+ return yield* output.invalid(`${NAME} prediction ${context.token.id} returned no images`);
101
+ const predictTime = prediction.metrics?.predict_time ?? undefined;
102
+ const completedAt = prediction.completed_at ?? undefined;
103
+ const expiresAt = completedAt === undefined ? undefined : Date.parse(completedAt) + Duration.toMillis(OUTPUT_RETENTION);
104
+ return new ImageResponse({
105
+ images: urls.map((url) => Media.url(url, { expiresAt })),
106
+ usage: predictTime === undefined ? undefined : { type: "compute", seconds: predictTime },
107
+ providerMetadata: { replicate: { predictionId: prediction.id } },
108
+ });
109
+ });
110
+ // ---------------------------------------------------------------------------
111
+ // 7. Protocol and route
112
+ // ---------------------------------------------------------------------------
113
+ export const protocol = MediaProtocol.queued({
114
+ id: ADAPTER,
115
+ name: NAME,
116
+ token: Token,
117
+ unsupported: ["images", "mask", "n", "size", "aspectRatio", "seed", "format"],
118
+ start: { body: { from: fromRequest }, decode: decodeStart },
119
+ status: { path: (token) => token.getURL, decode: decodeStatus },
120
+ result: { path: (token) => token.getURL, decode: decodeResult },
121
+ cancel: { method: "POST", path: (token) => token.cancelURL },
122
+ });
123
+ export const model = (input) => ImageModel.fromRoute({
124
+ id: ADAPTER,
125
+ provider: PROVIDER,
126
+ protocol,
127
+ baseURL: DEFAULT_BASE_URL,
128
+ path: ({ request }) => isOfficial(request.model.id) ? `/v1/models/${request.model.id}/predictions` : "/v1/predictions",
129
+ }, input);
130
+ export const ReplicateImages = {
131
+ protocol,
132
+ model,
133
+ };
@@ -0,0 +1,38 @@
1
+ import { Schema } from "effect";
2
+ import { ImageModel, ImageResponse, type ImageRequestFor } from "../image.js";
3
+ import { MediaProtocol } from "../route/media-protocol.js";
4
+ import { MediaRoute } from "../route/media.js";
5
+ export declare const DEFAULT_BASE_URL = "https://api.stability.ai";
6
+ export type StabilityStylePreset = "enhance" | "anime" | "photographic" | "digital-art" | "comic-book" | "fantasy-art" | "line-art" | "analog-film" | "neon-punk" | "isometric" | "low-poly" | "origami" | "modeling-compound" | "cinematic" | "3d-model" | "pixel-art" | "tile-texture" | (string & {});
7
+ export type StabilityImageOptions = {
8
+ readonly negative_prompt?: string;
9
+ readonly style_preset?: StabilityStylePreset;
10
+ readonly strength?: number;
11
+ readonly cfg_scale?: number;
12
+ } & Record<string, unknown>;
13
+ /** Creative upscale options; the one `images` source is the image to upscale. */
14
+ export type StabilityUpscaleOptions = {
15
+ readonly negative_prompt?: string;
16
+ readonly style_preset?: StabilityStylePreset;
17
+ readonly creativity?: number;
18
+ } & Record<string, unknown>;
19
+ export type Request = ImageRequestFor<StabilityImageOptions>;
20
+ export type UpscaleRequest = ImageRequestFor<StabilityUpscaleOptions>;
21
+ export declare const Token: Schema.Struct<{
22
+ readonly id: Schema.String;
23
+ }>;
24
+ export type Token = Schema.Schema.Type<typeof Token>;
25
+ export declare const protocol: MediaProtocol.Inline<Request, ImageResponse>;
26
+ export declare const upscaleProtocol: MediaProtocol.Queued<UpscaleRequest, ImageResponse, {
27
+ readonly id: string;
28
+ }>;
29
+ export declare const model: (input: MediaRoute.ModelInput) => ImageModel<StabilityImageOptions>;
30
+ export declare const upscaleModel: (input: Omit<MediaRoute.ModelInput, "id">) => ImageModel<StabilityUpscaleOptions>;
31
+ export declare const StabilityImages: {
32
+ readonly protocol: MediaProtocol.Inline<Request, ImageResponse>;
33
+ readonly upscaleProtocol: MediaProtocol.Queued<UpscaleRequest, ImageResponse, {
34
+ readonly id: string;
35
+ }>;
36
+ readonly model: (input: MediaRoute.ModelInput) => ImageModel<StabilityImageOptions>;
37
+ readonly upscaleModel: (input: Omit<MediaRoute.ModelInput, "id">) => ImageModel<StabilityUpscaleOptions>;
38
+ };