Browse Source

chore: generate

opencode-agent[bot] 1 month ago
parent
commit
1ef0fd5d01
1 changed files with 154 additions and 145 deletions
  1. 154 145
      packages/llm/DESIGN.md

+ 154 - 145
packages/llm/DESIGN.md

@@ -176,7 +176,7 @@ const request = LLM.request({
 })
 
 // Current API: this performs one provider turn, despite the broad name.
-const response = yield* LLM.generate(request)
+const response = yield * LLM.generate(request)
 
 // Current API: execution also needs LLMClient.layer and RequestExecutor services.
 ```
@@ -242,14 +242,15 @@ executable tools. Call-level values override model defaults.
 Provider-specific options are inferred from the concrete model:
 
 ```ts
-yield* LLM.generate({
-  model: OpenAI.model("gpt-4.1-mini"),
-  prompt: "Hello",
-  provider: {
-    store: false,
-    // OpenAI-specific autocomplete here; no `{ openai: ... }` nesting.
-  },
-})
+yield *
+  LLM.generate({
+    model: OpenAI.model("gpt-4.1-mini"),
+    prompt: "Hello",
+    provider: {
+      store: false,
+      // OpenAI-specific autocomplete here; no `{ openai: ... }` nesting.
+    },
+  })
 ```
 
 Code choosing between providers dynamically must narrow the model before using
@@ -278,12 +279,14 @@ provider.
 ### Inline input
 
 ```ts
-const result = yield* LLM.generate({
-  model,
-  system: "You are concise.",
-  prompt: "Summarize this pull request.",
-  generation: { maxTokens: 500 },
-})
+const result =
+  yield *
+  LLM.generate({
+    model,
+    system: "You are concise.",
+    prompt: "Summarize this pull request.",
+    generation: { maxTokens: 500 },
+  })
 ```
 
 ### Reusable portable request
@@ -296,7 +299,7 @@ const request = LLM.request({
 })
 
 // Bind process-local execution behavior only when running.
-const result = yield* LLM.generate({ model, request })
+const result = yield * LLM.generate({ model, request })
 ```
 
 `LLM.request(...)` returns a plain immutable object. Use ordinary object spread
@@ -362,11 +365,13 @@ const tools = {
   }),
 }
 
-const result = yield* LLM.generate({
-  model,
-  prompt: "What is the weather in London?",
-  tools,
-})
+const result =
+  yield *
+  LLM.generate({
+    model,
+    prompt: "What is the weather in London?",
+    tools,
+  })
 
 // The runtime advertises definitions, dispatches calls, records results, and
 // continues provider turns automatically.
@@ -387,15 +392,14 @@ successful result with `stopReason: "max-turns"`, not an Effect failure.
 ### Custom stopping
 
 ```ts
-const result = yield* LLM.generate({
-  model,
-  prompt,
-  tools,
-  stopWhen: StopWhen.any(
-    StopWhen.turnCount(8),
-    StopWhen.hasToolCall("finalize"),
-  ),
-})
+const result =
+  yield *
+  LLM.generate({
+    model,
+    prompt,
+    tools,
+    stopWhen: StopWhen.any(StopWhen.turnCount(8), StopWhen.hasToolCall("finalize")),
+  })
 ```
 
 `stopWhen` accepts one predicate. Composition is explicit through combinators
@@ -427,17 +431,13 @@ const request = LLM.request({
   tools: Tool.toDefinitions(tools),
 })
 
-const events = yield* LLM.stream(request).pipe(Stream.runCollect)
+const events = yield * LLM.stream(request).pipe(Stream.runCollect)
 const call = Array.from(events).find(LLMEvent.is.toolCall)
 
 if (call && !call.providerExecuted) {
-  const dispatched = yield* ToolRuntime.dispatch(tools, call)
+  const dispatched = yield * ToolRuntime.dispatch(tools, call)
   const followUp = LLM.updateRequest(request, {
-    messages: [
-      ...request.messages,
-      Message.assistant([call]),
-      Message.tool({ ...call, result: dispatched.result }),
-    ],
+    messages: [...request.messages, Message.assistant([call]), Message.tool({ ...call, result: dispatched.result })],
   })
   // Caller must invoke the provider again and repeat the loop.
 }
