@opencode/ai 2.0.16 → 2.0.18
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 +187 -129
- package/dist/ai-client.d.ts +8 -0
- package/dist/ai-client.js +12 -0
- package/dist/experimental/evaluation-client.d.ts +3 -3
- package/dist/experimental/evaluation-client.js +1 -1
- package/dist/experimental/evaluation.js +1 -1
- package/dist/generation.d.ts +6 -10
- package/dist/generation.js +17 -20
- package/dist/image-client.d.ts +59 -24
- package/dist/image-client.js +16 -40
- package/dist/image.d.ts +15 -27
- package/dist/image.js +4 -15
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/llm.d.ts +7 -5
- package/dist/llm.js +10 -4
- package/dist/media-client.d.ts +30 -0
- package/dist/media-client.js +51 -0
- package/dist/media-model.d.ts +9 -10
- package/dist/media-model.js +10 -12
- package/dist/media.js +2 -2
- package/dist/promise.d.ts +68 -30
- package/dist/promise.js +52 -31
- package/dist/protocols/alibaba-chat.js +4 -1
- package/dist/protocols/alibaba-messages.d.ts +1 -1
- package/dist/protocols/alibaba-messages.js +6 -4
- package/dist/protocols/anthropic-messages.d.ts +34 -34
- package/dist/protocols/anthropic-messages.js +14 -8
- package/dist/protocols/assemblyai-transcription.d.ts +2 -1
- package/dist/protocols/assemblyai-transcription.js +14 -16
- package/dist/protocols/bedrock-converse.d.ts +7 -0
- package/dist/protocols/bedrock-converse.js +37 -11
- package/dist/protocols/bfl-images.d.ts +7 -1
- package/dist/protocols/bfl-images.js +36 -36
- package/dist/protocols/cartesia-speech.d.ts +18 -2
- package/dist/protocols/cartesia-speech.js +10 -16
- package/dist/protocols/deepgram-speech.d.ts +19 -3
- package/dist/protocols/deepgram-speech.js +11 -12
- package/dist/protocols/deepgram-transcription.d.ts +2 -1
- package/dist/protocols/deepgram-transcription.js +9 -13
- package/dist/protocols/elevenlabs-speech.d.ts +19 -3
- package/dist/protocols/elevenlabs-speech.js +9 -13
- package/dist/protocols/fal-images.d.ts +2 -1
- package/dist/protocols/fal-images.js +30 -40
- package/dist/protocols/fal-video.d.ts +2 -2
- package/dist/protocols/fal-video.js +9 -24
- package/dist/protocols/gemini.d.ts +13 -13
- package/dist/protocols/gemini.js +16 -7
- package/dist/protocols/google-images.d.ts +3 -3
- package/dist/protocols/google-images.js +11 -21
- package/dist/protocols/google-speech.d.ts +16 -0
- package/dist/protocols/google-speech.js +20 -16
- package/dist/protocols/google-transcription.d.ts +2 -1
- package/dist/protocols/google-transcription.js +8 -20
- package/dist/protocols/google-video.d.ts +2 -2
- package/dist/protocols/google-video.js +14 -30
- package/dist/protocols/meta-images.d.ts +3 -4
- package/dist/protocols/meta-images.js +12 -21
- package/dist/protocols/meta-messages.d.ts +5 -5
- package/dist/protocols/meta-responses.js +6 -4
- package/dist/protocols/open-responses.d.ts +4 -3
- package/dist/protocols/open-responses.js +4 -8
- package/dist/protocols/openai-chat.js +3 -4
- package/dist/protocols/openai-images.d.ts +7 -5
- package/dist/protocols/openai-images.js +65 -66
- package/dist/protocols/openai-responses.d.ts +10 -10
- package/dist/protocols/openai-responses.js +31 -30
- package/dist/protocols/openai-speech.d.ts +21 -1
- package/dist/protocols/openai-speech.js +11 -12
- package/dist/protocols/openai-transcription.d.ts +4 -0
- package/dist/protocols/openai-transcription.js +44 -32
- package/dist/protocols/replicate-images.js +11 -17
- package/dist/protocols/runway-video.d.ts +3 -3
- package/dist/protocols/runway-video.js +11 -17
- package/dist/protocols/shared.d.ts +11 -17
- package/dist/protocols/shared.js +11 -40
- package/dist/protocols/stability-images.d.ts +2 -1
- package/dist/protocols/stability-images.js +24 -36
- package/dist/protocols/utils/fal-queue.d.ts +1 -3
- package/dist/protocols/utils/fal-queue.js +4 -6
- package/dist/protocols/utils/media-input.d.ts +15 -1
- package/dist/protocols/utils/media-input.js +22 -0
- package/dist/protocols/utils/responses-checkpoint.js +3 -7
- package/dist/protocols/utils/responses-compaction.d.ts +3 -1
- package/dist/protocols/utils/responses-compaction.js +16 -3
- package/dist/protocols/utils/speech-stream.d.ts +3 -3
- package/dist/protocols/utils/speech-stream.js +1 -4
- package/dist/protocols/utils/tool-schema.d.ts +2 -2
- package/dist/protocols/utils/tool-schema.js +29 -9
- package/dist/protocols/xai-images.d.ts +4 -4
- package/dist/protocols/xai-images.js +14 -29
- package/dist/protocols/xai-responses.js +1 -1
- package/dist/protocols/xai-video.js +12 -18
- package/dist/protocols/zai-images.d.ts +2 -2
- package/dist/protocols/zai-images.js +11 -14
- package/dist/protocols/zai-messages.d.ts +1 -1
- package/dist/provider-error.js +7 -1
- package/dist/providers/alibaba.d.ts +1 -1
- package/dist/providers/amazon-bedrock.d.ts +2 -0
- package/dist/providers/amazon-bedrock.js +1 -0
- package/dist/providers/anthropic-compatible.d.ts +5 -5
- package/dist/providers/anthropic.d.ts +5 -5
- package/dist/providers/assemblyai.d.ts +1 -1
- package/dist/providers/assemblyai.js +5 -10
- package/dist/providers/azure.d.ts +4 -4
- package/dist/providers/azure.js +2 -2
- package/dist/providers/black-forest-labs.d.ts +1 -1
- package/dist/providers/black-forest-labs.js +5 -10
- package/dist/providers/cartesia.d.ts +1 -1
- package/dist/providers/cartesia.js +5 -10
- package/dist/providers/cloudflare-ai-gateway.d.ts +14 -14
- package/dist/providers/deepgram.d.ts +1 -1
- package/dist/providers/deepgram.js +6 -12
- package/dist/providers/elevenlabs.d.ts +1 -1
- package/dist/providers/elevenlabs.js +5 -10
- package/dist/providers/fal.d.ts +1 -1
- package/dist/providers/fal.js +5 -12
- package/dist/providers/google-vertex-messages.d.ts +5 -5
- package/dist/providers/google-vertex.d.ts +3 -3
- package/dist/providers/google.d.ts +3 -3
- package/dist/providers/google.js +7 -12
- package/dist/providers/meta.d.ts +8 -8
- package/dist/providers/meta.js +4 -8
- package/dist/providers/minimax.d.ts +5 -5
- package/dist/providers/moonshot.d.ts +5 -5
- package/dist/providers/openai-options.d.ts +3 -9
- package/dist/providers/openai-options.js +4 -7
- package/dist/providers/openai.d.ts +9 -10
- package/dist/providers/openai.js +11 -12
- package/dist/providers/opencode-zen.js +1 -1
- package/dist/providers/openrouter.d.ts +6 -7
- package/dist/providers/openrouter.js +10 -5
- package/dist/providers/replicate.d.ts +1 -1
- package/dist/providers/replicate.js +5 -10
- package/dist/providers/runway.d.ts +1 -1
- package/dist/providers/runway.js +5 -10
- package/dist/providers/stability.d.ts +1 -1
- package/dist/providers/stability.js +6 -11
- package/dist/providers/typesafe-ai.js +1 -1
- package/dist/providers/vercel-ai-gateway.js +1 -1
- package/dist/providers/xai.js +5 -10
- package/dist/providers/zai-coding-plan.d.ts +1 -1
- package/dist/providers/zai.js +4 -8
- package/dist/route/auth.d.ts +1 -1
- package/dist/route/auth.js +14 -13
- package/dist/route/client.d.ts +9 -7
- package/dist/route/client.js +6 -8
- package/dist/route/endpoint.d.ts +1 -0
- package/dist/route/endpoint.js +2 -2
- package/dist/route/executor-service.d.ts +4 -2
- package/dist/route/executor-service.js +2 -1
- package/dist/route/executor.d.ts +3 -1
- package/dist/route/executor.js +7 -0
- package/dist/route/framing.d.ts +10 -1
- package/dist/route/framing.js +45 -5
- package/dist/route/index.d.ts +1 -1
- package/dist/route/media-protocol.d.ts +49 -35
- package/dist/route/media-protocol.js +77 -65
- package/dist/route/media.d.ts +10 -16
- package/dist/route/media.js +46 -62
- package/dist/route/protocol.d.ts +3 -1
- package/dist/schema/options.d.ts +6 -3
- package/dist/schema/options.js +7 -3
- package/dist/speech-client.d.ts +62 -17
- package/dist/speech-client.js +17 -21
- package/dist/speech.d.ts +178 -21
- package/dist/speech.js +13 -8
- package/dist/testing.d.ts +2 -2
- package/dist/transcription-client.d.ts +78 -24
- package/dist/transcription-client.js +9 -40
- package/dist/transcription.d.ts +15 -27
- package/dist/transcription.js +4 -10
- package/dist/utils/json.d.ts +4 -0
- package/dist/utils/json.js +4 -0
- package/dist/utils/media-type.d.ts +2 -1
- package/dist/utils/media-type.js +3 -1
- package/dist/video-client.d.ts +55 -24
- package/dist/video-client.js +16 -36
- package/dist/video.d.ts +18 -26
- package/dist/video.js +15 -16
- package/package.json +3 -3
- package/dist/protocols/utils/meta-image.d.ts +0 -2
- package/dist/protocols/utils/meta-image.js +0 -13
- package/dist/protocols/utils/openai-image.d.ts +0 -5
- package/dist/protocols/utils/openai-image.js +0 -18
package/README.md
CHANGED
|
@@ -1,40 +1,38 @@
|
|
|
1
1
|
# @opencode/ai
|
|
2
2
|
|
|
3
|
-
Schema-first
|
|
3
|
+
Schema-first APIs for text, images, video, speech, and transcription, built with Effect.
|
|
4
4
|
|
|
5
5
|
```ts
|
|
6
|
-
import { Effect
|
|
7
|
-
import {
|
|
8
|
-
import { RequestExecutor } from "@opencode/ai/route"
|
|
6
|
+
import { Effect } from "effect"
|
|
7
|
+
import { AIClient, LLM } from "@opencode/ai"
|
|
9
8
|
import { OpenAI } from "@opencode/ai/providers"
|
|
10
9
|
|
|
11
10
|
const openai = OpenAI.configure({ apiKey: process.env.OPENAI_API_KEY })
|
|
12
11
|
|
|
13
|
-
const request = LLM.request({
|
|
14
|
-
model: openai.responses("gpt-4o-mini"), // `.chat(...)` selects the Chat Completions API instead
|
|
15
|
-
system: "You are concise.",
|
|
16
|
-
prompt: "Say hello in one short sentence.",
|
|
17
|
-
generation: { maxTokens: 40 },
|
|
18
|
-
})
|
|
19
|
-
|
|
20
12
|
const program = Effect.gen(function* () {
|
|
21
|
-
const response = yield*
|
|
13
|
+
const response = yield* LLM.generate({
|
|
14
|
+
model: openai.responses("gpt-4o-mini"), // `.chat(...)` selects the Chat Completions API instead
|
|
15
|
+
system: "You are concise.",
|
|
16
|
+
prompt: "Say hello in one short sentence.",
|
|
17
|
+
generation: { maxTokens: 40 },
|
|
18
|
+
})
|
|
22
19
|
console.log(response.text)
|
|
23
20
|
})
|
|
24
21
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
await Effect.runPromise(program.pipe(Effect.provide(llmLayer)))
|
|
22
|
+
// Every modality client plus the HTTP request executor; `AIClient.layerWith(executor)` swaps the executor.
|
|
23
|
+
await Effect.runPromise(program.pipe(Effect.provide(AIClient.layer)))
|
|
28
24
|
```
|
|
29
25
|
|
|
30
|
-
Run `
|
|
26
|
+
Run `LLM.stream(...)` instead of `generate` when you want incremental `LLMEvent`s. Both accept input or a prebuilt
|
|
27
|
+
`LLM.request(...)`. The event stream is provider-neutral — same shape across OpenAI Chat, OpenAI Responses,
|
|
28
|
+
Anthropic Messages, Gemini, Bedrock Converse, and any OpenAI-compatible deployment.
|
|
31
29
|
|
|
32
|
-
The same configured facade names image models. `Image.
|
|
33
|
-
returns `Media.Asset`s with lazily decoded bytes:
|
|
30
|
+
The same configured facade names image, video, speech, and transcription models. `Image.generate` resolves the
|
|
31
|
+
provider's image route from the model and returns `Media.Asset`s with lazily decoded bytes:
|
|
34
32
|
|
|
35
33
|
```ts
|
|
36
34
|
import { NodeFileSystem } from "@effect/platform-node"
|
|
37
|
-
import { Image,
|
|
35
|
+
import { Image, Media } from "@opencode/ai"
|
|
38
36
|
|
|
39
37
|
const image = Effect.gen(function* () {
|
|
40
38
|
const response = yield* Image.generate({
|
|
@@ -46,21 +44,38 @@ const image = Effect.gen(function* () {
|
|
|
46
44
|
yield* Media.write(response.image, "./garden.png")
|
|
47
45
|
})
|
|
48
46
|
|
|
49
|
-
// `
|
|
50
|
-
|
|
47
|
+
// `Media.file` / `Media.write` use the Effect `FileSystem` service; provide your platform's layer.
|
|
48
|
+
await Effect.runPromise(image.pipe(Effect.provide(AIClient.layer), Effect.provide(NodeFileSystem.layer)))
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Advanced: each client also has its own `layer`, which requires `RequestExecutor.Service`. Compose client layers with
|
|
52
|
+
`Layer.provideMerge`, not `Layer.provide`: `asset.bytes()`, `Media.write`, and Gemini's `media` output parts need the
|
|
53
|
+
executor too, and hiding it fails type-checking with `RequestExecutorService` left in the requirements.
|
|
54
|
+
|
|
55
|
+
To share a policy such as logging across every client, wrap the executor once with `RequestExecutor.middleware`:
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
import { RequestExecutor } from "@opencode/ai/route"
|
|
59
|
+
|
|
60
|
+
const logged = RequestExecutor.middleware((request, next) =>
|
|
61
|
+
Effect.log(`${request.method} ${request.url}`).pipe(Effect.andThen(next(request))),
|
|
62
|
+
)
|
|
51
63
|
|
|
52
|
-
|
|
64
|
+
const everything = AIClient.layerWith(logged) // or AI.make({ layer: logged })
|
|
53
65
|
```
|
|
54
66
|
|
|
55
|
-
Prefer promises? `@opencode/ai/promise` exposes the same LLM and
|
|
67
|
+
Prefer promises? `@opencode/ai/promise` exposes the same LLM and media APIs over one managed runtime, plus asset
|
|
68
|
+
helpers; `ai.file` and `ai.write` load `node:fs/promises` on first use, so no Effect `FileSystem` is needed:
|
|
56
69
|
|
|
57
70
|
```ts
|
|
58
71
|
import { AI } from "@opencode/ai/promise"
|
|
59
72
|
|
|
60
73
|
const ai = AI.make()
|
|
61
|
-
const
|
|
74
|
+
const input = { model: openai.responses("gpt-4o-mini"), prompt: "Say hello." }
|
|
75
|
+
const text = await ai.llm.generate(input)
|
|
62
76
|
const generated = await ai.image.generate({ model: openai.image("gpt-image-2"), prompt: "A lighthouse" })
|
|
63
|
-
|
|
77
|
+
await ai.write(generated.image, "./lighthouse.png") // also ai.file(path), ai.bytes(asset), ai.base64(asset), ai.materialize(asset)
|
|
78
|
+
for await (const event of ai.llm.stream(ai.llm.request(input))) {
|
|
64
79
|
// LLMEvent
|
|
65
80
|
}
|
|
66
81
|
await ai.dispose()
|
|
@@ -322,10 +337,9 @@ and `moonshot/responses`; each exports `model(modelID, settings)`.
|
|
|
322
337
|
MiniMax defaults to its Messages API and reads `MINIMAX_API_KEY` when `apiKey` is omitted:
|
|
323
338
|
|
|
324
339
|
```ts
|
|
325
|
-
import { Effect
|
|
326
|
-
import {
|
|
340
|
+
import { Effect } from "effect"
|
|
341
|
+
import { AIClient, LLM } from "@opencode/ai"
|
|
327
342
|
import { MiniMax } from "@opencode/ai/providers"
|
|
328
|
-
import { RequestExecutor } from "@opencode/ai/route"
|
|
329
343
|
|
|
330
344
|
const minimax = MiniMax.configure({ apiKey: process.env.MINIMAX_API_KEY })
|
|
331
345
|
const request = LLM.request({
|
|
@@ -335,8 +349,7 @@ const request = LLM.request({
|
|
|
335
349
|
generation: { maxTokens: 1536 },
|
|
336
350
|
})
|
|
337
351
|
|
|
338
|
-
const
|
|
339
|
-
const response = await Effect.runPromise(LLMClient.generate(request).pipe(Effect.provide(layer)))
|
|
352
|
+
const response = await Effect.runPromise(LLM.generate(request).pipe(Effect.provide(AIClient.layer)))
|
|
340
353
|
console.log(response.text)
|
|
341
354
|
```
|
|
342
355
|
|
|
@@ -405,14 +418,14 @@ Use `Image.generate` for one-off generation or editing:
|
|
|
405
418
|
import { Image, Media } from "@opencode/ai"
|
|
406
419
|
|
|
407
420
|
const generation = Image.generate({
|
|
408
|
-
model: meta("muse-image-1.0"),
|
|
421
|
+
model: meta.image("muse-image-1.0"),
|
|
409
422
|
prompt: "A flat black square on a white background.",
|
|
410
423
|
n: 1,
|
|
411
424
|
providerOptions: { reasoningStrength: "low" },
|
|
412
425
|
})
|
|
413
426
|
|
|
414
427
|
const edit = Image.generate({
|
|
415
|
-
model: meta("muse-image-1.0"),
|
|
428
|
+
model: meta.image("muse-image-1.0"),
|
|
416
429
|
prompt: "Make the square purple.",
|
|
417
430
|
images: [Media.bytes(imageBytes, "image/webp")],
|
|
418
431
|
format: "png",
|
|
@@ -459,6 +472,25 @@ const program = Effect.gen(function* () {
|
|
|
459
472
|
})
|
|
460
473
|
```
|
|
461
474
|
|
|
475
|
+
Common fields are portable in shape, not in support. Unsupported fields fail with a typed `AIError` before any network
|
|
476
|
+
call rather than being dropped, so check this table before swapping only the `model`:
|
|
477
|
+
|
|
478
|
+
| Provider | `n` | `size` | `aspectRatio` | `seed` | `format` | `images` | `mask` |
|
|
479
|
+
| --------------------- | --- | --------- | ------------- | ------ | -------- | -------------------------------- | ------------------- |
|
|
480
|
+
| OpenAI | ✓¹ | ✓ | ✗ | ✗ | ✓ | ✓ | ✓ |
|
|
481
|
+
| Google (Gemini) | 1 | ✗ | ✓ | ✓ | ✗ | ✓ (no public URLs) | ✗ |
|
|
482
|
+
| xAI | ✓ | ✗ | ✓ | ✗ | ✗ | ✓ | ✗ |
|
|
483
|
+
| Z.ai | ✗ | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ |
|
|
484
|
+
| Meta | ✓ | ✓ (hint) | ✗ | ✗ | ✓ | ✓ | ✗ |
|
|
485
|
+
| Black Forest Labs | 1 | per model | per model | ✓ | ✓ | per model (1–8) | `flux-pro-1.0-fill` |
|
|
486
|
+
| fal | ✓ | per model | per model | ✓ | ✓ | 1 (several on `/edit`, `/multi`) | ✓ |
|
|
487
|
+
| Replicate | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ (use `providerOptions`) | ✗ |
|
|
488
|
+
| Stability `image` | 1 | ✗ | ✓ | ✓ | ✓ | 1 (not on `core`) | ✗ |
|
|
489
|
+
| Stability `upscale()` | ✗ | ✗ | ✗ | ✓ | ✓ | exactly 1 (required) | ✗ |
|
|
490
|
+
|
|
491
|
+
✓ lowers natively; ✗ fails whenever the field is set (including `n: 1`); `1` means `n > 1` fails. ¹ `Image.stream` on OpenAI generates one image. fal
|
|
492
|
+
rejects `size` and `aspectRatio` together; which one a fal or BFL model takes depends on the model.
|
|
493
|
+
|
|
462
494
|
`Media.Asset` is the one asset type shared by image requests, image responses, LLM messages, and tool results.
|
|
463
495
|
`asset.source` is the serializable `Media.Source` (`bytes`, `base64`, `url`, or `ref`); `asset.bytes()`,
|
|
464
496
|
`asset.base64()`, and `asset.dataUrl()` decode or download lazily and cache; `asset.materialize()` pulls a `url`
|
|
@@ -468,9 +500,8 @@ asset into owned bytes before the provider URL expires. Construct assets with `M
|
|
|
468
500
|
Pass ordered image inputs to the same method for editing, composition, or image-conditioned generation:
|
|
469
501
|
|
|
470
502
|
```ts
|
|
471
|
-
const
|
|
472
|
-
yield
|
|
473
|
-
Image.generate({
|
|
503
|
+
const composed = Effect.gen(function* () {
|
|
504
|
+
const response = yield* Image.generate({
|
|
474
505
|
model,
|
|
475
506
|
prompt: "Combine these product photos into one studio scene",
|
|
476
507
|
images: [
|
|
@@ -481,23 +512,25 @@ const response =
|
|
|
481
512
|
providerOptions,
|
|
482
513
|
http,
|
|
483
514
|
})
|
|
515
|
+
return response.images
|
|
516
|
+
})
|
|
484
517
|
```
|
|
485
518
|
|
|
486
519
|
`Media.ref(provider, id)` represents provider file handles such as OpenAI file IDs or Gemini Files URIs; routes
|
|
487
|
-
only forward refs that belong to their own provider
|
|
488
|
-
|
|
489
|
-
|
|
520
|
+
only forward refs that belong to their own provider (OpenAI, xAI, and Gemini images accept them). No shipped route
|
|
521
|
+
returns a ref yet, and `asset.bytes()` / `materialize()` on a ref fail by design. Raw strings are not accepted as
|
|
522
|
+
image inputs, avoiding ambiguity between base64, URLs, and provider IDs. Empty or omitted `images` uses text-to-image generation; a
|
|
523
|
+
non-empty array selects the provider's edit behavior (see the table above for routes that limit the count). OpenAI
|
|
490
524
|
uses multipart for byte/data-URL edits and its JSON reference body for URL or file-ID edits. The common `mask`
|
|
491
525
|
field selects inpainting; routes that cannot honor it fail with `UnsupportedOperation`:
|
|
492
526
|
|
|
493
527
|
```ts
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
})
|
|
528
|
+
const inpainted = Image.generate({
|
|
529
|
+
model: openai.image("gpt-image-2"),
|
|
530
|
+
prompt,
|
|
531
|
+
images: [Media.bytes(sourceBytes, "image/png")],
|
|
532
|
+
mask: Media.bytes(maskBytes, "image/png"),
|
|
533
|
+
})
|
|
501
534
|
```
|
|
502
535
|
|
|
503
536
|
On multipart requests, `http.body` can override option fields but not structural `model`, `prompt`, `image[]`,
|
|
@@ -508,31 +541,31 @@ not accept image inputs. These cases fail with a typed `AIError` before network
|
|
|
508
541
|
Provider-native image options belong to each request. Raw `http.body` fields have final precedence over them:
|
|
509
542
|
|
|
510
543
|
```ts
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
})
|
|
544
|
+
const medium = Image.generate({
|
|
545
|
+
model: openai.image("gpt-image-2"),
|
|
546
|
+
prompt,
|
|
547
|
+
providerOptions: { quality: "medium" },
|
|
548
|
+
http,
|
|
549
|
+
})
|
|
518
550
|
```
|
|
519
551
|
|
|
520
552
|
xAI image models use the same request API with xAI-native controls:
|
|
521
553
|
|
|
522
554
|
```ts
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
555
|
+
import { XAI } from "@opencode/ai/providers"
|
|
556
|
+
|
|
557
|
+
const xai = Image.generate({
|
|
558
|
+
model: XAI.configure({ apiKey }).image("any-model-id"),
|
|
559
|
+
prompt,
|
|
560
|
+
n: 2,
|
|
561
|
+
aspectRatio: "16:9",
|
|
562
|
+
providerOptions: {
|
|
563
|
+
resolution: "1k",
|
|
564
|
+
responseFormat: "b64_json",
|
|
565
|
+
future_option: true,
|
|
566
|
+
},
|
|
567
|
+
http,
|
|
568
|
+
})
|
|
536
569
|
```
|
|
537
570
|
|
|
538
571
|
Google's current Gemini image models use the same direct API:
|
|
@@ -542,7 +575,7 @@ import { Google } from "@opencode/ai/providers"
|
|
|
542
575
|
|
|
543
576
|
const googleProgram = Effect.gen(function* () {
|
|
544
577
|
const response = yield* Image.generate({
|
|
545
|
-
model: Google.configure({ apiKey })("any-model-id"),
|
|
578
|
+
model: Google.configure({ apiKey }).image("any-model-id"),
|
|
546
579
|
prompt: "A robot tending a rooftop garden",
|
|
547
580
|
aspectRatio: "16:9",
|
|
548
581
|
seed: 42,
|
|
@@ -567,17 +600,18 @@ their mapped aliases, and `http.body` is the final deep overlay. The selected mo
|
|
|
567
600
|
Z.ai image models infer open Z.ai-native options from the selected model:
|
|
568
601
|
|
|
569
602
|
```ts
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
603
|
+
import { ZAI } from "@opencode/ai/providers"
|
|
604
|
+
|
|
605
|
+
const zai = Image.generate({
|
|
606
|
+
model: ZAI.configure({ apiKey }).image("any-model-id"),
|
|
607
|
+
prompt,
|
|
608
|
+
providerOptions: {
|
|
609
|
+
quality: "hd",
|
|
610
|
+
userID: "user-123",
|
|
611
|
+
future_option: true,
|
|
612
|
+
},
|
|
613
|
+
http,
|
|
614
|
+
})
|
|
581
615
|
```
|
|
582
616
|
|
|
583
617
|
Z.ai does not include trustworthy MIME metadata for output URLs, so generated images use
|
|
@@ -587,16 +621,17 @@ persist the bytes promptly if they must remain available.
|
|
|
587
621
|
### Partial images
|
|
588
622
|
|
|
589
623
|
OpenAI's GPT image models stream previews. `Image.stream` sends `stream: true` with `partialImages` (0–3, default 2)
|
|
590
|
-
and emits `image-partial` events before each final `image`; `Image.generate` keeps the plain JSON request
|
|
591
|
-
`dall-e-*` models do not stream and fail typed:
|
|
624
|
+
and emits `image-partial` events before each final `image`; `Image.generate` keeps the plain JSON request:
|
|
592
625
|
|
|
593
626
|
```ts
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
627
|
+
import { Stream } from "effect"
|
|
628
|
+
import { ImageEvent } from "@opencode/ai"
|
|
629
|
+
|
|
630
|
+
const previews = Image.stream({
|
|
631
|
+
model: openai.image("gpt-image-2"),
|
|
632
|
+
prompt: "A lighthouse at dusk",
|
|
633
|
+
providerOptions: { partialImages: 2 },
|
|
634
|
+
}).pipe(Stream.runForEach((event) => (ImageEvent.is.imagePartial(event) ? showPreview(event.image) : Effect.void)))
|
|
600
635
|
```
|
|
601
636
|
|
|
602
637
|
The provider may send fewer previews than requested when the final image is ready first.
|
|
@@ -612,13 +647,20 @@ import { BlackForestLabs, Stability } from "@opencode/ai/providers"
|
|
|
612
647
|
|
|
613
648
|
const bfl = BlackForestLabs.configure({ apiKey: process.env.BFL_API_KEY })
|
|
614
649
|
|
|
615
|
-
const
|
|
616
|
-
|
|
650
|
+
const submit = Effect.gen(function* () {
|
|
651
|
+
const generation = yield* Image.start({ model: bfl.image("flux-2-pro"), prompt, size: "1024x768" })
|
|
652
|
+
persist({ provider: "black-forest-labs", modelID: "flux-2-pro", token: generation.token })
|
|
653
|
+
})
|
|
617
654
|
|
|
618
|
-
const
|
|
619
|
-
const
|
|
655
|
+
const finish = Effect.gen(function* () {
|
|
656
|
+
const saved = load()
|
|
657
|
+
const resumed = yield* Image.resume(bfl.image(saved.modelID), saved.token)
|
|
658
|
+
return yield* resumed.await({ poll: { interval: "2 seconds" } })
|
|
659
|
+
})
|
|
620
660
|
```
|
|
621
661
|
|
|
662
|
+
The token carries no route identity, so persist the provider and model ID alongside it: `resume` needs the model.
|
|
663
|
+
|
|
622
664
|
- **Black Forest Labs** — results are downloaded before returning, because `result.sample` expires in 10 minutes.
|
|
623
665
|
- **Replicate** — inputs are model-defined, so only `prompt` lowers: sizing, count, seed, format, and files go in
|
|
624
666
|
`providerOptions` under the model's names, with files as `Media.Asset` (data URLs up to 256 KB, larger by URL).
|
|
@@ -628,12 +670,13 @@ const response = yield * resumed.await({ poll: { interval: "2 seconds" } })
|
|
|
628
670
|
|
|
629
671
|
```ts
|
|
630
672
|
const stability = Stability.configure({ apiKey: process.env.STABILITY_API_KEY })
|
|
631
|
-
const upscaled =
|
|
632
|
-
yield
|
|
633
|
-
Image.generate(
|
|
634
|
-
{ model: stability.upscale(), prompt: "A lighthouse", images: [
|
|
673
|
+
const upscaled = Effect.gen(function* () {
|
|
674
|
+
const small = yield* Media.file("./small.png")
|
|
675
|
+
return yield* Image.generate(
|
|
676
|
+
{ model: stability.upscale(), prompt: "A lighthouse", images: [small] },
|
|
635
677
|
{ poll: { interval: "5 seconds" } },
|
|
636
678
|
)
|
|
679
|
+
})
|
|
637
680
|
```
|
|
638
681
|
|
|
639
682
|
Imagen is not available: Google shut it down on the Gemini API, and Vertex discontinued the Imagen 4 models on
|
|
@@ -655,7 +698,7 @@ const program = Effect.gen(function* () {
|
|
|
655
698
|
})
|
|
656
699
|
```
|
|
657
700
|
|
|
658
|
-
The hosted result is represented as a provider-executed tool call and tool result
|
|
701
|
+
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.
|
|
659
702
|
|
|
660
703
|
## Video generation
|
|
661
704
|
|
|
@@ -666,8 +709,8 @@ Common fields (`frames`, `references`, `video`, `durationSeconds`, `aspectRatio`
|
|
|
666
709
|
under `providerOptions`, inferred from the selected model.
|
|
667
710
|
|
|
668
711
|
```ts
|
|
669
|
-
import { Video
|
|
670
|
-
import { Google } from "@opencode/ai/providers"
|
|
712
|
+
import { Video } from "@opencode/ai"
|
|
713
|
+
import { Google, Runway } from "@opencode/ai/providers"
|
|
671
714
|
|
|
672
715
|
const google = Google.configure({ apiKey: process.env.GOOGLE_GENERATIVE_AI_API_KEY })
|
|
673
716
|
|
|
@@ -696,6 +739,7 @@ const controlled = Effect.gen(function* () {
|
|
|
696
739
|
generation.id // provider operation / task / request id
|
|
697
740
|
generation.status // "queued" | "running" | "completed" | "failed" | "cancelled" | "expired"
|
|
698
741
|
generation.token // route-owned JSON: `{ operation }`, `{ requestID }`, `{ taskID }`, or fal's follow-up URLs
|
|
742
|
+
// The token carries no route identity: persist the provider and model ID alongside it, since `resume` needs the model.
|
|
699
743
|
const saved = JSON.stringify(generation.token)
|
|
700
744
|
|
|
701
745
|
const resumed = yield* Video.resume(google.video("veo-3.1-generate-preview"), JSON.parse(saved))
|
|
@@ -706,8 +750,8 @@ const controlled = Effect.gen(function* () {
|
|
|
706
750
|
const events = Video.stream({ model: Runway.configure({ apiKey }).video("gen4.5"), prompt }, { poll })
|
|
707
751
|
```
|
|
708
752
|
|
|
709
|
-
|
|
710
|
-
|
|
753
|
+
Status polls, result fetches, cancels, and asset downloads all run through the same request executor with the route's
|
|
754
|
+
auth. `Generation.await` and `Generation.events` fail with a
|
|
711
755
|
`Timeout` reason when `poll.timeout` (default 10 minutes) elapses. Failed,
|
|
712
756
|
cancelled, and expired generations fail typed with the provider's terminal document on `reason.body`; moderation
|
|
713
757
|
outcomes (Veo `raiMediaFilteredReasons`, xAI `respect_moderation`, Runway `SAFETY.*` codes) surface as `notices` when
|
|
@@ -726,15 +770,18 @@ Provider notes:
|
|
|
726
770
|
- **Runway** expects pixel ratios in `aspectRatio` for most models (`"1280:720"`), pins `X-Runway-Version`, reports
|
|
727
771
|
`usage: { type: "credits" }`, and its output URLs expire after 24–48 hours.
|
|
728
772
|
|
|
729
|
-
The promise client exposes the same surface: `ai.video.start(...)` resolves to a handle with `await`, `
|
|
730
|
-
`cancel`, and `token`; `ai.video.generate`, `ai.video.resume(model, token)`, and
|
|
731
|
-
API.
|
|
773
|
+
The promise client exposes the same surface: `ai.video.start(...)` resolves to a handle with `await`, `events`,
|
|
774
|
+
`result`, `refresh`, `cancel`, and `token`; `ai.video.generate`, `ai.video.resume(model, token)`, and
|
|
775
|
+
`ai.video.stream` mirror the Effect API. The handle's `status` and `progress` are a snapshot from when it was
|
|
776
|
+
created; `refresh()` resolves to a new handle.
|
|
732
777
|
|
|
733
778
|
```ts
|
|
734
779
|
import { ai } from "@opencode/ai/promise"
|
|
735
780
|
|
|
736
781
|
const generation = await ai.video.start({ model, prompt })
|
|
737
|
-
const
|
|
782
|
+
for await (const event of generation.events({ poll: { interval: 10_000 } })) console.log(event.type)
|
|
783
|
+
const video = await generation.result({ signal })
|
|
784
|
+
await ai.write(video.video, "./kite.mp4")
|
|
738
785
|
```
|
|
739
786
|
|
|
740
787
|
## Speech generation
|
|
@@ -786,16 +833,16 @@ ElevenLabs and Cartesia. `{ id }` selects an OpenAI custom voice (`{ id: "voice_
|
|
|
786
833
|
plain string elsewhere. There is no cross-provider voice catalog. `format` is the container-level word (`mp3`, `wav`,
|
|
787
834
|
`pcm`, `opus`, `aac`, `flac`); sample rates and bitrates live under `providerOptions`, and a value the route cannot
|
|
788
835
|
produce fails as `UnsupportedOperation`. Streams buffer every chunk so `finish` can carry the whole clip.
|
|
789
|
-
`SpeechClient.layer` needs `RequestExecutor.Service`.
|
|
790
836
|
|
|
791
837
|
Provider notes:
|
|
792
838
|
|
|
793
839
|
- **OpenAI** streams over SSE (`stream_format: "sse"`), which is also the only place it reports token usage; `tts-1`
|
|
794
840
|
and `tts-1-hd` do not support SSE and stream the raw audio body instead. `pcm` is 24 kHz 16-bit mono. `language`
|
|
795
841
|
and `timestamps` are not supported.
|
|
796
|
-
- **Gemini TTS** returns
|
|
797
|
-
|
|
798
|
-
|
|
842
|
+
- **Gemini TTS** returns the provider's default output: WAV for Gemini 3.8 TTS `generate`, raw 16-bit PCM
|
|
843
|
+
(`audio/L16;codec=pcm;rate=24000`) otherwise. `pcm` is the only explicit `format` it accepts, and it fails typed on
|
|
844
|
+
Gemini 3.8 `generate`; the route never wraps PCM as WAV. Style is directed in the text, so `instructions` and
|
|
845
|
+
`speed` fail typed. Only `gemini-3.1-flash-tts-preview` and later support streaming. Two-speaker audio goes through
|
|
799
846
|
`providerOptions.speechConfig.multiSpeakerVoiceConfig`.
|
|
800
847
|
- **ElevenLabs** requires `voice` (the path voice id) and authenticates with `xi-api-key`. `format` maps to the
|
|
801
848
|
`output_format` query parameter (`mp3_44100_128`, `pcm_24000`, `wav_24000`, `opus_48000_64`);
|
|
@@ -814,7 +861,7 @@ The promise client mirrors the Effect API; `ai.speech.stream` is an `AsyncIterab
|
|
|
814
861
|
import { ai } from "@opencode/ai/promise"
|
|
815
862
|
|
|
816
863
|
const response = await ai.speech.generate({ model, text: "Hello from OpenCode.", voice: "coral" })
|
|
817
|
-
await
|
|
864
|
+
await ai.write(response.audio, "hello.mp3")
|
|
818
865
|
|
|
819
866
|
for await (const event of ai.speech.stream({ model, text: "Hello from OpenCode.", voice: "coral" })) {
|
|
820
867
|
if (event.type === "audio-delta") player.write(event.chunk)
|
|
@@ -831,6 +878,7 @@ facades. Common fields (`language`, `prompt`, `timestamps: "none" | "segment" |
|
|
|
831
878
|
natively or fail with a typed `AIError` before any network call; a route may return more than asked.
|
|
832
879
|
|
|
833
880
|
```ts
|
|
881
|
+
import { Console, Effect, Stream } from "effect"
|
|
834
882
|
import { Media, Transcription, TranscriptionEvent } from "@opencode/ai"
|
|
835
883
|
import { AssemblyAI, Deepgram, OpenAI } from "@opencode/ai/providers"
|
|
836
884
|
|
|
@@ -857,7 +905,7 @@ const program = Effect.gen(function* () {
|
|
|
857
905
|
Stream.runDrain,
|
|
858
906
|
)
|
|
859
907
|
|
|
860
|
-
// Queued: persist the token
|
|
908
|
+
// Queued: persist the token with the provider and model ID (the token alone cannot pick the model), resume, and await.
|
|
861
909
|
const model = AssemblyAI.configure({ apiKey }).transcription("universal-3-5-pro")
|
|
862
910
|
const generation = yield* Transcription.start({ model, audio })
|
|
863
911
|
const resumed = yield* Transcription.resume(model, JSON.parse(JSON.stringify(generation.token)))
|
|
@@ -866,7 +914,7 @@ const program = Effect.gen(function* () {
|
|
|
866
914
|
```
|
|
867
915
|
|
|
868
916
|
Inline routes emit only `finish` from `stream` (no faked deltas); queued routes emit `generation-queued` /
|
|
869
|
-
`generation-progress` before it.
|
|
917
|
+
`generation-progress` before it.
|
|
870
918
|
|
|
871
919
|
Provider notes:
|
|
872
920
|
|
|
@@ -878,6 +926,7 @@ Provider notes:
|
|
|
878
926
|
The promise client mirrors the Effect API:
|
|
879
927
|
|
|
880
928
|
```ts
|
|
929
|
+
const audio = await ai.file("./call.mp3")
|
|
881
930
|
const text = (await ai.transcription.generate({ model, audio })).text
|
|
882
931
|
for await (const event of ai.transcription.stream({ model, audio })) if (event.type === "text-delta") write(event.delta)
|
|
883
932
|
const generation = await ai.transcription.start({ model: assemblyai, audio })
|
|
@@ -887,7 +936,7 @@ const transcript = await generation.await({ poll: { interval: 3_000 } })
|
|
|
887
936
|
## Public API
|
|
888
937
|
|
|
889
938
|
- **`LLM.request({...})`** — build a provider-neutral `LLMRequest`. Accepts ergonomic inputs (`system: string`, `prompt: string`) that normalize into the canonical Schema classes.
|
|
890
|
-
- **`LLM.generate` / `LLM.stream`** —
|
|
939
|
+
- **`LLM.generate` / `LLM.stream`** — run direct input or an `LLMRequest` through `LLMClient` for one-import use.
|
|
891
940
|
- **`Message.user(...)` / `Message.assistant(...)` / `Message.tool(...)`** — message constructors from the canonical schema model.
|
|
892
941
|
- **`LanguageModel.make(...)` / `ToolCallPart.make(...)` / `ToolResultPart.make(...)` / `ToolDefinition.make(...)`** — model and tool-related constructors from the canonical schema model.
|
|
893
942
|
- **`LLMEvent.is.*`** — typed guards (`is.textDelta`, `is.toolCall`, `is.finish`, …) for filtering streams.
|
|
@@ -895,9 +944,11 @@ const transcript = await generation.await({ poll: { interval: 3_000 } })
|
|
|
895
944
|
- **`ImageClient`** — Effect service and layer for image execution, parallel to `LLMClient`.
|
|
896
945
|
- **`Media`** — the shared asset type (`Media.Asset`, `Media.Source`) and constructors used by messages, tool results, and media requests.
|
|
897
946
|
- **`Generation`** — provider-neutral handle for an in-flight media generation (`await`, `refresh`, `cancel`, `events`) used by queued media routes.
|
|
947
|
+
- **`Video.request` / `generate` / `stream` / `start` / `resume`** — queued video generation through a provider-neutral request; `VideoClient` is its Effect service and layer.
|
|
898
948
|
- **`Speech.request` / `Speech.generate` / `Speech.stream`** — text-to-speech through a provider-neutral request; `SpeechClient` is its Effect service and layer.
|
|
899
949
|
- **`Transcription.request` / `generate` / `stream` / `start` / `resume`** — speech-to-text over inline, streaming, and queued routes; `TranscriptionClient` is its Effect service and layer.
|
|
900
|
-
-
|
|
950
|
+
- **`AIClient.layer` / `AIClient.layerWith(executor)`** — every modality client plus the request executor in one layer.
|
|
951
|
+
- **`@opencode/ai/promise`** — `AI.make({ layer? })` and a default `ai` client exposing `llm`, `image`, `video`, `speech`, and `transcription` as Promise / `AsyncIterable` APIs, plus `file`, `write`, `bytes`, `base64`, and `materialize` for assets.
|
|
901
952
|
|
|
902
953
|
## Testing
|
|
903
954
|
|
|
@@ -960,11 +1011,13 @@ This is different from prompt caching, server-side history storage, or truncatio
|
|
|
960
1011
|
Prefer this operation, where supported, when the application owns compaction policy and durable context updates.
|
|
961
1012
|
|
|
962
1013
|
```ts
|
|
963
|
-
const
|
|
964
|
-
const
|
|
965
|
-
|
|
1014
|
+
const compacted = Effect.gen(function* () {
|
|
1015
|
+
const result = yield* LLMClient.compact(request)
|
|
1016
|
+
const next = LLMRequest.update(request, {
|
|
1017
|
+
messages: result.replacement,
|
|
1018
|
+
})
|
|
1019
|
+
return yield* LLMClient.generate(next)
|
|
966
1020
|
})
|
|
967
|
-
const response = yield * LLMClient.generate(next)
|
|
968
1021
|
```
|
|
969
1022
|
|
|
970
1023
|
`replacement` replaces the complete input window. Do not append it to the original transcript or extract only the encrypted item: the provider may retain additional messages in its output. Retained user and assistant messages remain ordinary messages with typed text, media, or reasoning parts, in their original order. Provider-specific message IDs, status, and phase use `providerMetadata`, not a raw output array hidden in an assistant message. Unsupported returned item types fail explicitly.
|
|
@@ -980,16 +1033,16 @@ The input must still fit the model's context window. Explicit compaction is not
|
|
|
980
1033
|
OpenAI Responses also exposes a separate, explicitly selected mechanism:
|
|
981
1034
|
|
|
982
1035
|
```ts
|
|
983
|
-
const
|
|
984
|
-
yield
|
|
985
|
-
LLMClient.compact(request, {
|
|
1036
|
+
const checkpoint = Effect.gen(function* () {
|
|
1037
|
+
const result = yield* LLMClient.compact(request, {
|
|
986
1038
|
mechanism: "trigger",
|
|
987
1039
|
webSocket, // Optional: without it, the request uses HTTP/SSE.
|
|
988
1040
|
})
|
|
989
1041
|
|
|
990
|
-
result.checkpoint // Successful encrypted CompactionPart.
|
|
991
|
-
result.responseID
|
|
992
|
-
result.usage
|
|
1042
|
+
result.checkpoint // Successful encrypted CompactionPart.
|
|
1043
|
+
result.responseID
|
|
1044
|
+
result.usage
|
|
1045
|
+
})
|
|
993
1046
|
```
|
|
994
1047
|
|
|
995
1048
|
This appends a native `compaction_trigger` control item to the full input and sends a normal Responses request, with tools and instructions retained, `stream: true`, `store: false`, and parallel tool calls enabled. It removes normal-answer text/output-format controls, forced tool choices, output-token/tool-call limits, and automatic `context_management`. Body overlays cannot replace `input` or supply `previous_response_id`/`conversation`; the complete canonical history is required for safe stateless replay. Request metadata, auth, headers, query parameters, service tier, and supported prompt-cache settings are preserved.
|
|
@@ -1003,9 +1056,11 @@ The supplied WebSocket executor can reuse a compatible append baseline for the c
|
|
|
1003
1056
|
Trigger support is separate from endpoint support. Only the OpenAI Responses route advertises it; Azure, xAI, Chat, and compatible Responses routes do not inherit it. Untyped calls still fail before sending: missing route capabilities return `UnsupportedOperation`, while unknown mechanism names and invalid inputs return `InvalidRequest`. Dynamic callers must narrow for the selected mechanism:
|
|
1004
1057
|
|
|
1005
1058
|
```ts
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
}
|
|
1059
|
+
const narrowed = Effect.gen(function* () {
|
|
1060
|
+
if (LLMClient.canCompact(request, { mechanism: "trigger" })) {
|
|
1061
|
+
const result = yield* LLMClient.compact(request, { mechanism: "trigger" })
|
|
1062
|
+
}
|
|
1063
|
+
})
|
|
1009
1064
|
```
|
|
1010
1065
|
|
|
1011
1066
|
This capability describes protocol implementation, **not universal availability on OpenAI API deployments**. The host application owns subscription/deployment eligibility, OAuth, endpoint selection, and deployment-specific headers. Local protocol/socket tests do not establish live provider support.
|
|
@@ -1014,9 +1069,10 @@ This capability describes protocol implementation, **not universal availability
|
|
|
1014
1069
|
|
|
1015
1070
|
`providerOptions.contextManagement` lets the provider decide when to compact during an ordinary `generate` or `stream` call. This is an advanced option for callers that own persistence and recovery: persist the complete assistant message, including its checkpoint, before continuing. Enabling the option does not provide durable checkpoint storage, interruption recovery, or model-switch policy. Keep the prior context until a successful checkpoint has been persisted.
|
|
1016
1071
|
|
|
1017
|
-
|
|
1072
|
+
Enable OpenAI compaction with typed provider options:
|
|
1018
1073
|
|
|
1019
1074
|
```ts
|
|
1075
|
+
import { Effect } from "effect"
|
|
1020
1076
|
import { LLM, LLMClient, LLMRequest, Message } from "@opencode/ai"
|
|
1021
1077
|
import { OpenAI } from "@opencode/ai/providers"
|
|
1022
1078
|
|
|
@@ -1027,9 +1083,11 @@ const request = LLM.request({
|
|
|
1027
1083
|
contextManagement: [{ type: "compaction", compactThreshold: 200_000 }],
|
|
1028
1084
|
},
|
|
1029
1085
|
})
|
|
1030
|
-
const
|
|
1031
|
-
const
|
|
1032
|
-
|
|
1086
|
+
const continued = Effect.gen(function* () {
|
|
1087
|
+
const response = yield* LLMClient.generate(request)
|
|
1088
|
+
return LLMRequest.update(request, {
|
|
1089
|
+
messages: [...request.messages, response.message, Message.user("Continue")],
|
|
1090
|
+
})
|
|
1033
1091
|
})
|
|
1034
1092
|
```
|
|
1035
1093
|
|
|
@@ -1257,7 +1315,7 @@ Compose a route with `Route.make({ protocol, endpoint, auth, framing, ... })`. T
|
|
|
1257
1315
|
|
|
1258
1316
|
## Effect
|
|
1259
1317
|
|
|
1260
|
-
This package is built on Effect. Public methods return `Effect` or `Stream`; provide `
|
|
1318
|
+
This package is built on Effect. Public methods return `Effect` or `Stream`; provide `AIClient.layer` (or `AIClient.layerWith(executor)`) for every modality, then import the provider/protocol modules for the routes you use. The example at `example/tutorial.ts` is a runnable walkthrough.
|
|
1261
1319
|
|
|
1262
1320
|
## See also
|
|
1263
1321
|
|
|
@@ -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";
|