@opencode/ai 0.0.0-dev-20115 → 0.0.0-dev-20116
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 +170 -113
- 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/image-client.d.ts +6 -4
- package/dist/image-client.js +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/media.js +2 -2
- package/dist/promise.d.ts +42 -20
- package/dist/promise.js +37 -11
- package/dist/protocols/openai-speech.d.ts +5 -1
- package/dist/protocols/shared.js +2 -1
- package/dist/route/auth.d.ts +1 -1
- package/dist/route/auth.js +14 -13
- package/dist/route/client.d.ts +7 -5
- package/dist/route/client.js +2 -1
- 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/index.d.ts +1 -1
- package/dist/speech-client.d.ts +6 -4
- package/dist/speech-client.js +2 -1
- package/dist/testing.d.ts +2 -2
- package/dist/transcription-client.d.ts +6 -4
- package/dist/transcription-client.js +2 -1
- package/dist/transcription.d.ts +2 -2
- package/dist/utils/media-type.d.ts +2 -1
- package/dist/utils/media-type.js +3 -1
- package/dist/video-client.d.ts +6 -4
- package/dist/video-client.js +2 -1
- package/dist/video.d.ts +2 -2
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
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 })
|
|
@@ -18,23 +17,23 @@ const request = LLM.request({
|
|
|
18
17
|
})
|
|
19
18
|
|
|
20
19
|
const program = Effect.gen(function* () {
|
|
21
|
-
const response = yield*
|
|
20
|
+
const response = yield* LLM.generate(request)
|
|
22
21
|
console.log(response.text)
|
|
23
22
|
})
|
|
24
23
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
await Effect.runPromise(program.pipe(Effect.provide(llmLayer)))
|
|
24
|
+
// Every modality client plus the HTTP request executor; `AIClient.layerWith(executor)` swaps the executor.
|
|
25
|
+
await Effect.runPromise(program.pipe(Effect.provide(AIClient.layer)))
|
|
28
26
|
```
|
|
29
27
|
|
|
30
|
-
Run `
|
|
28
|
+
Run `LLM.stream(request)` instead of `generate` when you want incremental `LLMEvent`s. The event stream is provider-neutral — same shape across OpenAI Chat, OpenAI Responses,
|
|
29
|
+
Anthropic Messages, Gemini, Bedrock Converse, and any OpenAI-compatible deployment.
|
|
31
30
|
|
|
32
|
-
The same configured facade names image models. `Image.
|
|
33
|
-
returns `Media.Asset`s with lazily decoded bytes:
|
|
31
|
+
The same configured facade names image, video, speech, and transcription models. `Image.generate` resolves the
|
|
32
|
+
provider's image route from the model and returns `Media.Asset`s with lazily decoded bytes:
|
|
34
33
|
|
|
35
34
|
```ts
|
|
36
35
|
import { NodeFileSystem } from "@effect/platform-node"
|
|
37
|
-
import { Image,
|
|
36
|
+
import { Image, Media } from "@opencode/ai"
|
|
38
37
|
|
|
39
38
|
const image = Effect.gen(function* () {
|
|
40
39
|
const response = yield* Image.generate({
|
|
@@ -46,13 +45,28 @@ const image = Effect.gen(function* () {
|
|
|
46
45
|
yield* Media.write(response.image, "./garden.png")
|
|
47
46
|
})
|
|
48
47
|
|
|
49
|
-
// `
|
|
50
|
-
|
|
48
|
+
// `Media.file` / `Media.write` use the Effect `FileSystem` service; provide your platform's layer.
|
|
49
|
+
await Effect.runPromise(image.pipe(Effect.provide(AIClient.layer), Effect.provide(NodeFileSystem.layer)))
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Advanced: each client also has its own `layer`, which requires `RequestExecutor.Service`. Compose client layers with
|
|
53
|
+
`Layer.provideMerge`, not `Layer.provide`: `asset.bytes()`, `Media.write`, and Gemini's `media` output parts need the
|
|
54
|
+
executor too, and hiding it fails type-checking with `RequestExecutorService` left in the requirements.
|
|
55
|
+
|
|
56
|
+
To share a policy such as logging across every client, wrap the executor once with `RequestExecutor.middleware`:
|
|
51
57
|
|
|
52
|
-
|
|
58
|
+
```ts
|
|
59
|
+
import { RequestExecutor } from "@opencode/ai/route"
|
|
60
|
+
|
|
61
|
+
const logged = RequestExecutor.middleware((request, next) =>
|
|
62
|
+
Effect.log(`${request.method} ${request.url}`).pipe(Effect.andThen(next(request))),
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
const everything = AIClient.layerWith(logged) // or AI.make({ layer: logged })
|
|
53
66
|
```
|
|
54
67
|
|
|
55
|
-
Prefer promises? `@opencode/ai/promise` exposes the same LLM and
|
|
68
|
+
Prefer promises? `@opencode/ai/promise` exposes the same LLM and media APIs over one managed runtime, plus asset
|
|
69
|
+
helpers; `ai.file` and `ai.write` load `node:fs/promises` on first use, so no Effect `FileSystem` is needed:
|
|
56
70
|
|
|
57
71
|
```ts
|
|
58
72
|
import { AI } from "@opencode/ai/promise"
|
|
@@ -60,6 +74,7 @@ import { AI } from "@opencode/ai/promise"
|
|
|
60
74
|
const ai = AI.make()
|
|
61
75
|
const text = await ai.llm.generate({ model: openai.responses("gpt-4o-mini"), prompt: "Say hello." })
|
|
62
76
|
const generated = await ai.image.generate({ model: openai.image("gpt-image-2"), prompt: "A lighthouse" })
|
|
77
|
+
await ai.write(generated.image, "./lighthouse.png") // also ai.file(path), ai.bytes(asset), ai.base64(asset), ai.materialize(asset)
|
|
63
78
|
for await (const event of ai.llm.stream({ model: openai.responses("gpt-4o-mini"), prompt: "Stream hello." })) {
|
|
64
79
|
// LLMEvent
|
|
65
80
|
}
|
|
@@ -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`) | ✓ |
|
|
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
|
|
@@ -591,12 +625,14 @@ and emits `image-partial` events before each final `image`; `Image.generate` kee
|
|
|
591
625
|
`dall-e-*` models do not stream and fail typed:
|
|
592
626
|
|
|
593
627
|
```ts
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
628
|
+
import { Stream } from "effect"
|
|
629
|
+
import { ImageEvent } from "@opencode/ai"
|
|
630
|
+
|
|
631
|
+
const previews = Image.stream({
|
|
632
|
+
model: openai.image("gpt-image-2"),
|
|
633
|
+
prompt: "A lighthouse at dusk",
|
|
634
|
+
providerOptions: { partialImages: 2 },
|
|
635
|
+
}).pipe(Stream.runForEach((event) => (ImageEvent.is.imagePartial(event) ? showPreview(event.image) : Effect.void)))
|
|
600
636
|
```
|
|
601
637
|
|
|
602
638
|
The provider may send fewer previews than requested when the final image is ready first.
|
|
@@ -612,13 +648,20 @@ import { BlackForestLabs, Stability } from "@opencode/ai/providers"
|
|
|
612
648
|
|
|
613
649
|
const bfl = BlackForestLabs.configure({ apiKey: process.env.BFL_API_KEY })
|
|
614
650
|
|
|
615
|
-
const
|
|
616
|
-
|
|
651
|
+
const submit = Effect.gen(function* () {
|
|
652
|
+
const generation = yield* Image.start({ model: bfl.image("flux-2-pro"), prompt, size: "1024x768" })
|
|
653
|
+
persist({ provider: "black-forest-labs", modelID: "flux-2-pro", token: generation.token })
|
|
654
|
+
})
|
|
617
655
|
|
|
618
|
-
const
|
|
619
|
-
const
|
|
656
|
+
const finish = Effect.gen(function* () {
|
|
657
|
+
const saved = load()
|
|
658
|
+
const resumed = yield* Image.resume(bfl.image(saved.modelID), saved.token)
|
|
659
|
+
return yield* resumed.await({ poll: { interval: "2 seconds" } })
|
|
660
|
+
})
|
|
620
661
|
```
|
|
621
662
|
|
|
663
|
+
The token carries no route identity, so persist the provider and model ID alongside it: `resume` needs the model.
|
|
664
|
+
|
|
622
665
|
- **Black Forest Labs** — results are downloaded before returning, because `result.sample` expires in 10 minutes.
|
|
623
666
|
- **Replicate** — inputs are model-defined, so only `prompt` lowers: sizing, count, seed, format, and files go in
|
|
624
667
|
`providerOptions` under the model's names, with files as `Media.Asset` (data URLs up to 256 KB, larger by URL).
|
|
@@ -628,12 +671,13 @@ const response = yield * resumed.await({ poll: { interval: "2 seconds" } })
|
|
|
628
671
|
|
|
629
672
|
```ts
|
|
630
673
|
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: [
|
|
674
|
+
const upscaled = Effect.gen(function* () {
|
|
675
|
+
const small = yield* Media.file("./small.png")
|
|
676
|
+
return yield* Image.generate(
|
|
677
|
+
{ model: stability.upscale(), prompt: "A lighthouse", images: [small] },
|
|
635
678
|
{ poll: { interval: "5 seconds" } },
|
|
636
679
|
)
|
|
680
|
+
})
|
|
637
681
|
```
|
|
638
682
|
|
|
639
683
|
Imagen is not available: Google shut it down on the Gemini API, and Vertex discontinued the Imagen 4 models on
|
|
@@ -666,8 +710,8 @@ Common fields (`frames`, `references`, `video`, `durationSeconds`, `aspectRatio`
|
|
|
666
710
|
under `providerOptions`, inferred from the selected model.
|
|
667
711
|
|
|
668
712
|
```ts
|
|
669
|
-
import { Video
|
|
670
|
-
import { Google } from "@opencode/ai/providers"
|
|
713
|
+
import { Video } from "@opencode/ai"
|
|
714
|
+
import { Google, Runway } from "@opencode/ai/providers"
|
|
671
715
|
|
|
672
716
|
const google = Google.configure({ apiKey: process.env.GOOGLE_GENERATIVE_AI_API_KEY })
|
|
673
717
|
|
|
@@ -696,6 +740,7 @@ const controlled = Effect.gen(function* () {
|
|
|
696
740
|
generation.id // provider operation / task / request id
|
|
697
741
|
generation.status // "queued" | "running" | "completed" | "failed" | "cancelled" | "expired"
|
|
698
742
|
generation.token // route-owned JSON: `{ operation }`, `{ requestID }`, `{ taskID }`, or fal's follow-up URLs
|
|
743
|
+
// The token carries no route identity: persist the provider and model ID alongside it, since `resume` needs the model.
|
|
699
744
|
const saved = JSON.stringify(generation.token)
|
|
700
745
|
|
|
701
746
|
const resumed = yield* Video.resume(google.video("veo-3.1-generate-preview"), JSON.parse(saved))
|
|
@@ -706,8 +751,8 @@ const controlled = Effect.gen(function* () {
|
|
|
706
751
|
const events = Video.stream({ model: Runway.configure({ apiKey }).video("gen4.5"), prompt }, { poll })
|
|
707
752
|
```
|
|
708
753
|
|
|
709
|
-
|
|
710
|
-
|
|
754
|
+
Status polls, result fetches, cancels, and asset downloads all run through the same request executor with the route's
|
|
755
|
+
auth. `Generation.await` and `Generation.events` fail with a
|
|
711
756
|
`Timeout` reason when `poll.timeout` (default 10 minutes) elapses. Failed,
|
|
712
757
|
cancelled, and expired generations fail typed with the provider's terminal document on `reason.body`; moderation
|
|
713
758
|
outcomes (Veo `raiMediaFilteredReasons`, xAI `respect_moderation`, Runway `SAFETY.*` codes) surface as `notices` when
|
|
@@ -726,15 +771,18 @@ Provider notes:
|
|
|
726
771
|
- **Runway** expects pixel ratios in `aspectRatio` for most models (`"1280:720"`), pins `X-Runway-Version`, reports
|
|
727
772
|
`usage: { type: "credits" }`, and its output URLs expire after 24–48 hours.
|
|
728
773
|
|
|
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.
|
|
774
|
+
The promise client exposes the same surface: `ai.video.start(...)` resolves to a handle with `await`, `events`,
|
|
775
|
+
`result`, `refresh`, `cancel`, and `token`; `ai.video.generate`, `ai.video.resume(model, token)`, and
|
|
776
|
+
`ai.video.stream` mirror the Effect API. The handle's `status` and `progress` are a snapshot from when it was
|
|
777
|
+
created; `refresh()` resolves to a new handle.
|
|
732
778
|
|
|
733
779
|
```ts
|
|
734
780
|
import { ai } from "@opencode/ai/promise"
|
|
735
781
|
|
|
736
782
|
const generation = await ai.video.start({ model, prompt })
|
|
737
|
-
const
|
|
783
|
+
for await (const event of generation.events({ poll: { interval: 10_000 } })) console.log(event.type)
|
|
784
|
+
const video = await generation.result({ signal })
|
|
785
|
+
await ai.write(video.video, "./kite.mp4")
|
|
738
786
|
```
|
|
739
787
|
|
|
740
788
|
## Speech generation
|
|
@@ -786,7 +834,6 @@ ElevenLabs and Cartesia. `{ id }` selects an OpenAI custom voice (`{ id: "voice_
|
|
|
786
834
|
plain string elsewhere. There is no cross-provider voice catalog. `format` is the container-level word (`mp3`, `wav`,
|
|
787
835
|
`pcm`, `opus`, `aac`, `flac`); sample rates and bitrates live under `providerOptions`, and a value the route cannot
|
|
788
836
|
produce fails as `UnsupportedOperation`. Streams buffer every chunk so `finish` can carry the whole clip.
|
|
789
|
-
`SpeechClient.layer` needs `RequestExecutor.Service`.
|
|
790
837
|
|
|
791
838
|
Provider notes:
|
|
792
839
|
|
|
@@ -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 })
|
|
@@ -897,7 +946,8 @@ const transcript = await generation.await({ poll: { interval: 3_000 } })
|
|
|
897
946
|
- **`Generation`** — provider-neutral handle for an in-flight media generation (`await`, `refresh`, `cancel`, `events`) used by queued media routes.
|
|
898
947
|
- **`Speech.request` / `Speech.generate` / `Speech.stream`** — text-to-speech through a provider-neutral request; `SpeechClient` is its Effect service and layer.
|
|
899
948
|
- **`Transcription.request` / `generate` / `stream` / `start` / `resume`** — speech-to-text over inline, streaming, and queued routes; `TranscriptionClient` is its Effect service and layer.
|
|
900
|
-
-
|
|
949
|
+
- **`AIClient.layer` / `AIClient.layerWith(executor)`** — every modality client plus the request executor in one layer.
|
|
950
|
+
- **`@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
951
|
|
|
902
952
|
## Testing
|
|
903
953
|
|
|
@@ -960,11 +1010,13 @@ This is different from prompt caching, server-side history storage, or truncatio
|
|
|
960
1010
|
Prefer this operation, where supported, when the application owns compaction policy and durable context updates.
|
|
961
1011
|
|
|
962
1012
|
```ts
|
|
963
|
-
const
|
|
964
|
-
const
|
|
965
|
-
|
|
1013
|
+
const compacted = Effect.gen(function* () {
|
|
1014
|
+
const result = yield* LLMClient.compact(request)
|
|
1015
|
+
const next = LLMRequest.update(request, {
|
|
1016
|
+
messages: result.replacement,
|
|
1017
|
+
})
|
|
1018
|
+
return yield* LLMClient.generate(next)
|
|
966
1019
|
})
|
|
967
|
-
const response = yield * LLMClient.generate(next)
|
|
968
1020
|
```
|
|
969
1021
|
|
|
970
1022
|
`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 +1032,16 @@ The input must still fit the model's context window. Explicit compaction is not
|
|
|
980
1032
|
OpenAI Responses also exposes a separate, explicitly selected mechanism:
|
|
981
1033
|
|
|
982
1034
|
```ts
|
|
983
|
-
const
|
|
984
|
-
yield
|
|
985
|
-
LLMClient.compact(request, {
|
|
1035
|
+
const checkpoint = Effect.gen(function* () {
|
|
1036
|
+
const result = yield* LLMClient.compact(request, {
|
|
986
1037
|
mechanism: "trigger",
|
|
987
1038
|
webSocket, // Optional: without it, the request uses HTTP/SSE.
|
|
988
1039
|
})
|
|
989
1040
|
|
|
990
|
-
result.checkpoint // Successful encrypted CompactionPart.
|
|
991
|
-
result.responseID
|
|
992
|
-
result.usage
|
|
1041
|
+
result.checkpoint // Successful encrypted CompactionPart.
|
|
1042
|
+
result.responseID
|
|
1043
|
+
result.usage
|
|
1044
|
+
})
|
|
993
1045
|
```
|
|
994
1046
|
|
|
995
1047
|
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 +1055,11 @@ The supplied WebSocket executor can reuse a compatible append baseline for the c
|
|
|
1003
1055
|
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
1056
|
|
|
1005
1057
|
```ts
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
}
|
|
1058
|
+
const narrowed = Effect.gen(function* () {
|
|
1059
|
+
if (LLMClient.canCompact(request, { mechanism: "trigger" })) {
|
|
1060
|
+
const result = yield* LLMClient.compact(request, { mechanism: "trigger" })
|
|
1061
|
+
}
|
|
1062
|
+
})
|
|
1009
1063
|
```
|
|
1010
1064
|
|
|
1011
1065
|
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 +1068,10 @@ This capability describes protocol implementation, **not universal availability
|
|
|
1014
1068
|
|
|
1015
1069
|
`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
1070
|
|
|
1017
|
-
|
|
1071
|
+
Enable OpenAI compaction with typed provider options:
|
|
1018
1072
|
|
|
1019
1073
|
```ts
|
|
1074
|
+
import { Effect } from "effect"
|
|
1020
1075
|
import { LLM, LLMClient, LLMRequest, Message } from "@opencode/ai"
|
|
1021
1076
|
import { OpenAI } from "@opencode/ai/providers"
|
|
1022
1077
|
|
|
@@ -1027,9 +1082,11 @@ const request = LLM.request({
|
|
|
1027
1082
|
contextManagement: [{ type: "compaction", compactThreshold: 200_000 }],
|
|
1028
1083
|
},
|
|
1029
1084
|
})
|
|
1030
|
-
const
|
|
1031
|
-
const
|
|
1032
|
-
|
|
1085
|
+
const continued = Effect.gen(function* () {
|
|
1086
|
+
const response = yield* LLMClient.generate(request)
|
|
1087
|
+
return LLMRequest.update(request, {
|
|
1088
|
+
messages: [...request.messages, response.message, Message.user("Continue")],
|
|
1089
|
+
})
|
|
1033
1090
|
})
|
|
1034
1091
|
```
|
|
1035
1092
|
|
|
@@ -1257,7 +1314,7 @@ Compose a route with `Route.make({ protocol, endpoint, auth, framing, ... })`. T
|
|
|
1257
1314
|
|
|
1258
1315
|
## Effect
|
|
1259
1316
|
|
|
1260
|
-
This package is built on Effect. Public methods return `Effect` or `Stream`; provide `
|
|
1317
|
+
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
1318
|
|
|
1262
1319
|
## See also
|
|
1263
1320
|
|
|
@@ -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,
|
|
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.
|
|
58
|
+
export const fetchLayer = layer.pipe(Layer.provideMerge(RequestExecutor.fetchLayer));
|
|
59
59
|
export const EvaluationClient = {
|
|
60
60
|
Service,
|
|
61
61
|
layer,
|
package/dist/image-client.d.ts
CHANGED
|
@@ -9,17 +9,19 @@ export interface Interface {
|
|
|
9
9
|
readonly start: <Options extends ImageOptions>(request: ImageRequestFor<Options>) => Effect.Effect<Generation<ImageResponse>, AIError>;
|
|
10
10
|
readonly resume: <Options extends ImageOptions>(model: ImageModel<Options>, token: unknown) => Effect.Effect<Generation<ImageResponse>, AIError>;
|
|
11
11
|
}
|
|
12
|
-
declare const
|
|
13
|
-
export declare class
|
|
12
|
+
declare const ImageClientService_base: Context.ServiceClass<ImageClientService, "@opencode/ImageClient", Interface>;
|
|
13
|
+
export declare class ImageClientService extends ImageClientService_base {
|
|
14
14
|
}
|
|
15
|
+
export declare const Service: typeof ImageClientService;
|
|
16
|
+
export type Service = ImageClientService;
|
|
15
17
|
export declare const generate: <Options extends ImageOptions>(request: ImageRequestFor<Options>, options?: AwaitOptions) => Effect.Effect<ImageResponse, AIError, Service>;
|
|
16
18
|
export declare const stream: <Options extends ImageOptions>(request: ImageRequestFor<Options>, options?: AwaitOptions) => Stream.Stream<ImageEvent, AIError, Service>;
|
|
17
19
|
export declare const start: <Options extends ImageOptions>(request: ImageRequestFor<Options>) => Effect.Effect<Generation<ImageResponse>, AIError, Service>;
|
|
18
20
|
export declare const resume: <Options extends ImageOptions>(model: ImageModel<Options>, token: unknown) => Effect.Effect<Generation<ImageResponse>, AIError, Service>;
|
|
19
21
|
export declare const layer: Layer.Layer<Service, never, RequestExecutor.Service>;
|
|
20
22
|
export declare const ImageClient: {
|
|
21
|
-
readonly Service: typeof
|
|
22
|
-
readonly layer: Layer.Layer<
|
|
23
|
+
readonly Service: typeof ImageClientService;
|
|
24
|
+
readonly layer: Layer.Layer<ImageClientService, never, import("./route/executor-service.js").RequestExecutorService>;
|
|
23
25
|
readonly generate: <Options extends ImageOptions>(request: ImageRequestFor<Options>, options?: AwaitOptions) => Effect.Effect<ImageResponse, AIError, Service>;
|
|
24
26
|
readonly stream: <Options extends ImageOptions>(request: ImageRequestFor<Options>, options?: AwaitOptions) => Stream.Stream<ImageEvent, AIError, Service>;
|
|
25
27
|
readonly start: <Options extends ImageOptions>(request: ImageRequestFor<Options>) => Effect.Effect<Generation<ImageResponse>, AIError, Service>;
|