@opencode/ai 2.0.15 → 2.0.17

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 (224) hide show
  1. package/README.md +441 -99
  2. package/dist/ai-client.d.ts +8 -0
  3. package/dist/ai-client.js +12 -0
  4. package/dist/experimental/evaluation-client.d.ts +3 -3
  5. package/dist/experimental/evaluation-client.js +1 -1
  6. package/dist/experimental/evaluation.js +1 -1
  7. package/dist/generation.d.ts +39 -29
  8. package/dist/generation.js +62 -36
  9. package/dist/image-client.d.ts +59 -17
  10. package/dist/image-client.js +16 -24
  11. package/dist/image.d.ts +394 -55
  12. package/dist/image.js +48 -57
  13. package/dist/index.d.ts +15 -2
  14. package/dist/index.js +10 -0
  15. package/dist/llm.d.ts +7 -5
  16. package/dist/llm.js +10 -4
  17. package/dist/media-client.d.ts +30 -0
  18. package/dist/media-client.js +51 -0
  19. package/dist/media-model.d.ts +43 -0
  20. package/dist/media-model.js +47 -0
  21. package/dist/media.d.ts +10 -9
  22. package/dist/media.js +11 -12
  23. package/dist/promise.d.ts +473 -15
  24. package/dist/promise.js +71 -13
  25. package/dist/protocols/alibaba-chat.d.ts +12 -0
  26. package/dist/protocols/alibaba-chat.js +4 -1
  27. package/dist/protocols/alibaba-messages.d.ts +1 -1
  28. package/dist/protocols/alibaba-messages.js +6 -4
  29. package/dist/protocols/alibaba-responses.d.ts +2 -2
  30. package/dist/protocols/anthropic-messages.d.ts +34 -34
  31. package/dist/protocols/anthropic-messages.js +14 -9
  32. package/dist/protocols/assemblyai-transcription.d.ts +41 -0
  33. package/dist/protocols/assemblyai-transcription.js +136 -0
  34. package/dist/protocols/bedrock-converse.d.ts +7 -0
  35. package/dist/protocols/bedrock-converse.js +39 -19
  36. package/dist/protocols/bfl-images.d.ts +38 -0
  37. package/dist/protocols/bfl-images.js +153 -0
  38. package/dist/protocols/cartesia-speech.d.ts +143 -0
  39. package/dist/protocols/cartesia-speech.js +120 -0
  40. package/dist/protocols/deepgram-speech.d.ts +135 -0
  41. package/dist/protocols/deepgram-speech.js +91 -0
  42. package/dist/protocols/deepgram-transcription.d.ts +26 -0
  43. package/dist/protocols/deepgram-transcription.js +125 -0
  44. package/dist/protocols/elevenlabs-speech.d.ts +138 -0
  45. package/dist/protocols/elevenlabs-speech.js +111 -0
  46. package/dist/protocols/fal-images.d.ts +25 -0
  47. package/dist/protocols/fal-images.js +104 -0
  48. package/dist/protocols/fal-video.d.ts +29 -0
  49. package/dist/protocols/fal-video.js +73 -0
  50. package/dist/protocols/gemini.d.ts +22 -22
  51. package/dist/protocols/gemini.js +19 -36
  52. package/dist/protocols/google-images.d.ts +3 -3
  53. package/dist/protocols/google-images.js +12 -34
  54. package/dist/protocols/google-speech.d.ts +146 -0
  55. package/dist/protocols/google-speech.js +88 -0
  56. package/dist/protocols/google-transcription.d.ts +174 -0
  57. package/dist/protocols/google-transcription.js +126 -0
  58. package/dist/protocols/google-video.d.ts +26 -0
  59. package/dist/protocols/google-video.js +142 -0
  60. package/dist/protocols/meta-images.d.ts +3 -4
  61. package/dist/protocols/meta-images.js +13 -29
  62. package/dist/protocols/meta-messages.d.ts +5 -5
  63. package/dist/protocols/meta-responses.d.ts +4 -4
  64. package/dist/protocols/meta-responses.js +6 -4
  65. package/dist/protocols/open-responses.d.ts +10 -9
  66. package/dist/protocols/open-responses.js +4 -9
  67. package/dist/protocols/openai-chat.d.ts +84 -0
  68. package/dist/protocols/openai-chat.js +28 -17
  69. package/dist/protocols/openai-compatible-chat.d.ts +12 -0
  70. package/dist/protocols/openai-compatible-responses.d.ts +2 -2
  71. package/dist/protocols/openai-images.d.ts +130 -7
  72. package/dist/protocols/openai-images.js +143 -91
  73. package/dist/protocols/openai-responses.d.ts +20 -20
  74. package/dist/protocols/openai-responses.js +31 -31
  75. package/dist/protocols/openai-speech.d.ts +136 -0
  76. package/dist/protocols/openai-speech.js +97 -0
  77. package/dist/protocols/openai-transcription.d.ts +211 -0
  78. package/dist/protocols/openai-transcription.js +202 -0
  79. package/dist/protocols/replicate-images.d.ts +28 -0
  80. package/dist/protocols/replicate-images.js +127 -0
  81. package/dist/protocols/runway-video.d.ts +38 -0
  82. package/dist/protocols/runway-video.js +140 -0
  83. package/dist/protocols/shared.d.ts +21 -17
  84. package/dist/protocols/shared.js +27 -36
  85. package/dist/protocols/stability-images.d.ts +39 -0
  86. package/dist/protocols/stability-images.js +136 -0
  87. package/dist/protocols/utils/fal-queue.d.ts +26 -0
  88. package/dist/protocols/utils/fal-queue.js +67 -0
  89. package/dist/protocols/utils/gemini-generate-content.d.ts +65 -0
  90. package/dist/protocols/utils/gemini-generate-content.js +65 -0
  91. package/dist/protocols/utils/gemini-json-schema.d.ts +3 -0
  92. package/dist/protocols/utils/gemini-json-schema.js +76 -0
  93. package/dist/protocols/utils/media-input.d.ts +23 -1
  94. package/dist/protocols/utils/media-input.js +40 -0
  95. package/dist/protocols/utils/responses-checkpoint.js +3 -7
  96. package/dist/protocols/utils/responses-compaction.d.ts +3 -1
  97. package/dist/protocols/utils/responses-compaction.js +16 -3
  98. package/dist/protocols/utils/speech-stream.d.ts +49 -0
  99. package/dist/protocols/utils/speech-stream.js +64 -0
  100. package/dist/protocols/utils/tool-schema.d.ts +2 -2
  101. package/dist/protocols/utils/tool-schema.js +62 -19
  102. package/dist/protocols/xai-images.d.ts +4 -4
  103. package/dist/protocols/xai-images.js +14 -40
  104. package/dist/protocols/xai-responses.d.ts +2 -2
  105. package/dist/protocols/xai-responses.js +1 -1
  106. package/dist/protocols/xai-video.d.ts +34 -0
  107. package/dist/protocols/xai-video.js +141 -0
  108. package/dist/protocols/zai-chat.d.ts +13 -1
  109. package/dist/protocols/zai-images.d.ts +2 -2
  110. package/dist/protocols/zai-images.js +11 -14
  111. package/dist/protocols/zai-messages.d.ts +1 -1
  112. package/dist/provider-error.js +10 -1
  113. package/dist/providers/alibaba.d.ts +15 -3
  114. package/dist/providers/amazon-bedrock-mantle.d.ts +14 -2
  115. package/dist/providers/amazon-bedrock.d.ts +2 -0
  116. package/dist/providers/amazon-bedrock.js +1 -0
  117. package/dist/providers/anthropic-compatible.d.ts +5 -5
  118. package/dist/providers/anthropic.d.ts +5 -5
  119. package/dist/providers/assemblyai.d.ts +25 -0
  120. package/dist/providers/assemblyai.js +24 -0
  121. package/dist/providers/azure.d.ts +20 -8
  122. package/dist/providers/azure.js +2 -2
  123. package/dist/providers/baseten.d.ts +24 -0
  124. package/dist/providers/black-forest-labs.d.ts +25 -0
  125. package/dist/providers/black-forest-labs.js +23 -0
  126. package/dist/providers/cartesia.d.ts +24 -0
  127. package/dist/providers/cartesia.js +17 -0
  128. package/dist/providers/cerebras.d.ts +24 -0
  129. package/dist/providers/cloudflare-ai-gateway.d.ts +42 -18
  130. package/dist/providers/cloudflare-workers-ai.d.ts +24 -0
  131. package/dist/providers/deepgram.d.ts +29 -0
  132. package/dist/providers/deepgram.js +25 -0
  133. package/dist/providers/deepinfra.d.ts +24 -0
  134. package/dist/providers/deepseek.d.ts +24 -0
  135. package/dist/providers/elevenlabs.d.ts +24 -0
  136. package/dist/providers/elevenlabs.js +23 -0
  137. package/dist/providers/fal.d.ts +29 -0
  138. package/dist/providers/fal.js +26 -0
  139. package/dist/providers/fireworks.d.ts +24 -0
  140. package/dist/providers/google-vertex-chat.d.ts +12 -0
  141. package/dist/providers/google-vertex-messages.d.ts +5 -5
  142. package/dist/providers/google-vertex-responses.d.ts +2 -2
  143. package/dist/providers/google-vertex.d.ts +6 -6
  144. package/dist/providers/google.d.ts +21 -6
  145. package/dist/providers/google.js +13 -9
  146. package/dist/providers/groq.d.ts +24 -0
  147. package/dist/providers/index.d.ts +9 -0
  148. package/dist/providers/index.js +9 -0
  149. package/dist/providers/meta.d.ts +22 -10
  150. package/dist/providers/meta.js +4 -8
  151. package/dist/providers/minimax.d.ts +19 -7
  152. package/dist/providers/moonshot.d.ts +19 -7
  153. package/dist/providers/moonshot.js +3 -3
  154. package/dist/providers/openai-compatible-responses.d.ts +2 -2
  155. package/dist/providers/openai-compatible.d.ts +12 -0
  156. package/dist/providers/openai-options.d.ts +3 -9
  157. package/dist/providers/openai-options.js +4 -7
  158. package/dist/providers/openai.d.ts +33 -12
  159. package/dist/providers/openai.js +17 -9
  160. package/dist/providers/opencode-zen.js +1 -1
  161. package/dist/providers/openrouter.d.ts +54 -7
  162. package/dist/providers/openrouter.js +10 -5
  163. package/dist/providers/replicate.d.ts +25 -0
  164. package/dist/providers/replicate.js +17 -0
  165. package/dist/providers/runway.d.ts +24 -0
  166. package/dist/providers/runway.js +17 -0
  167. package/dist/providers/stability.d.ts +28 -0
  168. package/dist/providers/stability.js +18 -0
  169. package/dist/providers/togetherai.d.ts +24 -0
  170. package/dist/providers/typesafe-ai.js +1 -1
  171. package/dist/providers/vercel-ai-gateway.js +1 -1
  172. package/dist/providers/xai.d.ts +17 -0
  173. package/dist/providers/xai.js +7 -9
  174. package/dist/providers/zai-coding-plan.d.ts +16 -4
  175. package/dist/providers/zai.d.ts +13 -1
  176. package/dist/providers/zai.js +4 -8
  177. package/dist/route/auth.d.ts +5 -2
  178. package/dist/route/auth.js +20 -13
  179. package/dist/route/client.d.ts +9 -7
  180. package/dist/route/client.js +6 -8
  181. package/dist/route/endpoint.d.ts +1 -0
  182. package/dist/route/endpoint.js +2 -2
  183. package/dist/route/executor-service.d.ts +4 -2
  184. package/dist/route/executor-service.js +2 -1
  185. package/dist/route/executor.d.ts +3 -1
  186. package/dist/route/executor.js +7 -0
  187. package/dist/route/framing.d.ts +15 -2
  188. package/dist/route/framing.js +53 -4
  189. package/dist/route/index.d.ts +1 -1
  190. package/dist/route/media-protocol.d.ts +145 -18
  191. package/dist/route/media-protocol.js +96 -28
  192. package/dist/route/media.d.ts +54 -9
  193. package/dist/route/media.js +194 -38
  194. package/dist/route/protocol.d.ts +3 -1
  195. package/dist/schema/events.d.ts +0 -6
  196. package/dist/schema/messages.d.ts +0 -3
  197. package/dist/schema/options.d.ts +10 -6
  198. package/dist/schema/options.js +10 -5
  199. package/dist/speech-client.d.ts +66 -0
  200. package/dist/speech-client.js +21 -0
  201. package/dist/speech.d.ts +1307 -0
  202. package/dist/speech.js +124 -0
  203. package/dist/testing.d.ts +2 -2
  204. package/dist/transcription-client.d.ts +82 -0
  205. package/dist/transcription-client.js +13 -0
  206. package/dist/transcription.d.ts +1492 -0
  207. package/dist/transcription.js +127 -0
  208. package/dist/utils/bytes.d.ts +1 -0
  209. package/dist/utils/bytes.js +10 -0
  210. package/dist/utils/json.d.ts +4 -0
  211. package/dist/utils/json.js +4 -0
  212. package/dist/utils/media-type.d.ts +3 -1
  213. package/dist/utils/media-type.js +25 -2
  214. package/dist/video-client.d.ts +59 -0
  215. package/dist/video-client.js +20 -0
  216. package/dist/video.d.ts +1351 -0
  217. package/dist/video.js +118 -0
  218. package/package.json +3 -3
  219. package/dist/protocols/utils/gemini-tool-schema.d.ts +0 -2
  220. package/dist/protocols/utils/gemini-tool-schema.js +0 -103
  221. package/dist/protocols/utils/meta-image.d.ts +0 -2
  222. package/dist/protocols/utils/meta-image.js +0 -13
  223. package/dist/protocols/utils/openai-image.d.ts +0 -5
  224. package/dist/protocols/utils/openai-image.js +0 -18
