@opencode/ai 0.0.0-dev-20175 → 0.0.0-dev-20180

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 CHANGED
@@ -699,7 +699,7 @@ const program = Effect.gen(function* () {
699
699
  })
700
700
  ```
701
701
 
702
- 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.
702
+ The hosted result is represented as a provider-executed tool call and a tool result whose content carries the generated image as a file. Gemini image-capable models instead emit a first-class `media` `LLMEvent` for inline image output (`response.message` then carries a `media` part). Retaining `response.message` preserves the generated image for continuation on both routes.
703
703
 
704
704
  ## Video generation
705
705
 
@@ -840,9 +840,10 @@ Provider notes:
840
840
  - **OpenAI** streams over SSE (`stream_format: "sse"`), which is also the only place it reports token usage; `tts-1`
841
841
  and `tts-1-hd` do not support SSE and stream the raw audio body instead. `pcm` is 24 kHz 16-bit mono. `language`
842
842
  and `timestamps` are not supported.
843
- - **Gemini TTS** returns raw 16-bit PCM only (`audio/L16;codec=pcm;rate=24000`), so any `format` other than `pcm`
844
- fails typed; wrap the samples yourself. Style is directed in the text, so `instructions` and `speed` fail typed.
845
- Only `gemini-3.1-flash-tts-preview` and later support streaming. Two-speaker audio goes through
843
+ - **Gemini TTS** returns the provider's default output: WAV for Gemini 3.8 TTS `generate`, raw 16-bit PCM
844
+ (`audio/L16;codec=pcm;rate=24000`) otherwise. `pcm` is the only explicit `format` it accepts, and it fails typed on
845
+ Gemini 3.8 `generate`; the route never wraps PCM as WAV. Style is directed in the text, so `instructions` and
846
+ `speed` fail typed. Only `gemini-3.1-flash-tts-preview` and later support streaming. Two-speaker audio goes through
846
847
  `providerOptions.speechConfig.multiSpeakerVoiceConfig`.
847
848
  - **ElevenLabs** requires `voice` (the path voice id) and authenticates with `xi-api-key`. `format` maps to the
848
849
  `output_format` query parameter (`mp3_44100_128`, `pcm_24000`, `wav_24000`, `opus_48000_64`);
@@ -944,6 +945,7 @@ const transcript = await generation.await({ poll: { interval: 3_000 } })
944
945
  - **`ImageClient`** — Effect service and layer for image execution, parallel to `LLMClient`.
945
946
  - **`Media`** — the shared asset type (`Media.Asset`, `Media.Source`) and constructors used by messages, tool results, and media requests.
946
947
  - **`Generation`** — provider-neutral handle for an in-flight media generation (`await`, `refresh`, `cancel`, `events`) used by queued media routes.
948
+ - **`Video.request` / `generate` / `stream` / `start` / `resume`** — queued video generation through a provider-neutral request; `VideoClient` is its Effect service and layer.
947
949
  - **`Speech.request` / `Speech.generate` / `Speech.stream`** — text-to-speech through a provider-neutral request; `SpeechClient` is its Effect service and layer.
948
950
  - **`Transcription.request` / `generate` / `stream` / `start` / `resume`** — speech-to-text over inline, streaming, and queued routes; `TranscriptionClient` is its Effect service and layer.
949
951
  - **`AIClient.layer` / `AIClient.layerWith(executor)`** — every modality client plus the request executor in one layer.
@@ -65,9 +65,10 @@ export declare class Generation<Response> {
65
65
  await(options?: AwaitOptions): Effect.Effect<Response, AIError>;
66
66
  cancel(): Effect.Effect<void, AIError>;
67
67
  /**
68
- * Status observations as a stream, ending after the first terminal observation. Each poll is bounded by the time
69
- * remaining until `poll.timeout`, so a hung status request fails the stream instead of stalling it. (`Stream.interruptWhen`
70
- * would express this directly but deadlocks under `TestClock` when the source completes while the timer sleeps.)
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.)
71
72
  */
72
73
  events(options?: AwaitOptions): Stream.Stream<Event, AIError>;
73
74
  private event;
@@ -62,9 +62,10 @@ export class Generation {
62
62
  return this.route.cancel ?? Effect.void;
63
63
  }
64
64
  /**
65
- * Status observations as a stream, ending after the first terminal observation. Each poll is bounded by the time
66
- * remaining until `poll.timeout`, so a hung status request fails the stream instead of stalling it. (`Stream.interruptWhen`
67
- * would express this directly but deadlocks under `TestClock` when the source completes while the timer sleeps.)
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.)
68
69
  */
69
70
  events(options) {
70
71
  if (this.terminal)
@@ -72,11 +73,16 @@ export class Generation {
72
73
  const timeout = Duration.fromInputUnsafe(options?.poll?.timeout ?? DEFAULT_POLL_TIMEOUT);
73
74
  return Stream.unwrap(Clock.currentTimeMillis.pipe(Effect.map((start) => {
74
75
  const deadline = start + Duration.toMillis(timeout);
75
- const refresh = Clock.currentTimeMillis.pipe(Effect.flatMap((now) => this.refresh().pipe(Effect.timeoutOrElse({
76
- duration: Duration.millis(Math.max(0, deadline - now)),
77
- orElse: () => this.timeoutError(timeout),
78
- }))));
79
- return Stream.fromEffectSchedule(refresh, this.schedule(options?.poll)).pipe(Stream.takeUntil((generation) => generation.terminal), Stream.map((generation) => generation.event()));
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()));
80
86
  })));
81
87
  }
82
88
  event() {
@@ -38,6 +38,9 @@ const queryParameters = (request) => {
38
38
  });
39
39
  };
40
40
  const fromRequest = Effect.fn("DeepgramSpeech.fromRequest")(function* (request) {
41
+ // Not in `unsupported`: that list would also reject `timestamps: false`, which asks for nothing.
42
+ if (request.timestamps === true)
43
+ return yield* route.unsupported("media.timestamps", `${route.name} does not return timestamps`);
41
44
  if (request.format !== undefined &&
42
45
  FORMATS[request.format] === undefined &&
43
46
  request.providerOptions?.encoding === undefined)
@@ -74,7 +77,7 @@ const finish = (state, context) => {
74
77
  // 7. Protocol and route
75
78
  // ---------------------------------------------------------------------------
76
79
  export const protocol = MediaProtocol.stream(route, {
77
- unsupported: ["voice", "language", "instructions", "timestamps"],
80
+ unsupported: ["voice", "language", "instructions"],
78
81
  body: { from: fromRequest },
79
82
  frames: (bytes) => bytes,
80
83
  initial: () => ({ chunks: [] }),
@@ -20,6 +20,9 @@ const decodeChunk = route.decodeFrame(GenerateContentChunk);
20
20
  // 5. Request body construction
21
21
  // ---------------------------------------------------------------------------
22
22
  const fromRequest = Effect.fn("GoogleSpeech.fromRequest")(function* (request) {
23
+ // Not in `unsupported`: that list would also reject `timestamps: false`, which asks for nothing.
24
+ if (request.timestamps === true)
25
+ return yield* route.unsupported("media.timestamps", `${route.name} does not return timestamps`);
23
26
  if (request.format === "pcm" && request.mode === "generate" && /^gemini-3\.8-.*-tts(?:-|$)/.test(request.model.id))
24
27
  return yield* route.unsupported("media.format", `${route.name} returns WAV by default for Gemini 3.8 TTS unary requests; omit the format to accept it`);
25
28
  if (request.format !== undefined && request.format !== "pcm")
@@ -66,7 +69,7 @@ const finish = (state, context) => {
66
69
  // 7. Protocol and route
67
70
  // ---------------------------------------------------------------------------
68
71
  export const protocol = MediaProtocol.stream(route, {
69
- unsupported: ["instructions", "speed", "timestamps"],
72
+ unsupported: ["instructions", "speed"],
70
73
  body: { from: fromRequest },
71
74
  frames: (bytes, context) => GeminiGenerateContent.frames(bytes, context.request.mode),
72
75
  initial: () => ({ chunks: [] }),
@@ -31,6 +31,9 @@ const decodeEvent = route.decodeFrame(SpeechStreamEvent);
31
31
  // `sse` is not supported for `tts-1` or `tts-1-hd`; those models stream the raw audio body instead.
32
32
  const supportsSse = (model) => !/^tts-1(-hd)?(-|$)/.test(model);
33
33
  const fromRequest = Effect.fn("OpenAISpeech.fromRequest")(function* (request) {
34
+ // Not in `unsupported`: that list would also reject `timestamps: false`, which asks for nothing.
35
+ if (request.timestamps === true)
36
+ return yield* route.unsupported("media.timestamps", `${route.name} does not return timestamps`);
34
37
  return MediaProtocol.json(mergeJsonRecords({
35
38
  model: request.model.id,
36
39
  input: request.text,
@@ -80,7 +83,7 @@ const finish = (state, context) => {
80
83
  // 7. Protocol and route
81
84
  // ---------------------------------------------------------------------------
82
85
  export const protocol = MediaProtocol.stream(route, {
83
- unsupported: ["language", "timestamps"],
86
+ unsupported: ["language"],
84
87
  body: { from: fromRequest },
85
88
  frames: (bytes, context) => (isSse(context.body) ? Framing.sse.frame(bytes) : bytes),
86
89
  initial: () => ({ chunks: [], done: false }),
@@ -196,7 +196,11 @@ const encode = (body, headers) => {
196
196
  apply: HttpClientRequest.bodyFormData(body.value),
197
197
  };
198
198
  };
199
- /** Common fields are never silently dropped: a present field the protocol declared unsupported fails typed. */
199
+ /**
200
+ * Common fields are never silently dropped: a present field the protocol declared unsupported fails typed. `false`
201
+ * counts as present because some booleans mean something when false (video `audio`); protocols reject opt-in
202
+ * booleans such as speech `timestamps` with `=== true` in `body.from` instead of listing them.
203
+ */
200
204
  const rejectUnsupported = (route, provider, request, unsupported) => {
201
205
  const present = (unsupported ?? []).filter((field) => {
202
206
  const value = request[field];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
- "version": "0.0.0-dev-20175",
3
+ "version": "0.0.0-dev-20180",
4
4
  "name": "@opencode/ai",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -34,7 +34,7 @@
34
34
  "devDependencies": {
35
35
  "@clack/prompts": "1.0.0-alpha.1",
36
36
  "@effect/platform-node": "4.0.0-rc.112",
37
- "@opencode/http-recorder": "0.0.0-dev-20175",
37
+ "@opencode/http-recorder": "0.0.0-dev-20180",
38
38
  "@tsconfig/bun": "1.0.9",
39
39
  "@types/bun": "1.4.0",
40
40
  "@typescript/native-preview": "7.0.0-dev.20251207.1",
@@ -44,7 +44,7 @@
44
44
  "@aws-sdk/credential-providers": "3.1057.0",
45
45
  "@smithy/eventstream-codec": "4.2.14",
46
46
  "@smithy/util-utf8": "4.2.2",
47
- "@opencode/schema": "0.0.0-dev-20175",
47
+ "@opencode/schema": "0.0.0-dev-20180",
48
48
  "aws4fetch": "1.0.20",
49
49
  "effect": "4.0.0-rc.112",
50
50
  "google-auth-library": "10.5.0"