@@ -452,17 +452,19 @@ OpenCode and other durable runtimes need to own persistence, tool settlement,
 and continuation. They use the explicit turn API:
 
 ```ts
-const result = yield* LLM.generateTurn({
-  model,
-  request,
-  // Definitions only. generateTurn never dispatches local handlers.
-  tools: {
-    getWeather: Tool.definition({
-      description: "Get current weather for a city.",
-      parameters: WeatherInput,
-    }),
-  },
-})
+const result =
+  yield *
+  LLM.generateTurn({
+    model,
+    request,
+    // Definitions only. generateTurn never dispatches local handlers.
+    tools: {
+      getWeather: Tool.definition({
+        description: "Get current weather for a city.",
+        parameters: WeatherInput,
+      }),
+    },
+  })
 
 // Persist the TurnResult and settle calls durably before the next turn.
 for (const call of result.toolCalls) {
@@ -494,19 +496,21 @@ const request = LLM.request({
   },
 })
 
-const result = yield* LLM.generate({
-  model,
-  request,
-  tools: {
-    getWeather: Tool.make({
-      description: "Get current weather for a city.",
-      parameters: WeatherInput,
-      success: WeatherOutput,
-      execute: getWeather,
-      formatError,
-    }),
-  },
-})
+const result =
+  yield *
+  LLM.generate({
+    model,
+    request,
+    tools: {
+      getWeather: Tool.make({
+        description: "Get current weather for a city.",
+        parameters: WeatherInput,
+        success: WeatherOutput,
+        execute: getWeather,
+        formatError,
+      }),
+    },
+  })
 ```
 
 Definitions and handlers match by record key. Before the first provider call,
@@ -516,13 +520,15 @@ binding. Missing or incompatible bindings fail with a typed tool-binding error.
 Provider-hosted tools are distinct typed values:
 
 ```ts
-const result = yield* LLM.generate({
-  model: OpenAI.model("gpt-4.1"),
-  prompt: "Find today's relevant announcements.",
-  tools: {
-    search: OpenAI.tool.webSearch({ searchContextSize: "medium" }),
-  },
-})
+const result =
+  yield *
+  LLM.generate({
+    model: OpenAI.model("gpt-4.1"),
+    prompt: "Find today's relevant announcements.",
+    tools: {
+      search: OpenAI.tool.webSearch({ searchContextSize: "medium" }),
+    },
+  })
 ```
 
 Hosted tools do not pretend to have local handlers, and callers do not inspect a
@@ -589,11 +595,13 @@ const Weather = Schema.Struct({
   highCelsius: Schema.Number,
 })
 
-const result = yield* LLM.generate({
-  model,
-  prompt: "Give me today's weather for London.",
-  output: Weather,
-})
+const result =
+  yield *
+  LLM.generate({
+    model,
+    prompt: "Give me today's weather for London.",
+    output: Weather,
+  })
 
 // Inferred from Weather.
 result.output.city
@@ -612,11 +620,13 @@ Advanced callers may override the strategy when exact provider semantics matter.
 
 ```ts
 // Current API is a separate operation and always forces a synthetic tool.
-const result = yield* LLM.generateObject({
-  model,
-  prompt,
-  schema: Weather,
-})
+const result =
+  yield *
+  LLM.generateObject({
+    model,
+    prompt,
+    schema: Weather,
+  })
 ```
 
 The proposal unifies generation and lets capabilities choose the strategy rather
