mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-03 08:46:15 -04:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 16df2467fd |
@@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
## Conventions
|
## 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
|
## Tests
|
||||||
|
|
||||||
@@ -223,7 +223,7 @@ Routes lower these into provider-native assistant tool-call messages and tool-re
|
|||||||
|
|
||||||
### Tool dispatch
|
### 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
|
```ts
|
||||||
const get_weather = tool({
|
const get_weather = tool({
|
||||||
@@ -240,7 +240,7 @@ const get_weather = tool({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const tools = { get_weather, get_time, ... }
|
const tools = { get_weather, get_time, ... }
|
||||||
const events = yield* LLM.stream(
|
const events = yield* LLM.streamTurn(
|
||||||
LLMRequest.update(request, { tools: Tool.toDefinitions(tools) }),
|
LLMRequest.update(request, { tools: Tool.toDefinitions(tools) }),
|
||||||
).pipe(Stream.runCollect)
|
).pipe(Stream.runCollect)
|
||||||
|
|
||||||
|
|||||||
@@ -175,8 +175,8 @@ const request = LLM.request({
|
|||||||
prompt: "Say hello.",
|
prompt: "Say hello.",
|
||||||
})
|
})
|
||||||
|
|
||||||
// Current API: this performs one provider turn, despite the broad name.
|
// Current API: this performs one provider turn.
|
||||||
const response = yield * LLM.generate(request)
|
const response = yield * LLM.generateTurn(request)
|
||||||
|
|
||||||
// Current API: execution also needs LLMClient.layer and RequestExecutor services.
|
// Current API: execution also needs LLMClient.layer and RequestExecutor services.
|
||||||
```
|
```
|
||||||
@@ -432,7 +432,7 @@ const request = LLM.request({
|
|||||||
tools: Tool.toDefinitions(tools),
|
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)
|
const call = Array.from(events).find(LLMEvent.is.toolCall)
|
||||||
|
|
||||||
if (call && !call.providerExecuted) {
|
if (call && !call.providerExecuted) {
|
||||||
@@ -1076,7 +1076,7 @@ The redesign intentionally removes or changes these current concepts:
|
|||||||
| Current | Proposed |
|
| Current | Proposed |
|
||||||
| --------------------------------------- | ----------------------------------------------------------- |
|
| --------------------------------------- | ----------------------------------------------------------- |
|
||||||
| Mandatory `LLM.request({ model, ... })` | Inline calls or model-free portable requests |
|
| 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.generate/stream` | `LLM.generateTurn/streamTurn` for one turn |
|
||||||
| `LLMClient.layer` requirement | Standard Effect requirements exposed directly |
|
| `LLMClient.layer` requirement | Standard Effect requirements exposed directly |
|
||||||
| Public `Route` mental model | Hidden behind executable `Model` |
|
| Public `Route` mental model | Hidden behind executable `Model` |
|
||||||
|
|||||||
@@ -176,7 +176,7 @@ Conversational image generation remains part of the LLM interaction. OpenAI Resp
|
|||||||
|
|
||||||
```ts
|
```ts
|
||||||
const program = Effect.gen(function* () {
|
const program = Effect.gen(function* () {
|
||||||
const response = yield* LLM.generate(
|
const response = yield* LLM.generateTurn(
|
||||||
LLM.request({
|
LLM.request({
|
||||||
model: OpenAI.configure({ apiKey }).responses("gpt-5"),
|
model: OpenAI.configure({ apiKey }).responses("gpt-5"),
|
||||||
prompt: "Design a solarpunk rooftop garden, then show me.",
|
prompt: "Design a solarpunk rooftop garden, then show me.",
|
||||||
@@ -193,7 +193,7 @@ The hosted result is represented as a provider-executed tool call and tool resul
|
|||||||
## Public API
|
## Public API
|
||||||
|
|
||||||
- **`LLM.request({...})`** — build a provider-neutral `LLMRequest`. Accepts ergonomic inputs (`system: string`, `prompt: string`) that normalize into the canonical Schema classes.
|
- **`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.
|
- **`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.
|
- **`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.
|
- **`LLMClient.prepare(request)`** — compile a request through protocol body construction, validation, and HTTP preparation without sending. Useful for inspection and testing.
|
||||||
|
|||||||
@@ -334,7 +334,7 @@ Final request call site stays boring:
|
|||||||
```ts
|
```ts
|
||||||
const response =
|
const response =
|
||||||
yield *
|
yield *
|
||||||
LLM.generate(
|
LLM.generateTurn(
|
||||||
LLM.request({
|
LLM.request({
|
||||||
model: DeepSeek.model("deepseek-chat"),
|
model: DeepSeek.model("deepseek-chat"),
|
||||||
prompt: "Hello.",
|
prompt: "Hello.",
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ const rawOverlayExample = LLM.request({
|
|||||||
// 3. `generate` sends the request and collects the event stream into one
|
// 3. `generate` sends the request and collects the event stream into one
|
||||||
// response object. `response.text` is the collected text output.
|
// response object. `response.text` is the collected text output.
|
||||||
const generateOnce = Effect.gen(function* () {
|
const generateOnce = Effect.gen(function* () {
|
||||||
const response = yield* LLM.generate(request)
|
const response = yield* LLM.generateTurn(request)
|
||||||
|
|
||||||
console.log("\n== generate ==")
|
console.log("\n== generate ==")
|
||||||
console.log("generated text:", response.text)
|
console.log("generated text:", response.text)
|
||||||
@@ -74,7 +74,7 @@ const generateOnce = Effect.gen(function* () {
|
|||||||
|
|
||||||
// 4. `stream` exposes provider output as common `LLMEvent`s for UIs that want
|
// 4. `stream` exposes provider output as common `LLMEvent`s for UIs that want
|
||||||
// incremental text, reasoning, tool input, usage, or finish events.
|
// incremental text, reasoning, tool input, usage, or finish events.
|
||||||
const streamText = LLM.stream(request).pipe(
|
const streamText = LLM.streamTurn(request).pipe(
|
||||||
Stream.tap((event) =>
|
Stream.tap((event) =>
|
||||||
Effect.sync(() => {
|
Effect.sync(() => {
|
||||||
if (event.type === "text-delta") process.stdout.write(`\ntext: ${event.text}`)
|
if (event.type === "text-delta") process.stdout.write(`\ntext: ${event.text}`)
|
||||||
@@ -106,7 +106,7 @@ const streamWithTools = Effect.gen(function* () {
|
|||||||
generation: { maxTokens: 80, temperature: 0 },
|
generation: { maxTokens: 80, temperature: 0 },
|
||||||
tools: Tool.toDefinitions(tools),
|
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) {
|
for (const event of events) {
|
||||||
if (event.type === "tool-call") console.log("tool call", event.name, event.input)
|
if (event.type === "tool-call") console.log("tool call", event.name, event.input)
|
||||||
if (event.type === "text-delta") process.stdout.write(event.text)
|
if (event.type === "text-delta") process.stdout.write(event.text)
|
||||||
|
|||||||
@@ -31,9 +31,9 @@ export type RequestInput = Omit<
|
|||||||
readonly http?: HttpOptions.Input
|
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) => {
|
export const request = (input: RequestInput) => {
|
||||||
const {
|
const {
|
||||||
|
|||||||
@@ -412,7 +412,7 @@ const streamRequestWith = (runtime: TransportRuntime) => (request: LLMRequest) =
|
|||||||
)
|
)
|
||||||
|
|
||||||
const generateWith = (stream: Interface["stream"]) =>
|
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 state = yield* stream(request).pipe(Stream.runFold(LLMResponse.empty, LLMResponse.reduce))
|
||||||
const response = LLMResponse.complete(state)
|
const response = LLMResponse.complete(state)
|
||||||
if (response) return response
|
if (response) return response
|
||||||
|
|||||||
@@ -23,6 +23,10 @@ import * as AnthropicMessages from "@opencode-ai/ai/protocols/anthropic-messages
|
|||||||
describe("public exports", () => {
|
describe("public exports", () => {
|
||||||
test("root exposes app-facing runtime APIs", () => {
|
test("root exposes app-facing runtime APIs", () => {
|
||||||
expect(LLM.request).toBeFunction()
|
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.Service).toBeFunction()
|
||||||
expect(LLMClient.layer).toBeDefined()
|
expect(LLMClient.layer).toBeDefined()
|
||||||
expect(ImageInput.bytes).toBeFunction()
|
expect(ImageInput.bytes).toBeFunction()
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ describe("Cloudflare", () => {
|
|||||||
|
|
||||||
it.effect("posts to the derived gateway endpoint with bearer auth", () =>
|
it.effect("posts to the derived gateway endpoint with bearer auth", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const response = yield* LLM.generate(
|
const response = yield* LLM.generateTurn(
|
||||||
LLM.request({
|
LLM.request({
|
||||||
model: CloudflareAIGateway.configure({
|
model: CloudflareAIGateway.configure({
|
||||||
accountId: "test-account",
|
accountId: "test-account",
|
||||||
@@ -104,7 +104,7 @@ describe("Cloudflare", () => {
|
|||||||
index: 0,
|
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(
|
Effect.provide(
|
||||||
dynamicResponse((input) =>
|
dynamicResponse((input) =>
|
||||||
Effect.succeed(
|
Effect.succeed(
|
||||||
@@ -150,7 +150,7 @@ describe("Cloudflare", () => {
|
|||||||
|
|
||||||
it.effect("supports authenticated AI Gateway plus upstream provider auth", () =>
|
it.effect("supports authenticated AI Gateway plus upstream provider auth", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
yield* LLM.generate(
|
yield* LLM.generateTurn(
|
||||||
LLM.request({
|
LLM.request({
|
||||||
model: CloudflareAIGateway.configure({
|
model: CloudflareAIGateway.configure({
|
||||||
accountId: "test-account",
|
accountId: "test-account",
|
||||||
@@ -221,7 +221,7 @@ describe("Cloudflare", () => {
|
|||||||
|
|
||||||
it.effect("posts direct Workers AI requests to the account endpoint with bearer auth", () =>
|
it.effect("posts direct Workers AI requests to the account endpoint with bearer auth", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const response = yield* LLM.generate(
|
const response = yield* LLM.generateTurn(
|
||||||
LLM.request({
|
LLM.request({
|
||||||
model: CloudflareWorkersAI.configure({
|
model: CloudflareWorkersAI.configure({
|
||||||
accountId: "test-account",
|
accountId: "test-account",
|
||||||
@@ -256,7 +256,7 @@ describe("Cloudflare", () => {
|
|||||||
|
|
||||||
it.effect("supports direct Workers AI token aliases through auth config", () =>
|
it.effect("supports direct Workers AI token aliases through auth config", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
yield* LLM.generate(
|
yield* LLM.generateTurn(
|
||||||
LLM.request({
|
LLM.request({
|
||||||
model: CloudflareWorkersAI.configure({
|
model: CloudflareWorkersAI.configure({
|
||||||
accountId: "test-account",
|
accountId: "test-account",
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ describe("OpenAI Responses image generation recorded", () => {
|
|||||||
partialImages: 0,
|
partialImages: 0,
|
||||||
}),
|
}),
|
||||||
]
|
]
|
||||||
const response = yield* LLM.generate(
|
const response = yield* LLM.generateTurn(
|
||||||
LLM.request({
|
LLM.request({
|
||||||
model: openai.responses("gpt-5-mini"),
|
model: openai.responses("gpt-5-mini"),
|
||||||
messages: [initial],
|
messages: [initial],
|
||||||
@@ -49,7 +49,7 @@ describe("OpenAI Responses image generation recorded", () => {
|
|||||||
expect(result.result.value[0].mime).toBe("image/jpeg")
|
expect(result.result.value[0].mime).toBe("image/jpeg")
|
||||||
expect(result.result.value[0].uri.startsWith("data:image/jpeg;base64,")).toBe(true)
|
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({
|
LLM.request({
|
||||||
model: openai.responses("gpt-5-mini"),
|
model: openai.responses("gpt-5-mini"),
|
||||||
messages: [initial, response.message, Message.user("Now make the triangle blue.")],
|
messages: [initial, response.message, Message.user("Now make the triangle blue.")],
|
||||||
|
|||||||
@@ -32,9 +32,9 @@ Tool.make({
|
|||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
LLM.stream(request)
|
LLM.streamTurn(request)
|
||||||
LLM.generate(LLMRequest.update(request, { tools: toDefinitions({ schemaOnly }) }))
|
LLM.generateTurn(LLMRequest.update(request, { tools: toDefinitions({ schemaOnly }) }))
|
||||||
ToolRuntime.dispatch({ executable }, { type: "tool-call", id: "call_1", name: "executable", input: { city: "Paris" } })
|
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.
|
// @ts-expect-error High-level tool orchestration overloads are intentionally not supported.
|
||||||
LLM.stream({ request, tools: { schemaOnly } })
|
LLM.streamTurn({ request, tools: { schemaOnly } })
|
||||||
|
|||||||
Reference in New Issue
Block a user