@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/README.md CHANGED
@@ -602,6 +602,86 @@ const program = Effect.gen(function* () {
602
602
 
603
603
  The hosted result is represented as a provider-executed tool call and tool result, and the generated image is also emitted as a first-class `media` `LLMEvent` (`response.message` then carries a `media` part). Gemini image-capable models emit the same `media` event for inline image output. Retaining `response.message` preserves the generated image for continuation on both routes.
604
604
 
605
+ ## Video generation
606
+
607
+ Video mirrors `Image` with one difference: every provider is asynchronous, so the route is a submit-then-poll
608
+ `Generation`. Models come from `.video(...)` selectors on the `Google` (Veo), `XAI`, `Fal`, and `Runway` facades.
609
+ Common fields (`frames`, `references`, `video`, `durationSeconds`, `aspectRatio`, `resolution`, `audio`, `n`, `seed`,
610
+ `negativePrompt`) lower natively or fail with a typed `AIError` before any network call; provider-native controls live
611
+ under `providerOptions`, inferred from the selected model.
612
+
613
+ ```ts
614
+ import { Video, VideoClient } from "@opencode/ai"
615
+ import { Google } from "@opencode/ai/providers"
616
+
617
+ const google = Google.configure({ apiKey: process.env.GOOGLE_GENERATIVE_AI_API_KEY })
618
+
619
+ // Simple: submit and wait.
620
+ const program = Effect.gen(function* () {
621
+ const response = yield* Video.generate(
622
+ {
623
+ model: google.video("veo-3.1-generate-preview"),
624
+ prompt: "Panning wide shot of a calico kitten sleeping in the sunshine",
625
+ aspectRatio: "16:9",
626
+ resolution: "1080p",
627
+ durationSeconds: 8,
628
+ providerOptions: { personGeneration: "allow_adult" },
629
+ },
630
+ { poll: { interval: "10 seconds", timeout: "10 minutes" } },
631
+ )
632
+ // Veo serves files for two days behind the API key. The asset knows the deadline (`expiresAt`) and carries the
633
+ // download credentials only on the live instance (`asset.headers`), never in `source` or JSON: materialize
634
+ // before persisting, or the persisted URL cannot be fetched again.
635
+ return yield* response.video.materialize()
636
+ })
637
+
638
+ // Explicit control: keep the handle, persist the token, resume elsewhere.
639
+ const controlled = Effect.gen(function* () {
640
+ const generation = yield* Video.start({ model: google.video("veo-3.1-generate-preview"), prompt })
641
+ generation.id // provider operation / task / request id
642
+ generation.status // "queued" | "running" | "completed" | "failed" | "cancelled" | "expired"
643
+ generation.token // route-owned JSON: `{ operation }`, `{ requestID }`, `{ taskID }`, or fal's follow-up URLs
644
+ const saved = JSON.stringify(generation.token)
645
+
646
+ const resumed = yield* Video.resume(google.video("veo-3.1-generate-preview"), JSON.parse(saved))
647
+ return yield* resumed.await({ poll: { interval: "10 seconds" } })
648
+ })
649
+
650
+ // Progress as a stream: generation-queued | generation-progress | video | finish.
651
+ const events = Video.stream({ model: Runway.configure({ apiKey }).video("gen4.5"), prompt }, { poll })
652
+ ```
653
+
654
+ `VideoClient.layer` needs `RequestExecutor.Service`, and status polls, result fetches, cancels, and asset downloads
655
+ all run through the same executor with the route's auth. `Generation.await` and `Generation.events` fail with a
656
+ `Timeout` reason when `poll.timeout` (default 10 minutes) elapses. Failed,
657
+ cancelled, and expired generations fail typed with the provider's terminal document on `reason.body`; moderation
658
+ outcomes (Veo `raiMediaFilteredReasons`, xAI `respect_moderation`, Runway `SAFETY.*` codes) surface as `notices` when
659
+ a video is still returned and as a `ContentPolicy` reason when nothing is.
660
+
661
+ Provider notes:
662
+
663
+ - **Google Veo** takes inline bytes only (materialize `url` assets first); `frames.last` requires `frames.first`;
664
+ audio is always on, so `audio: false` fails typed; one video per request. Output URLs need the API key to
665
+ download, which the returned asset holds transiently (see above).
666
+ - **xAI** sends a `video` input to `/videos/edits`, or `/videos/extensions` with `providerOptions.mode: "extend"`.
667
+ `seed` and `negativePrompt` are not supported.
668
+ - **fal** endpoints are model-specific: `durationSeconds`, `references`, and `frames.last` fail typed and belong in
669
+ `providerOptions` under the model's own names (`duration: "8s"`, `end_image_url`, …). Auth is
670
+ `Authorization: Key <FAL_KEY>`.
671
+ - **Runway** expects pixel ratios in `aspectRatio` for most models (`"1280:720"`), pins `X-Runway-Version`, reports
672
+ `usage: { type: "credits" }`, and its output URLs expire after 24–48 hours.
673
+
674
+ The promise client exposes the same surface: `ai.video.start(...)` resolves to a handle with `await`, `refresh`,
675
+ `cancel`, and `token`; `ai.video.generate`, `ai.video.resume(model, token)`, and `ai.video.stream` mirror the Effect
676
+ API.
677
+
678
+ ```ts
679
+ import { ai } from "@opencode/ai/promise"
680
+
681
+ const generation = await ai.video.start({ model, prompt })
682
+ const video = await generation.await({ poll: { interval: 10_000 }, signal })
683
+ ```
684
+
605
685
  ## Public API
606
686
 
607
687
  - **`LLM.request({...})`** — build a provider-neutral `LLMRequest`. Accepts ergonomic inputs (`system: string`, `prompt: string`) that normalize into the canonical Schema classes.
@@ -12,13 +12,13 @@ export interface Snapshot {
12
12
  readonly expiresAt?: number;
13
13
  }
14
14
  /**
15
- * Route-owned generation operations. `token` is the route's serializable handle (operation name, task id, response URL)
16
- * so a generation can be resumed from another process; its shape is opaque to `Generation`.
15
+ * Route-owned generation operations for one generation. The media route decodes its serializable token once (from the
16
+ * submission response or a `resume` input) and closes over it, so `Generation` never sees the token's shape.
17
17
  */
18
18
  export interface Route<Response> {
19
- readonly status: (token: unknown) => Effect.Effect<Snapshot, AIError>;
20
- readonly result: (token: unknown) => Effect.Effect<Response, AIError>;
21
- readonly cancel?: (token: unknown) => Effect.Effect<void, AIError>;
19
+ readonly status: Effect.Effect<Snapshot, AIError>;
20
+ readonly result: Effect.Effect<Response, AIError>;
21
+ readonly cancel?: Effect.Effect<void, AIError>;
22
22
  /** Provider polling hint (e.g. `openai-poll-after-ms`) that overrides the default interval for the next poll. */
23
23
  readonly pollHint?: (snapshot: Snapshot) => Duration.Duration | undefined;
24
24
  }
@@ -28,6 +28,9 @@ export interface Poll {
28
28
  /** Full override of the polling schedule; `interval` and `pollHint` are ignored when supplied. */
29
29
  readonly schedule?: Schedule.Schedule<unknown, Snapshot>;
30
30
  }
31
+ export interface AwaitOptions {
32
+ readonly poll?: Poll;
33
+ }
31
34
  export declare const DEFAULT_POLL_INTERVAL: Duration.Duration;
32
35
  export declare const DEFAULT_POLL_TIMEOUT: Duration.Duration;
33
36
  export type Event = {
@@ -45,25 +48,32 @@ export type Event = {
45
48
  };
46
49
  export declare class Generation<Response> {
47
50
  readonly route: Route<Response>;
51
+ /** Route-owned serializable JSON; pass it to the modality's `resume` from another process. */
48
52
  readonly token: unknown;
49
53
  readonly id: string;
50
54
  readonly status: Status;
51
55
  readonly progress?: number;
52
56
  readonly position?: number;
53
57
  readonly expiresAt?: number;
54
- constructor(route: Route<Response>, token: unknown, snapshot: Snapshot);
58
+ constructor(route: Route<Response>,
59
+ /** Route-owned serializable JSON; pass it to the modality's `resume` from another process. */
60
+ token: unknown, snapshot: Snapshot);
55
61
  get snapshot(): Snapshot;
56
62
  get terminal(): boolean;
57
63
  refresh(): Effect.Effect<Generation<Response>, AIError>;
64
+ /** Fetch the result without polling; non-completed terminal generations fail with the provider's terminal body. */
65
+ result(): Effect.Effect<Response, AIError>;
58
66
  /** Poll until the generation reaches a terminal status, then fetch the result. Fails with a `Timeout` reason on deadline. */
59
- await(options?: {
60
- readonly poll?: Poll;
61
- }): Effect.Effect<Response, AIError>;
67
+ await(options?: AwaitOptions): Effect.Effect<Response, AIError>;
62
68
  cancel(): Effect.Effect<void, AIError>;
63
- /** Status observations as a stream, ending after the first terminal observation. */
64
- events(options?: {
65
- readonly poll?: Poll;
66
- }): Stream.Stream<Event, AIError>;
69
+ /**
70
+ * Status observations as a stream, ending after the first terminal observation. Each poll is bounded by the time
71
+ * remaining until `poll.timeout`, so a hung status request fails the stream instead of stalling it. (`Stream.interruptWhen`
72
+ * would express this directly but deadlocks under `TestClock` when the source completes while the timer sleeps.)
73
+ */
74
+ events(options?: AwaitOptions): Stream.Stream<Event, AIError>;
75
+ private event;
76
+ private timeoutError;
67
77
  private poll;
68
78
  private schedule;
69
79
  }
@@ -1,4 +1,4 @@
1
- import { Duration, Effect, Schedule, Schema, Stream } from "effect";
1
+ import { Clock, Duration, Effect, Schedule, Schema, Stream } from "effect";
2
2
  import { AIError, TimeoutError } from "./schema/errors.js";
3
3
  export const Status = Schema.Literals(["queued", "running", "completed", "failed", "cancelled", "expired"]);
4
4
  export const DEFAULT_POLL_INTERVAL = Duration.seconds(5);
@@ -12,7 +12,9 @@ export class Generation {
12
12
  progress;
13
13
  position;
14
14
  expiresAt;
15
- constructor(route, token, snapshot) {
15
+ constructor(route,
16
+ /** Route-owned serializable JSON; pass it to the modality's `resume` from another process. */
17
+ token, snapshot) {
16
18
  this.route = route;
17
19
  this.token = token;
18
20
  this.id = snapshot.id;
@@ -34,7 +36,11 @@ export class Generation {
34
36
  return TERMINAL.has(this.status);
35
37
  }
36
38
  refresh() {
37
- return this.route.status(this.token).pipe(Effect.map((snapshot) => new Generation(this.route, this.token, snapshot)));
39
+ return this.route.status.pipe(Effect.map((snapshot) => new Generation(this.route, this.token, snapshot)));
40
+ }
41
+ /** Fetch the result without polling; non-completed terminal generations fail with the provider's terminal body. */
42
+ result() {
43
+ return this.route.result;
38
44
  }
39
45
  /** Poll until the generation reaches a terminal status, then fetch the result. Fails with a `Timeout` reason on deadline. */
40
46
  await(options) {
@@ -42,31 +48,43 @@ export class Generation {
42
48
  const settled = this.terminal ? Effect.succeed(this) : this.poll(options?.poll);
43
49
  return settled.pipe(
44
50
  // Non-completed terminal states also go through `result` so the route can surface its provider failure body.
45
- Effect.flatMap((generation) => generation.route.result(generation.token)), Effect.timeoutOrElse({
46
- duration: timeout,
47
- orElse: () => new AIError({
48
- reason: new TimeoutError({
49
- message: `Generation ${this.id} did not finish within ${Duration.format(timeout)}`,
50
- timeoutMs: Duration.toMillis(timeout),
51
- }),
52
- }),
53
- }));
51
+ Effect.flatMap((generation) => generation.result()), Effect.timeoutOrElse({ duration: timeout, orElse: () => this.timeoutError(timeout) }));
54
52
  }
55
53
  cancel() {
56
- return this.route.cancel?.(this.token) ?? Effect.void;
54
+ return this.route.cancel ?? Effect.void;
57
55
  }
58
- /** Status observations as a stream, ending after the first terminal observation. */
56
+ /**
57
+ * Status observations as a stream, ending after the first terminal observation. Each poll is bounded by the time
58
+ * remaining until `poll.timeout`, so a hung status request fails the stream instead of stalling it. (`Stream.interruptWhen`
59
+ * would express this directly but deadlocks under `TestClock` when the source completes while the timer sleeps.)
60
+ */
59
61
  events(options) {
60
- const observations = this.terminal
61
- ? Stream.make(this)
62
- : Stream.fromEffectSchedule(this.refresh(), this.schedule(options?.poll)).pipe(Stream.takeUntil((generation) => generation.terminal));
63
- return observations.pipe(Stream.map((generation) => {
64
- if (generation.terminal)
65
- return { type: "generation-finished", id: generation.id, status: generation.status };
66
- if (generation.status === "queued")
67
- return { type: "generation-queued", id: generation.id, position: generation.position };
68
- return { type: "generation-progress", id: generation.id, progress: generation.progress };
69
- }));
62
+ if (this.terminal)
63
+ return Stream.make(this.event());
64
+ const timeout = Duration.fromInputUnsafe(options?.poll?.timeout ?? DEFAULT_POLL_TIMEOUT);
65
+ return Stream.unwrap(Clock.currentTimeMillis.pipe(Effect.map((start) => {
66
+ const deadline = start + Duration.toMillis(timeout);
67
+ const refresh = Clock.currentTimeMillis.pipe(Effect.flatMap((now) => this.refresh().pipe(Effect.timeoutOrElse({
68
+ duration: Duration.millis(Math.max(0, deadline - now)),
69
+ orElse: () => this.timeoutError(timeout),
70
+ }))));
71
+ return Stream.fromEffectSchedule(refresh, this.schedule(options?.poll)).pipe(Stream.takeUntil((generation) => generation.terminal), Stream.map((generation) => generation.event()));
72
+ })));
73
+ }
74
+ event() {
75
+ if (this.terminal)
76
+ return { type: "generation-finished", id: this.id, status: this.status };
77
+ if (this.status === "queued")
78
+ return { type: "generation-queued", id: this.id, position: this.position };
79
+ return { type: "generation-progress", id: this.id, progress: this.progress };
80
+ }
81
+ timeoutError(timeout) {
82
+ return new AIError({
83
+ reason: new TimeoutError({
84
+ message: `Generation ${this.id} did not finish within ${Duration.format(timeout)}`,
85
+ timeoutMs: Duration.toMillis(timeout),
86
+ }),
87
+ });
70
88
  }
71
89
  poll(poll) {
72
90
  return this.refresh().pipe(Effect.repeat({ schedule: this.schedule(poll), until: (generation) => generation.terminal }));
@@ -17,7 +17,7 @@ export const layer = Layer.effect(Service, Effect.gen(function* () {
17
17
  return Service.of({
18
18
  generate,
19
19
  // Inline routes have no partial frames yet; the stream is the completed response expanded into events.
20
- stream: (request) => Stream.unwrap(generate(request).pipe(Effect.map((response) => Stream.fromIterable(responseEvents(response))))),
20
+ stream: (request) => Stream.fromIterableEffect(Effect.map(generate(request), responseEvents)),
21
21
  });
22
22
  }));
23
23
  export const ImageClient = {
package/dist/image.d.ts CHANGED
@@ -1,47 +1,25 @@
1
1
  import { Effect, Schema, Stream } from "effect";
2
2
  import { Media } from "./media.js";
3
- import { Endpoint } from "./route/endpoint.js";
3
+ import { MediaModel } from "./media-model.js";
4
4
  import { MediaRoute } from "./route/media.js";
5
5
  import type { MediaProtocol } from "./route/media-protocol.js";
6
- import { AIError, HttpOptions, ModelID, ProviderID } from "./schema/index.js";
6
+ import { AIError, HttpOptions } from "./schema/index.js";
7
7
  import { Service } from "./image-client.js";
8
8
  export type ImageOptions = Record<string, unknown>;
9
9
  export type ImageRoute<Options extends ImageOptions = ImageOptions> = MediaRoute.Route<ImageRequestFor<Options>, ImageResponse>;
10
- export declare class ImageModel<Options extends ImageOptions = ImageOptions> {
11
- protected readonly _Options: (options: Options) => Options;
12
- readonly id: ModelID;
13
- readonly provider: ProviderID;
14
- readonly route: ImageRoute<Options>;
15
- readonly http?: HttpOptions;
16
- constructor(input: ImageModel.Input<Options>);
17
- static make<Options extends ImageOptions = ImageOptions>(input: ImageModel.MakeInput<Options>): ImageModel<Options>;
10
+ export declare class ImageModel<Options extends ImageOptions = ImageOptions> extends MediaModel<ImageRoute<Options>, Options> {
11
+ protected readonly _ImageModel: void;
12
+ static make<Options extends ImageOptions = ImageOptions>(input: MediaModel.Input<ImageRoute<Options>>): ImageModel<Options>;
18
13
  /** Compose an inline image protocol with its canonical path into a model for one deployment. */
19
14
  static fromRoute<Options extends ImageOptions = ImageOptions>(route: ImageModel.RouteInput<Options>, input: MediaRoute.ModelInput): ImageModel<Options>;
20
15
  }
21
16
  export declare namespace ImageModel {
22
- interface Input<Options extends ImageOptions = ImageOptions> {
23
- readonly id: ModelID;
24
- readonly provider: ProviderID;
25
- readonly route: ImageRoute<Options>;
26
- readonly http?: HttpOptions;
27
- }
28
- interface MakeInput<Options extends ImageOptions = ImageOptions> extends Omit<Input<Options>, "id" | "provider"> {
29
- readonly id: string | ModelID;
30
- readonly provider: string | ProviderID;
31
- }
32
- interface RouteInput<Options extends ImageOptions = ImageOptions> {
33
- readonly id: string;
34
- readonly provider: string | ProviderID;
35
- readonly protocol: MediaProtocol.Inline<ImageRequestFor<Options>, ImageResponse>;
36
- readonly path: Endpoint.EndpointPart<MediaProtocol.Body, ImageRequestFor<Options>>;
37
- /** Canonical base URL; `ModelInput.baseURL` overrides it per deployment. */
38
- readonly baseURL?: string;
39
- }
17
+ type RouteInput<Options extends ImageOptions = ImageOptions> = MediaModel.RouteInput<ImageRequestFor<Options>, MediaProtocol.Inline<ImageRequestFor<Options>, ImageResponse>>;
40
18
  }
41
19
  export declare const ImageModelSchema: Schema.declare<ImageModel<ImageOptions>, ImageModel<ImageOptions>>;
42
20
  export type ImageSize = `${number}x${number}`;
43
21
  export declare const ImageSize: Schema.declare<`${number}x${number}`, `${number}x${number}`>;
44
- export type ImageAspectRatio = `${number}:${number}`;
22
+ export type ImageAspectRatio = Media.AspectRatio;
45
23
  export declare const ImageAspectRatio: Schema.declare<`${number}:${number}`, `${number}:${number}`>;
46
24
  export type ImageFormat = "png" | "jpeg" | "webp" | (string & {});
47
25
  declare const ImageRequest_base: Schema.Class<ImageRequest, Schema.Struct<{
@@ -62,7 +40,6 @@ declare const ImageRequest_base: Schema.Class<ImageRequest, Schema.Struct<{
62
40
  readonly url: Schema.String;
63
41
  readonly mediaType: Schema.optional<Schema.String>;
64
42
  readonly expiresAt: Schema.optional<Schema.Number>;
65
- readonly headers: Schema.optional<Schema.$Record<Schema.String, Schema.String>>;
66
43
  }>, Schema.Struct<{
67
44
  readonly type: Schema.Literal<"ref">;
68
45
  readonly provider: Schema.brand<Schema.String, "AI.ProviderID">;
@@ -95,7 +72,6 @@ declare const ImageRequest_base: Schema.Class<ImageRequest, Schema.Struct<{
95
72
  readonly url: Schema.String;
96
73
  readonly mediaType: Schema.optional<Schema.String>;
97
74
  readonly expiresAt: Schema.optional<Schema.Number>;
98
- readonly headers: Schema.optional<Schema.$Record<Schema.String, Schema.String>>;
99
75
  }>, Schema.Struct<{
100
76
  readonly type: Schema.Literal<"ref">;
101
77
  readonly provider: Schema.brand<Schema.String, "AI.ProviderID">;
@@ -150,7 +126,6 @@ declare const ImageResponse_base: Schema.Class<ImageResponse, Schema.Struct<{
150
126
  readonly url: Schema.String;
151
127
  readonly mediaType: Schema.optional<Schema.String>;
152
128
  readonly expiresAt: Schema.optional<Schema.Number>;
153
- readonly headers: Schema.optional<Schema.$Record<Schema.String, Schema.String>>;
154
129
  }>, Schema.Struct<{
155
130
  readonly type: Schema.Literal<"ref">;
156
131
  readonly provider: Schema.brand<Schema.String, "AI.ProviderID">;
@@ -214,7 +189,6 @@ export declare const ImageOutputEvent: Schema.Struct<{
214
189
  readonly url: Schema.String;
215
190
  readonly mediaType: Schema.optional<Schema.String>;
216
191
  readonly expiresAt: Schema.optional<Schema.Number>;
217
- readonly headers: Schema.optional<Schema.$Record<Schema.String, Schema.String>>;
218
192
  }>, Schema.Struct<{
219
193
  readonly type: Schema.Literal<"ref">;
220
194
  readonly provider: Schema.brand<Schema.String, "AI.ProviderID">;
@@ -278,7 +252,6 @@ declare const imageEventTagged: Schema.toTaggedUnion<"type", readonly [Schema.St
278
252
  readonly url: Schema.String;
279
253
  readonly mediaType: Schema.optional<Schema.String>;
280
254
  readonly expiresAt: Schema.optional<Schema.Number>;
281
- readonly headers: Schema.optional<Schema.$Record<Schema.String, Schema.String>>;
282
255
  }>, Schema.Struct<{
283
256
  readonly type: Schema.Literal<"ref">;
284
257
  readonly provider: Schema.brand<Schema.String, "AI.ProviderID">;
@@ -341,7 +314,6 @@ export declare const ImageEvent: Schema.Union<readonly [Schema.Struct<{
341
314
  readonly url: Schema.String;
342
315
  readonly mediaType: Schema.optional<Schema.String>;
343
316
  readonly expiresAt: Schema.optional<Schema.Number>;
344
- readonly headers: Schema.optional<Schema.$Record<Schema.String, Schema.String>>;
345
317
  }>, Schema.Struct<{
346
318
  readonly type: Schema.Literal<"ref">;
347
319
  readonly provider: Schema.brand<Schema.String, "AI.ProviderID">;
@@ -406,7 +378,6 @@ export declare const ImageEvent: Schema.Union<readonly [Schema.Struct<{
406
378
  readonly url: Schema.String;
407
379
  readonly mediaType: Schema.optional<Schema.String>;
408
380
  readonly expiresAt: Schema.optional<Schema.Number>;
409
- readonly headers: Schema.optional<Schema.$Record<Schema.String, Schema.String>>;
410
381
  }>, Schema.Struct<{
411
382
  readonly type: Schema.Literal<"ref">;
412
383
  readonly provider: Schema.brand<Schema.String, "AI.ProviderID">;
package/dist/image.js CHANGED
@@ -1,42 +1,20 @@
1
1
  import { Effect, Schema, Stream } from "effect";
2
2
  import { Media } from "./media.js";
3
- import { Endpoint } from "./route/endpoint.js";
3
+ import { MediaModel, composeRoute, tryRequest } from "./media-model.js";
4
4
  import { MediaRoute } from "./route/media.js";
5
- import { AIError, HttpOptions, InvalidRequestError, MediaUsage, ModelID, ProviderID, ProviderMetadata, } from "./schema/index.js";
5
+ import { AIError, HttpOptions, MediaUsage, ProviderMetadata } from "./schema/index.js";
6
6
  import { ImageClient, Service } from "./image-client.js";
7
- export class ImageModel {
8
- id;
9
- provider;
10
- route;
11
- http;
12
- constructor(input) {
13
- this.id = input.id;
14
- this.provider = input.provider;
15
- this.route = input.route;
16
- this.http = input.http;
17
- }
7
+ export class ImageModel extends MediaModel {
18
8
  static make(input) {
19
- return new ImageModel({
20
- id: ModelID.make(input.id),
21
- provider: ProviderID.make(input.provider),
22
- route: input.route,
23
- http: input.http,
24
- });
9
+ return new ImageModel(input);
25
10
  }
26
11
  /** Compose an inline image protocol with its canonical path into a model for one deployment. */
27
12
  static fromRoute(route, input) {
28
- return ImageModel.make({
13
+ return new ImageModel({
29
14
  id: input.id,
30
15
  provider: route.provider,
31
16
  http: input.http,
32
- route: MediaRoute.make({
33
- id: route.id,
34
- provider: route.provider,
35
- protocol: route.protocol,
36
- endpoint: Endpoint.path(route.path, { baseURL: input.baseURL ?? route.baseURL }),
37
- auth: input.auth,
38
- headers: input.headers,
39
- }),
17
+ route: composeRoute(MediaRoute.inline, route, input),
40
18
  });
41
19
  }
42
20
  }
@@ -44,7 +22,7 @@ export const ImageModelSchema = Schema.declare((value) => value instanceof Image
44
22
  expected: "Image.Model",
45
23
  });
46
24
  export const ImageSize = Schema.declare((value) => typeof value === "string" && /^\d+x\d+$/.test(value), { title: "ImageSize" });
47
- export const ImageAspectRatio = Schema.declare((value) => typeof value === "string" && /^\d+(?:\.\d+)?:\d+(?:\.\d+)?$/.test(value), { title: "ImageAspectRatio" });
25
+ export const ImageAspectRatio = Media.AspectRatio;
48
26
  export class ImageRequest extends Schema.Class("Image.Request")({
49
27
  model: ImageModelSchema,
50
28
  prompt: Schema.String,
@@ -109,15 +87,7 @@ export function request(input) {
109
87
  http: input.http === undefined ? undefined : HttpOptions.make(input.http),
110
88
  });
111
89
  }
112
- const requestEffect = (input) => Effect.try({
113
- try: () => request(input),
114
- catch: (error) => new AIError({
115
- reason: new InvalidRequestError({
116
- message: error instanceof Error ? error.message : String(error),
117
- cause: error,
118
- }),
119
- }),
120
- });
90
+ const requestEffect = (input) => tryRequest(() => request(input));
121
91
  export function generate(input) {
122
92
  return requestEffect(input).pipe(Effect.flatMap((request) => ImageClient.generate(request)));
123
93
  }
package/dist/index.d.ts CHANGED
@@ -9,9 +9,13 @@ export * from "./schema/index.js";
9
9
  export { ImageAspectRatio, ImageEvent, ImageModel, ImageModelSchema, ImageRequest, ImageResponse, ImageSize, } from "./image.js";
10
10
  export type { ImageFormat, ImageModelOptions, ImageOptions, ImageRequestFor, ImageRequestInput, ImageRoute, } from "./image.js";
11
11
  export { Image } from "./image.js";
12
+ export { VideoClient } from "./video-client.js";
13
+ export { VideoAspectRatio, VideoEvent, VideoFrames, VideoModel, VideoModelSchema, VideoRequest, VideoResponse, } from "./video.js";
14
+ export type { VideoModelOptions, VideoOptions, VideoRequestFor, VideoRequestInput, VideoResolution, VideoRoute, } from "./video.js";
15
+ export { Video } from "./video.js";
12
16
  export { Media } from "./media.js";
13
17
  export { Generation } from "./generation.js";
14
- export type { Event as GenerationEvent, Poll, Route as GenerationRoute, Snapshot as GenerationSnapshot, Status as GenerationStatus } from "./generation.js";
18
+ export type { AwaitOptions as GenerationAwaitOptions, Event as GenerationEvent, Poll, Route as GenerationRoute, Snapshot as GenerationSnapshot, Status as GenerationStatus, } from "./generation.js";
15
19
  export { Tool, ToolFailure, toDefinitions } from "./tool.js";
16
20
  export { ToolRuntime } from "./tool-runtime.js";
17
21
  export type { DispatchResult as ToolDispatchResult, ToolSettlement } from "./tool-runtime.js";
package/dist/index.js CHANGED
@@ -7,6 +7,9 @@ export { isContextOverflow, isContextOverflowFailure } from "./provider-error.js
7
7
  export * from "./schema/index.js";
8
8
  export { ImageAspectRatio, ImageEvent, ImageModel, ImageModelSchema, ImageRequest, ImageResponse, ImageSize, } from "./image.js";
9
9
  export { Image } from "./image.js";
10
+ export { VideoClient } from "./video-client.js";
11
+ export { VideoAspectRatio, VideoEvent, VideoFrames, VideoModel, VideoModelSchema, VideoRequest, VideoResponse, } from "./video.js";
12
+ export { Video } from "./video.js";
10
13
  export { Media } from "./media.js";
11
14
  export { Generation } from "./generation.js";
12
15
  export { Tool, ToolFailure, toDefinitions } from "./tool.js";
@@ -0,0 +1,42 @@
1
+ import { Effect } from "effect";
2
+ import { Endpoint } from "./route/endpoint.js";
3
+ import type { MediaRoute } from "./route/media.js";
4
+ import type { MediaProtocol } from "./route/media-protocol.js";
5
+ import { AIError, HttpOptions, ModelID, ProviderID } from "./schema/index.js";
6
+ /**
7
+ * What every media model carries: ids, the configured route, and deployment `http` overlays. Modality classes
8
+ * (`ImageModel`, `VideoModel`) extend it with their route type and a nominal marker so one cannot stand in for the
9
+ * other in requests.
10
+ */
11
+ export declare class MediaModel<Route, Options> {
12
+ protected readonly _Options: (options: Options) => Options;
13
+ readonly id: ModelID;
14
+ readonly provider: ProviderID;
15
+ readonly route: Route;
16
+ readonly http?: HttpOptions;
17
+ constructor(input: MediaModel.Input<Route>);
18
+ }
19
+ export declare namespace MediaModel {
20
+ interface Input<Route> {
21
+ readonly id: string | ModelID;
22
+ readonly provider: string | ProviderID;
23
+ readonly route: Route;
24
+ readonly http?: HttpOptions;
25
+ }
26
+ /** A protocol plus its canonical start path; `ModelInput.baseURL` overrides `baseURL` per deployment. */
27
+ interface RouteInput<Request extends MediaRoute.MediaRequest, Protocol> {
28
+ readonly id: string;
29
+ readonly provider: string | ProviderID;
30
+ readonly protocol: Protocol;
31
+ readonly path: Endpoint.EndpointPart<MediaProtocol.Body, Request>;
32
+ readonly baseURL?: string;
33
+ /** Headers the protocol requires on every call, such as a pinned API version; deployment headers win. */
34
+ readonly headers?: Record<string, string>;
35
+ }
36
+ }
37
+ /** Compose a protocol route input with one deployment through `MediaRoute.inline` or `MediaRoute.queued`. */
38
+ export declare const composeRoute: <Request extends MediaRoute.MediaRequest, Protocol, Route>(compose: (input: MediaRoute.Composition<Request> & {
39
+ readonly protocol: Protocol;
40
+ }) => Route, route: MediaModel.RouteInput<Request, Protocol>, input: MediaRoute.ModelInput) => Route;
41
+ /** Lift a synchronous Schema-class constructor into a typed `InvalidRequest` failure. */
42
+ export declare const tryRequest: <A>(make: () => A) => Effect.Effect<A, AIError>;
@@ -0,0 +1,39 @@
1
+ import { Effect } from "effect";
2
+ import { Endpoint } from "./route/endpoint.js";
3
+ import { AIError, HttpOptions, InvalidRequestError, ModelID, ProviderID } from "./schema/index.js";
4
+ /**
5
+ * What every media model carries: ids, the configured route, and deployment `http` overlays. Modality classes
6
+ * (`ImageModel`, `VideoModel`) extend it with their route type and a nominal marker so one cannot stand in for the
7
+ * other in requests.
8
+ */
9
+ export class MediaModel {
10
+ id;
11
+ provider;
12
+ route;
13
+ http;
14
+ constructor(input) {
15
+ this.id = ModelID.make(input.id);
16
+ this.provider = ProviderID.make(input.provider);
17
+ this.route = input.route;
18
+ this.http = input.http;
19
+ }
20
+ }
21
+ /** Compose a protocol route input with one deployment through `MediaRoute.inline` or `MediaRoute.queued`. */
22
+ export const composeRoute = (compose, route, input) => compose({
23
+ id: route.id,
24
+ provider: route.provider,
25
+ protocol: route.protocol,
26
+ endpoint: Endpoint.path(route.path, { baseURL: input.baseURL ?? route.baseURL }),
27
+ auth: input.auth,
28
+ headers: route.headers === undefined && input.headers === undefined ? undefined : { ...route.headers, ...input.headers },
29
+ });
30
+ /** Lift a synchronous Schema-class constructor into a typed `InvalidRequest` failure. */
31
+ export const tryRequest = (make) => Effect.try({
32
+ try: make,
33
+ catch: (error) => new AIError({
34
+ reason: new InvalidRequestError({
35
+ message: error instanceof Error ? error.message : String(error),
36
+ cause: error,
37
+ }),
38
+ }),
39
+ });
package/dist/media.d.ts CHANGED
@@ -19,8 +19,6 @@ export declare const Source: Schema.Union<readonly [Schema.Struct<{
19
19
  readonly mediaType: Schema.optional<Schema.String>;
20
20
  /** Epoch milliseconds after which the provider no longer serves the URL. */
21
21
  readonly expiresAt: Schema.optional<Schema.Number>;
22
- /** Headers required to fetch the URL, such as provider auth for Veo downloads. */
23
- readonly headers: Schema.optional<Schema.$Record<Schema.String, Schema.String>>;
24
22
  }>, Schema.Struct<{
25
23
  readonly type: Schema.Literal<"ref">;
26
24
  readonly provider: Schema.brand<Schema.String, "AI.ProviderID">;
@@ -28,6 +26,8 @@ export declare const Source: Schema.Union<readonly [Schema.Struct<{
28
26
  readonly mediaType: Schema.optional<Schema.String>;
29
27
  }>]>;
30
28
  export type Source = Schema.Schema.Type<typeof Source>;
29
+ export type AspectRatio = `${number}:${number}`;
30
+ export declare const AspectRatio: Schema.declare<`${number}:${number}`, `${number}:${number}`>;
31
31
  export declare const Kind: Schema.Literals<readonly ["image", "video", "audio", "document", "other"]>;
32
32
  export type Kind = Schema.Schema.Type<typeof Kind>;
33
33
  export declare const kindOf: (mediaType: string) => Kind;
@@ -65,6 +65,8 @@ export declare class Asset {
65
65
  /** Epoch milliseconds after which a `url` source stops resolving. */
66
66
  readonly expiresAt?: number;
67
67
  readonly providerMetadata?: ProviderMetadata;
68
+ /** Transient download credentials for `url` sources; see `Asset.Input.headers`. */
69
+ readonly headers?: Record<string, string>;
68
70
  constructor(input: Asset.Input);
69
71
  /** Inline payload without effects, for protocols that embed base64 or data URLs directly. */
70
72
  inline(): Inline | undefined;
@@ -84,9 +86,6 @@ export declare class Asset {
84
86
  } | {
85
87
  readonly type: "url";
86
88
  readonly url: string;
87
- readonly headers?: {
88
- readonly [x: string]: string;
89
- } | undefined;
90
89
  readonly mediaType?: string | undefined;
91
90
  readonly expiresAt?: number | undefined;
92
91
  } | {
@@ -122,6 +121,12 @@ export declare namespace Asset {
122
121
  readonly source: Source;
123
122
  readonly info?: Info;
124
123
  readonly providerMetadata?: ProviderMetadata;
124
+ /**
125
+ * Headers required to download a `url` source, such as the provider API key Veo demands for its file URIs.
126
+ * They are runtime-only: never part of `source`, `toJSON()`, or `AssetSchema`, so a persisted asset cannot leak
127
+ * credentials and cannot be downloaded again after a round-trip. Call `materialize()` before persisting.
128
+ */
129
+ readonly headers?: Record<string, string>;
125
130
  }
126
131
  }
127
132
  /** JSON form of an asset: the serializable `Source` plus caller-supplied metadata. `bytes` sources encode as base64. */
@@ -140,8 +145,6 @@ export declare const AssetEncoded: Schema.Struct<{
140
145
  readonly mediaType: Schema.optional<Schema.String>;
141
146
  /** Epoch milliseconds after which the provider no longer serves the URL. */
142
147
  readonly expiresAt: Schema.optional<Schema.Number>;
143
- /** Headers required to fetch the URL, such as provider auth for Veo downloads. */
144
- readonly headers: Schema.optional<Schema.$Record<Schema.String, Schema.String>>;
145
148
  }>, Schema.Struct<{
146
149
  readonly type: Schema.Literal<"ref">;
147
150
  readonly provider: Schema.brand<Schema.String, "AI.ProviderID">;
@@ -175,8 +178,6 @@ export declare const AssetSchema: Schema.decodeTo<Schema.declare<Asset, Asset>,
175
178
  readonly mediaType: Schema.optional<Schema.String>;
176
179
  /** Epoch milliseconds after which the provider no longer serves the URL. */
177
180
  readonly expiresAt: Schema.optional<Schema.Number>;
178
- /** Headers required to fetch the URL, such as provider auth for Veo downloads. */
179
- readonly headers: Schema.optional<Schema.$Record<Schema.String, Schema.String>>;
180
181
  }>, Schema.Struct<{
181
182
  readonly type: Schema.Literal<"ref">;
182
183
  readonly provider: Schema.brand<Schema.String, "AI.ProviderID">;