@@ -679,11 +689,12 @@ cache boundaries where explicit caching is supported and does nothing on the wir
 where providers cache implicitly.
 
 ```ts
-yield* LLM.generate({
-  model,
-  prompt,
-  cache: "none", // Explicit opt-out.
-})
+yield *
+  LLM.generate({
+    model,
+    prompt,
+    cache: "none", // Explicit opt-out.
+  })
 ```
 
 Granular cache policy remains available as an advanced request option.
@@ -705,13 +716,14 @@ silently inherit custom retry policies.
 ### Timeouts
 
 ```ts
-yield* LLM.generate({
-  model,
-  prompt,
-  timeout: "2 minutes",       // Entire run, including tools.
-  turnTimeout: "30 seconds",  // Each provider turn.
-  tools,
-})
+yield *
+  LLM.generate({
+    model,
+    prompt,
+    timeout: "2 minutes", // Entire run, including tools.
+    turnTimeout: "30 seconds", // Each provider turn.
+    tools,
+  })
 ```
 
 Exact Duration input spelling follows Effect conventions. Individual tools may
@@ -740,10 +752,11 @@ retry, or redirect control flow.
 ```ts
 const model = OpenAI.model("gpt-4.1", {
   hooks: {
-    request: (request) => Effect.succeed({
-      ...request,
-      metadata: { ...request.metadata, tenant: "acme" },
-    }),
+    request: (request) =>
+      Effect.succeed({
+        ...request,
+        metadata: { ...request.metadata, tenant: "acme" },
+      }),
     body: (body, context) => auditBody(body, context),
     transport: (request) => signInternalGatewayRequest(request),
     event: (event) => redactProviderMetadata(event),
@@ -775,15 +788,16 @@ The request customization ladder is:
 5. Experimental provider-definition or protocol patching
 
 ```ts
-yield* LLM.generate({
-  model,
-  prompt,
-  http: {
-    headers: { "x-experimental": "1" },
-    query: { debug: "true" },
-    body: { newlyReleasedProviderField: true },
-  },
-})
+yield *
+  LLM.generate({
+    model,
+    prompt,
+    http: {
+      headers: { "x-experimental": "1" },
+      query: { debug: "true" },
+      body: { newlyReleasedProviderField: true },
+    },
+  })
 ```
 
 Raw overlays are intentional last-resort support for provider features that ship
@@ -905,7 +919,7 @@ Schemas live in a dedicated namespace/subpath instead of flooding root exports:
 ```ts
 import { LLMSchema } from "@opencode-ai/ai/schema"
 
-const request = yield* Schema.decodeUnknown(LLMSchema.Request)(input)
+const request = yield * Schema.decodeUnknown(LLMSchema.Request)(input)
 ```
 
 Schemas cover only serializable domain values:
@@ -928,10 +942,7 @@ Provider authoring is public but experimental.
 ### Declarative provider definition
 
 ```ts
