@opencode/ai 0.0.0-dev-20035 → 0.0.0-dev-20039

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 (53) hide show
  1. package/README.md +80 -0
  2. package/dist/generation.d.ts +23 -13
  3. package/dist/generation.js +42 -24
  4. package/dist/image-client.js +1 -1
  5. package/dist/image.d.ts +7 -36
  6. package/dist/image.js +8 -38
  7. package/dist/index.d.ts +5 -1
  8. package/dist/index.js +3 -0
  9. package/dist/media-model.d.ts +42 -0
  10. package/dist/media-model.js +39 -0
  11. package/dist/media.d.ts +10 -9
  12. package/dist/media.js +9 -10
  13. package/dist/promise.d.ts +126 -3
  14. package/dist/promise.js +18 -1
  15. package/dist/protocols/fal-video.d.ts +39 -0
  16. package/dist/protocols/fal-video.js +139 -0
  17. package/dist/protocols/google-video.d.ts +26 -0
  18. package/dist/protocols/google-video.js +158 -0
  19. package/dist/protocols/meta-images.js +2 -9
  20. package/dist/protocols/openai-images.js +1 -12
  21. package/dist/protocols/runway-video.d.ts +38 -0
  22. package/dist/protocols/runway-video.js +146 -0
  23. package/dist/protocols/shared.d.ts +10 -0
  24. package/dist/protocols/shared.js +17 -0
  25. package/dist/protocols/xai-images.js +1 -12
  26. package/dist/protocols/xai-video.d.ts +34 -0
  27. package/dist/protocols/xai-video.js +147 -0
  28. package/dist/protocols/zai-chat.d.ts +1 -1
  29. package/dist/providers/fal.d.ts +24 -0
  30. package/dist/providers/fal.js +29 -0
  31. package/dist/providers/google.d.ts +5 -0
  32. package/dist/providers/google.js +5 -2
  33. package/dist/providers/index.d.ts +2 -0
  34. package/dist/providers/index.js +2 -0
  35. package/dist/providers/runway.d.ts +24 -0
  36. package/dist/providers/runway.js +22 -0
  37. package/dist/providers/xai.d.ts +5 -0
  38. package/dist/providers/xai.js +5 -2
  39. package/dist/providers/zai-coding-plan.d.ts +1 -1
  40. package/dist/providers/zai.d.ts +1 -1
  41. package/dist/route/auth.d.ts +4 -1
  42. package/dist/route/auth.js +6 -0
  43. package/dist/route/media-protocol.d.ts +68 -2
  44. package/dist/route/media-protocol.js +32 -3
  45. package/dist/route/media.d.ts +28 -5
  46. package/dist/route/media.js +95 -14
  47. package/dist/schema/events.d.ts +0 -6
  48. package/dist/schema/messages.d.ts +0 -3
  49. package/dist/video-client.d.ts +28 -0
  50. package/dist/video-client.js +43 -0
  51. package/dist/video.d.ts +1373 -0
  52. package/dist/video.js +131 -0
  53. package/package.json +3 -3
package/dist/media.js CHANGED
@@ -27,8 +27,6 @@ const UrlSource = Schema.Struct({
27
27
  mediaType: Schema.optional(Schema.String),
28
28
  /** Epoch milliseconds after which the provider no longer serves the URL. */
29
29
  expiresAt: Schema.optional(Schema.Number),
30
- /** Headers required to fetch the URL, such as provider auth for Veo downloads. */
31
- headers: Schema.optional(Schema.Record(Schema.String, Schema.String)),
32
30
  });
33
31
  /** A provider-side handle: OpenAI `file_id`, Gemini file URI, `gs://`, `runway://`, or a prior generation id. */