@@ -0,0 +1,8 @@
1
+ import { Layer } from "effect";
2
+ import { RequestExecutor } from "./route/executor.js";
3
+ /** Every modality client over `executor`, which stays in the output so `asset.bytes()` and `Media.write` resolve. */
4
+ export declare const layerWith: <E, R>(executor: Layer.Layer<RequestExecutor.Service, E, R>) => Layer.Layer<import("./route/executor-service.js").RequestExecutorService | import("./route/client.js").LLMClientService | import("./image-client.js").ImageClientService | import("./speech-client.js").SpeechClientService | import("./transcription-client.js").TranscriptionClientService | import("./video-client.js").VideoClientService, E, R>;
5
+ /** Every modality client plus the executor over `RequestExecutor.fetchLayer`: the one layer most programs need. */
6
+ export declare const layer: Layer.Layer<import("./route/executor-service.js").RequestExecutorService | import("./route/client.js").LLMClientService | import("./image-client.js").ImageClientService | import("./speech-client.js").SpeechClientService | import("./transcription-client.js").TranscriptionClientService | import("./video-client.js").VideoClientService, never, never>;
7
+ export type Services = Layer.Success<typeof layer>;
8
+ export * as AIClient from "./ai-client.js";
@@ -0,0 +1,12 @@
1
+ import { Layer } from "effect";
2
+ import { ImageClient } from "./image-client.js";
3
+ import { LLMClient } from "./route/client.js";
4
+ import { RequestExecutor } from "./route/executor.js";
5
+ import { SpeechClient } from "./speech-client.js";
6
+ import { TranscriptionClient } from "./transcription-client.js";
7
+ import { VideoClient } from "./video-client.js";
8
+ /** Every modality client over `executor`, which stays in the output so `asset.bytes()` and `Media.write` resolve. */
9
+ export const layerWith = (executor) => Layer.mergeAll(LLMClient.layer, ImageClient.layer, VideoClient.layer, SpeechClient.layer, TranscriptionClient.layer).pipe(Layer.provideMerge(executor));
10
+ /** Every modality client plus the executor over `RequestExecutor.fetchLayer`: the one layer most programs need. */
11
+ export const layer = layerWith(RequestExecutor.fetchLayer);
12
+ export * as AIClient from "./ai-client.js";
@@ -11,11 +11,11 @@ export declare class Service extends Service_base {
11
11
  }
