Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions packages/ai/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

## Conventions

Per-type constructors live on the type, not as top-level re-exports. Use `Message.system(...)`, `Message.user(...)`, `Message.assistant(...)`, `Message.tool(...)`, `Model.make(...)`, `ToolDefinition.make(...)`, `ToolCallPart.make(...)`, `ToolResultPart.make(...)`, `ToolChoice.make(...)`, `ToolChoice.named(...)`, `SystemPart.make(...)`, and `GenerationOptions.make(...)` directly. The top-level `LLM` namespace is reserved for request-shaped call APIs: `LLM.request`, `LLM.generate`, `LLM.stream`, `LLM.updateRequest`, and `LLM.generateObject`. Two ways to construct the same thing is one too many.
Per-type constructors live on the type, not as top-level re-exports. Use `Message.system(...)`, `Message.user(...)`, `Message.assistant(...)`, `Message.tool(...)`, `Model.make(...)`, `ToolDefinition.make(...)`, `ToolCallPart.make(...)`, `ToolResultPart.make(...)`, `ToolChoice.make(...)`, `ToolChoice.named(...)`, `SystemPart.make(...)`, and `GenerationOptions.make(...)` directly. The top-level `LLM` namespace is reserved for request-shaped call APIs: `LLM.request`, `LLM.generateTurn`, `LLM.streamTurn`, and `LLM.generateObject`. Two ways to construct the same thing is one too many.

## Tests

Expand Down Expand Up @@ -223,7 +223,7 @@ Routes lower these into provider-native assistant tool-call messages and tool-re

### Tool dispatch

`LLM.stream(request)` and `LLM.generate(request)` each run exactly one provider turn. Add tool schemas to `request.tools` with `Tool.toDefinitions(tools)`. When a caller wants the package's typed one-call execution behavior, pass each canonical local `tool-call` event to `ToolRuntime.dispatch(tools, call)`.
`LLM.streamTurn(request)` and `LLM.generateTurn(request)` each run exactly one provider turn. Add tool schemas to `request.tools` with `Tool.toDefinitions(tools)`. When a caller wants the package's typed one-call execution behavior, pass each canonical local `tool-call` event to `ToolRuntime.dispatch(tools, call)`.

```ts
const get_weather = tool({
Expand All @@ -240,7 +240,7 @@ const get_weather = tool({
})

const tools = { get_weather, get_time, ... }
const events = yield* LLM.stream(
const events = yield* LLM.streamTurn(
LLMRequest.update(request, { tools: Tool.toDefinitions(tools) }),
).pipe(Stream.runCollect)

Expand Down
8 changes: 4 additions & 4 deletions packages/ai/DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,8 +175,8 @@ const request = LLM.request({
prompt: "Say hello.",
})

// Current API: this performs one provider turn, despite the broad name.
const response = yield * LLM.generate(request)
// Current API: this performs one provider turn.
const response = yield * LLM.generateTurn(request)

// Current API: execution also needs LLMClient.layer and RequestExecutor services.
```
Expand Down Expand Up @@ -432,7 +432,7 @@ const request = LLM.request({
tools: Tool.toDefinitions(tools),
})

const events = yield * LLM.stream(request).pipe(Stream.runCollect)
const events = yield * LLM.streamTurn(request).pipe(Stream.runCollect)
const call = Array.from(events).find(LLMEvent.is.toolCall)