34
32
  const RefSource = Schema.Struct({
@@ -40,9 +38,7 @@ const RefSource = Schema.Struct({
40
38
  export const Source = Schema.Union([BytesSource, Base64Source, UrlSource, RefSource])
41
39
  .pipe(Schema.toTaggedUnion("type"))
42
40
  .annotate({ identifier: "Media.Source" });
43
- // ---------------------------------------------------------------------------
44
- // Kind, Info, Notice
45
- // ---------------------------------------------------------------------------
41
+ export const AspectRatio = Schema.declare((value) => typeof value === "string" && /^\d+(?:\.\d+)?:\d+(?:\.\d+)?$/.test(value), { title: "Media.AspectRatio" });
46
42
  export const Kind = Schema.Literals(["image", "video", "audio", "document", "other"]);
47
43
  export const kindOf = (mediaType) => {
48
44
  const lower = mediaType.toLowerCase();
@@ -85,6 +81,8 @@ export class Asset {
85
81
  /** Epoch milliseconds after which a `url` source stops resolving. */
86
82
  expiresAt;
87
83
  providerMetadata;
84
+ /** Transient download credentials for `url` sources; see `Asset.Input.headers`. */
85
+ headers;
88
86
  // Derived payload forms are cached on the instance because every protocol lowering re-reads the same payload. The
89
87
  // cache is check-then-set (concurrent first reads of a `url` source may both download) and is never observable
90
88
  // through `source`, so round-tripping through `Media.from(asset.source)` stays lossless.
@@ -100,6 +98,7 @@ export class Asset {
100
98
  this.info = input.info;
101
99
  this.expiresAt = input.source.type === "url" ? input.source.expiresAt : undefined;
102
100
  this.providerMetadata = input.providerMetadata;
101
+ this.headers = input.source.type === "url" ? input.headers : undefined;
103
102
  }
104
103
  /** Inline payload without effects, for protocols that embed base64 or data URLs directly. */
105
104
  inline() {
@@ -122,7 +121,7 @@ export class Asset {
122
121
  return Effect.fail(invalid(`Cannot materialize provider ref ${source.provider}:${source.id}`));
123
122
  const decoded = source.type === "base64"
124
123
  ? Effect.fromResult(Encoding.decodeBase64(source.data)).pipe(Effect.mapError((cause) => invalid(`Media asset contains invalid base64 data`, cause)))
125
- : download(source);
124
+ : download(source, this.headers);
126
125
  return decoded.pipe(Effect.tap((data) => Effect.sync(() => (this.#bytes = data))));
127
126
  });
128
127
  }
@@ -177,9 +176,9 @@ export const AssetSchema = AssetEncoded.pipe(Schema.decodeTo(AssetInstance, {
177
176
  decode: SchemaGetter.transform((encoded) => new Asset(encoded)),
178
177
  encode: SchemaGetter.transform(encodeAsset),
179
178
  }));
180
- const download = Effect.fn("Media.download")(function* (source) {
179
+ const download = Effect.fn("Media.download")(function* (source, headers) {
181
180
  const executor = yield* Service;
182
- const response = yield* executor.execute(HttpClientRequest.get(source.url).pipe(HttpClientRequest.setHeaders(source.headers ?? {})));
181
+ const response = yield* executor.execute(HttpClientRequest.get(source.url).pipe(HttpClientRequest.setHeaders(headers ?? {})));
183
182
  const buffer = yield* response.arrayBuffer.pipe(Effect.mapError((cause) => new AIError({
184
183
  reason: new InvalidProviderOutputError({
185
184
  message: `Failed to read media from ${source.url}`,
@@ -193,8 +192,8 @@ export const from = (source, options) => new Asset({ ...options, source });
193
192
  export const bytes = (data, mediaType, options) => from({ type: "bytes", data, mediaType: mediaType ?? detectMediaType(data) ?? OCTET_STREAM }, options);
194
193
  export const base64 = (data, mediaType, options) => from({ type: "base64", data, mediaType }, options);
195
194
  export const url = (value, options) => {
196
- const { mediaType, expiresAt, headers, ...rest } = options ?? {};
197
- return from({ type: "url", url: value, mediaType, expiresAt, headers }, rest);
195
+ const { mediaType, expiresAt, ...rest } = options ?? {};
196
+ return from({ type: "url", url: value, mediaType, expiresAt }, rest);
198
197
  };
199
198
  export const ref = (provider, id, mediaType, options) => from({ type: "ref", provider: ProviderID.make(provider), id, mediaType }, options);
200
199
  const DATA_URL = /^data:([^;,]+)(?:;[^,]*)*;base64,(.*)$/s;
package/dist/promise.d.ts CHANGED
@@ -1,13 +1,16 @@
1
1
  import { Effect, Layer } from "effect";
2
+ import type { AwaitOptions, Snapshot } from "./generation.js";
2
3
  import { ImageModel, ImageRequest, type ImageRequestInput } from "./image.js";
3
4
  import { ImageClient } from "./image-client.js";
4
5
  import { LLMClient } from "./route/client.js";
5
6
  import { RequestExecutor } from "./route/executor.js";
6
7
  import { LanguageModel, LLMRequest } from "./schema/index.js";
7
8
  import type { RequestInput } from "./llm.js";
9
+ import { VideoModel, VideoRequest, type VideoOptions, type VideoRequestInput } from "./video.js";
10
+ import { VideoClient } from "./video-client.js";
8
11
  /**
9
- * Promise-first entrypoint for scripts and non-Effect callers. One `ManagedRuntime` hosts the LLM and image clients
10
- * over a request executor; every method runs the corresponding Effect API and rethrows `AIError` unchanged.
12
+ * Promise-first entrypoint for scripts and non-Effect callers. One `ManagedRuntime` hosts the LLM, image, and video
13
+ * clients over a request executor; every method runs the corresponding Effect API and rethrows `AIError` unchanged.
11
14
  */
12
15
  export interface Options {
13
16
  /** Executor layer; defaults to `RequestExecutor.fetchLayer`. Inject a recorder or middleware here. */
@@ -16,7 +19,15 @@ export interface Options {
16
19
  export interface RunOptions {
17
20
  readonly signal?: AbortSignal;
18
21
  }
19
- export type Services = Layer.Success<typeof LLMClient.layer> | Layer.Success<typeof ImageClient.layer> | RequestExecutor.Service;
22
+ export type Services = Layer.Success<typeof LLMClient.layer> | Layer.Success<typeof ImageClient.layer> | Layer.Success<typeof VideoClient.layer> | RequestExecutor.Service;
23
+ /** Promise view of a `Generation`: its snapshot plus `await`, `refresh`, and `cancel` returning promises. */
24
+ export type GenerationHandle<Response> = Snapshot & {
25
+ /** Serializable JSON; pass it back to `resume` from another process. */
26
+ readonly token: unknown;
27
+ readonly await: (options?: AwaitOptions & RunOptions) => Promise<Response>;
28
+ readonly refresh: (options?: RunOptions) => Promise<GenerationHandle<Response>>;
29
+ readonly cancel: (options?: RunOptions) => Promise<void>;
30
+ };
20
31
  export declare const make: (options?: Options) => {
21
32
  run: <A, E>(effect: Effect.Effect<A, E, Services>, options?: RunOptions) => Promise<A>;
22
33
  llm: {
@@ -281,6 +292,62 @@ export declare const make: (options?: Options) => {
281
292
  }[] | undefined;
282
293
  }>;
283
294
  };
295
+ video: {
296
+ request: typeof import("./video.js").request;
297
+ start: <const Model extends VideoModel>(input: VideoRequestInput<Model> | VideoRequest, options?: RunOptions) => Promise<GenerationHandle<import("./video.js").VideoResponse>>;
298
+ generate: <const Model extends VideoModel>(input: VideoRequestInput<Model> | VideoRequest, options?: AwaitOptions & RunOptions) => Promise<import("./video.js").VideoResponse>;
299
+ resume: <Options extends VideoOptions>(model: VideoModel<Options>, token: unknown, options?: RunOptions) => Promise<GenerationHandle<import("./video.js").VideoResponse>>;
300
+ stream: <const Model extends VideoModel>(input: VideoRequestInput<Model> | VideoRequest, options?: AwaitOptions & RunOptions) => AsyncIterable<{
301
+ readonly id: string;
302
+ readonly type: "generation-queued";
303
+ readonly position?: number | undefined;
304
+ } | {
305
+ readonly id: string;
306
+ readonly type: "generation-progress";
307
+ readonly progress?: number | undefined;
308
+ } | {
309
+ readonly type: "video";
310
+ readonly index: number;
311
+ readonly video: import("./media.js").Asset;
312
+ } | {
313
+ readonly type: "finish";
314
+ readonly providerMetadata?: {
315
+ readonly [x: string]: {
316
+ readonly [x: string]: unknown;
317
+ };
318
+ } | undefined;
319
+ readonly usage?: {
320
+ readonly type: "tokens";
321
+ readonly input?: number | undefined;
322
+ readonly output?: number | undefined;
323
+ readonly total?: number | undefined;
324
+ readonly details?: {
325
+ readonly [x: string]: unknown;
326
+ } | undefined;
327
+ } | {
328
+ readonly type: "seconds";
329
+ readonly seconds: number;
330
+ } | {
331
+ readonly type: "characters";
332
+ readonly characters: number;
333
+ } | {
334
+ readonly type: "credits";
335
+ readonly credits: number;
336
+ } | {
337
+ readonly type: "compute";
338
+ readonly seconds: number;
339
+ } | undefined;
340
+ readonly notices?: readonly {
341
+ readonly type: "other" | "moderated" | "filtered";
342
+ readonly message: string;
343
+ readonly providerMetadata?: {
344
+ readonly [x: string]: {
345
+ readonly [x: string]: unknown;
346
+ };
347
+ } | undefined;
348
+ }[] | undefined;
349
+ }>;
350
+ };
284
351
  dispose: () => Promise<void>;
285
352
  };
286
353
  export type Client = ReturnType<typeof make>;
@@ -549,6 +616,62 @@ export declare const ai: {
549
616
  }[] | undefined;
550
617
  }>;
551
618
  };
619
+ video: {
620
+ request: typeof import("./video.js").request;
621
+ start: <const Model extends VideoModel>(input: VideoRequestInput<Model> | VideoRequest, options?: RunOptions) => Promise<GenerationHandle<import("./video.js").VideoResponse>>;
622
+ generate: <const Model extends VideoModel>(input: VideoRequestInput<Model> | VideoRequest, options?: AwaitOptions & RunOptions) => Promise<import("./video.js").VideoResponse>;
623
+ resume: <Options extends VideoOptions>(model: VideoModel<Options>, token: unknown, options?: RunOptions) => Promise<GenerationHandle<import("./video.js").VideoResponse>>;
624
+ stream: <const Model extends VideoModel>(input: VideoRequestInput<Model> | VideoRequest, options?: AwaitOptions & RunOptions) => AsyncIterable<{
625
+ readonly id: string;
626
+ readonly type: "generation-queued";
627
+ readonly position?: number | undefined;
628
+ } | {
629
+ readonly id: string;
630
+ readonly type: "generation-progress";
631
+ readonly progress?: number | undefined;
632
+ } | {
633
+ readonly type: "video";
634
+ readonly index: number;
635
+ readonly video: import("./media.js").Asset;
636
+ } | {
637
+ readonly type: "finish";
638
+ readonly providerMetadata?: {
639
+ readonly [x: string]: {
640
+ readonly [x: string]: unknown;
641
+ };
642
+ } | undefined;
643
+ readonly usage?: {
644
+ readonly type: "tokens";
645
+ readonly input?: number | undefined;
646
+ readonly output?: number | undefined;
647
+ readonly total?: number | undefined;
648
+ readonly details?: {
649
+ readonly [x: string]: unknown;
650
+ } | undefined;
651
+ } | {
652
+ readonly type: "seconds";
653
+ readonly seconds: number;
654
+ } | {
655
+ readonly type: "characters";
656
+ readonly characters: number;
657
+ } | {
658
+ readonly type: "credits";
659
+ readonly credits: number;
660
+ } | {
661
+ readonly type: "compute";
662
+ readonly seconds: number;
663
+ } | undefined;
664
+ readonly notices?: readonly {
665
+ readonly type: "other" | "moderated" | "filtered";
666
+ readonly message: string;
667
+ readonly providerMetadata?: {
668
+ readonly [x: string]: {
669
+ readonly [x: string]: unknown;
670
+ };
671
+ } | undefined;
672
+ }[] | undefined;
673
+ }>;
674
+ };
552
675
  dispose: () => Promise<void>;
553
676
  };
554
677
  export * as AI from "./promise.js";
package/dist/promise.js CHANGED
@@ -5,6 +5,8 @@ import { LLM } from "./index.js";
5
5
  import { LLMClient } from "./route/client.js";
6
6
  import { RequestExecutor } from "./route/executor.js";
7
7
  import { LanguageModel, LLMRequest } from "./schema/index.js";
8
+ import { Video, VideoModel, VideoRequest } from "./video.js";
9
+ import { VideoClient } from "./video-client.js";
8
10
  const abortEffect = (signal) => signal === undefined
9
11
  ? Effect.never
10
12
  : Effect.callback((resume) => {
@@ -17,13 +19,21 @@ const abortEffect = (signal) => signal === undefined
17
19
  return Effect.sync(() => signal.removeEventListener("abort", onAbort));
18
20
  });
19
21
  export const make = (options = {}) => {
20
- const runtime = ManagedRuntime.make(Layer.mergeAll(LLMClient.layer, ImageClient.layer).pipe(Layer.provideMerge(options.layer ?? RequestExecutor.fetchLayer)));
22
+ const runtime = ManagedRuntime.make(Layer.mergeAll(LLMClient.layer, ImageClient.layer, VideoClient.layer).pipe(Layer.provideMerge(options.layer ?? RequestExecutor.fetchLayer)));
21
23
  /** Run any package Effect (for example `asset.bytes()`) inside this runtime. */
22
24
  const run = (effect, options) => runtime.runPromise(effect, { signal: options?.signal });
23
25
  const iterate = (stream, options) => Stream.toAsyncIterable(Stream.unwrap(runtime.contextEffect.pipe(Effect.map((context) => stream.pipe(Stream.interruptWhen(abortEffect(options?.signal)), Stream.provideContext(context))))));
26
+ const handle = (generation) => ({
27
+ ...generation.snapshot,
28
+ token: generation.token,
29
+ await: (options) => run(generation.await({ poll: options?.poll }), options),
30
+ refresh: (options) => run(generation.refresh(), options).then(handle),
31
+ cancel: (options) => run(generation.cancel(), options),
32
+ });
24
33
  // The typed `generate`/`stream` overloads take a concrete input or a request, not the union; normalize once here.
25
34
  const llmRequest = (input) => (input instanceof LLMRequest ? input : LLM.request(input));
26
35
  const imageRequest = (input) => input instanceof ImageRequest ? input : Image.request(input);
36
+ const videoRequest = (input) => input instanceof VideoRequest ? input : Video.request(input);
27
37
  return {
28
38
  run,
29
39
  llm: {
@@ -36,6 +46,13 @@ export const make = (options = {}) => {
36
46
  generate: (input, options) => run(Image.generate(imageRequest(input)), options),
37
47
  stream: (input, options) => iterate(Image.stream(imageRequest(input)), options),
38
48
  },
49
+ video: {
50
+ request: Video.request,
51
+ start: (input, options) => run(Video.start(videoRequest(input)), options).then(handle),
52
+ generate: (input, options) => run(Video.generate(videoRequest(input), { poll: options?.poll }), options),
53
+ resume: (model, token, options) => run(Video.resume(model, token), options).then(handle),
54
+ stream: (input, options) => iterate(Video.stream(videoRequest(input), { poll: options?.poll }), options),
55
+ },
39
56
  dispose: () => runtime.dispose(),
40
57
  };
41
58
  };
@@ -0,0 +1,39 @@
1
+ import { Schema } from "effect";
2
+ import { MediaProtocol } from "../route/media-protocol.js";
3
+ import { MediaRoute } from "../route/media.js";
4
+ import { VideoModel, VideoResponse, type VideoRequestFor } from "../video.js";
5
+ export declare const DEFAULT_BASE_URL = "https://queue.fal.run";
6
+ export type FalVideoString<Known extends string> = Known | (string & {});
7
+ /**
8
+ * Provider-native input. fal video endpoints are model-specific: `duration` is a string enum whose values differ per
9
+ * model (`"8s"` for Veo, `"5"` for Kling), and last-frame fields are named per model (`end_image_url`,
10
+ * `last_frame_url`, `tail_image_url`), so those pass through here instead of lowering from common fields.
11
+ */
12
+ export type FalVideoOptions = {
13
+ readonly duration?: FalVideoString<"4s" | "6s" | "8s" | "5" | "10">;
14
+ } & Record<string, unknown>;
15
+ export type Request = VideoRequestFor<FalVideoOptions>;
16
+ /** fal hands back absolute follow-up URLs on submit; they are authoritative for status, result, and cancel. */
17
+ export declare const Token: Schema.Struct<{
18
+ readonly requestID: Schema.String;
19
+ readonly statusURL: Schema.String;
20
+ readonly responseURL: Schema.String;
21
+ readonly cancelURL: Schema.String;
22
+ }>;
23
+ export type Token = Schema.Schema.Type<typeof Token>;
24
+ export declare const protocol: MediaProtocol.Queued<Request, VideoResponse, {
25
+ readonly requestID: string;
26
+ readonly statusURL: string;
27
+ readonly responseURL: string;
28
+ readonly cancelURL: string;
29
+ }>;
30
+ export declare const model: (input: MediaRoute.ModelInput) => VideoModel<FalVideoOptions>;
31
+ export declare const FalVideo: {
32
+ readonly protocol: MediaProtocol.Queued<Request, VideoResponse, {
33
+ readonly requestID: string;
34
+ readonly statusURL: string;
35
+ readonly responseURL: string;
36
+ readonly cancelURL: string;
37
+ }>;
38
+ readonly model: (input: MediaRoute.ModelInput) => VideoModel<FalVideoOptions>;
39
+ };
@@ -0,0 +1,139 @@
1
+ import { Effect, Schema } from "effect";
2
+ import { Media } from "../media.js";
3
+ import { MediaProtocol } from "../route/media-protocol.js";
4
+ import { MediaRoute } from "../route/media.js";
5
+ import { ProviderID, mergeJsonRecords } from "../schema/index.js";
6
+ import { VideoModel, VideoResponse } from "../video.js";
7
+ import { ProviderShared, optionalNull } from "./shared.js";
8
+ const ADAPTER = "fal-video";
9
+ const NAME = "fal Video";
10
+ const PROVIDER = ProviderID.make("fal");
11
+ export const DEFAULT_BASE_URL = "https://queue.fal.run";
12
+ // ---------------------------------------------------------------------------
13
+ // 2. Token and response schemas
14
+ // ---------------------------------------------------------------------------
15
+ /** fal hands back absolute follow-up URLs on submit; they are authoritative for status, result, and cancel. */
16
+ export const Token = Schema.Struct({
17
+ requestID: Schema.String,
18
+ statusURL: Schema.String,
19
+ responseURL: Schema.String,
20
+ cancelURL: Schema.String,
21
+ });
22
+ const StartResponse = Schema.Struct({
23
+ request_id: Schema.String,
24
+ status_url: Schema.String,
25
+ response_url: Schema.String,
26
+ cancel_url: Schema.String,
27
+ queue_position: optionalNull(Schema.Number),
28
+ });
29
+ const QueueStatus = Schema.Struct({
30
+ status: Schema.String,
31
+ queue_position: optionalNull(Schema.Number),
32
+ error: optionalNull(Schema.Unknown),
33
+ });
34
+ const QueueResult = Schema.StructWithRest(Schema.Struct({
35
+ video: Schema.Struct({
36
+ url: Schema.String,
37
+ content_type: optionalNull(Schema.String),
38
+ file_name: optionalNull(Schema.String),
39
+ file_size: optionalNull(Schema.Number),
40
+ }),
41
+ seed: optionalNull(Schema.Number),
42
+ }), [Schema.Record(Schema.String, Schema.Unknown)]);
43
+ const STATUS = {
44
+ IN_QUEUE: "queued",
45
+ IN_PROGRESS: "running",
46
+ COMPLETED: "completed",
47
+ };
48
+ // ---------------------------------------------------------------------------
49
+ // 5. Request body construction
50
+ // ---------------------------------------------------------------------------
51
+ // fal accepts public URLs and data URIs; there is no provider file handle to forward.
52
+ const mediaUrl = (asset) => ProviderShared.mediaReference(asset, undefined, NAME).pipe(Effect.map((reference) => reference.value));
53
+ const fromRequest = Effect.fn("FalVideo.fromRequest")(function* (request) {
54
+ if (request.frames?.last !== undefined)
55
+ return yield* ProviderShared.unsupportedOperation({
56
+ operation: "video.frames.last",
57
+ provider: PROVIDER,
58
+ route: ADAPTER,
59
+ message: `${NAME} names the last frame per model; pass it through providerOptions (e.g. end_image_url) instead of frames.last`,
60
+ });
61
+ const imageUrl = request.frames?.first === undefined ? undefined : yield* mediaUrl(request.frames.first);
62
+ const videoUrl = request.video === undefined ? undefined : yield* mediaUrl(request.video);
63
+ return MediaProtocol.json(mergeJsonRecords({
64
+ prompt: request.prompt,
65
+ negative_prompt: request.negativePrompt,
66
+ seed: request.seed,
67
+ aspect_ratio: request.aspectRatio,
68
+ resolution: request.resolution,
69
+ generate_audio: request.audio,
70
+ image_url: imageUrl,
71
+ video_url: videoUrl,
72
+ }, request.providerOptions, request.http?.body) ?? {});
73
+ });
74
+ // ---------------------------------------------------------------------------
75
+ // 6. Response decoding
76
+ // ---------------------------------------------------------------------------
77
+ const decodeStart = MediaProtocol.decodeStarted(ADAPTER, NAME, StartResponse, (value) => ({
78
+ token: {
79
+ requestID: value.request_id,
80
+ statusURL: value.status_url,
81
+ responseURL: value.response_url,
82
+ cancelURL: value.cancel_url,
83
+ },
84
+ snapshot: { id: value.request_id, status: "queued", position: value.queue_position ?? undefined },
85
+ }));
86
+ const decodeQueueStatus = MediaProtocol.decodeJson(ADAPTER, NAME, QueueStatus);
87
+ const decodeQueueResult = MediaProtocol.decodeJson(ADAPTER, NAME, QueueResult);
88
+ const decodeStatus = Effect.fn("FalVideo.decodeStatus")(function* (response, context) {
89
+ const output = yield* decodeQueueStatus(response);
90
+ const decoded = output.value;
91
+ const status = yield* MediaProtocol.status(STATUS, decoded.status, output);
92
+ // fal reports request failures as COMPLETED with an `error`; the response endpoint carries the details.
93
+ const failed = status === "completed" && decoded.error !== undefined && decoded.error !== null;
94
+ return {
95
+ id: context.token.requestID,
96
+ status: failed ? "failed" : status,
97
+ position: status === "queued" ? (decoded.queue_position ?? undefined) : undefined,
98
+ };
99
+ });
100
+ const decodeResult = Effect.fn("FalVideo.decodeResult")(function* (response, context) {
101
+ const output = yield* decodeQueueResult(response);
102
+ const { video, seed, ...rest } = output.value;
103
+ return new VideoResponse({
104
+ videos: [Media.url(video.url, { mediaType: video.content_type ?? "video/mp4" })],
105
+ providerMetadata: {
106
+ fal: {
107
+ requestId: context.token.requestID,
108
+ seed: seed ?? undefined,
109
+ fileName: video.file_name ?? undefined,
110
+ fileSize: video.file_size ?? undefined,
111
+ ...rest,
112
+ },
113
+ },
114
+ });
115
+ });
116
+ // ---------------------------------------------------------------------------
117
+ // 7. Protocol and route
118
+ // ---------------------------------------------------------------------------
119
+ export const protocol = MediaProtocol.queued({
120
+ id: ADAPTER,
121
+ name: NAME,
122
+ token: Token,
123
+ unsupported: ["n", "durationSeconds", "references"],
124
+ start: { body: { from: fromRequest }, decode: decodeStart },
125
+ status: { path: (token) => token.statusURL, decode: decodeStatus },
126
+ result: { path: (token) => token.responseURL, decode: decodeResult },
127
+ cancel: { method: "PUT", path: (token) => token.cancelURL },
128
+ });
129
+ export const model = (input) => VideoModel.fromRoute({
130
+ id: ADAPTER,
131
+ provider: PROVIDER,
132
+ protocol,
133
+ baseURL: DEFAULT_BASE_URL,
134
+ path: ({ request }) => `/${request.model.id}`,
135
+ }, input);
136
+ export const FalVideo = {
137
+ protocol,
138
+ model,
139
+ };
@@ -0,0 +1,26 @@
1
+ import { Schema } from "effect";
2
+ import { MediaProtocol } from "../route/media-protocol.js";
3
+ import { MediaRoute } from "../route/media.js";
4
+ import { VideoModel, VideoResponse, type VideoRequestFor } from "../video.js";
5
+ export declare const DEFAULT_BASE_URL = "https://generativelanguage.googleapis.com/v1beta";
6
+ export type GoogleVideoString<Known extends string> = Known | (string & {});
7
+ /** Provider-native `parameters`. Common fields (`aspectRatio`, `resolution`, `durationSeconds`, `seed`) live on the request. */
8
+ export type GoogleVideoOptions = {
9
+ readonly personGeneration?: GoogleVideoString<"allow_all" | "allow_adult" | "dont_allow">;
10
+ } & Record<string, unknown>;
11
+ export type Request = VideoRequestFor<GoogleVideoOptions>;
12
+ /** The long-running operation name, e.g. `models/veo-3.1-generate-preview/operations/abc123`. */
13
+ export declare const Token: Schema.Struct<{
14
+ readonly operation: Schema.String;
15
+ }>;
16
+ export type Token = Schema.Schema.Type<typeof Token>;
17
+ export declare const protocol: MediaProtocol.Queued<Request, VideoResponse, {
18
+ readonly operation: string;
19
+ }>;
20
+ export declare const model: (input: MediaRoute.ModelInput) => VideoModel<GoogleVideoOptions>;
21
+ export declare const GoogleVideo: {
22
+ readonly protocol: MediaProtocol.Queued<Request, VideoResponse, {
23
+ readonly operation: string;
24
+ }>;
25
+ readonly model: (input: MediaRoute.ModelInput) => VideoModel<GoogleVideoOptions>;
26
+ };
@@ -0,0 +1,158 @@
1
+ import { Duration, Effect, Schema } from "effect";
2
+ import { Media } from "../media.js";
3
+ import { MediaProtocol } from "../route/media-protocol.js";
4
+ import { MediaRoute } from "../route/media.js";
5
+ import { ProviderID, mergeJsonRecords } from "../schema/index.js";
6
+ import { VideoModel, VideoResponse } from "../video.js";
7
+ import { ProviderShared, optionalArray } from "./shared.js";
8
+ const ADAPTER = "google-video";
9
+ const NAME = "Google Veo";
10
+ const PROVIDER = ProviderID.make("google");
11
+ export const DEFAULT_BASE_URL = "https://generativelanguage.googleapis.com/v1beta";
12
+ /** Veo keeps generated files for two days; the asset carries that deadline so callers materialize in time. */
13
+ const FILE_RETENTION = Duration.days(2);
14
+ // ---------------------------------------------------------------------------
15
+ // 2. Token and response schemas
16
+ // ---------------------------------------------------------------------------
17
+ /** The long-running operation name, e.g. `models/veo-3.1-generate-preview/operations/abc123`. */
18
+ export const Token = Schema.Struct({ operation: Schema.String });
19
+ const StartResponse = Schema.Struct({ name: Schema.String });
20
+ const Operation = Schema.Struct({
21
+ done: Schema.optional(Schema.Boolean),
22
+ error: Schema.optional(Schema.Struct({ message: Schema.optional(Schema.String) })),
23
+ response: Schema.optional(Schema.Struct({
24
+ generateVideoResponse: Schema.optional(Schema.Struct({
25
+ generatedSamples: optionalArray(Schema.Struct({
26
+ video: Schema.optional(Schema.Struct({
27
+ uri: Schema.optional(Schema.String),
28
+ mimeType: Schema.optional(Schema.String),
29
+ })),
30
+ })),
31
+ raiMediaFilteredCount: Schema.optional(Schema.Number),
32
+ raiMediaFilteredReasons: optionalArray(Schema.String),
33
+ })),
34
+ })),
35
+ metadata: Schema.optional(Schema.Unknown),
36
+ });
37
+ // ---------------------------------------------------------------------------
38
+ // 5. Request body construction
39
+ // ---------------------------------------------------------------------------
40
+ // Veo takes inline media only; a prior Veo output is `Media.url` with transient auth, so materialize it first.
41
+ const inlineMedia = (asset) => ProviderShared.requireInlineMedia(NAME, asset).pipe(Effect.map((inline) => ({ inlineData: { mimeType: inline.mime, data: inline.base64 } })));
42
+ const fromRequest = Effect.fn("GoogleVideo.fromRequest")(function* (request) {
43
+ if (request.n !== undefined && request.n > 1)
44
+ return yield* ProviderShared.unsupportedOperation({
45
+ operation: "video.n",
46
+ provider: PROVIDER,
47
+ route: ADAPTER,
48
+ message: `${NAME} generates one video per request; call it once per video instead of n=${request.n}`,
49
+ });
50
+ if (request.audio === false)
51
+ return yield* ProviderShared.unsupportedOperation({
52
+ operation: "video.audio",
53
+ provider: PROVIDER,
54
+ route: ADAPTER,
55
+ message: `${NAME} always generates audio; audio: false cannot be honored`,
56
+ });
57
+ if (request.frames?.last !== undefined && request.frames.first === undefined)
58
+ return yield* ProviderShared.invalidRequest(`${NAME} requires frames.first when frames.last is set`);
59
+ const image = request.frames?.first === undefined ? undefined : yield* inlineMedia(request.frames.first);
60
+ const lastFrame = request.frames?.last === undefined ? undefined : yield* inlineMedia(request.frames.last);
61
+ const video = request.video === undefined ? undefined : yield* inlineMedia(request.video);
62
+ const referenceImages = yield* Effect.forEach(request.references ?? [], (asset) => inlineMedia(asset).pipe(Effect.map((image) => ({ image, referenceType: "asset" }))));
63
+ return MediaProtocol.json(mergeJsonRecords({
64
+ instances: [
65
+ {
66
+ prompt: request.prompt,
67
+ image,
68
+ lastFrame,
69
+ referenceImages: referenceImages.length === 0 ? undefined : referenceImages,
70
+ video,
71
+ },
72
+ ],
73
+ parameters: mergeJsonRecords({
74
+ aspectRatio: request.aspectRatio,
75
+ resolution: request.resolution,
76
+ durationSeconds: request.durationSeconds,
77
+ negativePrompt: request.negativePrompt,
78
+ seed: request.seed,
79
+ }, request.providerOptions),
80
+ }, request.http?.body) ?? {});
81
+ });
82
+ // ---------------------------------------------------------------------------
83
+ // 6. Response decoding
84
+ // ---------------------------------------------------------------------------
85
+ const decodeStart = MediaProtocol.decodeStarted(ADAPTER, NAME, StartResponse, (value) => ({
86
+ token: { operation: value.name },
87
+ snapshot: { id: value.name, status: "running" },
88
+ }));
89
+ // Operations carry no status string: not done is running, done with `error` failed, otherwise completed.
90
+ const statusOf = (operation) => {
91
+ if (operation.done !== true)
92
+ return "running";
93
+ return operation.error === undefined ? "completed" : "failed";
94
+ };
95
+ const decodeOperation = MediaProtocol.decodeJson(ADAPTER, NAME, Operation);
96
+ const decodeStatus = Effect.fn("GoogleVideo.decodeStatus")(function* (response, context) {
97
+ const output = yield* decodeOperation(response);
98
+ return { id: context.token.operation, status: statusOf(output.value) };
99
+ });
100
+ const decodeResult = Effect.fn("GoogleVideo.decodeResult")(function* (response, context) {
101
+ const output = yield* decodeOperation(response);
102
+ const operation = output.value;
103
+ const status = statusOf(operation);
104
+ if (status === "running")
105
+ return yield* output.invalid(`${NAME} operation ${context.token.operation} has not finished`);
106
+ if (status === "failed")
107
+ return yield* output.ended("failed", `${NAME} operation failed${operation.error?.message === undefined ? "" : `: ${operation.error.message}`}`);
108
+ const generated = operation.response?.generateVideoResponse;
109
+ // Downloads require the same API key as the poll; the asset carries it transiently and follows the redirect.
110
+ const videos = yield* Effect.forEach((generated?.generatedSamples ?? []).flatMap((sample) => sample.video?.uri === undefined ? [] : [{ uri: sample.video.uri, mimeType: sample.video.mimeType }]), (video) => MediaProtocol.expiringUrl(video.uri, FILE_RETENTION, {
111
+ mediaType: video.mimeType ?? "video/mp4",
112
+ headers: context.auth,
113
+ }));
114
+ const reasons = generated?.raiMediaFilteredReasons ?? [];
115
+ const notices = reasons.map((reason) => ({
116
+ type: "filtered",
117
+ message: `${NAME} filtered media: ${reason}`,
118
+ providerMetadata: { google: { raiMediaFilteredReason: reason } },
119
+ }));
120
+ if (videos.length === 0 && (reasons.length > 0 || (generated?.raiMediaFilteredCount ?? 0) > 0))
121
+ return yield* output.contentPolicy(`${NAME} filtered every video${reasons.length === 0 ? "" : `: ${reasons.join("; ")}`}`);
122
+ if (videos.length === 0)
123
+ return yield* output.invalid(`${NAME} operation completed without any video`);
124
+ return new VideoResponse({
125
+ videos,
126
+ notices: notices.length === 0 ? undefined : notices,
127
+ providerMetadata: {
128
+ google: {
129
+ operation: context.token.operation,
130
+ raiMediaFilteredCount: generated?.raiMediaFilteredCount,
131
+ metadata: operation.metadata,
132
+ },
133
+ },
134
+ });
135
+ });
136
+ // ---------------------------------------------------------------------------
137
+ // 7. Protocol and route
138
+ // ---------------------------------------------------------------------------
139
+ const operationPath = (token) => `/${token.operation}`;
140
+ export const protocol = MediaProtocol.queued({
141
+ id: ADAPTER,
142
+ name: NAME,
143
+ token: Token,
144
+ start: { body: { from: fromRequest }, decode: decodeStart },
145
+ status: { path: operationPath, decode: decodeStatus },
146
+ result: { path: operationPath, decode: decodeResult },
147
+ });
148
+ export const model = (input) => VideoModel.fromRoute({
149
+ id: ADAPTER,
150
+ provider: PROVIDER,
151
+ protocol,
152
+ baseURL: DEFAULT_BASE_URL,
153
+ path: ({ request }) => `/models/${request.model.id}:predictLongRunning`,
154
+ }, input);
155
+ export const GoogleVideo = {
156
+ protocol,
157
+ model,
158
+ };
@@ -37,15 +37,8 @@ const Response = Schema.Struct({
37
37
  // 5. Request body construction
38
38
  // ---------------------------------------------------------------------------
39
39
  const isEdit = (request) => (request.images?.length ?? 0) > 0;
40
- const reference = (asset) => {
41
- const inline = asset.inline();
42
- if (inline)
43
- return Effect.succeed({ image_url: inline.dataUrl });
44
- const url = ProviderShared.mediaUrl(asset);
45
- if (url)
46
- return Effect.succeed({ image_url: url });
47
- return Effect.fail(ProviderShared.invalidRequest(`${NAME} accepts image bytes and URLs`));
48
- };
40
+ // Meta has no file handles: refs are rejected even when they name this provider.
41
+ const reference = (asset) => ProviderShared.mediaReference(asset, undefined, NAME).pipe(Effect.map((item) => ({ image_url: item.value })));
49
42
  const fromRequest = Effect.fn("MetaImages.fromRequest")(function* (request) {
50
43
  const images = yield* Effect.forEach(request.images ?? [], reference);
51
44
  const { responseFormat, reasoningStrength, toolEnablement, ...native } = request.providerOptions ?? {};