12
12
  export declare const evaluate: <Options extends EvaluationOptions, const Questions extends EvaluationQuestions>(request: EvaluationRequestFor<Options, Questions>) => Effect.Effect<EvaluationResponseFor<Questions>, AIError, Service>;
13
13
  export declare const layer: Layer.Layer<Service, never, RequestExecutor.Service>;
14
- export declare const fetchLayer: Layer.Layer<Service, never, never>;
14
+ export declare const fetchLayer: Layer.Layer<import("../route/executor-service.js").RequestExecutorService | Service, never, never>;
15
15
  export declare const EvaluationClient: {
16
16
  readonly Service: typeof Service;
17
- readonly layer: Layer.Layer<Service, never, RequestExecutor.Service>;
18
- readonly fetchLayer: Layer.Layer<Service, never, never>;
17
+ readonly layer: Layer.Layer<Service, never, import("../route/executor-service.js").RequestExecutorService>;
18
+ readonly fetchLayer: Layer.Layer<import("../route/executor-service.js").RequestExecutorService | Service, never, never>;
19
19
  readonly evaluate: <Options extends EvaluationOptions, const Questions extends EvaluationQuestions>(request: EvaluationRequestFor<Options, Questions>) => Effect.Effect<EvaluationResponseFor<Questions>, AIError, Service>;
20
20
  };