if (call && !call.providerExecuted) {
Expand Down Expand Up @@ -1076,7 +1076,7 @@ The redesign intentionally removes or changes these current concepts:
| Current | Proposed |
| --------------------------------------- | ----------------------------------------------------------- |
| Mandatory `LLM.request({ model, ... })` | Inline calls or model-free portable requests |
| `LLM.generate` means one turn | `LLM.generate` means complete run |
| No complete-run API | Add `LLM.generate` / `LLM.stream` |
| `LLMClient.generate/stream` | `LLM.generateTurn/streamTurn` for one turn |
| `LLMClient.layer` requirement | Standard Effect requirements exposed directly |
| Public `Route` mental model | Hidden behind executable `Model` |
Expand Down
4 changes: 2 additions & 2 deletions packages/ai/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ Conversational image generation remains part of the LLM interaction. OpenAI Resp

```ts
const program = Effect.gen(function* () {
const response = yield* LLM.generate(
const response = yield* LLM.generateTurn(
LLM.request({
model: OpenAI.configure({ apiKey }).responses("gpt-5"),
prompt: "Design a solarpunk rooftop garden, then show me.",
Expand All @@ -193,7 +193,7 @@ The hosted result is represented as a provider-executed tool call and tool resul
## Public API

- **`LLM.request({...})`** — build a provider-neutral `LLMRequest`. Accepts ergonomic inputs (`system: string`, `prompt: string`) that normalize into the canonical Schema classes.
- **`LLM.generate` / `LLM.stream`** — re-exported from `LLMClient` for one-import use.
- **`LLM.generateTurn` / `LLM.streamTurn`** — execute exactly one provider turn, re-exported from `LLMClient` for one-import use.
- **`Message.user(...)` / `Message.assistant(...)` / `Message.tool(...)`** — message constructors from the canonical schema model.
- **`Model.make(...)` / `ToolCallPart.make(...)` / `ToolResultPart.make(...)` / `ToolDefinition.make(...)`** — model and tool-related constructors from the canonical schema model.
- **`LLMClient.prepare(request)`** — compile a request through protocol body construction, validation, and HTTP preparation without sending. Useful for inspection and testing.
Expand Down
2 changes: 1 addition & 1 deletion packages/ai/example/call-sites.md
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,7 @@ Final request call site stays boring:
```ts
const response =
yield *
LLM.generate(
LLM.generateTurn(
LLM.request({
model: DeepSeek.model("deepseek-chat"),
prompt: "Hello.",
Expand Down
6 changes: 3 additions & 3 deletions packages/ai/example/tutorial.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ const rawOverlayExample = LLM.request({
// 3. `generate` sends the request and collects the event stream into one
// response object. `response.text` is the collected text output.
const generateOnce = Effect.gen(function* () {
const response = yield* LLM.generate(request)
const response = yield* LLM.generateTurn(request)

console.log("\n== generate ==")
console.log("generated text:", response.text)
Expand All @@ -74,7 +74,7 @@ const generateOnce = Effect.gen(function* () {

// 4. `stream` exposes provider output as common `LLMEvent`s for UIs that want
// incremental text, reasoning, tool input, usage, or finish events.
const streamText = LLM.stream(request).pipe(
const streamText = LLM.streamTurn(request).pipe(
Stream.tap((event) =>
Effect.sync(() => {
if (event.type === "text-delta") process.stdout.write(`\ntext: ${event.text}`)
Expand Down Expand Up @@ -106,7 +106,7 @@ const streamWithTools = Effect.gen(function* () {
generation: { maxTokens: 80, temperature: 0 },
tools: Tool.toDefinitions(tools),
})
const events = Array.from(yield* LLM.stream(request).pipe(Stream.runCollect))
const events = Array.from(yield* LLM.streamTurn(request).pipe(Stream.runCollect))
for (const event of events) {
if (event.type === "tool-call") console.log("tool call", event.name, event.input)
if (event.type === "text-delta") process.stdout.write(event.text)
Expand Down
4 changes: 2 additions & 2 deletions packages/ai/src/llm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ export type RequestInput = Omit<
readonly http?: HttpOptions.Input
}

export const generate = LLMClient.generate
export const generateTurn = LLMClient.generate

export const stream = LLMClient.stream
export const streamTurn = LLMClient.stream

export const request = (input: RequestInput) => {
const {
Expand Down
2 changes: 1 addition & 1 deletion packages/ai/src/route/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -412,7 +412,7 @@ const streamRequestWith = (runtime: TransportRuntime) => (request: LLMRequest) =
)

const generateWith = (stream: Interface["stream"]) =>
Effect.fn("LLM.generate")(function* (request: LLMRequest) {
Effect.fn("LLM.generateTurn")(function* (request: LLMRequest) {
const state = yield* stream(request).pipe(Stream.runFold(LLMResponse.empty, LLMResponse.reduce))
const response = LLMResponse.complete(state)
if (response) return response
Expand Down
4 changes: 4 additions & 0 deletions packages/ai/test/exports.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ import * as AnthropicMessages from "@opencode-ai/ai/protocols/anthropic-messages
describe("public exports", () => {
test("root exposes app-facing runtime APIs", () => {
expect(LLM.request).toBeFunction()
expect(LLM.generateTurn).toBeFunction()
expect(LLM.streamTurn).toBeFunction()
expect(LLM).not.toHaveProperty("generate")
expect(LLM).not.toHaveProperty("stream")
expect(LLMClient.Service).toBeFunction()
expect(LLMClient.layer).toBeDefined()
expect(ImageInput.bytes).toBeFunction()
Expand Down
10 changes: 5 additions & 5 deletions packages/ai/test/provider/cloudflare.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ describe("Cloudflare", () => {

it.effect("posts to the derived gateway endpoint with bearer auth", () =>
Effect.gen(function* () {
const response = yield* LLM.generate(
const response = yield* LLM.generateTurn(
LLM.request({
model: CloudflareAIGateway.configure({
accountId: "test-account",
Expand Down Expand Up @@ -104,7 +104,7 @@ describe("Cloudflare", () => {
index: 0,
},
]
const response = yield* LLM.generate(LLM.request({ model, prompt: "Say hello." })).pipe(
const response = yield* LLM.generateTurn(LLM.request({ model, prompt: "Say hello." })).pipe(
Effect.provide(
dynamicResponse((input) =>
Effect.succeed(
Expand Down Expand Up @@ -150,7 +150,7 @@ describe("Cloudflare", () => {

it.effect("supports authenticated AI Gateway plus upstream provider auth", () =>
Effect.gen(function* () {
yield* LLM.generate(
yield* LLM.generateTurn(
LLM.request({
model: CloudflareAIGateway.configure({
accountId: "test-account",
Expand Down Expand Up @@ -221,7 +221,7 @@ describe("Cloudflare", () => {

it.effect("posts direct Workers AI requests to the account endpoint with bearer auth", () =>
Effect.gen(function* () {
const response = yield* LLM.generate(
const response = yield* LLM.generateTurn(
LLM.request({
model: CloudflareWorkersAI.configure({
accountId: "test-account",
Expand Down Expand Up @@ -256,7 +256,7 @@ describe("Cloudflare", () => {

it.effect("supports direct Workers AI token aliases through auth config", () =>
Effect.gen(function* () {
yield* LLM.generate(
yield* LLM.generateTurn(
LLM.request({
model: CloudflareWorkersAI.configure({
accountId: "test-account",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ describe("OpenAI Responses image generation recorded", () => {
partialImages: 0,
}),
]
const response = yield* LLM.generate(
const response = yield* LLM.generateTurn(
LLM.request({
model: openai.responses("gpt-5-mini"),
messages: [initial],
Expand All @@ -49,7 +49,7 @@ describe("OpenAI Responses image generation recorded", () => {
expect(result.result.value[0].mime).toBe("image/jpeg")
expect(result.result.value[0].uri.startsWith("data:image/jpeg;base64,")).toBe(true)

const edited = yield* LLM.generate(
const edited = yield* LLM.generateTurn(
LLM.request({
model: openai.responses("gpt-5-mini"),
messages: [initial, response.message, Message.user("Now make the triangle blue.")],
Expand Down
6 changes: 3 additions & 3 deletions packages/ai/test/tool.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ Tool.make({
],
})

LLM.stream(request)
LLM.generate(LLMRequest.update(request, { tools: toDefinitions({ schemaOnly }) }))
LLM.streamTurn(request)
LLM.generateTurn(LLMRequest.update(request, { tools: toDefinitions({ schemaOnly }) }))
ToolRuntime.dispatch({ executable }, { type: "tool-call", id: "call_1", name: "executable", input: { city: "Paris" } })

// @ts-expect-error High-level tool orchestration overloads are intentionally not supported.
LLM.stream({ request, tools: { schemaOnly } })
LLM.streamTurn({ request, tools: { schemaOnly } })
Loading