-import {
-  Provider,
-  Protocol,
-} from "@opencode-ai/ai/provider"
+import { Provider, Protocol } from "@opencode-ai/ai/provider"
 
 export const ExampleAI = Provider.define({
   id: "example",
@@ -996,9 +1007,7 @@ SDK integrations that motivated this package.
 const PatchedResponses = OpenAIResponses.with({
   body: {
     fromRequest: (request) =>
-      OpenAIResponses.body.fromRequest(request).pipe(
-        Effect.map((body) => ({ ...body, custom_field: true })),
-      ),
+      OpenAIResponses.body.fromRequest(request).pipe(Effect.map((body) => ({ ...body, custom_field: true }))),
   },
   stream: {
     step: patchResponsesStep,
@@ -1042,45 +1051,45 @@ providers, and there is no preferred all-providers barrel.
 
 ## Defaults
 
-| Concern | Default |
-| --- | --- |
-| `LLM.generate` semantics | Complete Model Run |
-| `LLM.generateTurn` semantics | Exactly one Provider Turn |
-| Maximum turns | 20 |
-| Turn-limit outcome | Successful `max-turns` result |
-| Tool execution | Automatic in runs |
-| Tool concurrency | Concurrent, bounded, deterministic result order |
-| Prompt caching | `auto` |
-| Retries | Conservative, pre-output transient failures only |
-| Structured output | Capability-selected native or tool strategy |
-| Capability mismatch | Typed failure before network execution |
-| Unknown model capability | Conservative protocol baseline |
-| Telemetry content | Metadata only |
-| Cost | Estimated aggregate or unavailable |
-| Cancellation | Interruption/rejection, never successful completion |
+| Concern                      | Default                                             |
+| ---------------------------- | --------------------------------------------------- |
+| `LLM.generate` semantics     | Complete Model Run                                  |
+| `LLM.generateTurn` semantics | Exactly one Provider Turn                           |
+| Maximum turns                | 20                                                  |
+| Turn-limit outcome           | Successful `max-turns` result                       |
+| Tool execution               | Automatic in runs                                   |
+| Tool concurrency             | Concurrent, bounded, deterministic result order     |
+| Prompt caching               | `auto`                                              |
+| Retries                      | Conservative, pre-output transient failures only    |
+| Structured output            | Capability-selected native or tool strategy         |
+| Capability mismatch          | Typed failure before network execution              |
+| Unknown model capability     | Conservative protocol baseline                      |
+| Telemetry content            | Metadata only                                       |
+| Cost                         | Estimated aggregate or unavailable                  |
+| Cancellation                 | Interruption/rejection, never successful completion |
 
 ## Clean-break Migration
 
 The redesign intentionally removes or changes these current concepts:
 
-| Current | Proposed |
-| --- | --- |
-| `@opencode-ai/llm` | `@opencode-ai/ai` |
-| Mandatory `LLM.request({ model, ... })` | Inline calls or model-free portable requests |
-| `LLM.generate` means one turn | `LLM.generate` means complete run |
-| `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` |
-| `Provider.make` structural helper | Experimental declarative `Provider.define` |
-| Schema classes as canonical values | Plain immutable values plus schema subpath |
-| `LLM.updateRequest` | Object spread |
-| `Tool.toDefinitions` in normal calls | Named executable tool records |
-| Manual `ToolRuntime.dispatch` loop | Automatic run dispatch; explicit turn API for orchestration |
-| `providerOptions: { openai: ... }` | Model-typed `provider: ...` |
-| `generateObject` | Typed `output` option on `generate` |
-| One event union for provider output | Separate `TurnEvent` and `RunEvent` unions |
-| `providerExecuted` dispatch check | Distinct hosted-tool constructors |
-| One wrapped `LLMError` | Tagged domain error union |
+| Current                                 | Proposed                                                    |
+| --------------------------------------- | ----------------------------------------------------------- |
+| `@opencode-ai/llm`                      | `@opencode-ai/ai`                                           |
+| Mandatory `LLM.request({ model, ... })` | Inline calls or model-free portable requests                |
+| `LLM.generate` means one turn           | `LLM.generate` means complete run                           |
+| `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`                            |
+| `Provider.make` structural helper       | Experimental declarative `Provider.define`                  |
+| Schema classes as canonical values      | Plain immutable values plus schema subpath                  |
+| `LLM.updateRequest`                     | Object spread                                               |
+| `Tool.toDefinitions` in normal calls    | Named executable tool records                               |
+| Manual `ToolRuntime.dispatch` loop      | Automatic run dispatch; explicit turn API for orchestration |
+| `providerOptions: { openai: ... }`      | Model-typed `provider: ...`                                 |
+| `generateObject`                        | Typed `output` option on `generate`                         |
+| One event union for provider output     | Separate `TurnEvent` and `RunEvent` unions                  |
+| `providerExecuted` dispatch check       | Distinct hosted-tool constructors                           |
+| One wrapped `LLMError`                  | Tagged domain error union                                   |
 
 OpenCode should migrate to `generateTurn` / `streamTurn`, preserving its durable
 prompt admission, persistence, permission, tool settlement, and continuation