@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.
- package/README.md +80 -0
- package/dist/generation.d.ts +23 -13
- package/dist/generation.js +42 -24
- package/dist/image-client.js +1 -1
- package/dist/image.d.ts +7 -36
- package/dist/image.js +8 -38
- package/dist/index.d.ts +5 -1
- package/dist/index.js +3 -0
- package/dist/media-model.d.ts +42 -0
- package/dist/media-model.js +39 -0
- package/dist/media.d.ts +10 -9
- package/dist/media.js +9 -10
- package/dist/promise.d.ts +126 -3
- package/dist/promise.js +18 -1
- package/dist/protocols/fal-video.d.ts +39 -0
- package/dist/protocols/fal-video.js +139 -0
- package/dist/protocols/google-video.d.ts +26 -0
- package/dist/protocols/google-video.js +158 -0
- package/dist/protocols/meta-images.js +2 -9
- package/dist/protocols/openai-images.js +1 -12
- package/dist/protocols/runway-video.d.ts +38 -0
- package/dist/protocols/runway-video.js +146 -0
- package/dist/protocols/shared.d.ts +10 -0
- package/dist/protocols/shared.js +17 -0
- package/dist/protocols/xai-images.js +1 -12
- package/dist/protocols/xai-video.d.ts +34 -0
- package/dist/protocols/xai-video.js +147 -0
- package/dist/protocols/zai-chat.d.ts +1 -1
- package/dist/providers/fal.d.ts +24 -0
- package/dist/providers/fal.js +29 -0
- package/dist/providers/google.d.ts +5 -0
- package/dist/providers/google.js +5 -2
- package/dist/providers/index.d.ts +2 -0
- package/dist/providers/index.js +2 -0
- package/dist/providers/runway.d.ts +24 -0
- package/dist/providers/runway.js +22 -0
- package/dist/providers/xai.d.ts +5 -0
- package/dist/providers/xai.js +5 -2
- package/dist/providers/zai-coding-plan.d.ts +1 -1
- package/dist/providers/zai.d.ts +1 -1
- package/dist/route/auth.d.ts +4 -1
- package/dist/route/auth.js +6 -0
- package/dist/route/media-protocol.d.ts +68 -2
- package/dist/route/media-protocol.js +32 -3
- package/dist/route/media.d.ts +28 -5
- package/dist/route/media.js +95 -14
- package/dist/schema/events.d.ts +0 -6
- package/dist/schema/messages.d.ts +0 -3
- package/dist/video-client.d.ts +28 -0
- package/dist/video-client.js +43 -0
- package/dist/video.d.ts +1373 -0
- package/dist/video.js +131 -0
- 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(
|
|
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,
|
|
197
|
-
return from({ type: "url", url: value, mediaType, expiresAt
|
|
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
|
|
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
|
-
|
|
41
|
-
|
|
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 ?? {};
|