21
21
  export {};
@@ -55,7 +55,7 @@ export const layer = Layer.effect(Service, Effect.gen(function* () {
55
55
  })),
56
56
  });
57
57
  }));
58
- export const fetchLayer = layer.pipe(Layer.provide(RequestExecutor.fetchLayer));
58
+ export const fetchLayer = layer.pipe(Layer.provideMerge(RequestExecutor.fetchLayer));
59
59
  export const EvaluationClient = {
60
60
  Service,
61
61
  layer,
@@ -94,7 +94,7 @@ export function request(input) {
94
94
  return new EvaluationRequest({
95
95
  ...input,
96
96
  model: input.model,
97
- http: input.http === undefined ? undefined : HttpOptions.make(input.http),
97
+ http: HttpOptions.make(input.http),
98
98
  });
99
99
  }
100
100
  export function run(input) {
@@ -1,4 +1,4 @@
1
- import { Duration, Effect, Schedule, Schema, Stream } from "effect";
1
+ import { Duration, Effect, Schema, Stream } from "effect";
2
2
  import { AIError } from "./schema/errors.js";
3
3
  export declare const Status: Schema.Literals<readonly ["queued", "running", "completed", "failed", "cancelled", "expired"]>;
4
4
  export type Status = Schema.Schema.Type<typeof Status>;
@@ -9,61 +9,71 @@ export interface Snapshot {
9
9
  /** Normalized 0..1 when the provider reports progress. */
10
10
  readonly progress?: number;
11
11
  readonly position?: number;
12
- readonly expiresAt?: number;
13
12
  }
14
13
  /**
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`.
14
+ * Route-owned generation operations for one generation. The media route decodes its serializable token once (from the
15
+ * submission response or a `resume` input) and closes over it, so `Generation` never sees the token's shape.
17
16
  */
18
17
  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>;
22
- /** Provider polling hint (e.g. `openai-poll-after-ms`) that overrides the default interval for the next poll. */
23
- readonly pollHint?: (snapshot: Snapshot) => Duration.Duration | undefined;
18
+ readonly status: Effect.Effect<Snapshot, AIError>;
19
+ readonly result: Effect.Effect<Response, AIError>;
20
+ readonly cancel?: Effect.Effect<void, AIError>;
24
21
  }
25
22
  export interface Poll {
26
23
  readonly interval?: Duration.Input;
27
24
  readonly timeout?: Duration.Input;
28
- /** Full override of the polling schedule; `interval` and `pollHint` are ignored when supplied. */
29
- readonly schedule?: Schedule.Schedule<unknown, Snapshot>;
25
+ }
26
+ export interface AwaitOptions {
27
+ readonly poll?: Poll;
30
28
  }
31
29
  export declare const DEFAULT_POLL_INTERVAL: Duration.Duration;
32
30
  export declare const DEFAULT_POLL_TIMEOUT: Duration.Duration;
33
- export type Event = {
34
- readonly type: "generation-queued";
35
- readonly id: string;
36
- readonly position?: number;
37
- } | {
38
- readonly type: "generation-progress";
39
- readonly id: string;
40
- readonly progress?: number;
41
- } | {
31
+ export declare const QueuedEvent: Schema.Struct<{
32
+ readonly type: Schema.tag<"generation-queued">;
33
+ readonly id: Schema.String;
34
+ readonly position: Schema.optional<Schema.Number>;
35
+ }>;
36
+ export declare const ProgressEvent: Schema.Struct<{
37
+ readonly type: Schema.tag<"generation-progress">;
38
+ readonly id: Schema.String;
39
+ readonly progress: Schema.optional<Schema.Number>;
40
+ }>;
41
+ export type Observation = Schema.Schema.Type<typeof QueuedEvent> | Schema.Schema.Type<typeof ProgressEvent>;
42
+ export type Event = Observation | {
42
43
  readonly type: "generation-finished";
43
44
  readonly id: string;
44
45
  readonly status: Status;
45
46
  };
47
+ export declare const isTerminal: (status: Status) => boolean;
46
48
  export declare class Generation<Response> {
47
49
  readonly route: Route<Response>;
50
+ /** Route-owned serializable JSON; pass it to the modality's `resume` from another process. */
48
51
  readonly token: unknown;
49
52
  readonly id: string;
50
53
  readonly status: Status;
51
54
  readonly progress?: number;
52
55
  readonly position?: number;
53
- readonly expiresAt?: number;
54
- constructor(route: Route<Response>, token: unknown, snapshot: Snapshot);
56
+ constructor(route: Route<Response>,
57
+ /** Route-owned serializable JSON; pass it to the modality's `resume` from another process. */
58
+ token: unknown, snapshot: Snapshot);
55
59
  get snapshot(): Snapshot;
56
60
  get terminal(): boolean;
57
61
  refresh(): Effect.Effect<Generation<Response>, AIError>;
62
+ /** Fetch the result without polling; non-completed terminal generations fail with the provider's terminal body. */
63
+ result(): Effect.Effect<Response, AIError>;
58
64
  /** 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>;
65
+ await(options?: AwaitOptions): Effect.Effect<Response, AIError>;
62
66
  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>;
67
+ /**
68
+ * Status observations as a stream, ending after the first terminal observation. Each poll and each sleep between polls
69
+ * is bounded by the time remaining until `poll.timeout`, so a hung status request or a long interval fails the stream at
70
+ * the deadline instead of stalling it. (`Stream.interruptWhen` would express this directly but deadlocks under
71
+ * `TestClock` when the source completes while the timer sleeps.)
72
+ */
73
+ events(options?: AwaitOptions): Stream.Stream<Event, AIError>;
74
+ private event;
75
+ private timeoutError;
67
76
  private poll;
68
77
  private schedule;
69
78
  }
79
+ export declare const resultEvents: <Response, A>(generation: Generation<Response>, expand: (response: Response) => ReadonlyArray<A>, options?: AwaitOptions) => Stream.Stream<Observation | A, AIError>;
@@ -1,9 +1,20 @@
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);
5
5
  export const DEFAULT_POLL_TIMEOUT = Duration.minutes(10);
6
+ export const QueuedEvent = Schema.Struct({
7
+ type: Schema.tag("generation-queued"),
8
+ id: Schema.String,
9
+ position: Schema.optional(Schema.Number),
10
+ }).annotate({ identifier: "Generation.Event.Queued" });
11
+ export const ProgressEvent = Schema.Struct({
12
+ type: Schema.tag("generation-progress"),
13
+ id: Schema.String,
14
+ progress: Schema.optional(Schema.Number),
15
+ }).annotate({ identifier: "Generation.Event.Progress" });
6
16
  const TERMINAL = new Set(["completed", "failed", "cancelled", "expired"]);
17
+ export const isTerminal = (status) => TERMINAL.has(status);
7
18
  export class Generation {
8
19
  route;
9
20
  token;
@@ -11,15 +22,15 @@ export class Generation {
11
22
  status;
12
23
  progress;
13
24
  position;
14
- expiresAt;
15
- constructor(route, token, snapshot) {
25
+ constructor(route,
26
+ /** Route-owned serializable JSON; pass it to the modality's `resume` from another process. */
27
+ token, snapshot) {
16
28
  this.route = route;
17
29
  this.token = token;
18
30
  this.id = snapshot.id;
19
31
  this.status = snapshot.status;
20
32
  this.progress = snapshot.progress;
21
33
  this.position = snapshot.position;
22
- this.expiresAt = snapshot.expiresAt;
23
34
  }
24
35
  get snapshot() {
25
36
  return {
@@ -27,14 +38,17 @@ export class Generation {
27
38
  status: this.status,
28
39
  progress: this.progress,
29
40
  position: this.position,
30
- expiresAt: this.expiresAt,
31
41
  };
32
42
  }
33
43
  get terminal() {
34
- return TERMINAL.has(this.status);
44
+ return isTerminal(this.status);
35
45
  }
36
46
  refresh() {
37
- return this.route.status(this.token).pipe(Effect.map((snapshot) => new Generation(this.route, this.token, snapshot)));
47
+ return this.route.status.pipe(Effect.map((snapshot) => new Generation(this.route, this.token, snapshot)));
48
+ }
49
+ /** Fetch the result without polling; non-completed terminal generations fail with the provider's terminal body. */
50
+ result() {
51
+ return this.route.result;
38
52
  }
39
53
  /** Poll until the generation reaches a terminal status, then fetch the result. Fails with a `Timeout` reason on deadline. */
40
54
  await(options) {
@@ -42,43 +56,55 @@ export class Generation {
42
56
  const settled = this.terminal ? Effect.succeed(this) : this.poll(options?.poll);
43
57
  return settled.pipe(
44
58
  // 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
- }));
59
+ Effect.flatMap((generation) => generation.result()), Effect.timeoutOrElse({ duration: timeout, orElse: () => this.timeoutError(timeout) }));
54
60
  }
55
61
  cancel() {
56
- return this.route.cancel?.(this.token) ?? Effect.void;
62
+ return this.route.cancel ?? Effect.void;
57
63
  }
58
- /** Status observations as a stream, ending after the first terminal observation. */
64
+ /**
65
+ * Status observations as a stream, ending after the first terminal observation. Each poll and each sleep between polls
66
+ * is bounded by the time remaining until `poll.timeout`, so a hung status request or a long interval fails the stream at
67
+ * the deadline instead of stalling it. (`Stream.interruptWhen` would express this directly but deadlocks under
68
+ * `TestClock` when the source completes while the timer sleeps.)
69
+ */
59
70
  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
- }));
71
+ if (this.terminal)
72
+ return Stream.make(this.event());
73
+ const timeout = Duration.fromInputUnsafe(options?.poll?.timeout ?? DEFAULT_POLL_TIMEOUT);
74
+ return Stream.unwrap(Clock.currentTimeMillis.pipe(Effect.map((start) => {
75
+ const deadline = start + Duration.toMillis(timeout);
76
+ // Fail before polling once the deadline has passed: a fast status request could otherwise win the zero-budget
77
+ // race and schedule another zero-delay poll.
78
+ const refresh = Clock.currentTimeMillis.pipe(Effect.flatMap((now) => now >= deadline
79
+ ? this.timeoutError(timeout)
80
+ : this.refresh().pipe(Effect.timeoutOrElse({
81
+ duration: Duration.millis(deadline - now),
82
+ orElse: () => this.timeoutError(timeout),
83
+ }))));
84
+ const schedule = this.schedule(options?.poll).pipe(Schedule.modifyDelay((meta) => Effect.succeed(Duration.min(meta.duration, Duration.millis(Math.max(0, deadline - meta.now))))));
85
+ return Stream.fromEffectSchedule(refresh, schedule).pipe(Stream.takeUntil((generation) => generation.terminal), Stream.map((generation) => generation.event()));
86
+ })));
87
+ }
88
+ event() {
89
+ if (this.terminal)
90
+ return { type: "generation-finished", id: this.id, status: this.status };
91
+ if (this.status === "queued")
92
+ return { type: "generation-queued", id: this.id, position: this.position };
93
+ return { type: "generation-progress", id: this.id, progress: this.progress };
94
+ }
95
+ timeoutError(timeout) {
96
+ return new AIError({
97
+ reason: new TimeoutError({
98
+ message: `Generation ${this.id} did not finish within ${Duration.format(timeout)}`,
99
+ timeoutMs: Duration.toMillis(timeout),
100
+ }),
101
+ });
70
102
  }
