Skip to content

Commit 041202b

Browse files
authored
docs(ai): fix stale README and type-check examples (#59179)
1 parent d5bb993 commit 041202b

3 files changed

Lines changed: 70 additions & 38 deletions

File tree

‎packages/ai/README.md‎

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# @opencode/ai
22

3-
Schema-first APIs for text, images, video, speech, and transcription, built with Effect.
3+
Schema-first APIs for text, images, video, speech, transcription, and decisions, built with Effect.
44

55
`@opencode/ai` is the Promise API: one managed runtime behind the LLM and media methods, plus asset helpers. `ai.file`
66
and `ai.write` load `node:fs/promises` on first use, so no Effect `FileSystem` is needed:
@@ -62,13 +62,11 @@ live under `providerMetadata.vertex`. The default is still full-history replay w
6262

6363
```ts
6464
const model = Google.configure({ apiKey }).interactions("gemini-3.8-flash")
65-
const response =
66-
yield *
67-
LLM.generate({
68-
model,
69-
prompt: "Say hello.",
70-
providerOptions: { thinkingLevel: "low", thinkingSummaries: "auto", store: true },
71-
})
65+
const response = yield* LLM.generate({
66+
model,
67+
prompt: "Say hello.",
68+
providerOptions: { thinkingLevel: "low", thinkingSummaries: "auto", store: true },
69+
})
7270
```
7371

7472
Interactions supports text output, streamed function calls, native tool results, thought signatures, and multimodal
@@ -93,7 +91,8 @@ const image = Effect.gen(function* () {
9391
const response = yield* Image.generate({
9492
model: openai.image("gpt-image-2"),
9593
prompt: "A robot tending a rooftop garden",
96-
size: "1024x1024",
94+
aspect: "1:1",
95+
resolution: "1K",
9796
providerOptions: { quality: "high" }, // typed per image model
9897
})
9998
yield* Media.write(response.image, "./garden.png")
@@ -329,10 +328,11 @@ is unit-tested for all six regions.
329328
### Images
330329

331330
`alibaba.image(...)` uses the native multimodal generation API (`/api/v1/services/aigc/multimodal-generation/generation`)
332-
for Qwen-Image (`qwen-image-2.0`, `qwen-image-2.0-pro`) and Wan (`wan2.7-image`, `wan2.7-image-pro`). `size` lowers to
333-
`W*H`, and `images` are sent as URLs or data URLs for editing. Native `parameters` include `negativePrompt`,
334-
`promptExtend`, `watermark`, `thinkingMode`, and `enableSequential`; raw keys such as `bbox_list` or `size: "2K"`
335-
pass through. Results are URLs that Model Studio keeps for 24 hours. A complete `baseURL` for images includes `/api/v1`.
331+
for Qwen-Image (`qwen-image-2.0`, `qwen-image-2.0-pro`) and Wan (`wan2.7-image`, `wan2.7-image-pro`). `aspect` and
332+
`resolution` lower to a `W*H` size, `providerOptions.size` sets it exactly, and `images` are sent as URLs or data URLs
333+
for editing. Native `parameters` include `size`, `seed`, `negativePrompt`, `promptExtend`, `watermark`, `thinkingMode`,
334+
and `enableSequential`; raw keys such as `bbox_list` pass through. Results are URLs that Model Studio keeps for 24
335+
hours. A complete `baseURL` for images includes `/api/v1`.
336336

337337
## Z.AI
338338

@@ -1102,8 +1102,10 @@ Everything below is exported from `@opencode/ai/effect` unless noted.
11021102
- **`Video.request` / `generate` / `stream` / `start` / `resume`** — queued video generation through a provider-neutral request; `VideoClient` is its Effect service and layer.
11031103
- **`Speech.request` / `Speech.generate` / `Speech.stream`** — text-to-speech through a provider-neutral request; `SpeechClient` is its Effect service and layer.
11041104
- **`Transcription.request` / `generate` / `stream` / `start` / `resume`** — speech-to-text over inline, streaming, and queued routes; `TranscriptionClient` is its Effect service and layer.
1105+
- **`Decision.request` / `Decision.generate`** — typed predicate, choice, and score questions over text, JSON, and media input; `DecisionClient` is its Effect service and layer.
11051106
- **`AIClient.layer` / `AIClient.layerWith(executor)`** — every modality client plus the request executor in one layer.
1106-
- **`@opencode/ai`** (Promise root) — `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.
1107+
- **`@opencode/ai`** (Promise root) — `AI.make({ layer? })` and a default `ai` client exposing `llm`, `image`, `video`, `speech`, `transcription`, and `decision` as Promise / `AsyncIterable` APIs, `run(effect)` for any package Effect, plus `file`, `write`, `bytes`, `base64`, and `materialize` for assets.
1108+
- **`@opencode/ai/experimental`** — APIs that may still change: `Extraction` (typed values read out of text) and `Realtime` (`Realtime.connect` starts an OpenAI, xAI, or Gemini voice session and returns a WebRTC answer SDP or a short-lived WebSocket URL and token).
11071109

11081110
## Testing
11091111

@@ -1381,12 +1383,12 @@ import { OpenAI, CloudflareAIGateway } from "@opencode/ai/providers"
13811383

13821384
const openai = OpenAI.configure({ apiKey: process.env.OPENAI_API_KEY }).responses("gpt-4o-mini")
13831385
const gateway = CloudflareAIGateway.configure({
1384-
accountId: process.env.CLOUDFLARE_ACCOUNT_ID,
1386+
accountId,
13851387
gatewayApiKey: process.env.CLOUDFLARE_API_TOKEN,
13861388
}).model("workers-ai/@cf/meta/llama-3.1-8b-instruct")
13871389
```
13881390

1389-
Included LLM providers: OpenAI, Anthropic, Google (Gemini), Google Vertex, Amazon Bedrock, Azure OpenAI, Baseten, Cerebras, Cohere, Cloudflare AI Gateway, Cloudflare Workers AI, DeepInfra, DeepSeek, Fireworks, Groq, Mistral, OpenRouter, TogetherAI, Vercel AI Gateway, and xAI. Z.ai currently exposes image generation. Generic Chat Completions, Responses, and Anthropic Messages-compatible entrypoints support custom endpoints.
1391+
Included LLM providers: OpenAI, Anthropic, Google (Gemini), Google Vertex, Amazon Bedrock (Converse and Mantle), Azure OpenAI, Alibaba, Baseten, Cerebras, Cohere, Cloudflare AI Gateway, Cloudflare Workers AI, DeepInfra, DeepSeek, DigitalOcean, Fireworks, Groq, Meta, MiniMax, Mistral, Moonshot, OpenRouter, TogetherAI, Venice, Vercel AI Gateway, xAI, Z.ai, and Z.ai Coding Plan. TypeSafe AI and OpenCode expose decision models only. Generic Chat Completions, Responses, and Anthropic Messages-compatible entrypoints support custom endpoints.
13901392

13911393
Each named provider owns its module, endpoint, authentication, and route setup. Providers with the same wire format compose the shared protocol directly:
13921394

@@ -1496,5 +1498,5 @@ This package is built on Effect. Public methods in `@opencode/ai/effect` return
14961498
## See also
14971499

14981500
- `AGENTS.md` — architecture, route construction, contributor guide
1499-
- `example/tutorial.ts` — runnable end-to-end walkthrough
1501+
- `example/tutorial.ts` — runnable end-to-end walkthrough (`bun example/tutorial.ts <example>`)
15001502
- `test/provider/*.test.ts` — fixture-first protocol tests; `*.recorded.test.ts` files cover live cassettes

‎packages/ai/example/tutorial.ts‎

Lines changed: 50 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
11
import { Config, Effect, Formatter, Schema, Stream } from "effect"
2+
import { HttpClientResponse } from "effect/http"
23
import { NodeFileSystem } from "@effect/platform-node"
34
import { AIClient, Image, LLM, LLMRequest, Media, Message, ProviderID, Tool, ToolRuntime } from "@opencode/ai/effect"
4-
import { Route, Auth, Endpoint, Framing, Protocol } from "@opencode/ai/route"
5+
import { Route, Auth, Endpoint, Framing, Protocol, RequestExecutor } from "@opencode/ai/route"
56
import { OpenAI } from "@opencode/ai/providers"
67

78
/**
89
* A runnable walkthrough of the LLM package use-site API.
910
*
1011
* Run from `packages/ai` with an OpenAI key in the environment:
1112
*
12-
* OPENAI_API_KEY=... bun example/tutorial.ts
13+
* OPENAI_API_KEY=... bun example/tutorial.ts [example]
14+
*
15+
* where `example` is one of the keys of `examples` at the bottom (default
16+
* `tools`). The `fake` example runs offline and needs no key.
1317
*
1418
* The file is intentionally written as a normal TypeScript program. You can
1519
* hover imports and local values to see how the public API is typed.
@@ -188,7 +192,7 @@ const FakeProtocol = Protocol.make<FakeBody, string, string, void>({
188192
event: Schema.String,
189193
initial: () => undefined,
190194
step: (_, frame) => Effect.succeed([undefined, [{ type: "text-delta", id: "text-0", text: frame }]] as const),
191-
onHalt: () => [{ type: "finish", reason: { normalized: "stop" } }],
195+
onHalt: () => Effect.succeed([{ type: "finish", reason: { normalized: "stop" } }]),
192196
},
193197
})
194198

@@ -213,16 +217,34 @@ const FakeEcho = {
213217
}),
214218
}
215219

220+
// 7. Run the fake provider offline. `RequestExecutor.middleware` sees every HTTP
221+
// request before it is sent; here it answers with two SSE frames instead of
222+
// calling the network.
223+
const fakeExecutor = RequestExecutor.middleware((request) =>
224+
Effect.succeed(
225+
HttpClientResponse.fromWeb(
226+
request,
227+
new Response("data: hello\n\ndata: from fake-echo\n\n", { headers: { "content-type": "text/event-stream" } }),
228+
),
229+
),
230+
)
231+
232+
const generateFake = Effect.gen(function* () {
233+
const response = yield* LLM.generate({ model: FakeEcho.configure().model("echo-1"), prompt: "Say hello." })
234+
console.log("\n== fake provider ==")
235+
console.log("generated text:", response.text)
236+
})
237+
216238
// 8. Image generation uses the same facade and the same request/generate shape.
217239
// `response.image` is a `Media.Asset`: bytes decode lazily and are cached, and
218240
// `Media.write` persists them through the Effect `FileSystem`.
219241
const generateImage = Effect.gen(function* () {
220242
const response = yield* Image.generate({
221243
model: openai.image("gpt-image-1-mini"),
222244
prompt: "A flat black circle centered on a plain white background.",
223-
size: "1024x1024",
224-
format: "jpeg",
225-
providerOptions: { quality: "low" },
245+
aspect: "1:1",
246+
// Exact pixel sizes and output formats are provider options.
247+
providerOptions: { quality: "low", outputFormat: "jpeg" },
226248
})
227249

228250
console.log("\n== image ==")
@@ -233,17 +255,25 @@ const generateImage = Effect.gen(function* () {
233255
})
234256

235257
// Provide every modality client and the HTTP request executor once with
236-
// `AIClient.layer` (`AIClient.layerWith(executor)` swaps the executor). Keep one
237-
// path enabled at a time so the tutorial can demonstrate generate, stream, or
238-
// tool-loop behavior without spending tokens on every example.
239-
240-
const program = Effect.gen(function* () {
241-
// yield* generateOnce
242-
// yield* streamText
243-
// yield* generateStructuredObject
244-
// yield* generateDynamicObject.pipe(Effect.andThen((response) => Effect.sync(() => console.log(response.object))))
245-
// yield* generateImage
246-
yield* streamWithTools
247-
}).pipe(Effect.provide(AIClient.layer))
248-
249-
Effect.runPromise(program)
258+
// `AIClient.layer`; `AIClient.layerWith(executor)` swaps the executor, as the
259+
// fake example does. One example runs at a time so the tutorial does not spend
260+
// tokens on every path.
261+
const examples: Record<string, Effect.Effect<void, unknown, AIClient.Services>> = {
262+
generate: generateOnce,
263+
stream: streamText,
264+
tools: streamWithTools,
265+
object: generateStructuredObject,
266+
"dynamic-object": generateDynamicObject.pipe(
267+
Effect.andThen((response) => Effect.sync(() => console.log(response.object))),
268+
),
269+
image: generateImage,
270+
fake: generateFake,
271+
}
272+
273+
const name = process.argv[2] ?? "tools"
274+
if (!Object.hasOwn(examples, name))
275+
throw new Error(`Unknown example ${name}; pick one of ${Object.keys(examples).join(", ")}`)
276+
277+
Effect.runPromise(
278+
examples[name].pipe(Effect.provide(name === "fake" ? AIClient.layerWith(fakeExecutor) : AIClient.layer)),
279+
)

‎packages/ai/tsconfig.types.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,5 @@
55
"noEmit": true,
66
"rootDir": "."
77
},
8-
"include": ["test/**/*.types.ts", "test/testing.test.ts", "test/provider/checkpoint.test.ts"]
8+
"include": ["example/**/*.ts", "test/**/*.types.ts", "test/testing.test.ts", "test/provider/checkpoint.test.ts"]
99
}

0 commit comments

Comments
 (0)