71
103
  poll(poll) {
72
104
  return this.refresh().pipe(Effect.repeat({ schedule: this.schedule(poll), until: (generation) => generation.terminal }));
73
105
  }
74
106
  schedule(poll) {
75
- if (poll?.schedule)
76
- return poll.schedule.pipe(Schedule.setInputType());
77
- const interval = poll?.interval ?? DEFAULT_POLL_INTERVAL;
78
- const pollHint = this.route.pollHint;
79
- const spaced = Schedule.spaced(interval).pipe(Schedule.setInputType());
80
- if (!pollHint)
81
- return spaced;
82
- return spaced.pipe(Schedule.modifyDelay((metadata) => Effect.succeed(pollHint(metadata.input.snapshot) ?? interval)));
107
+ return Schedule.spaced(poll?.interval ?? DEFAULT_POLL_INTERVAL);
83
108
  }
84
109
  }
110
+ export const resultEvents = (generation, expand, options) => generation.events(options).pipe(Stream.filter((event) => event.type !== "generation-finished"), Stream.concat(Stream.fromIterableEffect(Effect.map(generation.result(), expand))));
@@ -1,21 +1,63 @@
1
- import { Context, Effect, Layer, Stream } from "effect";
2
- import { RequestExecutor } from "./route/executor.js";
3
- import type { AIError } from "./schema/index.js";
4
- import { type ImageEvent, type ImageOptions, type ImageRequestFor, type ImageResponse } from "./image.js";
5
- export interface Interface {
6
- readonly generate: <Options extends ImageOptions>(request: ImageRequestFor<Options>) => Effect.Effect<ImageResponse, AIError>;
7
- readonly stream: <Options extends ImageOptions>(request: ImageRequestFor<Options>) => Stream.Stream<ImageEvent, AIError>;
1
+ import { Context } from "effect";
2
+ import { MediaClient } from "./media-client.js";
3
+ import { type ImageEvent, type ImageRequestFor, type ImageResponse } from "./image.js";
4
+ export type Interface = MediaClient.Interface<ImageRequestFor, ImageEvent, ImageResponse>;
5
+ declare const ImageClientService_base: Context.ServiceClass<ImageClientService, "@opencode/ImageClient", Interface>;
6
+ export declare class ImageClientService extends ImageClientService_base {
8
7
  }
9
- declare const Service_base: Context.ServiceClass<Service, "@opencode/ImageClient", Interface>;
10
- export declare class Service extends Service_base {
11
- }
12
- export declare const generate: <Options extends ImageOptions>(request: ImageRequestFor<Options>) => Effect.Effect<ImageResponse, AIError, Service>;
13
- export declare const stream: <Options extends ImageOptions>(request: ImageRequestFor<Options>) => Stream.Stream<ImageEvent, AIError, Service>;
14
- export declare const layer: Layer.Layer<Service, never, RequestExecutor.Service>;
8
+ export declare const Service: typeof ImageClientService;
9
+ export type Service = ImageClientService;
15
10
  export declare const ImageClient: {
16
- readonly Service: typeof Service;
17
- readonly layer: Layer.Layer<Service, never, RequestExecutor.Service>;
18
- readonly generate: <Options extends ImageOptions>(request: ImageRequestFor<Options>) => Effect.Effect<ImageResponse, AIError, Service>;
19
- readonly stream: <Options extends ImageOptions>(request: ImageRequestFor<Options>) => Stream.Stream<ImageEvent, AIError, Service>;
11
+ readonly layer: import("effect/Layer").Layer<ImageClientService, never, import("./route/executor-service.js").RequestExecutorService>;
12
+ readonly generate: (request: ImageRequestFor, options?: import("./generation.js").AwaitOptions) => import("effect/Effect").Effect<ImageResponse, import("./index.js").AIError, ImageClientService>;
13
+ readonly stream: (request: ImageRequestFor, options?: import("./generation.js").AwaitOptions) => import("effect/Stream").Stream<import("./generation.js").Observation | {
14
+ readonly type: "image-partial";
15
+ readonly index: number;
16
+ readonly image: import("./media.js").Asset;
17
+ } | {
18
+ readonly type: "image";
19
+ readonly index: number;
20
+ readonly image: import("./media.js").Asset;
21
+ } | {
22
+ readonly type: "finish";
23
+ readonly providerMetadata?: {
24
+ readonly [x: string]: {
25
+ readonly [x: string]: unknown;
26
+ };
27
+ } | undefined;
28
+ readonly usage?: {
29
+ readonly type: "tokens";
30
+ readonly input?: number | undefined;
31
+ readonly output?: number | undefined;
32
+ readonly total?: number | undefined;
33
+ readonly details?: {
34
+ readonly [x: string]: unknown;
35
+ } | undefined;
36
+ } | {
37
+ readonly type: "seconds";
38
+ readonly seconds: number;
39
+ } | {
40
+ readonly type: "characters";
41
+ readonly characters: number;
42
+ } | {
43
+ readonly type: "credits";
44
+ readonly credits: number;
45
+ } | {
46
+ readonly type: "compute";
47
+ readonly seconds: number;
48
+ } | undefined;
49
+ readonly notices?: readonly {
50
+ readonly type: "other" | "moderated" | "filtered";
51
+ readonly message: string;
52
+ readonly providerMetadata?: {
53
+ readonly [x: string]: {
54
+ readonly [x: string]: unknown;
55
+ };
56
+ } | undefined;
57
+ }[] | undefined;
58
+ }, import("./index.js").AIError, ImageClientService>;
59
+ readonly start: (request: ImageRequestFor) => import("effect/Effect").Effect<import("./generation.js").Generation<ImageResponse>, import("./index.js").AIError, ImageClientService>;
60
+ readonly resume: (model: import("./image.js").ImageModel<import("./media-model.js").MediaModel.Options>, token: unknown) => import("effect/Effect").Effect<import("./generation.js").Generation<ImageResponse>, import("./index.js").AIError, ImageClientService>;
61
+ readonly Service: typeof ImageClientService;
20
62
  };
21
63
  export {};
@@ -1,28 +1,20 @@
1
- import { Context, Effect, Layer, Stream } from "effect";
2
- import { RequestExecutor } from "./route/executor.js";
3
- import { responseEvents, } from "./image.js";
4
- export class Service extends Context.Service()("@opencode/ImageClient") {
1
+ import { Context } from "effect";
2
+ import { MediaClient } from "./media-client.js";
3
+ import { ImageOutputEvent, ImageFinishEvent, } from "./image.js";
4
+ export class ImageClientService extends Context.Service()("@opencode/ImageClient") {
5
5
  }
6
- export const generate = (request) => Effect.gen(function* () {
7
- const client = yield* Service;
8
- return yield* client.generate(request);
9
- });
10
- export const stream = (request) => Stream.unwrap(Effect.gen(function* () {
11
- const client = yield* Service;
12
- return client.stream(request);
13
- }));
14
- export const layer = Layer.effect(Service, Effect.gen(function* () {
15
- const executor = yield* RequestExecutor.Service;
16
- const generate = (request) => request.model.route.generate(request, executor.execute);
17
- return Service.of({
18
- generate,
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))))),
21
- });
22
- }));
6
+ export const Service = ImageClientService;
23
7
  export const ImageClient = {
24
8
  Service,
25
- layer,
26
- generate,
27
- stream,
9
+ ...MediaClient.make(Service, {
10
+ modality: "image",
11
+ responseEvents: (response) => [
12
+ ...response.images.map((image, index) => ImageOutputEvent.make({ index, image })),
13
+ ImageFinishEvent.make({
14
+ usage: response.usage,
15
+ notices: response.notices,
16
+ providerMetadata: response.providerMetadata,
17
+ }),
18
+ ],
19
+ }),
28
20
  };