Просмотр исходного кода

refactor(ai): align multimodal naming (#40073)

Aiden Cline 2 недель назад
Родитель
Сommit
634b9b21dd
91 измененных файлов с 563 добавлено и 500 удалено
  1. 5 5
      packages/ai/AGENTS.md
  2. 6 6
      packages/ai/DESIGN.md
  3. 1 1
      packages/ai/README.md
  4. 10 10
      packages/ai/example/call-sites.md
  5. 4 4
      packages/ai/src/image-client.ts
  6. 5 8
      packages/ai/src/image.ts
  7. 4 4
      packages/ai/src/index.ts
  8. 28 21
      packages/ai/src/llm.ts
  9. 11 13
      packages/ai/src/protocols/anthropic-messages.ts
  10. 5 7
      packages/ai/src/protocols/bedrock-converse.ts
  11. 3 3
      packages/ai/src/protocols/google-images.ts
  12. 4 10
      packages/ai/src/protocols/open-responses.ts
  13. 3 3
      packages/ai/src/protocols/openai-chat.ts
  14. 2 2
      packages/ai/src/protocols/openai-compatible-chat.ts
  15. 2 2
      packages/ai/src/protocols/openai-compatible-responses.ts
  16. 2 2
      packages/ai/src/protocols/openai-images.ts
  17. 7 7
      packages/ai/src/protocols/shared.ts
  18. 3 3
      packages/ai/src/protocols/utils/image-input.ts
  19. 2 2
      packages/ai/src/protocols/utils/tool-schema.ts
  20. 5 5
      packages/ai/src/protocols/utils/tool-stream.ts
  21. 2 2
      packages/ai/src/protocols/xai-images.ts
  22. 2 2
      packages/ai/src/protocols/zai-images.ts
  23. 3 3
      packages/ai/src/provider-error.ts
  24. 2 2
      packages/ai/src/provider-package.ts
  25. 9 9
      packages/ai/src/provider.ts
  26. 2 2
      packages/ai/src/providers/azure.ts
  27. 6 6
      packages/ai/src/providers/github-copilot.ts
  28. 3 3
      packages/ai/src/providers/openrouter.ts
  29. 4 4
      packages/ai/src/providers/xai.ts
  30. 9 5
      packages/ai/src/route/auth-options.ts
  31. 7 7
      packages/ai/src/route/auth.ts
  32. 31 26
      packages/ai/src/route/client.ts
  33. 5 5
      packages/ai/src/route/executor.ts
  34. 2 2
      packages/ai/src/route/framing.ts
  35. 2 2
      packages/ai/src/route/index.ts
  36. 3 3
      packages/ai/src/route/protocol.ts
  37. 3 7
      packages/ai/src/route/transport/index.ts
  38. 10 10
      packages/ai/src/route/transport/websocket.ts
  39. 19 19
      packages/ai/src/schema/errors.ts
  40. 1 1
      packages/ai/src/schema/events.ts
  41. 4 3
      packages/ai/src/schema/ids.ts
  42. 2 2
      packages/ai/src/schema/messages.ts
  43. 53 44
      packages/ai/src/schema/options.ts
  44. 3 3
      packages/ai/src/testing.ts
  45. 3 3
      packages/ai/test/adapter.test.ts
  46. 4 4
      packages/ai/test/auth-options.types.ts
  47. 2 2
      packages/ai/test/auth.test.ts
  48. 10 2
      packages/ai/test/continuation-scenarios.ts
  49. 2 2
      packages/ai/test/endpoint.test.ts
  50. 20 20
      packages/ai/test/executor.test.ts
  51. 3 1
      packages/ai/test/exports.test.ts
  52. 3 3
      packages/ai/test/llm-option-types.types.ts
  53. 13 13
      packages/ai/test/llm.test.ts
  54. 4 4
      packages/ai/test/provider.types.ts
  55. 2 2
      packages/ai/test/provider/anthropic-messages.recorded.test.ts
  56. 4 6
      packages/ai/test/provider/anthropic-messages.test.ts
  57. 2 2
      packages/ai/test/provider/gemini.test.ts
  58. 3 3
      packages/ai/test/provider/openai-chat-reasoning.recorded.test.ts
  59. 9 7
      packages/ai/test/provider/openai-chat.test.ts
  60. 8 8
      packages/ai/test/provider/openai-responses.test.ts
  61. 2 2
      packages/ai/test/provider/pdf.recorded.test.ts
  62. 2 2
      packages/ai/test/recorded-golden.ts
  63. 4 4
      packages/ai/test/recorded-scenarios.ts
  64. 28 8
      packages/ai/test/schema.test.ts
  65. 2 2
      packages/ai/test/tool-stream.test.ts
  66. 9 8
      packages/core/src/aisdk.ts
  67. 2 2
      packages/core/src/generate.ts
  68. 5 5
      packages/core/src/model-resolver.ts
  69. 5 5
      packages/core/src/session/compaction.ts
  70. 2 2
      packages/core/src/session/generate.ts
  71. 2 2
      packages/core/src/session/runner/index.ts
  72. 27 11
      packages/core/src/session/runner/llm.ts
  73. 2 2
      packages/core/src/session/runner/model.ts
  74. 8 4
      packages/core/src/session/runner/retry.ts
  75. 3 3
      packages/core/src/session/title.ts
  76. 3 3
      packages/core/src/session/to-session-error.ts
  77. 2 2
      packages/core/test/aisdk.test.ts
  78. 2 2
      packages/core/test/generate.test.ts
  79. 6 6
      packages/core/test/model-resolver.test.ts
  80. 2 2
      packages/core/test/prompt-cache-diagnostics.test.ts
  81. 2 2
      packages/core/test/session-compact.test.ts
  82. 3 3
      packages/core/test/session-compaction.test.ts
  83. 3 3
      packages/core/test/session-error.test.ts
  84. 2 2
      packages/core/test/session-execution.test.ts
  85. 10 2
      packages/core/test/session-generate.test.ts
  86. 18 20
      packages/core/test/session-runner.test.ts
  87. 3 10
      packages/core/test/session-title.test.ts
  88. 4 3
      packages/core/test/shared-schema.test.ts
  89. 8 0
      packages/schema/src/ai.ts
  90. 1 0
      packages/schema/src/index.ts
  91. 0 6
      packages/schema/src/llm.ts

+ 5 - 5
packages/ai/AGENTS.md

@@ -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(...)`, `LanguageModel.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.
 
 - Keep provider-defined string enums forward-compatible. Expose known values for autocomplete while accepting future values with `Known | (string & {})`; use `Schema.String` at runtime unless rejecting unknown values is required for correctness.
 
@@ -76,7 +76,7 @@ export const route = Route.make({
 })
 ```
 
-Route defaults are request-shaping defaults such as `headers`, `limits`, `generation`, `providerOptions`, and `http`. Endpoint host/query belongs on the route endpoint. Selected `Model` values carry only model id, provider id, and the configured route value. Model capability/catalog metadata lives outside this package; protocol support is enforced by request lowering and typed `LLMError`s.
+Route defaults are request-shaping defaults such as `headers`, `limits`, `generation`, `providerOptions`, and `http`. Endpoint host/query belongs on the route endpoint. Selected `LanguageModel` values carry only model id, provider id, and the configured route value. Model capability/catalog metadata lives outside this package; protocol support is enforced by request lowering and typed `AIError`s.
 
 The four-axis decomposition is the reason DeepSeek, TogetherAI, Cerebras, Baseten, Fireworks, and DeepInfra all reuse `OpenAIChat.protocol` verbatim — each provider deployment is a 5-15 line `Route.make(...)` call instead of a 300-400 line route clone. Bug fixes in one protocol propagate to every consumer of that protocol in a single commit.
 
@@ -128,7 +128,7 @@ const selected = model("gpt-5", {
 })
 ```
 
-Keep semantic APIs as separate entrypoints, such as OpenAI `chat` and `responses`. Keep transport choices inside the semantic entrypoint settings, so OpenAI Responses HTTP and WebSocket share one entrypoint. Provider facades may still expose named selectors such as `responsesWebSocket` for direct typed call sites; the package-like contract maps its settings to those selectors before returning an executable `Model`.
+Keep semantic APIs as separate entrypoints, such as OpenAI `chat` and `responses`. Keep transport choices inside the semantic entrypoint settings, so OpenAI Responses HTTP and WebSocket share one entrypoint. Provider facades may still expose named selectors such as `responsesWebSocket` for direct typed call sites; the package-like contract maps its settings to those selectors before returning an executable `LanguageModel`.
 
 Do not expose `Route` in provider package settings. Route composition stays an implementation detail behind `model(...)`.
 
@@ -138,10 +138,10 @@ Do not expose `Route` in provider package settings. Route composition stays an i
 packages/ai/src/
   schema/                   canonical Schema model, split by concern
     ids.ts                  branded IDs, literal types, ProviderMetadata
-    options.ts              Generation/Provider/Http options, Limits, Model, cache policy
+    options.ts              Generation/Provider/Http options, Limits, LanguageModel, cache policy
     messages.ts             content parts, Message, ToolDefinition, LLMRequest
     events.ts               Usage, individual events, LLMEvent, LLMResponse
-    errors.ts               error reasons, LLMError, ToolFailure
+    errors.ts               error reasons, AIError, ToolFailure
     index.ts                barrel
   llm.ts                    request constructors and convenience helpers
   route/

+ 6 - 6
packages/ai/DESIGN.md

@@ -96,7 +96,7 @@ contains identity, capabilities, pricing metadata, provider-specific option
 types, reusable request-behavior defaults, and hidden execution behavior.
 
 Normal users do not need to learn the current `Route` composite. Protocol,
-endpoint, auth, transport, and hooks are bound behind `Model`.
+endpoint, auth, transport, and hooks are bound behind `LanguageModel`.
 
 ### Request
 
@@ -539,7 +539,7 @@ Hosted tools do not pretend to have local handlers, and callers do not inspect a
 
 ### Run stream
 
-`LLM.stream` returns an Effect `Stream<RunEvent, LLMError, Requirements>`.
+`LLM.stream` returns an Effect `Stream<RunEvent, AIError, Requirements>`.
 Run events explicitly expose orchestration boundaries:
 
 ```ts
@@ -828,11 +828,11 @@ portable semantic guarantee.
 
 ## Error Model
 
-The Effect error channel is a tagged domain union rather than one `LLMError`
+The Effect error channel is a tagged domain union rather than one `AIError`
 wrapper with nested reasons. Illustrative categories:
 
 ```ts
-type LLMError =
+type AIError =
   | AuthenticationError
   | InvalidRequestError
   | UnsupportedCapabilityError
@@ -1079,7 +1079,7 @@ The redesign intentionally removes or changes these current concepts:
 | `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`                            |
+| Public `Route` mental model             | Hidden behind executable `LanguageModel`                    |
 | `Provider.make` structural helper       | Experimental declarative `Provider.define`                  |
 | Schema classes as canonical values      | Plain immutable values plus schema subpath                  |
 | `LLM.updateRequest`                     | Object spread                                               |
@@ -1089,7 +1089,7 @@ The redesign intentionally removes or changes these current concepts:
 | `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                                   |
+| One wrapped `AIError`                   | Tagged domain error union                                   |
 
 OpenCode should migrate to `generateTurn` / `streamTurn`, preserving its durable
 prompt admission, persistence, permission, tool settlement, and continuation

+ 1 - 1
packages/ai/README.md

@@ -195,7 +195,7 @@ The hosted result is represented as a provider-executed tool call and tool resul
 - **`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.
 - **`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.
+- **`LanguageModel.make(...)` / `ToolCallPart.make(...)` / `ToolResultPart.make(...)` / `ToolDefinition.make(...)`** — model and tool-related constructors from the canonical schema model.
 - **`LLMEvent.is.*`** — typed guards (`is.textDelta`, `is.toolCall`, `is.finish`, …) for filtering streams.
 - **`Image.generate({...})`** — generate images through a provider-neutral image request and response model.
 - **`ImageClient`** — Effect service and layer for image execution, parallel to `LLMClient`.

+ 10 - 10
packages/ai/example/call-sites.md

@@ -33,7 +33,7 @@ Keep durable identity separate from runtime capability:
 
 - Durable identity is small serializable data like `{ providerID, modelID }` for
   config, sessions, logs, and catalogs.
-- Runtime capability is a `Model` with a route value, protocol, transport, auth,
+- Runtime capability is a `LanguageModel` with a route value, protocol, transport, auth,
   and defaults. It is allowed to contain functions and schemas.
 - If persisted identity needs to become executable, resolve it through an app
   boundary first. Do not make `LLMRequest` recover behavior from a global route
@@ -137,7 +137,7 @@ starts hiding the real provider-specific config.
    - accepts model id only
    - returns executable models
    - does not accept endpoint/auth/deployment overrides
-4. **Model**
+4. **Language Model**
    - model id
    - route value
    - provider id
@@ -164,7 +164,7 @@ execution mechanism:
 ```ts
 type ProviderFacade<APIs, Config> = {
   readonly id: ProviderID
-  readonly model: (id: string) => Model
+  readonly model: (id: string) => LanguageModel
   readonly configure: (input?: Config) => ProviderFacade<APIs, Config>
 } & APIs
 ```
@@ -181,8 +181,8 @@ export const OpenAI = {
   configure: configureOpenAI,
 } satisfies ProviderFacade<
   {
-    responses: (id: string) => Model
-    chat: (id: string) => Model
+    responses: (id: string) => LanguageModel
+    chat: (id: string) => LanguageModel
   },
   OpenAIConfig
 >
@@ -528,7 +528,7 @@ The chosen split is:
 ```txt
 Route = execution mechanics
 Provider facade = configured route group
-Model = selected executable model carrying route value
+LanguageModel = selected executable model carrying route value
 App boundary = explicit durable-config -> typed-provider call
 ```
 
@@ -549,13 +549,13 @@ App boundary = explicit durable-config -> typed-provider call
   entrypoint maps its scoped `transport` setting before constructing the model.
 - No separate public `LLMClient.layerWithWebSocket`. The runtime should expose one
   client layer with the available transport capabilities.
-- No executable `ModelRef`. The executable handle is `Model`; durable model
+- No executable `ModelRef`. The executable handle is `LanguageModel`; durable model
   identity stays separate and cannot execute on its own.
 
 ## Implementation Todo
 
-- [x] Replace the current executable `ModelRef` with `Model`.
-- [x] Change `Model.route` to carry a route value, not a `RouteID` string.
+- [x] Replace the current executable `ModelRef` with `LanguageModel`.
+- [x] Change `LanguageModel.route` to carry a route value, not a `RouteID` string.
 - [ ] Keep a separate durable model identity type for persisted/session/catalog
       data, likely `{ providerID, modelID }`, and make it clear that it cannot
       execute without resolver context.
@@ -566,7 +566,7 @@ App boundary = explicit durable-config -> typed-provider call
 - [x] Remove endpoint/auth escape hatches from route model selection; callers must
       configure endpoint/auth through `route.with(...)` or provider facades before
       calling `.model(...)`.
-- [x] Remove request-shaping defaults from `Model`; selected models now carry only
+- [x] Remove request-shaping defaults from `LanguageModel`; selected models now carry only
       id, provider, and configured route while defaults live on routes or requests.
 - [x] Rework `LLMClient.stream` / `generate` to read
       `request.model.route` directly instead of calling `registeredRoute(...)`.

+ 4 - 4
packages/ai/src/image-client.ts

@@ -1,25 +1,25 @@
 import { Context, Effect, Layer } from "effect"
 import { RequestExecutor } from "./route/executor"
 import type { ImageOptions, ImageRequest, ImageRequestFor, ImageResponse } from "./image"
-import type { LLMError } from "./schema"
+import type { AIError } from "./schema"
 
 export type Execute = RequestExecutor.Interface["execute"]
 
 export interface Interface {
   readonly generate: <Options extends ImageOptions>(
     request: ImageRequestFor<Options>,
-  ) => Effect.Effect<ImageResponse, LLMError>
+  ) => Effect.Effect<ImageResponse, AIError>
 }
 
 export class Service extends Context.Service<Service, Interface>()("@opencode/ImageClient") {}
 
 export const generate = <Options extends ImageOptions>(
   request: ImageRequestFor<Options>,
-): Effect.Effect<ImageResponse, LLMError> =>
+): Effect.Effect<ImageResponse, AIError> =>
   Effect.gen(function* () {
     const client = yield* Service
     return yield* client.generate(request)
-  }) as Effect.Effect<ImageResponse, LLMError>
+  }) as Effect.Effect<ImageResponse, AIError>
 
 export const layer: Layer.Layer<Service, never, RequestExecutor.Service> = Layer.effect(
   Service,

+ 5 - 8
packages/ai/src/image.ts

@@ -1,13 +1,10 @@
 import { Effect, Schema } from "effect"
-import { HttpOptions, InvalidRequestReason, LLMError, ModelID, ProviderID, ProviderMetadata, Usage } from "./schema"
+import { HttpOptions, InvalidRequestReason, AIError, ModelID, ProviderID, ProviderMetadata, Usage } from "./schema"
 import { ImageClient, Service, type Execute as ImageExecute } from "./image-client"
 
 export interface ImageRoute<Options extends ImageOptions = ImageOptions> {
   readonly id: string
-  readonly generate: (
-    request: ImageRequestFor<Options>,
-    execute: ImageExecute,
-  ) => Effect.Effect<ImageResponse, LLMError>
+  readonly generate: (request: ImageRequestFor<Options>, execute: ImageExecute) => Effect.Effect<ImageResponse, AIError>
 }
 
 export type ImageOptions = Record<string, unknown>
@@ -146,13 +143,13 @@ export function request(input: ImageRequest | ImageRequestInput) {
 
 export function generate<const Model extends object>(
   input: ImageRequestInput<Model>,
-): Effect.Effect<ImageResponse, LLMError, Service>
-export function generate(input: ImageRequest): Effect.Effect<ImageResponse, LLMError, Service>
+): Effect.Effect<ImageResponse, AIError, Service>
+export function generate(input: ImageRequest): Effect.Effect<ImageResponse, AIError, Service>
 export function generate(input: ImageRequest | ImageRequestInput) {
   return Effect.try({
     try: () => (input instanceof ImageRequest ? input : request(input)),
     catch: (error) =>
-      new LLMError({
+      new AIError({
         module: "Image",
         method: "generate",
         reason: new InvalidRequestReason({ message: error instanceof Error ? error.message : String(error) }),

+ 4 - 4
packages/ai/src/index.ts

@@ -5,8 +5,8 @@ export { Provider } from "./provider"
 export { ProviderPackage } from "./provider-package"
 export { isContextOverflow, isContextOverflowFailure } from "./provider-error"
 export type {
-  RouteModelInput,
-  RouteRoutedModelInput,
+  RouteLanguageModelInput,
+  RouteRoutedLanguageModelInput,
   Interface as LLMClientShape,
   Service as LLMClientService,
 } from "./route/client"
@@ -33,7 +33,7 @@ export type {
 export * as LLM from "./llm"
 export type {
   Definition as ProviderDefinition,
-  ModelFactory as ProviderModelFactory,
-  ModelOptions as ProviderModelOptions,
+  LanguageModelFactory as ProviderLanguageModelFactory,
+  LanguageModelOptions as ProviderLanguageModelOptions,
 } from "./provider"
 export type { Definition as ProviderPackageDefinition, Settings as ProviderPackageSettings } from "./provider-package"

+ 28 - 21
packages/ai/src/llm.ts

@@ -4,33 +4,33 @@ import {
   GenerationOptions,
   HttpOptions,
   InvalidProviderOutputReason,
-  LLMError,
+  AIError,
   LLMEvent,
   LLMRequest,
   LLMResponse,
   Message,
-  Model,
+  LanguageModel,
   SystemPart,
   ToolChoice,
   ToolDefinition,
   type ContentPart,
-  type ModelProviderOptions,
+  type LanguageModelProviderOptions,
 } from "./schema"
 import { make as makeTool, toDefinitions, type ToolSchema } from "./tool"
 
 /** Input accepted by `LLM.request`, normalized into the canonical `LLMRequest` class. */
-export type RequestInput<SelectedModel extends Model = Model> = Omit<
+export type RequestInput<SelectedLanguageModel extends LanguageModel = LanguageModel> = Omit<
   ConstructorParameters<typeof LLMRequest>[0],
   "model" | "system" | "messages" | "tools" | "toolChoice" | "generation" | "http" | "providerOptions"
 > & {
-  readonly model: SelectedModel
+  readonly model: SelectedLanguageModel
   readonly system?: string | SystemPart | ReadonlyArray<SystemPart>
   readonly prompt?: string | ContentPart | ReadonlyArray<ContentPart>
   readonly messages?: ReadonlyArray<Message | Message.Input>
   readonly tools?: ReadonlyArray<ToolDefinition.Input>
   readonly toolChoice?: ToolChoice.Input
   readonly generation?: GenerationOptions.Input
-  readonly providerOptions?: NoInfer<ModelProviderOptions<SelectedModel>>
+  readonly providerOptions?: NoInfer<LanguageModelProviderOptions<SelectedLanguageModel>>
   readonly http?: HttpOptions.Input
 }
 
@@ -38,7 +38,9 @@ export const generate = LLMClient.generate
 
 export const stream = LLMClient.stream
 
-export const request = <const SelectedModel extends Model>(input: RequestInput<SelectedModel>) => {
+export const request = <const SelectedLanguageModel extends LanguageModel>(
+  input: RequestInput<SelectedLanguageModel>,
+) => {
   const {
     system: requestSystem,
     prompt,
@@ -66,7 +68,10 @@ const GENERATE_OBJECT_TOOL_NAME = "generate_object"
 
 const GENERATE_OBJECT_TOOL_DESCRIPTION = "Return the structured result by calling this tool."
 
-type GenerateObjectBase<SelectedModel extends Model = Model> = Omit<RequestInput<SelectedModel>, "tools" | "toolChoice">
+type GenerateObjectBase<SelectedLanguageModel extends LanguageModel = LanguageModel> = Omit<
+  RequestInput<SelectedLanguageModel>,
+  "tools" | "toolChoice"
+>
 
 export class GenerateObjectResponse<T> {
   constructor(
@@ -83,13 +88,15 @@ export class GenerateObjectResponse<T> {
   }
 }
 
-export interface GenerateObjectOptions<S extends ToolSchema<any>, SelectedModel extends Model = Model>
-  extends GenerateObjectBase<SelectedModel> {
+export interface GenerateObjectOptions<
+  S extends ToolSchema<any>,
+  SelectedLanguageModel extends LanguageModel = LanguageModel,
+> extends GenerateObjectBase<SelectedLanguageModel> {
   readonly schema: S
 }
 
-export interface GenerateObjectDynamicOptions<SelectedModel extends Model = Model>
-  extends GenerateObjectBase<SelectedModel> {
+export interface GenerateObjectDynamicOptions<SelectedLanguageModel extends LanguageModel = LanguageModel>
+  extends GenerateObjectBase<SelectedLanguageModel> {
   /** Raw JSON Schema object describing the expected output shape. */
   readonly jsonSchema: JsonSchema.JsonSchema
 }
@@ -108,7 +115,7 @@ const runGenerateObject = Effect.fn("LLM.generateObject")(function* (
     (event) => LLMEvent.is.toolCall(event) && event.name === GENERATE_OBJECT_TOOL_NAME,
   )
   if (!call || !LLMEvent.is.toolCall(call))
-    return yield* new LLMError({
+    return yield* new AIError({
       module: "LLM",
       method: "generateObject",
       reason: new InvalidProviderOutputReason({
@@ -118,7 +125,7 @@ const runGenerateObject = Effect.fn("LLM.generateObject")(function* (
   const object = yield* tool._decode(call.input).pipe(
     Effect.mapError(
       (error) =>
-        new LLMError({
+        new AIError({
           module: "LLM",
           method: "generateObject",
           reason: new InvalidProviderOutputReason({
@@ -138,16 +145,16 @@ const runGenerateObject = Effect.fn("LLM.generateObject")(function* (
  * Two input modes:
  *
  * 1. `schema: EffectSchema<T>` — `.object` is decoded and typed as `T`.
- *    Decode failures surface as `LLMError`.
+ *    Decode failures surface as `AIError`.
  * 2. `jsonSchema: JsonSchema.JsonSchema` — `.object` is `unknown`. Use when
  *    the schema is only available at runtime (MCP, plugin manifests). Caller validates.
  */
-export function generateObject<const SelectedModel extends Model, S extends ToolSchema<any>>(
-  options: GenerateObjectOptions<S, SelectedModel>,
-): Effect.Effect<GenerateObjectResponse<Schema.Schema.Type<S>>, LLMError>
-export function generateObject<const SelectedModel extends Model>(
-  options: GenerateObjectDynamicOptions<SelectedModel>,
-): Effect.Effect<GenerateObjectResponse<unknown>, LLMError>
+export function generateObject<const SelectedLanguageModel extends LanguageModel, S extends ToolSchema<any>>(
+  options: GenerateObjectOptions<S, SelectedLanguageModel>,
+): Effect.Effect<GenerateObjectResponse<Schema.Schema.Type<S>>, AIError>
+export function generateObject<const SelectedLanguageModel extends LanguageModel>(
+  options: GenerateObjectDynamicOptions<SelectedLanguageModel>,
+): Effect.Effect<GenerateObjectResponse<unknown>, AIError>
 export function generateObject(options: GenerateObjectOptions<ToolSchema<any>> | GenerateObjectDynamicOptions) {
   if ("schema" in options) {
     const { schema, ...rest } = options

+ 11 - 13
packages/ai/src/protocols/anthropic-messages.ts

@@ -6,7 +6,7 @@ import { Endpoint } from "../route/endpoint"
 import { Framing } from "../route/framing"
 import { Protocol } from "../route/protocol"
 import {
-  LLMError,
+  AIError,
   LLMEvent,
   mergeJsonRecords,
   Usage,
@@ -242,16 +242,14 @@ const AnthropicUsage = Schema.StructWithRest(
     cache_creation_input_tokens: optionalNull(Schema.Number),
     cache_read_input_tokens: optionalNull(Schema.Number),
     server_tool_use: optionalNull(
-      Schema.StructWithRest(
-        Schema.Struct({ web_search_requests: Schema.optional(Schema.Number) }),
-        [Schema.Record(Schema.String, Schema.Unknown)],
-      ),
+      Schema.StructWithRest(Schema.Struct({ web_search_requests: Schema.optional(Schema.Number) }), [
+        Schema.Record(Schema.String, Schema.Unknown),
+      ]),
     ),
     output_tokens_details: optionalNull(
-      Schema.StructWithRest(
-        Schema.Struct({ thinking_tokens: Schema.optional(Schema.Number) }),
-        [Schema.Record(Schema.String, Schema.Unknown)],
-      ),
+      Schema.StructWithRest(Schema.Struct({ thinking_tokens: Schema.optional(Schema.Number) }), [
+        Schema.Record(Schema.String, Schema.Unknown),
+      ]),
     ),
   }),
   [Schema.Record(Schema.String, Schema.Unknown)],
@@ -725,8 +723,7 @@ const mergeUsage = (left: Usage | undefined, right: Usage | undefined) => {
     reasoningTokens,
     totalTokens: ProviderShared.totalTokens(inputTokens, outputTokens, undefined),
     providerMetadata: {
-      anthropic:
-        mergeJsonRecords(left.providerMetadata?.["anthropic"], right.providerMetadata?.["anthropic"]) ?? {},
+      anthropic: mergeJsonRecords(left.providerMetadata?.["anthropic"], right.providerMetadata?.["anthropic"]) ?? {},
     },
   })
 }
@@ -816,7 +813,8 @@ const onContentBlockStart = (state: ParserState, event: AnthropicEvent): StepRes
   if (block.type === "thinking" && block.thinking !== undefined) {
     const events: LLMEvent[] = []
     const id = `reasoning-${event.index ?? 0}`
-    const providerMetadata = block.signature === undefined ? undefined : anthropicMetadata({ signature: block.signature })
+    const providerMetadata =
+      block.signature === undefined ? undefined : anthropicMetadata({ signature: block.signature })
     const lifecycle = Lifecycle.reasoningStart(state.lifecycle, events, id, providerMetadata)
     return [
       {
@@ -980,7 +978,7 @@ const providerErrorMessage = (event: AnthropicEvent): string => {
 }
 
 const onError = (event: AnthropicEvent) =>
-  new LLMError({
+  new AIError({
     module: ADAPTER,
     method: "stream",
     reason: classifyProviderFailure({ message: providerErrorMessage(event), code: event.error?.type }),

+ 5 - 7
packages/ai/src/protocols/bedrock-converse.ts

@@ -3,7 +3,7 @@ import { Route } from "../route/client"
 import { Endpoint } from "../route/endpoint"
 import { Protocol } from "../route/protocol"
 import {
-  LLMError,
+  AIError,
   LLMEvent,
   Usage,
   type CacheHint,
@@ -11,7 +11,7 @@ import {
   type FinishReasonDetails,
   type JsonSchema,
   type LLMRequest,
-  type ModelToolSchemaCompatibility,
+  type LanguageModelToolSchemaCompatibility,
   type ProviderMetadata,
   type ReasoningPart,
   type ToolCallPart,
@@ -232,7 +232,7 @@ const lowerToolSpec = (tool: ToolDefinition, inputSchema: JsonSchema): BedrockTo
 })
 
 const lowerTools = (
-  compatibility: ModelToolSchemaCompatibility | undefined,
+  compatibility: LanguageModelToolSchemaCompatibility | undefined,
   breakpoints: BedrockCache.Breakpoints,
   tools: ReadonlyArray<ToolDefinition>,
 ): BedrockTool[] => {
@@ -274,9 +274,7 @@ const reasoningSignature = (part: ReasoningPart) => {
 
 const reasoningRedactedData = (part: ReasoningPart) => {
   const bedrock = part.providerMetadata?.bedrock
-  return ProviderShared.isRecord(bedrock) && typeof bedrock.redactedData === "string"
-    ? bedrock.redactedData
-    : undefined
+  return ProviderShared.isRecord(bedrock) && typeof bedrock.redactedData === "string" ? bedrock.redactedData : undefined
 }
 
 const lowerToolCall = (part: ToolCallPart): BedrockToolUseBlock => ({
@@ -656,7 +654,7 @@ const step = (state: ParserState, event: BedrockEvent) =>
       ] as const
     ).find((entry) => entry[1] !== undefined)
     if (exception) {
-      return yield* new LLMError({
+      return yield* new AIError({
         module: ADAPTER,
         method: "stream",
         reason: classifyProviderFailure({

+ 3 - 3
packages/ai/src/protocols/google-images.ts

@@ -11,7 +11,7 @@ import {
 import { Auth, type Definition as AuthDefinition } from "../route/auth"
 import {
   InvalidProviderOutputReason,
-  LLMError,
+  AIError,
   Usage,
   mergeHttpOptions,
   mergeJsonRecords,
@@ -125,7 +125,7 @@ const nativeOptions = (options: GoogleImageOptions | undefined) => {
 }
 
 const invalidOutput = (message: string, providerMetadata?: ProviderMetadata) =>
-  new LLMError({
+  new AIError({
     module: ADAPTER,
     method: "generate",
     reason: new InvalidProviderOutputReason({ message, route: ADAPTER, providerMetadata }),
@@ -285,7 +285,7 @@ export const model = (input: ModelInput) => {
   return ImageModel.make<GoogleImageOptions>({ id: input.id, provider: "google", route, http: input.http })
 }
 
-const googleImagePart = (image: ImageInput): Effect.Effect<Record<string, unknown>, LLMError> => {
+const googleImagePart = (image: ImageInput): Effect.Effect<Record<string, unknown>, AIError> => {
   if (image.type === "bytes")
     return Effect.succeed({ inlineData: { mimeType: image.mediaType, data: Encoding.encodeBase64(image.data) } })
   if (image.type === "file-uri") return Effect.succeed({ fileData: { mimeType: image.mediaType, fileUri: image.uri } })

+ 4 - 10
packages/ai/src/protocols/open-responses.ts

@@ -3,7 +3,7 @@ import type { Content } from "@opencode-ai/schema/tool"
 import { HttpTransport } from "../route/transport"
 import { Protocol } from "../route/protocol"
 import {
-  LLMError,
+  AIError,
   LLMEvent,
   Usage,
   type FinishReason,
@@ -434,10 +434,7 @@ const lowerMessages = Effect.fn("OpenResponses.lowerMessages")(function* (reques
         const groups = content.reduce<Array<{ phase: MessagePhase | null | undefined; parts: TextPart[] }>>(
           (groups, part) => {
             const metadata = part.providerMetadata?.[providerMetadataKey]
-            const phase =
-              ProviderShared.isRecord(metadata)
-                ? messagePhase(metadata.phase, extension)
-                : undefined
+            const phase = ProviderShared.isRecord(metadata) ? messagePhase(metadata.phase, extension) : undefined
             const group = groups.at(-1)
             if (group && group.phase === phase) group.parts.push(part)
             else groups.push({ phase, parts: [part] })
@@ -646,10 +643,7 @@ const onOutputTextDelta = (state: ParserState, event: Event, id: string): StepRe
   const phase = state.messagePhases[id]
   const metadata = phase === undefined ? undefined : providerMetadata(state, { phase })
   const lifecycle = Lifecycle.textStart(state.lifecycle, events, id, metadata)
-  return [
-    { ...state, lifecycle: Lifecycle.textDelta(lifecycle, events, id, event.delta) },
-    events,
-  ]
+  return [{ ...state, lifecycle: Lifecycle.textDelta(lifecycle, events, id, event.delta) }, events]
 }
 
 const onOutputTextDone = (state: ParserState, event: Event, id: string): StepResult => {
@@ -975,7 +969,7 @@ const providerErrorMessage = (event: Event, fallback: string): string => {
 const providerError = (state: ParserState, event: Event, fallback: string) => {
   const code = event.code || event.error?.code || event.response?.error?.code || undefined
   const message = providerErrorMessage(event, fallback)
-  return new LLMError({
+  return new AIError({
     module: state.id,
     method: "stream",
     reason: classifyProviderFailure({ message, code }),

+ 3 - 3
packages/ai/src/protocols/openai-chat.ts

@@ -6,7 +6,7 @@ import { Endpoint } from "../route/endpoint"
 import { HttpTransport } from "../route/transport"
 import { Protocol } from "../route/protocol"
 import {
-  LLMError,
+  AIError,
   LLMEvent,
   Usage,
   type FinishReason,
@@ -555,7 +555,7 @@ const mapFinishReason = (reason: string | null | undefined): FinishReason => {
 // OpenAI Chat reports `prompt_tokens` (inclusive total) with a
 // cached-read and cache-write subsets, and `completion_tokens` (inclusive
 // total) with a `reasoning_tokens` subset. We pass the inclusive totals
-// through and derive the non-cached breakdown so the `LLM.Usage` contract is
+// through and derive the non-cached breakdown so the `AI.Usage` contract is
 // satisfied on both sides.
 const mapUsage = (usage: OpenAIChatEvent["usage"]): Usage | undefined => {
   if (!usage) return undefined
@@ -645,7 +645,7 @@ const reasoningMetadata = (field: ParserState["reasoningField"], details?: Reado
 const step = (state: ParserState, event: OpenAIChatEvent) =>
   Effect.gen(function* () {
     if (event.error)
-      return yield* new LLMError({
+      return yield* new AIError({
         module: ADAPTER,
         method: "stream",
         reason: classifyProviderFailure({

+ 2 - 2
packages/ai/src/protocols/openai-compatible-chat.ts

@@ -1,11 +1,11 @@
-import { Route, type RouteRoutedModelInput } from "../route/client"
+import { Route, type RouteRoutedLanguageModelInput } from "../route/client"
 import { Endpoint } from "../route/endpoint"
 import { Framing } from "../route/framing"
 import * as OpenAIChat from "./openai-chat"
 
 const ADAPTER = "openai-compatible-chat"
 
-export type OpenAICompatibleChatModelInput = RouteRoutedModelInput
+export type OpenAICompatibleChatLanguageModelInput = RouteRoutedLanguageModelInput
 
 /**
  * Route for non-OpenAI providers that expose an OpenAI Chat-compatible

+ 2 - 2
packages/ai/src/protocols/openai-compatible-responses.ts

@@ -1,10 +1,10 @@
-import { Route, type RouteRoutedModelInput } from "../route/client"
+import { Route, type RouteRoutedLanguageModelInput } from "../route/client"
 import { Endpoint } from "../route/endpoint"
 import { OpenResponses } from "./open-responses"
 
 const ADAPTER = "openai-compatible-responses"
 
-export type OpenAICompatibleResponsesModelInput = RouteRoutedModelInput
+export type OpenAICompatibleResponsesLanguageModelInput = RouteRoutedLanguageModelInput
 
 /**
  * Deployment adapter for providers that expose an Open Responses-compatible

+ 2 - 2
packages/ai/src/protocols/openai-images.ts

@@ -11,7 +11,7 @@ import {
 import { Auth, type Definition as AuthDefinition } from "../route/auth"
 import {
   InvalidProviderOutputReason,
-  LLMError,
+  AIError,
   Usage,
   mergeHttpOptions,
   mergeJsonRecords,
@@ -85,7 +85,7 @@ const nativeOptions = (options: OpenAIImageOptions | undefined) => {
 }
 
 const invalidOutput = (message: string) =>
-  new LLMError({
+  new AIError({
     module: ADAPTER,
     method: "generate",
     reason: new InvalidProviderOutputReason({ message, route: ADAPTER }),

+ 7 - 7
packages/ai/src/protocols/shared.ts

@@ -6,7 +6,7 @@ import { Headers, HttpClientRequest } from "effect/unstable/http"
 import {
   InvalidProviderOutputReason,
   InvalidRequestReason,
-  LLMError,
+  AIError,
   type ContentPart,
   type LLMRequest,
   type MediaPart,
@@ -41,7 +41,7 @@ export interface ToolAccumulator {
  * when at least one is defined. Returns `undefined` when neither input nor
  * output is known so routes don't publish a misleading `0`.
  *
- * Under the additive `LLM.Usage` contract, `inputTokens` and `outputTokens`
+ * Under the additive `AI.Usage` contract, `inputTokens` and `outputTokens`
  * are the non-cached input and visible output only. The provider-supplied
  * `total` is the source of truth when present; the computed fallback
  * under-counts cache and reasoning by design and exists mainly so
@@ -88,7 +88,7 @@ export const sumTokens = (...values: ReadonlyArray<number | undefined>): number
 }
 
 export const eventError = (route: string, message: string, raw?: string) =>
-  new LLMError({
+  new AIError({
     module: "ProviderShared",
     method: "stream",
     reason: new InvalidProviderOutputReason({ route, message, raw }),
@@ -238,9 +238,9 @@ export const errorText = (error: unknown) => {
  * `decodeChunk` sees one JSON string per element. The SSE channel emits a
  * `Retry` control event on its error channel; we drop it here (we don't
  * implement client-driven retries) so the public error channel stays
- * `LLMError`.
+ * `AIError`.
  */
-export const sseFraming = (bytes: Stream.Stream<Uint8Array, LLMError>): Stream.Stream<string, LLMError> =>
+export const sseFraming = (bytes: Stream.Stream<Uint8Array, AIError>): Stream.Stream<string, AIError> =>
   bytes.pipe(
     Stream.decodeText(),
     Stream.pipeThroughChannel(Sse.decode()),
@@ -257,7 +257,7 @@ export const sseFraming = (bytes: Stream.Stream<Uint8Array, LLMError>): Stream.S
  * lands here.
  */
 export const invalidRequest = (message: string) =>
-  new LLMError({
+  new AIError({
     module: "ProviderShared",
     method: "request",
     reason: new InvalidRequestReason({ message }),
@@ -304,7 +304,7 @@ export const unsupportedContent = (
  * Build a `validate` step from a Schema decoder. Replaces the per-route
  * lambda body `(payload) => decode(payload).pipe(Effect.mapError((e) =>
  * invalid(e.message)))`. Any decode error is translated into
- * `LLMError` carrying the original parse-error message.
+ * `AIError` carrying the original parse-error message.
  */
 export const validateWith =
   <A, I, E extends { readonly message: string }>(decode: (input: I) => Effect.Effect<A, E>) =>

+ 3 - 3
packages/ai/src/protocols/utils/image-input.ts

@@ -1,9 +1,9 @@
 import { Effect, Encoding } from "effect"
 import type { ImageInput } from "../../image"
-import { InvalidRequestReason, LLMError } from "../../schema"
+import { InvalidRequestReason, AIError } from "../../schema"
 
 const invalid = (module: string, message: string) =>
-  new LLMError({
+  new AIError({
     module,
     method: "generate",
     reason: new InvalidRequestReason({ message }),
@@ -15,7 +15,7 @@ export const dataUrl = (input: Extract<ImageInput, { readonly type: "bytes" }>)
 export const decodeDataUrl = (
   url: string,
   module: string,
-): Effect.Effect<{ readonly mediaType: string; readonly data: Uint8Array } | undefined, LLMError> => {
+): Effect.Effect<{ readonly mediaType: string; readonly data: Uint8Array } | undefined, AIError> => {
   if (!url.startsWith("data:")) return Effect.succeed(undefined)
   const match = /^data:([^;,]+);base64,(.*)$/s.exec(url)
   if (!match) return Effect.fail(invalid(module, "Image data URLs must contain a MIME type and base64 data"))

+ 2 - 2
packages/ai/src/protocols/utils/tool-schema.ts

@@ -1,4 +1,4 @@
-import type { JsonSchema, ModelToolSchemaCompatibility } from "../../schema"
+import type { JsonSchema, LanguageModelToolSchemaCompatibility } from "../../schema"
 import { isRecord } from "../../utils/record"
 import { GeminiToolSchema } from "./gemini-tool-schema"
 
@@ -69,7 +69,7 @@ const gemini = (schema: JsonSchema): JsonSchema => GeminiToolSchema.convert(sche
 
 const modelCompatibility = (
   schema: JsonSchema,
-  compatibility: ModelToolSchemaCompatibility | undefined,
+  compatibility: LanguageModelToolSchemaCompatibility | undefined,
 ): JsonSchema => {
   if (compatibility === undefined) return schema
   switch (compatibility) {

+ 5 - 5
packages/ai/src/protocols/utils/tool-stream.ts

@@ -1,5 +1,5 @@
 import { Effect } from "effect"
-import { LLMError, LLMEvent, type ProviderMetadata, type ToolCall, type ToolInputError } from "../../schema"
+import { AIError, LLMEvent, type ProviderMetadata, type ToolCall, type ToolInputError } from "../../schema"
 import { eventError, parseToolInput, type ToolAccumulator } from "../shared"
 
 type StreamKey = string | number
@@ -112,8 +112,8 @@ const appendTool = <K extends StreamKey>(
   }
 }
 
-export const isError = <K extends StreamKey>(result: AppendOutcome<K> | LLMError): result is LLMError =>
-  result instanceof LLMError
+export const isError = <K extends StreamKey>(result: AppendOutcome<K> | AIError): result is AIError =>
+  result instanceof AIError
 
 /**
  * Register a tool call whose start event arrived before any argument deltas.
@@ -138,7 +138,7 @@ export const appendOrStart = <K extends StreamKey>(
   key: K,
   delta: { readonly id?: string; readonly name?: string; readonly text: string },
   missingToolMessage: string,
-): AppendOutcome<K> | LLMError => {
+): AppendOutcome<K> | AIError => {
   const current = tools[key]
   const id = current?.id ?? delta.id
   const name = current?.name ?? delta.name
@@ -167,7 +167,7 @@ export const appendExisting = <K extends StreamKey>(
   key: K,
   text: string,
   missingToolMessage: string,
-): AppendOutcome<K> | LLMError => {
+): AppendOutcome<K> | AIError => {
   const current = tools[key]
   if (!current) return eventError(route, missingToolMessage)
   if (text.length === 0) return { tools, tool: current, events: [] }

+ 2 - 2
packages/ai/src/protocols/xai-images.ts

@@ -4,7 +4,7 @@ import { GeneratedImage, ImageModel, ImageResponse, type ImageRequestFor, type I
 import { Auth, type Definition as AuthDefinition } from "../route/auth"
 import {
   InvalidProviderOutputReason,
-  LLMError,
+  AIError,
   Usage,
   mergeHttpOptions,
   mergeJsonRecords,
@@ -95,7 +95,7 @@ const nativeOptions = (options: XAIImageOptions | undefined) => {
 }
 
 const invalidOutput = (message: string) =>
-  new LLMError({
+  new AIError({
     module: ADAPTER,
     method: "generate",
     reason: new InvalidProviderOutputReason({ message, route: ADAPTER }),

+ 2 - 2
packages/ai/src/protocols/zai-images.ts

@@ -2,7 +2,7 @@ import { Effect, Schema } from "effect"
 import { Headers, HttpClientRequest } from "effect/unstable/http"
 import { GeneratedImage, ImageModel, ImageResponse, type ImageRequestFor, type ImageRoute } from "../image"
 import { Auth, type Definition as AuthDefinition } from "../route/auth"
-import { InvalidProviderOutputReason, LLMError, mergeHttpOptions, mergeJsonRecords, type HttpOptions } from "../schema"
+import { InvalidProviderOutputReason, AIError, mergeHttpOptions, mergeJsonRecords, type HttpOptions } from "../schema"
 import { ProviderShared } from "./shared"
 import { ImageInputs } from "./utils/image-input"
 
@@ -58,7 +58,7 @@ const nativeOptions = (options: ZAIImageOptions | undefined) => {
 }
 
 const invalidOutput = (message: string) =>
-  new LLMError({
+  new AIError({
     module: ADAPTER,
     method: "generate",
     reason: new InvalidProviderOutputReason({ message, route: ADAPTER }),

+ 3 - 3
packages/ai/src/provider-error.ts

@@ -3,7 +3,7 @@ import {
   AuthenticationReason,
   ContentPolicyReason,
   InvalidRequestReason,
-  LLMError,
+  AIError,
   ProviderErrorEvent,
   ProviderInternalReason,
   QuotaExceededReason,
@@ -53,7 +53,7 @@ export const isContextOverflow = (message: string) =>
 export const isPayloadTooLarge = (message: string) => payloadPatterns.some((pattern) => pattern.test(message))
 
 export const isContextOverflowFailure = (failure: unknown) =>
-  failure instanceof LLMError
+  failure instanceof AIError
     ? failure.reason._tag === "InvalidRequest" && failure.reason.classification === "context-overflow"
     : Schema.is(ProviderErrorEvent)(failure) && failure.classification === "context-overflow"
 
@@ -86,7 +86,7 @@ export interface ProviderFailure {
 
 // Keep HTTP failures and provider-reported stream failures on one typed path so
 // session retry policy never needs provider-specific string matching.
-export function classifyProviderFailure(input: ProviderFailure): LLMError["reason"] {
+export function classifyProviderFailure(input: ProviderFailure): AIError["reason"] {
   const body = input.http?.body ?? ""
   const codes = [input.code, ...providerCodes(body), ...providerCodes(input.message)]
     .filter((code): code is string => code !== undefined)

+ 2 - 2
packages/ai/src/provider-package.ts

@@ -1,4 +1,4 @@
-import type { Model, ProviderOptions } from "./schema"
+import type { LanguageModel, ProviderOptions } from "./schema"
 
 export interface Settings extends Readonly<Record<string, unknown>> {
   readonly baseURL?: string
@@ -15,7 +15,7 @@ export interface Definition<
   ProviderSettings extends Settings = Settings,
   Options extends ProviderOptions = ProviderOptions,
 > {
-  readonly model: (modelID: string, settings: ProviderSettings) => Model<Options>
+  readonly model: (modelID: string, settings: ProviderSettings) => LanguageModel<Options>
 }
 
 export * as ProviderPackage from "./provider-package"

+ 9 - 9
packages/ai/src/provider.ts

@@ -1,6 +1,6 @@
-import type { Model, ModelID, ProviderID } from "./schema"
+import type { LanguageModel, ModelID, ProviderID } from "./schema"
 
-export type ModelOptions = Pick<Model.Input, "defaults" | "compatibility">
+export type LanguageModelOptions = Pick<LanguageModel.Input, "defaults" | "compatibility">
 
 /**
  * Advanced structural provider definition helper. Built-in providers should
@@ -8,23 +8,23 @@ export type ModelOptions = Pick<Model.Input, "defaults" | "compatibility">
  * chosen before model selection. The optional `apis` map remains for external
  * structural providers that expose multiple route selectors behind one provider.
  */
-export type ModelFactory<Options extends ModelOptions = ModelOptions> = (
+export type LanguageModelFactory<Options extends LanguageModelOptions = LanguageModelOptions> = (
   id: string | ModelID,
   options?: Options,
-) => Model
+) => LanguageModel
 
-type AnyModelFactory = (...args: never[]) => Model
+type AnyLanguageModelFactory = (...args: never[]) => LanguageModel
 
-export interface Definition<Factory extends AnyModelFactory = ModelFactory> {
+export interface Definition<Factory extends AnyLanguageModelFactory = LanguageModelFactory> {
   readonly id: ProviderID
   readonly model: Factory
-  readonly apis?: Record<string, AnyModelFactory>
+  readonly apis?: Record<string, AnyLanguageModelFactory>
 }
 
 type DefinitionShape = {
   readonly id: ProviderID
-  readonly model: (...args: never[]) => Model
-  readonly apis?: Record<string, (...args: never[]) => Model>
+  readonly model: (...args: never[]) => LanguageModel
+  readonly apis?: Record<string, (...args: never[]) => LanguageModel>
 }
 
 type NoExtraFields<Input, Shape> = Input & Record<Exclude<keyof Input, keyof Shape>, never>

+ 2 - 2
packages/ai/src/providers/azure.ts

@@ -14,7 +14,7 @@ const routeAuth = Auth.remove("authorization")
 // (helper builds the URL) or `baseURL` directly.
 type AzureURL = AtLeastOne<{ readonly resourceName: string; readonly baseURL: string }>
 
-export type ModelOptions = AzureURL &
+export type LanguageModelOptions = AzureURL &
   RouteDefaultsInput &
   ProviderAuthOption<"optional"> & {
     readonly apiVersion?: string
@@ -22,7 +22,7 @@ export type ModelOptions = AzureURL &
     readonly useCompletionUrls?: boolean
     readonly providerOptions?: OpenAIProviderOptionsInput
   }
-export type Config = ModelOptions
+export type Config = LanguageModelOptions
 
 export type Settings = ProviderPackage.Settings &
   AzureURL & {

+ 6 - 6
packages/ai/src/providers/github-copilot.ts

@@ -9,14 +9,14 @@ export const id = ProviderID.make("github-copilot")
 
 // GitHub Copilot has no canonical public URL — callers (opencode, etc.) must
 // supply `baseURL` explicitly.
-export type ModelOptions = Omit<RouteDefaultsInput, "providerOptions"> &
+export type LanguageModelOptions = Omit<RouteDefaultsInput, "providerOptions"> &
   ProviderAuthOption<"optional"> & {
     readonly baseURL: string
     readonly endpoint?: "chat" | "responses"
     readonly providerOptions?: OpenAIProviderOptionsInput
   }
 
-export const shouldUseResponsesApi = (modelID: string | ModelID, endpoint?: ModelOptions["endpoint"]) => {
+export const shouldUseResponsesApi = (modelID: string | ModelID, endpoint?: LanguageModelOptions["endpoint"]) => {
   if (endpoint) return endpoint === "responses"
   const model = String(modelID)
   const match = /^gpt-(\d+)/.exec(model)
@@ -29,24 +29,24 @@ export const routes = [OpenAIResponses.route, OpenAIChat.route]
 const chatRoute = OpenAIChat.route.with({ provider: id })
 const responsesRoute = OpenAIResponses.route.with({ provider: id })
 
-const defaults = (options: ModelOptions) => {
+const defaults = (options: LanguageModelOptions) => {
   const { apiKey: _, auth: _auth, baseURL: _baseURL, endpoint: _endpoint, ...rest } = options
   return rest
 }
 
-const configuredResponsesRoute = (options: ModelOptions) =>
+const configuredResponsesRoute = (options: LanguageModelOptions) =>
   responsesRoute.with({
     endpoint: { baseURL: options.baseURL },
     auth: AuthOptions.bearer(options, []),
   })
 
-const configuredChatRoute = (options: ModelOptions) =>
+const configuredChatRoute = (options: LanguageModelOptions) =>
   chatRoute.with({
     endpoint: { baseURL: options.baseURL },
     auth: AuthOptions.bearer(options, []),
   })
 
-export const configure = (options: ModelOptions) => {
+export const configure = (options: LanguageModelOptions) => {
   const responsesRoute = configuredResponsesRoute(options)
   const chatRoute = configuredChatRoute(options)
   const responses = (modelID: string | ModelID) =>

+ 3 - 3
packages/ai/src/providers/openrouter.ts

@@ -76,7 +76,7 @@ export type OpenRouterProviderOptionsInput = ProviderOptions & {
   readonly openrouter?: OpenRouterOptions
 }
 
-export type ModelOptions = Omit<RouteDefaultsInput, "providerOptions"> &
+export type LanguageModelOptions = Omit<RouteDefaultsInput, "providerOptions"> &
   ProviderAuthOption<"optional"> & {
     readonly baseURL?: string
     readonly providerOptions?: OpenRouterProviderOptionsInput
@@ -175,7 +175,7 @@ export const route = Route.make({
 
 export const routes = [route]
 
-const configuredRoute = (input: ModelOptions) => {
+const configuredRoute = (input: LanguageModelOptions) => {
   const { apiKey: _, auth: _auth, baseURL, ...rest } = input
   return route.with({
     ...rest,
@@ -184,7 +184,7 @@ const configuredRoute = (input: ModelOptions) => {
   })
 }
 
-export const configure = (input: ModelOptions = {}) => {
+export const configure = (input: LanguageModelOptions = {}) => {
   const route = configuredRoute(input)
   return {
     id,

+ 4 - 4
packages/ai/src/providers/xai.ts

@@ -16,7 +16,7 @@ export type XAIProviderOptionsInput = ProviderOptions & {
   readonly xai?: OpenAIOptionsInput
 }
 
-export type ModelOptions = Omit<RouteDefaultsInput, "providerOptions"> &
+export type LanguageModelOptions = Omit<RouteDefaultsInput, "providerOptions"> &
   ProviderAuthOption<"optional"> & {
     readonly baseURL?: string
     readonly providerOptions?: XAIProviderOptionsInput
@@ -53,7 +53,7 @@ export const routes = [responsesRoute, chatRoute]
 
 const auth = (options: ProviderAuthOption<"optional">) => AuthOptions.bearer(options, "XAI_API_KEY")
 
-const configuredResponsesRoute = (input: ModelOptions) => {
+const configuredResponsesRoute = (input: LanguageModelOptions) => {
   const { apiKey: _, auth: _auth, baseURL, ...rest } = input
   return responsesRoute.with({
     ...rest,
@@ -62,7 +62,7 @@ const configuredResponsesRoute = (input: ModelOptions) => {
   })
 }
 
-const configuredChatRoute = (input: ModelOptions) => {
+const configuredChatRoute = (input: LanguageModelOptions) => {
   const { apiKey: _, auth: _auth, baseURL, ...rest } = input
   return chatRoute.with({
     ...rest,
@@ -71,7 +71,7 @@ const configuredChatRoute = (input: ModelOptions) => {
   })
 }
 
-export const configure = (input: ModelOptions = {}) => {
+export const configure = (input: LanguageModelOptions = {}) => {
   const responsesRoute = configuredResponsesRoute(input)
   const chatRoute = configuredChatRoute(input)
   const responses = (modelID: string | ModelID) => responsesRoute.model<XAIProviderOptionsInput>({ id: modelID })

+ 9 - 5
packages/ai/src/route/auth-options.ts

@@ -22,13 +22,17 @@ export type ProviderAuthOption<Mode extends ApiKeyMode> =
   | AuthOverride
   | (Mode extends "optional" ? OptionalApiKeyAuth : RequiredApiKeyAuth)
 
-export type ModelOptions<Base, Mode extends ApiKeyMode> = Omit<Base, "apiKey" | "auth"> & ProviderAuthOption<Mode>
+export type LanguageModelOptions<Base, Mode extends ApiKeyMode> = Omit<Base, "apiKey" | "auth"> &
+  ProviderAuthOption<Mode>
 
-export type ModelArgs<Base, Mode extends ApiKeyMode> = Mode extends "optional"
-  ? readonly [options?: ModelOptions<Base, Mode>]
-  : readonly [options: ModelOptions<Base, Mode>]
+export type LanguageModelArgs<Base, Mode extends ApiKeyMode> = Mode extends "optional"
+  ? readonly [options?: LanguageModelOptions<Base, Mode>]
+  : readonly [options: LanguageModelOptions<Base, Mode>]
 
-export type ModelFactory<Base, Mode extends ApiKeyMode, Model> = (id: string, ...args: ModelArgs<Base, Mode>) => Model
+export type LanguageModelFactory<Base, Mode extends ApiKeyMode, LanguageModel> = (
+  id: string,
+  ...args: LanguageModelArgs<Base, Mode>
+) => LanguageModel
 
 /**
  * Require at least one of the keys in `T`. Use for option shapes where any

+ 7 - 7
packages/ai/src/route/auth.ts

@@ -1,6 +1,6 @@
 import { Config, Effect, Redacted } from "effect"
 import { Headers } from "effect/unstable/http"
-import { AuthenticationReason, InvalidRequestReason, LLMError, type HttpOptions } from "../schema"
+import { AuthenticationReason, InvalidRequestReason, AIError, type HttpOptions } from "../schema"
 
 export class MissingCredentialError extends Error {
   readonly _tag = "MissingCredentialError"
@@ -11,7 +11,7 @@ export class MissingCredentialError extends Error {
 }
 
 export type CredentialError = MissingCredentialError | Config.ConfigError
-export type AuthError = CredentialError | LLMError
+export type AuthError = CredentialError | AIError
 type Secret = string | Redacted.Redacted | Config.Config<string | Redacted.Redacted>
 
 export interface AuthInput {
@@ -100,7 +100,7 @@ export const headers = (input: Headers.Input) =>
 
 export const remove = (name: string) => auth((input) => Effect.succeed(Headers.remove(input.headers, name)))
 
-export const custom = (apply: (input: AuthInput) => Effect.Effect<Headers.Headers, LLMError>) => auth(apply)
+export const custom = (apply: (input: AuthInput) => Effect.Effect<Headers.Headers, AIError>) => auth(apply)
 
 export const passthrough = none
 
@@ -134,9 +134,9 @@ export function bearerHeader(name: string, source?: Secret | Credential) {
   return render(source)
 }
 
-const toLLMError = (error: AuthError): LLMError => {
+const toAIError = (error: AuthError): AIError => {
   if (error instanceof MissingCredentialError || error instanceof Config.ConfigError) {
-    return new LLMError({
+    return new AIError({
       module: "Auth",
       method: "apply",
       reason:
@@ -150,7 +150,7 @@ const toLLMError = (error: AuthError): LLMError => {
 
 export const toEffect =
   (input: Definition) =>
-  (authInput: AuthInput): Effect.Effect<Headers.Headers, LLMError> =>
-    input.apply(authInput).pipe(Effect.mapError(toLLMError))
+  (authInput: AuthInput): Effect.Effect<Headers.Headers, AIError> =>
+    input.apply(authInput).pipe(Effect.mapError(toAIError))
 
 export * as Auth from "./auth"

+ 31 - 26
packages/ai/src/route/client.ts

@@ -10,15 +10,15 @@ import { WebSocketExecutor } from "./transport"
 import type { Protocol } from "./protocol"
 import { applyCachePolicy } from "../cache-policy"
 import * as ProviderShared from "../protocols/shared"
-import type { LLMError, ProtocolID, ProviderOptions } from "../schema"
+import type { ProtocolID, ProviderOptions } from "../schema"
 import {
+  AIError,
   GenerationOptions,
   HttpOptions,
   LLMRequest,
   LLMResponse,
-  Model,
-  ModelLimits,
-  LLMError as LLMErrorClass,
+  LanguageModel,
+  LanguageModelLimits,
   LLMEvent,
   ProviderID,
   mergeGenerationOptions,
@@ -30,7 +30,7 @@ export interface RouteBody<Body> {
   /** Schema for the validated provider-native body sent as the JSON request. */
   readonly schema: Schema.Codec<Body, unknown>
   /** Build the provider-native body from a common `LLMRequest`. */
-  readonly from: (request: LLMRequest) => Effect.Effect<Body, LLMError>
+  readonly from: (request: LLMRequest) => Effect.Effect<Body, AIError>
 }
 
 export interface Route<Body, Prepared = unknown> {
@@ -45,17 +45,19 @@ export interface Route<Body, Prepared = unknown> {
   readonly defaults: RouteDefaults
   readonly body: RouteBody<Body>
   readonly with: (patch: RoutePatch<Body, Prepared>) => Route<Body, Prepared>
-  readonly model: <Options extends ProviderOptions = ProviderOptions>(input: RouteMappedModelInput) => Model<Options>
+  readonly model: <Options extends ProviderOptions = ProviderOptions>(
+    input: RouteMappedLanguageModelInput,
+  ) => LanguageModel<Options>
   readonly prepareTransport: (
     body: Body,
     request: LLMRequest,
     options?: StreamOptions,
-  ) => Effect.Effect<Prepared, LLMError>
+  ) => Effect.Effect<Prepared, AIError>
   readonly streamPrepared: (
     prepared: Prepared,
     request: LLMRequest,
     runtime: TransportRuntime,
-  ) => Stream.Stream<LLMEvent, LLMError>
+  ) => Stream.Stream<LLMEvent, AIError>
 }
 
 // Route registries intentionally erase body generics after construction.
@@ -66,13 +68,13 @@ export type AnyRoute = Route<any, any>
 
 export type HttpOptionsInput = HttpOptions.Input
 
-export type RouteModelInput = Omit<Model.Input, "provider" | "route">
+export type RouteLanguageModelInput = Omit<LanguageModel.Input, "provider" | "route">
 
-export type RouteRoutedModelInput = Omit<Model.Input, "route">
+export type RouteRoutedLanguageModelInput = Omit<LanguageModel.Input, "route">
 
 export interface RouteDefaults {
   readonly headers?: Record<string, string>
-  readonly limits?: ModelLimits
+  readonly limits?: LanguageModelLimits
   readonly generation?: GenerationOptions
   readonly providerOptions?: ProviderOptions
   readonly http?: HttpOptions
@@ -80,7 +82,7 @@ export interface RouteDefaults {
 
 export interface RouteDefaultsInput {
   readonly headers?: Record<string, string>
-  readonly limits?: ModelLimits.Input
+  readonly limits?: LanguageModelLimits.Input
   readonly generation?: GenerationOptions.Input
   readonly providerOptions?: ProviderOptions
   readonly http?: HttpOptions.Input
@@ -94,14 +96,17 @@ export interface RoutePatch<Body, Prepared> extends RouteDefaultsInput {
   readonly endpoint?: EndpointPatch<Body>
 }
 
-type RouteMappedModelInput = RouteModelInput | RouteRoutedModelInput
+type RouteMappedLanguageModelInput = RouteLanguageModelInput | RouteRoutedLanguageModelInput
 
-const makeRouteModel = <Options extends ProviderOptions = ProviderOptions>(route: AnyRoute, mapped: RouteMappedModelInput) => {
+const makeRouteLanguageModel = <Options extends ProviderOptions = ProviderOptions>(
+  route: AnyRoute,
+  mapped: RouteMappedLanguageModelInput,
+) => {
   const provider = route.provider ?? ("provider" in mapped ? mapped.provider : undefined)
   if (!provider) throw new Error(`Route.model(${route.id}) requires a provider`)
   if (!endpointBaseURL(route.endpoint))
     throw new Error(`Route.model(${route.id}) requires an endpoint baseURL — configure it on the route first`)
-  return Model.make<Options>({
+  return LanguageModel.make<Options>({
     ...mapped,
     provider,
     route,
@@ -114,7 +119,7 @@ const mergeRouteDefaults = (base: RouteDefaults | undefined, patch: RouteDefault
     ...base,
     ...patch,
     headers,
-    limits: patch.limits === undefined ? base?.limits : ModelLimits.make(patch.limits),
+    limits: patch.limits === undefined ? base?.limits : LanguageModelLimits.make(patch.limits),
     generation: mergeGenerationOptions(generationOptions(base?.generation), generationOptions(patch.generation)),
     providerOptions: mergeProviderOptions(base?.providerOptions, patch.providerOptions),
     http: mergeHttpOptions(
@@ -154,11 +159,11 @@ export interface StreamOptions {
 }
 
 export interface StreamMethod {
-  (request: LLMRequest, options?: StreamOptions): Stream.Stream<LLMEvent, LLMError>
+  (request: LLMRequest, options?: StreamOptions): Stream.Stream<LLMEvent, AIError>
 }
 
 export interface GenerateMethod {
-  (request: LLMRequest, options?: StreamOptions): Effect.Effect<LLMResponse, LLMError>
+  (request: LLMRequest, options?: StreamOptions): Effect.Effect<LLMResponse, AIError>
 }
 
 export class Service extends Context.Service<Service, Interface>()("@opencode/LLMClient") {}
@@ -222,11 +227,11 @@ export interface MakeTransportInput<Body, Prepared, Frame, Event, State> {
 
 const streamError = (route: string, message: string, cause: Cause.Cause<unknown>) => {
   const failed = cause.reasons.find(Cause.isFailReason)?.error
-  if (failed instanceof LLMErrorClass) return failed
+  if (failed instanceof AIError) return failed
   return ProviderShared.eventError(route, message, Cause.pretty(cause))
 }
 
-const requireTerminalEvent = (route: string) => (events: Stream.Stream<LLMEvent, LLMError>) =>
+const requireTerminalEvent = (route: string) => (events: Stream.Stream<LLMEvent, AIError>) =>
   Stream.suspend(() => {
     let terminal = false
     return events.pipe(
@@ -292,8 +297,8 @@ function makeFromTransport<Body, Prepared, Frame, Event, State>(
           defaults: mergeRouteDefaults(route.defaults, defaults),
         })
       },
-      model: <Options extends ProviderOptions = ProviderOptions>(input: RouteMappedModelInput) =>
-        makeRouteModel<Options>(route, input),
+      model: <Options extends ProviderOptions = ProviderOptions>(input: RouteMappedLanguageModelInput) =>
+        makeRouteLanguageModel<Options>(route, input),
       prepareTransport: (body, request, options) =>
         routeInput.transport.prepare({
           body,
@@ -417,18 +422,18 @@ const generateWith = (stream: Interface["stream"]) =>
     )
   })
 
-export function stream(request: LLMRequest, options?: StreamOptions): Stream.Stream<LLMEvent, LLMError> {
+export function stream(request: LLMRequest, options?: StreamOptions): Stream.Stream<LLMEvent, AIError> {
   return Stream.unwrap(
     Effect.gen(function* () {
       return (yield* Service).stream(request, options)
     }),
-  ) as Stream.Stream<LLMEvent, LLMError>
+  ) as Stream.Stream<LLMEvent, AIError>
 }
 
-export function generate(request: LLMRequest, options?: StreamOptions): Effect.Effect<LLMResponse, LLMError> {
+export function generate(request: LLMRequest, options?: StreamOptions): Effect.Effect<LLMResponse, AIError> {
   return Effect.gen(function* () {
     return yield* (yield* Service).generate(request, options)
-  }) as Effect.Effect<LLMResponse, LLMError>
+  }) as Effect.Effect<LLMResponse, AIError>
 }
 
 export const streamRequest = (request: LLMRequest, options?: StreamOptions) =>

+ 5 - 5
packages/ai/src/route/executor.ts

@@ -12,7 +12,7 @@ import {
   HttpRateLimitDetails,
   HttpRequestDetails,
   HttpResponseDetails,
-  LLMError,
+  AIError,
   TransportReason,
 } from "../schema"
 import { classifyProviderFailure } from "../provider-error"
@@ -20,10 +20,10 @@ import { classifyProviderFailure } from "../provider-error"
 export interface Interface {
   readonly execute: (
     request: HttpClientRequest.HttpClientRequest,
-  ) => Effect.Effect<HttpClientResponse.HttpClientResponse, LLMError>
+  ) => Effect.Effect<HttpClientResponse.HttpClientResponse, AIError>
 }
 
-export class Service extends Context.Service<Service, Interface>()("@opencode/LLM/RequestExecutor") {}
+export class Service extends Context.Service<Service, Interface>()("@opencode/AI/RequestExecutor") {}
 
 const BODY_LIMIT = 16_384
 const REDACTED = "<redacted>"
@@ -220,7 +220,7 @@ const statusError =
       const retryAfter = retryAfterMs(headers)
       const rateLimit = rateLimitDetails(headers, retryAfter)
       const details = responseBody(body, request)
-      return yield* new LLMError({
+      return yield* new AIError({
         module: "RequestExecutor",
         method: "execute",
         reason: classifyProviderFailure({
@@ -246,7 +246,7 @@ const toHttpError = (redactedNames: ReadonlyArray<string | RegExp>) => (error: u
     readonly kind?: string | undefined
     readonly request?: HttpClientRequest.HttpClientRequest | undefined
   }) =>
-    new LLMError({
+    new AIError({
       module: "RequestExecutor",
       method: "execute",
       reason: new TransportReason({

+ 2 - 2
packages/ai/src/route/framing.ts

@@ -1,6 +1,6 @@
 import type { Stream } from "effect"
 import * as ProviderShared from "../protocols/shared"
-import type { LLMError } from "../schema"
+import type { AIError } from "../schema"
 
 /**
  * Decode a streaming HTTP response body into provider-protocol frames.
@@ -18,7 +18,7 @@ import type { LLMError } from "../schema"
  */
 export interface Definition<Frame> {
   readonly id: string
-  readonly frame: (bytes: Stream.Stream<Uint8Array, LLMError>) => Stream.Stream<Frame, LLMError>
+  readonly frame: (bytes: Stream.Stream<Uint8Array, AIError>) => Stream.Stream<Frame, AIError>
 }
 
 /** Server-Sent Events framing. Used by every JSON-streaming HTTP provider. */

+ 2 - 2
packages/ai/src/route/index.ts

@@ -1,8 +1,8 @@
 export { Route, LLMClient } from "./client"
 export type {
   Route as RouteShape,
-  RouteModelInput,
-  RouteRoutedModelInput,
+  RouteLanguageModelInput,
+  RouteRoutedLanguageModelInput,
   RouteDefaults,
   RouteDefaultsInput,
   AnyRoute,

+ 3 - 3
packages/ai/src/route/protocol.ts

@@ -1,5 +1,5 @@
 import { Schema, type Effect } from "effect"
-import type { LLMError, LLMEvent, LLMRequest, ProtocolID } from "../schema"
+import type { AIError, LLMEvent, LLMRequest, ProtocolID } from "../schema"
 
 /**
  * The semantic API contract of one model server family.
@@ -47,7 +47,7 @@ export interface ProtocolBody<Body> {
   /** Schema for the validated provider-native body sent as the JSON request. */
   readonly schema: Schema.Codec<Body, unknown>
   /** Build the provider-native body from a common `LLMRequest`. */
-  readonly from: (request: LLMRequest) => Effect.Effect<Body, LLMError>
+  readonly from: (request: LLMRequest) => Effect.Effect<Body, AIError>
 }
 
 export interface ProtocolStream<Frame, Event, State> {
@@ -56,7 +56,7 @@ export interface ProtocolStream<Frame, Event, State> {
   /** Initial parser state. Called once per response with the resolved request. */
   readonly initial: (request: LLMRequest) => State
   /** Translate one event into emitted `LLMEvent`s plus the next state. */
-  readonly step: (state: State, event: Event) => Effect.Effect<readonly [State, ReadonlyArray<LLMEvent>], LLMError>
+  readonly step: (state: State, event: Event) => Effect.Effect<readonly [State, ReadonlyArray<LLMEvent>], AIError>
   /** Optional request-completion signal for transports that do not end naturally. */
   readonly terminal?: (event: Event) => boolean
   /** Optional flush emitted when the framed stream ends. */

+ 3 - 7
packages/ai/src/route/transport/index.ts

@@ -3,7 +3,7 @@ import { Endpoint } from "../endpoint"
 import { Auth } from "../auth"
 import type { Interface as RequestExecutorInterface } from "../executor"
 import type { Interface as WebSocketExecutorInterface } from "./websocket"
-import type { LLMError, LLMRequest } from "../../schema"
+import type { AIError, LLMRequest } from "../../schema"
 
 export interface TransportRuntime {
   readonly http: RequestExecutorInterface
@@ -21,12 +21,8 @@ export type HttpRequestTransform = (request: HttpRequest) => Effect.Effect<void>
 
 export interface Transport<Body, Prepared, Frame> {
   readonly id: string
-  readonly prepare: (input: TransportPrepareInput<Body>) => Effect.Effect<Prepared, LLMError>
-  readonly frames: (
-    prepared: Prepared,
-    request: LLMRequest,
-    runtime: TransportRuntime,
-  ) => Stream.Stream<Frame, LLMError>
+  readonly prepare: (input: TransportPrepareInput<Body>) => Effect.Effect<Prepared, AIError>
+  readonly frames: (prepared: Prepared, request: LLMRequest, runtime: TransportRuntime) => Stream.Stream<Frame, AIError>
 }
 
 export interface TransportPrepareInput<Body> {

+ 10 - 10
packages/ai/src/route/transport/websocket.ts

@@ -1,6 +1,6 @@
 import { Cause, Context, Effect, Layer, Queue, Stream } from "effect"
 import { Headers } from "effect/unstable/http"
-import { LLMError, TransportReason } from "../../schema"
+import { AIError, TransportReason } from "../../schema"
 import * as HttpTransport from "./http"
 import type { Transport } from "./index"
 
@@ -10,13 +10,13 @@ export interface WebSocketRequest {
 }
 
 export interface WebSocketConnection {
-  readonly sendText: (message: string) => Effect.Effect<void, LLMError>
-  readonly messages: Stream.Stream<string | Uint8Array, LLMError>
+  readonly sendText: (message: string) => Effect.Effect<void, AIError>
+  readonly messages: Stream.Stream<string | Uint8Array, AIError>
   readonly close: Effect.Effect<void, never>
 }
 
 export interface Interface {
-  readonly open: (input: WebSocketRequest) => Effect.Effect<WebSocketConnection, LLMError>
+  readonly open: (input: WebSocketRequest) => Effect.Effect<WebSocketConnection, AIError>
 }
 
 type WebSocketConstructorWithHeaders = new (
@@ -24,14 +24,14 @@ type WebSocketConstructorWithHeaders = new (
   options?: { readonly headers?: Headers.Headers },
 ) => globalThis.WebSocket
 
-export class Service extends Context.Service<Service, Interface>()("@opencode/LLM/WebSocketExecutor") {}
+export class Service extends Context.Service<Service, Interface>()("@opencode/AI/WebSocketExecutor") {}
 
 const transportError = (
   method: string,
   message: string,
   input: { readonly url?: string; readonly kind?: string } = {},
 ) =>
-  new LLMError({
+  new AIError({
     module: "WebSocketExecutor",
     method,
     reason: new TransportReason({ message, url: input.url, kind: input.kind }),
@@ -59,7 +59,7 @@ const waitOpen = (ws: globalThis.WebSocket, input: WebSocketRequest) => {
       }),
     )
   }
-  return Effect.callback<void, LLMError>((resume, signal) => {
+  return Effect.callback<void, AIError>((resume, signal) => {
     const cleanup = () => {
       ws.removeEventListener("open", onOpen)
       ws.removeEventListener("error", onError)
@@ -138,10 +138,10 @@ export const layer: Layer.Layer<Service> = Layer.succeed(Service, Service.of({ o
 export const fromWebSocket = (
   ws: globalThis.WebSocket,
   input: WebSocketRequest,
-): Effect.Effect<WebSocketConnection, LLMError> =>
+): Effect.Effect<WebSocketConnection, AIError> =>
   Effect.gen(function* () {
     yield* waitOpen(ws, input)
-    const messages = yield* Queue.bounded<string | Uint8Array, LLMError | Cause.Done<void>>(128)
+    const messages = yield* Queue.bounded<string | Uint8Array, AIError | Cause.Done<void>>(128)
 
     const onMessage = (event: MessageEvent) => {
       if (typeof event.data === "string") return Queue.offerUnsafe(messages, event.data)
@@ -213,7 +213,7 @@ export interface JsonPrepared {
 }
 
 export interface JsonInput<Body, Message> {
-  readonly toMessage: (body: Body | Record<string, unknown>) => Effect.Effect<Message, LLMError>
+  readonly toMessage: (body: Body | Record<string, unknown>) => Effect.Effect<Message, AIError>
   readonly encodeMessage: (message: Message) => string
 }
 

+ 19 - 19
packages/ai/src/schema/errors.ts

@@ -5,25 +5,25 @@ import { ModelID, ProviderID, ProviderMetadata, RouteID } from "./ids"
 export const ProviderFailureClassification = Schema.Literals(["context-overflow", "payload-too-large"])
 export type ProviderFailureClassification = typeof ProviderFailureClassification.Type
 
-export class HttpRequestDetails extends Schema.Class<HttpRequestDetails>("LLM.HttpRequestDetails")({
+export class HttpRequestDetails extends Schema.Class<HttpRequestDetails>("AI.HttpRequestDetails")({
   method: Schema.String,
   url: Schema.String,
   headers: Schema.Record(Schema.String, Schema.String),
 }) {}
 
-export class HttpResponseDetails extends Schema.Class<HttpResponseDetails>("LLM.HttpResponseDetails")({
+export class HttpResponseDetails extends Schema.Class<HttpResponseDetails>("AI.HttpResponseDetails")({
   status: Schema.Number,
   headers: Schema.Record(Schema.String, Schema.String),
 }) {}
 
-export class HttpRateLimitDetails extends Schema.Class<HttpRateLimitDetails>("LLM.HttpRateLimitDetails")({
+export class HttpRateLimitDetails extends Schema.Class<HttpRateLimitDetails>("AI.HttpRateLimitDetails")({
   retryAfterMs: Schema.optional(Schema.Number),
   limit: Schema.optional(Schema.Record(Schema.String, Schema.String)),
   remaining: Schema.optional(Schema.Record(Schema.String, Schema.String)),
   reset: Schema.optional(Schema.Record(Schema.String, Schema.String)),
 }) {}
 
-export class HttpContext extends Schema.Class<HttpContext>("LLM.HttpContext")({
+export class HttpContext extends Schema.Class<HttpContext>("AI.HttpContext")({
   request: HttpRequestDetails,
   response: Schema.optional(HttpResponseDetails),
   body: Schema.optional(Schema.String),
@@ -32,7 +32,7 @@ export class HttpContext extends Schema.Class<HttpContext>("LLM.HttpContext")({
   rateLimit: Schema.optional(HttpRateLimitDetails),
 }) {}
 
-export class InvalidRequestReason extends Schema.Class<InvalidRequestReason>("LLM.Error.InvalidRequest")({
+export class InvalidRequestReason extends Schema.Class<InvalidRequestReason>("AI.Error.InvalidRequest")({
   _tag: Schema.tag("InvalidRequest"),
   message: Schema.String,
   parameter: Schema.optional(Schema.String),
@@ -41,18 +41,18 @@ export class InvalidRequestReason extends Schema.Class<InvalidRequestReason>("LL
   http: Schema.optional(HttpContext),
 }) {}
 
-export class NoRouteReason extends Schema.Class<NoRouteReason>("LLM.Error.NoRoute")({
+export class NoRouteReason extends Schema.Class<NoRouteReason>("AI.Error.NoRoute")({
   _tag: Schema.tag("NoRoute"),
   route: RouteID,
   provider: ProviderID,
   model: ModelID,
 }) {
   get message() {
-    return `No LLM route for ${this.provider}/${this.model} using ${this.route}`
+    return `No AI route for ${this.provider}/${this.model} using ${this.route}`
   }
 }
 
-export class AuthenticationReason extends Schema.Class<AuthenticationReason>("LLM.Error.Authentication")({
+export class AuthenticationReason extends Schema.Class<AuthenticationReason>("AI.Error.Authentication")({
   _tag: Schema.tag("Authentication"),
   message: Schema.String,
   kind: Schema.Literals(["missing", "invalid", "expired", "insufficient-permissions", "unknown"]),
@@ -60,7 +60,7 @@ export class AuthenticationReason extends Schema.Class<AuthenticationReason>("LL
   http: Schema.optional(HttpContext),
 }) {}
 
-export class RateLimitReason extends Schema.Class<RateLimitReason>("LLM.Error.RateLimit")({
+export class RateLimitReason extends Schema.Class<RateLimitReason>("AI.Error.RateLimit")({
   _tag: Schema.tag("RateLimit"),
   message: Schema.String,
   retryAfterMs: Schema.optional(Schema.Number),
@@ -69,21 +69,21 @@ export class RateLimitReason extends Schema.Class<RateLimitReason>("LLM.Error.Ra
   http: Schema.optional(HttpContext),
 }) {}
 
-export class QuotaExceededReason extends Schema.Class<QuotaExceededReason>("LLM.Error.QuotaExceeded")({
+export class QuotaExceededReason extends Schema.Class<QuotaExceededReason>("AI.Error.QuotaExceeded")({
   _tag: Schema.tag("QuotaExceeded"),
   message: Schema.String,
   providerMetadata: Schema.optional(ProviderMetadata),
   http: Schema.optional(HttpContext),
 }) {}
 
-export class ContentPolicyReason extends Schema.Class<ContentPolicyReason>("LLM.Error.ContentPolicy")({
+export class ContentPolicyReason extends Schema.Class<ContentPolicyReason>("AI.Error.ContentPolicy")({
   _tag: Schema.tag("ContentPolicy"),
   message: Schema.String,
   providerMetadata: Schema.optional(ProviderMetadata),
   http: Schema.optional(HttpContext),
 }) {}
 
-export class ProviderInternalReason extends Schema.Class<ProviderInternalReason>("LLM.Error.ProviderInternal")({
+export class ProviderInternalReason extends Schema.Class<ProviderInternalReason>("AI.Error.ProviderInternal")({
   _tag: Schema.tag("ProviderInternal"),
   message: Schema.String,
   status: Schema.optional(Schema.Number),
@@ -92,7 +92,7 @@ export class ProviderInternalReason extends Schema.Class<ProviderInternalReason>
   http: Schema.optional(HttpContext),
 }) {}
 
-export class TransportReason extends Schema.Class<TransportReason>("LLM.Error.Transport")({
+export class TransportReason extends Schema.Class<TransportReason>("AI.Error.Transport")({
   _tag: Schema.tag("Transport"),
   message: Schema.String,
   kind: Schema.optional(Schema.String),
@@ -101,7 +101,7 @@ export class TransportReason extends Schema.Class<TransportReason>("LLM.Error.Tr
 }) {}
 
 export class InvalidProviderOutputReason extends Schema.Class<InvalidProviderOutputReason>(
-  "LLM.Error.InvalidProviderOutput",
+  "AI.Error.InvalidProviderOutput",
 )({
   _tag: Schema.tag("InvalidProviderOutput"),
   message: Schema.String,
@@ -110,7 +110,7 @@ export class InvalidProviderOutputReason extends Schema.Class<InvalidProviderOut
   providerMetadata: Schema.optional(ProviderMetadata),
 }) {}
 
-export class UnknownProviderReason extends Schema.Class<UnknownProviderReason>("LLM.Error.UnknownProvider")({
+export class UnknownProviderReason extends Schema.Class<UnknownProviderReason>("AI.Error.UnknownProvider")({
   _tag: Schema.tag("UnknownProvider"),
   message: Schema.String,
   status: Schema.optional(Schema.Number),
@@ -118,7 +118,7 @@ export class UnknownProviderReason extends Schema.Class<UnknownProviderReason>("
   http: Schema.optional(HttpContext),
 }) {}
 
-export const LLMErrorReason = Schema.Union([
+export const AIErrorReason = Schema.Union([
   InvalidRequestReason,
   NoRouteReason,
   AuthenticationReason,
@@ -130,12 +130,12 @@ export const LLMErrorReason = Schema.Union([
   InvalidProviderOutputReason,
   UnknownProviderReason,
 ]).pipe(Schema.toTaggedUnion("_tag"))
-export type LLMErrorReason = Schema.Schema.Type<typeof LLMErrorReason>
+export type AIErrorReason = Schema.Schema.Type<typeof AIErrorReason>
 
-export class LLMError extends Schema.TaggedErrorClass<LLMError>()("LLM.Error", {
+export class AIError extends Schema.TaggedErrorClass<AIError>()("AI.Error", {
   module: Schema.String,
   method: Schema.String,
-  reason: LLMErrorReason,
+  reason: AIErrorReason,
 }) {
   override readonly cause = this.reason
 

+ 1 - 1
packages/ai/src/schema/events.ts

@@ -48,7 +48,7 @@ import { ProviderFailureClassification } from "./errors"
  * — for fields we don't normalize and for billing-level audit trails.
  * Matches the same escape-hatch field on `LLMEvent`.
  */
-export class Usage extends Schema.Class<Usage>("LLM.Usage")({
+export class Usage extends Schema.Class<Usage>("AI.Usage")({
   inputTokens: Schema.optional(Schema.Number),
   outputTokens: Schema.optional(Schema.Number),
   nonCachedInputTokens: Schema.optional(Schema.Number),

+ 4 - 3
packages/ai/src/schema/ids.ts

@@ -1,5 +1,6 @@
 import { Schema } from "effect"
-import { LLM, ProviderMetadata } from "@opencode-ai/schema/llm"
+import { ProviderMetadata } from "@opencode-ai/schema/ai"
+import { LLM } from "@opencode-ai/schema/llm"
 
 export { ProviderMetadata }
 
@@ -11,10 +12,10 @@ export type ProtocolID = Schema.Schema.Type<typeof ProtocolID>
 export const RouteID = Schema.String
 export type RouteID = Schema.Schema.Type<typeof RouteID>
 
-export const ModelID = Schema.String.pipe(Schema.brand("LLM.ModelID"))
+export const ModelID = Schema.String.pipe(Schema.brand("AI.ModelID"))
 export type ModelID = typeof ModelID.Type
 
-export const ProviderID = Schema.String.pipe(Schema.brand("LLM.ProviderID"))
+export const ProviderID = Schema.String.pipe(Schema.brand("AI.ProviderID"))
 export type ProviderID = typeof ProviderID.Type
 
 export const ResponseID = Schema.String

+ 2 - 2
packages/ai/src/schema/messages.ts

@@ -1,7 +1,7 @@
 import { Schema } from "effect"
 import { Tool } from "@opencode-ai/schema/tool"
 import { JsonSchema, MessageRole, ProviderMetadata } from "./ids"
-import { CacheHint, CachePolicy, GenerationOptions, HttpOptions, ModelSchema, ProviderOptions } from "./options"
+import { CacheHint, CachePolicy, GenerationOptions, HttpOptions, LanguageModelSchema, ProviderOptions } from "./options"
 import { isRecord } from "../utils/record"
 
 const systemPartSchema = Schema.Struct({
@@ -263,7 +263,7 @@ export namespace ToolChoice {
 
 export class LLMRequest extends Schema.Class<LLMRequest>("LLM.Request")({
   id: Schema.optional(Schema.String),
-  model: ModelSchema,
+  model: LanguageModelSchema,
   system: Schema.Array(SystemPart),
   messages: Schema.Array(Message),
   tools: Schema.Array(ToolDefinition),

+ 53 - 44
packages/ai/src/schema/options.ts

@@ -50,7 +50,7 @@ export const mergeProviderOptions = (
   return Object.keys(result).length === 0 ? undefined : result
 }
 
-export class HttpOptions extends Schema.Class<HttpOptions>("LLM.HttpOptions")({
+export class HttpOptions extends Schema.Class<HttpOptions>("AI.HttpOptions")({
   body: Schema.optional(JsonSchema),
   headers: Schema.optional(Schema.Record(Schema.String, Schema.String)),
   query: Schema.optional(Schema.Record(Schema.String, Schema.String)),
@@ -121,32 +121,32 @@ export const mergeGenerationOptions = (...items: ReadonlyArray<GenerationOptions
   return Object.values(result).some((value) => value !== undefined) ? result : undefined
 }
 
-export class ModelLimits extends Schema.Class<ModelLimits>("LLM.ModelLimits")({
+export class LanguageModelLimits extends Schema.Class<LanguageModelLimits>("LLM.LanguageModelLimits")({
   context: Schema.optional(Schema.Number),
   input: Schema.optional(Schema.Number),
   output: Schema.optional(Schema.Number),
 }) {}
 
-export namespace ModelLimits {
-  export type Input = ModelLimits | ConstructorParameters<typeof ModelLimits>[0]
+export namespace LanguageModelLimits {
+  export type Input = LanguageModelLimits | ConstructorParameters<typeof LanguageModelLimits>[0]
 
-  /** Normalize model limit input into the canonical `ModelLimits` class. */
+  /** Normalize model limit input into the canonical `LanguageModelLimits` class. */
   export const make = (input: Input | undefined) =>
-    input instanceof ModelLimits ? input : new ModelLimits(input ?? {})
+    input instanceof LanguageModelLimits ? input : new LanguageModelLimits(input ?? {})
 }
 
-export class ModelDefaults extends Schema.Class<ModelDefaults>("LLM.ModelDefaults")({
-  limits: Schema.optional(ModelLimits),
+export class LanguageModelDefaults extends Schema.Class<LanguageModelDefaults>("LLM.LanguageModelDefaults")({
+  limits: Schema.optional(LanguageModelLimits),
   generation: Schema.optional(GenerationOptions),
   providerOptions: Schema.optional(ProviderOptions),
   http: Schema.optional(HttpOptions),
 }) {}
 
-export namespace ModelDefaults {
+export namespace LanguageModelDefaults {
   export type Input =
-    | ModelDefaults
+    | LanguageModelDefaults
     | {
-        readonly limits?: ModelLimits.Input
+        readonly limits?: LanguageModelLimits.Input
         readonly generation?: GenerationOptions.Input
         readonly providerOptions?: ProviderOptions
         readonly http?: HttpOptions.Input
@@ -154,9 +154,9 @@ export namespace ModelDefaults {
 
   /** Normalize selected-model request defaults without applying precedence. */
   export const make = (input: Input) => {
-    if (input instanceof ModelDefaults) return input
-    return new ModelDefaults({
-      limits: input.limits === undefined ? undefined : ModelLimits.make(input.limits),
+    if (input instanceof LanguageModelDefaults) return input
+    return new LanguageModelDefaults({
+      limits: input.limits === undefined ? undefined : LanguageModelLimits.make(input.limits),
       generation: input.generation === undefined ? undefined : GenerationOptions.make(input.generation),
       providerOptions: input.providerOptions,
       http: input.http === undefined ? undefined : HttpOptions.make(input.http),
@@ -164,34 +164,39 @@ export namespace ModelDefaults {
   }
 }
 
-export const ModelToolSchemaCompatibility = Schema.Literals(["gemini", "moonshot"])
-export type ModelToolSchemaCompatibility = Schema.Schema.Type<typeof ModelToolSchemaCompatibility>
+export const LanguageModelToolSchemaCompatibility = Schema.Literals(["gemini", "moonshot"])
+export type LanguageModelToolSchemaCompatibility = Schema.Schema.Type<typeof LanguageModelToolSchemaCompatibility>
 
-export const ModelMaxTokensFieldCompatibility = Schema.Literals(["max_completion_tokens", "max_tokens"])
-export type ModelMaxTokensFieldCompatibility = Schema.Schema.Type<typeof ModelMaxTokensFieldCompatibility>
+export const LanguageModelMaxTokensFieldCompatibility = Schema.Literals(["max_completion_tokens", "max_tokens"])
+export type LanguageModelMaxTokensFieldCompatibility = Schema.Schema.Type<
+  typeof LanguageModelMaxTokensFieldCompatibility
+>
 
-export class ModelCompatibility extends Schema.Class<ModelCompatibility>("LLM.ModelCompatibility")({
-  toolSchema: Schema.optional(ModelToolSchemaCompatibility),
+export class LanguageModelCompatibility extends Schema.Class<LanguageModelCompatibility>(
+  "LLM.LanguageModelCompatibility",
+)({
+  toolSchema: Schema.optional(LanguageModelToolSchemaCompatibility),
   reasoningField: Schema.optional(Schema.String),
-  maxTokensField: Schema.optional(ModelMaxTokensFieldCompatibility),
+  maxTokensField: Schema.optional(LanguageModelMaxTokensFieldCompatibility),
 }) {}
 
-export namespace ModelCompatibility {
-  export type Input = ModelCompatibility | ConstructorParameters<typeof ModelCompatibility>[0]
+export namespace LanguageModelCompatibility {
+  export type Input = LanguageModelCompatibility | ConstructorParameters<typeof LanguageModelCompatibility>[0]
 
   /** Normalize model/upstream compatibility metadata without projecting requests. */
-  export const make = (input: Input) => (input instanceof ModelCompatibility ? input : new ModelCompatibility(input))
+  export const make = (input: Input) =>
+    input instanceof LanguageModelCompatibility ? input : new LanguageModelCompatibility(input)
 }
 
-export class Model<Options extends ProviderOptions = ProviderOptions> {
+export class LanguageModel<Options extends ProviderOptions = ProviderOptions> {
   declare protected readonly _ProviderOptions: Options
   readonly id: ModelID
   readonly provider: ProviderID
   readonly route: AnyRoute
-  readonly defaults?: ModelDefaults
-  readonly compatibility?: ModelCompatibility
+  readonly defaults?: LanguageModelDefaults
+  readonly compatibility?: LanguageModelCompatibility
 
-  constructor(input: Model.ConstructorInput) {
+  constructor(input: LanguageModel.ConstructorInput) {
     this.id = input.id
     this.provider = input.provider
     this.route = input.route
@@ -199,17 +204,18 @@ export class Model<Options extends ProviderOptions = ProviderOptions> {
     this.compatibility = input.compatibility
   }
 
-  static make<Options extends ProviderOptions = ProviderOptions>(input: Model.Input) {
-    return new Model<Options>({
+  static make<Options extends ProviderOptions = ProviderOptions>(input: LanguageModel.Input) {
+    return new LanguageModel<Options>({
       id: ModelID.make(input.id),
       provider: ProviderID.make(input.provider),
       route: input.route,
-      defaults: input.defaults === undefined ? undefined : ModelDefaults.make(input.defaults),
-      compatibility: input.compatibility === undefined ? undefined : ModelCompatibility.make(input.compatibility),
+      defaults: input.defaults === undefined ? undefined : LanguageModelDefaults.make(input.defaults),
+      compatibility:
+        input.compatibility === undefined ? undefined : LanguageModelCompatibility.make(input.compatibility),
     })
   }
 
-  static input<Options extends ProviderOptions>(model: Model<Options>): Model.ConstructorInput {
+  static input<Options extends ProviderOptions>(model: LanguageModel<Options>): LanguageModel.ConstructorInput {
     return {
       id: model.id,
       provider: model.provider,
@@ -219,37 +225,40 @@ export class Model<Options extends ProviderOptions = ProviderOptions> {
     }
   }
 
-  static update<Options extends ProviderOptions>(model: Model<Options>, patch: Partial<Model.Input>) {
+  static update<Options extends ProviderOptions>(model: LanguageModel<Options>, patch: Partial<LanguageModel.Input>) {
     if (Object.keys(patch).length === 0) return model
-    return Model.make<Options>({
-      ...Model.input(model),
+    return LanguageModel.make<Options>({
+      ...LanguageModel.input(model),
       ...patch,
     })
   }
 }
 
-export namespace Model {
+export namespace LanguageModel {
   export type ConstructorInput = {
     readonly id: ModelID
     readonly provider: ProviderID
     readonly route: AnyRoute
-    readonly defaults?: ModelDefaults
-    readonly compatibility?: ModelCompatibility
+    readonly defaults?: LanguageModelDefaults
+    readonly compatibility?: LanguageModelCompatibility
   }
 
   export type Input = Omit<ConstructorInput, "id" | "provider" | "defaults" | "compatibility"> & {
     readonly id: string | ModelID
     readonly provider: string | ProviderID
-    readonly defaults?: ModelDefaults.Input
-    readonly compatibility?: ModelCompatibility.Input
+    readonly defaults?: LanguageModelDefaults.Input
+    readonly compatibility?: LanguageModelCompatibility.Input
   }
 }
 
-export type ModelInput = Model.Input
+export type LanguageModelInput = LanguageModel.Input
 
-export type ModelProviderOptions<SelectedModel> = SelectedModel extends Model<infer Options> ? Options : never
+export type LanguageModelProviderOptions<SelectedModel> =
+  SelectedModel extends LanguageModel<infer Options> ? Options : never
 
-export const ModelSchema = Schema.declare((value): value is Model => value instanceof Model, { expected: "LLM.Model" })
+export const LanguageModelSchema = Schema.declare((value): value is LanguageModel => value instanceof LanguageModel, {
+  expected: "LLM.LanguageModel",
+})
 
 export class CacheHint extends Schema.Class<CacheHint>("LLM.CacheHint")({
   type: Schema.Literals(["ephemeral", "persistent"]),

+ 3 - 3
packages/ai/src/testing.ts

@@ -5,13 +5,13 @@ import {
   LLMEvent,
   LLMResponse,
   type FinishReasonDetails,
-  type LLMError,
+  type AIError,
   type LLMRequest,
   type UsageInput,
 } from "./schema"
 import { Context, Deferred, Effect, Latch, Layer, Queue, Scope, Stream } from "effect"
 
-export type Response = readonly LLMEvent[] | Stream.Stream<LLMEvent, LLMError>
+export type Response = readonly LLMEvent[] | Stream.Stream<LLMEvent, AIError>
 
 export type Gate = Readonly<{ started: Effect.Effect<void>; release: Effect.Effect<void> }>
 
@@ -63,7 +63,7 @@ export const textWithUsage = (value: string, id: string, inputTokens: number) =>
 
 export const tool = (id: string, name: string, input: unknown) => toolCalls(LLMEvent.toolCall({ id, name, input }))
 
-export const failAfter = (error: LLMError, ...events: readonly LLMEvent[]) =>
+export const failAfter = (error: AIError, ...events: readonly LLMEvent[]) =>
   Stream.fromIterable(events).pipe(Stream.concat(Stream.fail(error)))
 
 export const hangAfter = (...events: readonly LLMEvent[]) => Stream.concat(Stream.fromIterable(events), Stream.never)

+ 3 - 3
packages/ai/test/adapter.test.ts

@@ -3,11 +3,11 @@ import { Effect, Schema, Stream } from "effect"
 import { LLM, LLMRequest, LLMResponse } from "../src"
 import { Route, Endpoint, LLMClient, Protocol, type FramingDef } from "../src/route"
 import { compileRequest } from "../src/route/client"
-import { Model } from "../src/schema"
+import { LanguageModel } from "../src/schema"
 import { testEffect } from "./lib/effect"
 import { dynamicResponse } from "./lib/http"
 
-const updateModel = (model: Model, patch: Partial<Model.Input>) => Model.update(model, patch)
+const updateModel = (model: LanguageModel, patch: Partial<LanguageModel.Input>) => LanguageModel.update(model, patch)
 
 const Json = Schema.fromJsonString(Schema.Unknown)
 const encodeJson = Schema.encodeSync(Json)
@@ -86,7 +86,7 @@ const configuredGemini = gemini.with({ endpoint: { baseURL: "https://fake.local"
 
 const request = LLM.request({
   id: "req_1",
-  model: Model.make({
+  model: LanguageModel.make({
     id: "fake-model",
     provider: "fake-provider",
     route: configuredFake,

+ 4 - 4
packages/ai/test/auth-options.types.ts

@@ -1,6 +1,6 @@
 import { Config } from "effect"
 import { Auth } from "../src/route"
-import type { ModelFactory } from "../src/route/auth-options"
+import type { LanguageModelFactory } from "../src/route/auth-options"
 import * as OpenAIChat from "../src/protocols/openai-chat"
 import * as AmazonBedrock from "../src/providers/amazon-bedrock"
 import * as Anthropic from "../src/providers/anthropic"
@@ -23,13 +23,13 @@ type BaseOptions = {
   readonly headers?: Record<string, string>
 }
 
-type Model = {
+type LanguageModel = {
   readonly id: string
 }
 
 declare const auth: Auth.Definition
-declare const optionalAuthModel: ModelFactory<BaseOptions, "optional", Model>
-declare const requiredAuthModel: ModelFactory<BaseOptions, "required", Model>
+declare const optionalAuthModel: LanguageModelFactory<BaseOptions, "optional", LanguageModel>
+declare const requiredAuthModel: LanguageModelFactory<BaseOptions, "required", LanguageModel>
 const configApiKey = Config.redacted("OPENAI_API_KEY")
 
 OpenAIChat.route.model({ id: "gpt-4.1-mini" })

+ 2 - 2
packages/ai/test/auth.test.ts

@@ -4,12 +4,12 @@ import { Headers } from "effect/unstable/http"
 import { LLM } from "../src"
 import { Auth } from "../src/route/auth"
 import * as OpenAIChat from "../src/protocols/openai-chat"
-import { Model } from "../src/schema"
+import { LanguageModel } from "../src/schema"
 import { it } from "./lib/effect"
 
 const request = LLM.request({
   id: "req_auth",
-  model: Model.make({ id: "fake-model", provider: "fake", route: OpenAIChat.route }),
+  model: LanguageModel.make({ id: "fake-model", provider: "fake", route: OpenAIChat.route }),
   prompt: "hello",
 })
 

+ 10 - 2
packages/ai/test/continuation-scenarios.ts

@@ -1,4 +1,12 @@
-import { LLM, Message, ToolCallPart, ToolDefinition, ToolResultPart, type ContentPart, type Model } from "../src"
+import {
+  LLM,
+  Message,
+  ToolCallPart,
+  ToolDefinition,
+  ToolResultPart,
+  type ContentPart,
+  type LanguageModel,
+} from "../src"
 
 export const basicContinuation = ["system", "user-text", "assistant-text", "user-follow-up"] as const
 export const toolContinuation = ["tool-call", "tool-result"] as const
@@ -40,7 +48,7 @@ export const continuationTool = ToolDefinition.make({
 
 export function continuationRequest(input: {
   readonly id: string
-  readonly model: Model
+  readonly model: LanguageModel
   readonly features: ReadonlyArray<ContinuationFeature>
   readonly image?: string
 }) {

+ 2 - 2
packages/ai/test/endpoint.test.ts

@@ -2,11 +2,11 @@ import { describe, expect, test } from "bun:test"
 import { LLM } from "../src"
 import * as OpenAIChat from "../src/protocols/openai-chat"
 import { Endpoint } from "../src/route"
-import { Model } from "../src/schema"
+import { LanguageModel } from "../src/schema"
 
 const request = () =>
   LLM.request({
-    model: Model.make({
+    model: LanguageModel.make({
       id: "model-1",
       provider: "test",
       route: OpenAIChat.route,

+ 20 - 20
packages/ai/test/executor.test.ts

@@ -1,7 +1,7 @@
 import { describe, expect } from "bun:test"
 import { Effect, Layer, Ref } from "effect"
 import { Headers, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
-import { LLM, LLMError } from "../src"
+import { LLM, AIError } from "../src"
 import { LLMClient, RequestExecutor } from "../src/route"
 import * as OpenAIChat from "../src/protocols/openai-chat"
 import { dynamicResponse } from "./lib/http"
@@ -58,13 +58,13 @@ const countedResponsesLayer = (attempts: Ref.Ref<number>, responses: ReadonlyArr
     ),
   )
 
-const expectLLMError = (error: unknown) => {
-  expect(error).toBeInstanceOf(LLMError)
-  if (!(error instanceof LLMError)) throw new Error("expected LLMError")
+const expectAIError = (error: unknown) => {
+  expect(error).toBeInstanceOf(AIError)
+  if (!(error instanceof AIError)) throw new Error("expected AIError")
   return error
 }
 
-const errorHttp = (error: LLMError) => ("http" in error.reason ? error.reason.http : undefined)
+const errorHttp = (error: AIError) => ("http" in error.reason ? error.reason.http : undefined)
 
 describe("RequestExecutor", () => {
   it.effect("classifies context overflow responses", () =>
@@ -72,7 +72,7 @@ describe("RequestExecutor", () => {
       const executor = yield* RequestExecutor.Service
       const error = yield* executor.execute(request).pipe(Effect.flip)
 
-      expectLLMError(error)
+      expectAIError(error)
       expect(error.reason).toMatchObject({ _tag: "InvalidRequest", classification: "context-overflow" })
     }).pipe(
       Effect.provide(
@@ -90,7 +90,7 @@ describe("RequestExecutor", () => {
       const executor = yield* RequestExecutor.Service
       const error = yield* executor.execute(request).pipe(Effect.flip)
 
-      expectLLMError(error)
+      expectAIError(error)
       expect(error.reason).toMatchObject({
         _tag: "InvalidRequest",
         classification: "payload-too-large",
@@ -104,7 +104,7 @@ describe("RequestExecutor", () => {
       const executor = yield* RequestExecutor.Service
       const error = yield* executor.execute(request).pipe(Effect.flip)
 
-      expectLLMError(error)
+      expectAIError(error)
       expect(error.reason).toMatchObject({ _tag: "InvalidRequest" })
       expect("classification" in error.reason ? error.reason.classification : undefined).toBeUndefined()
     }).pipe(Effect.provide(responsesLayer([new Response("invalid parameter", { status: 400 })]))),
@@ -117,7 +117,7 @@ describe("RequestExecutor", () => {
           const executor = yield* RequestExecutor.Service
           const error = yield* executor.execute(request).pipe(Effect.flip)
 
-          expectLLMError(error)
+          expectAIError(error)
           expect(error.reason).toMatchObject({ _tag: "RateLimit" })
         }).pipe(Effect.provide(responsesLayer([new Response(body, { status: 400 })])))
 
@@ -134,7 +134,7 @@ describe("RequestExecutor", () => {
           const executor = yield* RequestExecutor.Service
           const error = yield* executor.execute(request).pipe(Effect.flip)
 
-          expectLLMError(error)
+          expectAIError(error)
           expect(error.reason).toMatchObject({ _tag: "ProviderInternal" })
         }).pipe(Effect.provide(responsesLayer([new Response(body, { status: 400 })])))
 
@@ -148,7 +148,7 @@ describe("RequestExecutor", () => {
       const executor = yield* RequestExecutor.Service
       const error = yield* executor.execute(request).pipe(Effect.flip)
 
-      expectLLMError(error)
+      expectAIError(error)
       expect(error).toMatchObject({
         reason: {
           _tag: "RateLimit",
@@ -190,7 +190,7 @@ describe("RequestExecutor", () => {
       const executor = yield* RequestExecutor.Service
       const error = yield* executor.execute(request).pipe(Effect.flip)
 
-      expectLLMError(error)
+      expectAIError(error)
       expect(errorHttp(error)?.request.headers["x-safe"]).toBe("<redacted>")
       expect(errorHttp(error)?.response?.headers["x-safe"]).toBe("<redacted>")
     }).pipe(
@@ -204,7 +204,7 @@ describe("RequestExecutor", () => {
       const executor = yield* RequestExecutor.Service
       const error = yield* executor.execute(request).pipe(Effect.flip)
 
-      expectLLMError(error)
+      expectAIError(error)
       expect(error.reason).toMatchObject({ _tag: "RateLimit" })
       expect(error.reason._tag === "RateLimit" ? error.reason.rateLimit : undefined).toEqual({
         retryAfterMs: 0,
@@ -237,7 +237,7 @@ describe("RequestExecutor", () => {
       const executor = yield* RequestExecutor.Service
       const error = yield* executor.execute(request).pipe(Effect.flip)
 
-      expectLLMError(error)
+      expectAIError(error)
       expect(error.reason).toMatchObject({ _tag: "ProviderInternal" })
       expect(errorHttp(error)?.rateLimit).toEqual({
         retryAfterMs: 0,
@@ -280,7 +280,7 @@ describe("RequestExecutor", () => {
         ),
       )
 
-      expectLLMError(error)
+      expectAIError(error)
       expect(error.reason).toMatchObject({ _tag: "ProviderInternal", status: 503 })
       expect(yield* Ref.get(attempts)).toBe(1)
     }),
@@ -293,7 +293,7 @@ describe("RequestExecutor", () => {
           const executor = yield* RequestExecutor.Service
           const error = yield* executor.execute(request).pipe(Effect.flip)
 
-          expectLLMError(error)
+          expectAIError(error)
           expect(error.reason).toMatchObject({ _tag: "ProviderInternal", status })
         }).pipe(
           Effect.provide(
@@ -316,7 +316,7 @@ describe("RequestExecutor", () => {
       const executor = yield* RequestExecutor.Service
       const error = yield* executor.execute(request).pipe(Effect.flip)
 
-      expectLLMError(error)
+      expectAIError(error)
       expect(error.reason).toMatchObject({ _tag: "Authentication" })
       expect(errorHttp(error)?.bodyTruncated).toBe(true)
       expect(errorHttp(error)?.body).toHaveLength(16_384)
@@ -335,7 +335,7 @@ describe("RequestExecutor", () => {
       const executor = yield* RequestExecutor.Service
       const error = yield* executor.execute(request).pipe(Effect.flip)
 
-      expectLLMError(error)
+      expectAIError(error)
       expect(errorHttp(error)?.body).toContain('"key":"<redacted>"')
       expect(errorHttp(error)?.body).toContain("api_key=<redacted>")
       expect(errorHttp(error)?.body).not.toContain("body-secret")
@@ -356,7 +356,7 @@ describe("RequestExecutor", () => {
       const executor = yield* RequestExecutor.Service
       const error = yield* executor.execute(secretRequest).pipe(Effect.flip)
 
-      expectLLMError(error)
+      expectAIError(error)
       expect(errorHttp(error)?.body).toContain("provider echoed <redacted>")
       expect(errorHttp(error)?.body).toContain("authorization <redacted>")
       expect(errorHttp(error)?.body).not.toContain("query-secret-123")
@@ -395,7 +395,7 @@ describe("RequestExecutor", () => {
         Effect.flip,
       )
 
-      expectLLMError(error)
+      expectAIError(error)
       expect(error.reason).toMatchObject({ _tag: "InvalidProviderOutput" })
       expect(yield* Ref.get(attempts)).toBe(1)
     }),

+ 3 - 1
packages/ai/test/exports.test.ts

@@ -1,5 +1,5 @@
 import { describe, expect, test } from "bun:test"
-import { ImageInput, LLM, LLMClient, Provider } from "@opencode-ai/ai"
+import { AIError, ImageInput, LanguageModel, LLM, LLMClient, Provider } from "@opencode-ai/ai"
 import { Route, Protocol } from "@opencode-ai/ai/route"
 import { Provider as ProviderSubpath } from "@opencode-ai/ai/provider"
 import {
@@ -26,6 +26,8 @@ describe("public exports", () => {
     expect(LLM.request).toBeFunction()
     expect(LLMClient.Service).toBeFunction()
     expect(LLMClient.layer).toBeDefined()
+    expect(AIError).toBeFunction()
+    expect(LanguageModel.make).toBeFunction()
     expect(ImageInput.bytes).toBeFunction()
     expect(Provider.make).toBeFunction()
     expect(ProviderSubpath.make).toBe(Provider.make)

+ 3 - 3
packages/ai/test/llm-option-types.types.ts

@@ -1,5 +1,5 @@
 import { Schema } from "effect"
-import { LLM, type Model, type ModelProviderOptions, type ProviderOptions } from "../src"
+import { LLM, type LanguageModel, type LanguageModelProviderOptions, type ProviderOptions } from "../src"
 import { OpenAIChat } from "../src/protocols"
 
 interface ExampleOptions {
@@ -40,8 +40,8 @@ LLM.generateObject({
   providerOptions: { example: { mode: false } },
 })
 
-declare const generic: Model
+declare const generic: LanguageModel
 LLM.request({ model: generic, prompt: "Hello", providerOptions: { arbitrary: { option: true } } })
 
-const options: ModelProviderOptions<typeof model> = { example: { mode: "fast" } }
+const options: LanguageModelProviderOptions<typeof model> = { example: { mode: "fast" } }
 void options

+ 13 - 13
packages/ai/test/llm.test.ts

@@ -6,7 +6,7 @@ import {
   GenerationOptions,
   LLMRequest,
   Message,
-  Model,
+  LanguageModel,
   ToolCallPart,
   ToolChoice,
   ToolDefinition,
@@ -20,13 +20,13 @@ describe("llm constructors", () => {
   test("builds canonical schema classes from ergonomic input", () => {
     const request = LLM.request({
       id: "req_1",
-      model: Model.make({ id: "fake-model", provider: "fake", route: chatRoute }),
+      model: LanguageModel.make({ id: "fake-model", provider: "fake", route: chatRoute }),
       system: "You are concise.",
       prompt: "Say hello.",
     })
 
     expect(request).toBeInstanceOf(LLMRequest)
-    expect(request.model).toBeInstanceOf(Model)
+    expect(request.model).toBeInstanceOf(LanguageModel)
     expect(request.messages[0]).toBeInstanceOf(Message)
     expect(request.system).toEqual([{ type: "text", text: "You are concise." }])
     expect(request.messages[0]?.content).toEqual([{ type: "text", text: "Say hello." }])
@@ -37,7 +37,7 @@ describe("llm constructors", () => {
   test("updates requests without spreading schema class instances", () => {
     const base = LLM.request({
       id: "req_1",
-      model: Model.make({ id: "fake-model", provider: "fake", route: chatRoute }),
+      model: LanguageModel.make({ id: "fake-model", provider: "fake", route: chatRoute }),
       prompt: "Say hello.",
     })
     const updated = LLMRequest.update(base, {
@@ -54,7 +54,7 @@ describe("llm constructors", () => {
 
   test("keeps request options separate from route defaults", () => {
     const request = LLM.request({
-      model: Model.make({
+      model: LanguageModel.make({
         id: "fake-model",
         provider: "fake",
         route: chatRoute.with({
@@ -81,7 +81,7 @@ describe("llm constructors", () => {
   test("updates canonical requests from the request datatype", () => {
     const base = LLM.request({
       id: "req_1",
-      model: Model.make({ id: "fake-model", provider: "fake", route: chatRoute }),
+      model: LanguageModel.make({ id: "fake-model", provider: "fake", route: chatRoute }),
       prompt: "Say hello.",
     })
     const updated = LLMRequest.update(base, { messages: [...base.messages, Message.assistant("Hi.")] })
@@ -94,19 +94,19 @@ describe("llm constructors", () => {
   })
 
   test("updates canonical models from the model datatype", () => {
-    const base = Model.make({
+    const base = LanguageModel.make({
       id: "fake-model",
       provider: "fake",
       route: chatRoute,
     })
-    const updated = Model.update(base, {
+    const updated = LanguageModel.update(base, {
       route: responsesRoute,
       defaults: { generation: { maxTokens: 20 } },
       compatibility: { toolSchema: "gemini" },
     })
-    const updatedInput = Model.input(updated)
+    const updatedInput = LanguageModel.input(updated)
 
-    expect(updated).toBeInstanceOf(Model)
+    expect(updated).toBeInstanceOf(LanguageModel)
     expect(String(updated.id)).toBe("fake-model")
     expect(updated.route).toBe(responsesRoute)
     expect(updated.defaults?.generation).toEqual({ maxTokens: 20 })
@@ -114,7 +114,7 @@ describe("llm constructors", () => {
     expect(updatedInput.defaults).toBe(updated.defaults)
     expect(updatedInput.compatibility).toBe(updated.compatibility)
     expect(String(updatedInput.provider)).toBe("fake")
-    expect(Model.update(updated, {})).toBe(updated)
+    expect(LanguageModel.update(updated, {})).toBe(updated)
   })
 
   test("carries model defaults and compatibility through route model selection", () => {
@@ -155,7 +155,7 @@ describe("llm constructors", () => {
     expect(ToolChoice.make("required")).toEqual(new ToolChoice({ type: "required" }))
     expect(
       LLM.request({
-        model: Model.make({
+        model: LanguageModel.make({
           id: "fake-model",
           provider: "fake",
           route: chatRoute,
@@ -181,7 +181,7 @@ describe("llm constructors", () => {
       { type: "text", text: "Use parameterized SQL.", cache: new CacheHint({ type: "ephemeral" }) },
     ])
     const request = LLM.request({
-      model: Model.make({ id: "fake-model", provider: "fake", route: chatRoute }),
+      model: LanguageModel.make({ id: "fake-model", provider: "fake", route: chatRoute }),
       system: "Initial operator prompt.",
       messages: [Message.user("Review this."), update],
     })

+ 4 - 4
packages/ai/test/provider.types.ts

@@ -1,9 +1,9 @@
 import { Provider } from "../src/provider"
-import { ProviderID, type Model } from "../src/schema"
+import { ProviderID, type LanguageModel } from "../src/schema"
 
-declare const model: (id: string) => Model
-declare const requiredModel: (id: string, options: { readonly baseURL: string }) => Model
-declare const chat: (id: string, options: { readonly apiKey: string }) => Model
+declare const model: (id: string) => LanguageModel
+declare const requiredModel: (id: string, options: { readonly baseURL: string }) => LanguageModel
+declare const chat: (id: string, options: { readonly apiKey: string }) => LanguageModel
 
 Provider.make({
   id: ProviderID.make("example"),

+ 2 - 2
packages/ai/test/provider/anthropic-messages.recorded.test.ts

@@ -1,6 +1,6 @@
 import { describe, expect } from "bun:test"
 import { Effect } from "effect"
-import { LLM, LLMError, Message, ToolCallPart } from "../../src"
+import { LLM, AIError, Message, ToolCallPart } from "../../src"
 import { LLMClient } from "../../src/route"
 import * as Anthropic from "../../src/providers/anthropic"
 import { weatherToolName } from "../recorded-scenarios"
@@ -37,7 +37,7 @@ describe("Anthropic Messages sad-path recorded", () => {
     Effect.gen(function* () {
       const error = yield* LLMClient.generate(malformedToolOrderRequest).pipe(Effect.flip)
 
-      expect(error).toBeInstanceOf(LLMError)
+      expect(error).toBeInstanceOf(AIError)
       expect(error.reason).toMatchObject({ _tag: "InvalidRequest" })
       expect(error.message).toContain("HTTP 400")
     }),

+ 4 - 6
packages/ai/test/provider/anthropic-messages.test.ts

@@ -1,7 +1,7 @@
 import { describe, expect } from "bun:test"
 import { Effect } from "effect"
 import { HttpClientRequest } from "effect/unstable/http"
-import { CacheHint, LLM, LLMError, LLMRequest, Message, ToolCallPart, ToolDefinition, Usage } from "../../src"
+import { CacheHint, LLM, AIError, LLMRequest, Message, ToolCallPart, ToolDefinition, Usage } from "../../src"
 import { Auth, LLMClient } from "../../src/route"
 import { compileRequest } from "../../src/route/client"
 import * as AnthropicMessages from "../../src/protocols/anthropic-messages"
@@ -684,9 +684,7 @@ describe("Anthropic Messages route", () => {
         ),
       )
 
-      expect(response.toolCalls).toMatchObject([
-        { id: "call_1", name: "lookup", input: { query: "weather" } },
-      ])
+      expect(response.toolCalls).toMatchObject([{ id: "call_1", name: "lookup", input: { query: "weather" } }])
     }),
   )
 
@@ -935,7 +933,7 @@ describe("Anthropic Messages route", () => {
 
       const error = yield* LLMClient.generate(request).pipe(Effect.provide(fixedResponse(body)), Effect.flip)
 
-      expect(error).toBeInstanceOf(LLMError)
+      expect(error).toBeInstanceOf(AIError)
       expect(error.message).toContain("Invalid JSON input for anthropic-messages tool call web_search")
     }),
   )
@@ -1009,7 +1007,7 @@ describe("Anthropic Messages route", () => {
         Effect.flip,
       )
 
-      expect(error).toBeInstanceOf(LLMError)
+      expect(error).toBeInstanceOf(AIError)
       expect(error.reason).toMatchObject({ _tag: "InvalidRequest" })
       expect(error.message).toContain("HTTP 400")
     }),

+ 2 - 2
packages/ai/test/provider/gemini.test.ts

@@ -1,6 +1,6 @@
 import { describe, expect } from "bun:test"
 import { Effect } from "effect"
-import { LLM, LLMError, LLMRequest, Message, ToolCallPart, ToolDefinition, Usage } from "../../src"
+import { LLM, AIError, LLMRequest, Message, ToolCallPart, ToolDefinition, Usage } from "../../src"
 import { Auth, LLMClient } from "../../src/route"
 import { compileRequest } from "../../src/route/client"
 import * as Gemini from "../../src/protocols/gemini"
@@ -712,7 +712,7 @@ describe("Gemini route", () => {
         Effect.flip,
       )
 
-      expect(error).toBeInstanceOf(LLMError)
+      expect(error).toBeInstanceOf(AIError)
       expect(error.reason).toMatchObject({ _tag: "InvalidProviderOutput" })
       expect(error.message).toContain("Invalid google/gemini stream event")
     }),

+ 3 - 3
packages/ai/test/provider/openai-chat-reasoning.recorded.test.ts

@@ -1,6 +1,6 @@
 import { describe, expect } from "bun:test"
 import { Effect } from "effect"
-import { LLM, LLMEvent, LLMResponse, Model } from "../../src"
+import { LLM, LLMEvent, LLMResponse, LanguageModel } from "../../src"
 import { OpenAIChat } from "../../src/protocols/openai-chat"
 import * as OpenAICompatible from "../../src/providers/openai-compatible"
 import * as OpenRouter from "../../src/providers/openrouter"
@@ -12,7 +12,7 @@ import { expectWeatherToolLoop, goldenWeatherToolLoopRequest, runWeatherToolLoop
 const cases = [
   {
     name: "OpenRouter",
-    model: Model.update(
+    model: LanguageModel.update(
       OpenRouter.configure({
         apiKey: process.env.OPENROUTER_API_KEY ?? "fixture",
         providerOptions: { openrouter: { reasoning: { max_tokens: 1024 } } },
@@ -25,7 +25,7 @@ const cases = [
   },
   {
     name: "Vercel AI Gateway",
-    model: Model.update(
+    model: LanguageModel.update(
       OpenAICompatible.configure({
         provider: "vercel-ai-gateway",
         baseURL: "https://ai-gateway.vercel.sh/v1",

+ 9 - 7
packages/ai/test/provider/openai-chat.test.ts

@@ -4,11 +4,11 @@ import { HttpClientRequest } from "effect/unstable/http"
 import {
   HttpOptions,
   LLM,
-  LLMError,
+  AIError,
   LLMEvent,
   LLMRequest,
   Message,
-  Model,
+  LanguageModel,
   ToolCallPart,
   ToolDefinition,
   Usage,
@@ -104,7 +104,7 @@ describe("OpenAI Chat route", () => {
     Effect.gen(function* () {
       const prepared = yield* compileRequest(
         LLM.request({
-          model: Model.update(model, { compatibility: { reasoningField: "vendor_reasoning" } }),
+          model: LanguageModel.update(model, { compatibility: { reasoningField: "vendor_reasoning" } }),
           messages: [
             Message.assistant([
               {
@@ -130,7 +130,7 @@ describe("OpenAI Chat route", () => {
     Effect.gen(function* () {
       const error = yield* compileRequest(
         LLM.request({
-          model: Model.update(model, { compatibility: { reasoningField: "content" } }),
+          model: LanguageModel.update(model, { compatibility: { reasoningField: "content" } }),
           messages: [Message.assistant([{ type: "reasoning", text: "thinking" }])],
         }),
       ).pipe(Effect.flip)
@@ -171,7 +171,9 @@ describe("OpenAI Chat route", () => {
   it.effect("adds native query params to the Chat Completions URL", () =>
     LLMClient.generate(
       LLMRequest.update(request, {
-        model: Model.update(model, { route: model.route.with({ endpoint: { query: { "api-version": "v1" } } }) }),
+        model: LanguageModel.update(model, {
+          route: model.route.with({ endpoint: { query: { "api-version": "v1" } } }),
+        }),
       }),
     ).pipe(
       Effect.provide(
@@ -624,7 +626,7 @@ describe("OpenAI Chat route", () => {
 
   it.effect("parses and replays a configured custom reasoning field", () =>
     Effect.gen(function* () {
-      const custom = Model.update(model, { compatibility: { reasoningField: "vendor_reasoning" } })
+      const custom = LanguageModel.update(model, { compatibility: { reasoningField: "vendor_reasoning" } })
       const response = yield* LLMClient.generate(LLMRequest.update(request, { model: custom })).pipe(
         Effect.provide(
           fixedResponse(
@@ -1172,7 +1174,7 @@ describe("OpenAI Chat route", () => {
         Effect.flip,
       )
 
-      expect(error).toBeInstanceOf(LLMError)
+      expect(error).toBeInstanceOf(AIError)
       expect(error.reason).toMatchObject({ _tag: "InvalidRequest" })
       expect(error.message).toContain("HTTP 400")
     }),

+ 8 - 8
packages/ai/test/provider/openai-responses.test.ts

@@ -3,11 +3,11 @@ import { ConfigProvider, Effect, Layer, Stream } from "effect"
 import { Headers, HttpClientRequest } from "effect/unstable/http"
 import {
   LLM,
-  LLMError,
+  AIError,
   LLMEvent,
   LLMRequest,
   Message,
-  Model,
+  LanguageModel,
   ToolCallPart,
   ToolDefinition,
   ToolResultPart,
@@ -304,7 +304,9 @@ describe("OpenAI Responses route", () => {
     Effect.gen(function* () {
       yield* LLMClient.generate(
         LLMRequest.update(request, {
-          model: Model.update(model, { route: model.route.with({ endpoint: { query: { "api-version": "v1" } } }) }),
+          model: LanguageModel.update(model, {
+            route: model.route.with({ endpoint: { query: { "api-version": "v1" } } }),
+          }),
         }),
       ).pipe(
         Effect.provide(
@@ -985,9 +987,7 @@ describe("OpenAI Responses route", () => {
 
       for (const event of events) {
         const error = yield* LLMClient.generate(request).pipe(
-          Effect.provide(
-            fixedResponse(sseEvents(event, { type: "response.completed", response: { id: "resp_1" } })),
-          ),
+          Effect.provide(fixedResponse(sseEvents(event, { type: "response.completed", response: { id: "resp_1" } }))),
           Effect.flip,
         )
         expect(error.reason._tag).toBe("InvalidProviderOutput")
@@ -1843,7 +1843,7 @@ describe("OpenAI Responses route", () => {
         Effect.flip,
       )
 
-      expect(error).toBeInstanceOf(LLMError)
+      expect(error).toBeInstanceOf(AIError)
       expect(error.reason).toMatchObject({ _tag: "RateLimit", message: "rate_limit_exceeded: Slow down" })
     }),
   )
@@ -2037,7 +2037,7 @@ describe("OpenAI Responses route", () => {
         Effect.flip,
       )
 
-      expect(error).toBeInstanceOf(LLMError)
+      expect(error).toBeInstanceOf(AIError)
       expect(error.reason).toMatchObject({ _tag: "InvalidRequest" })
       expect(error.message).toContain("HTTP 400")
     }),

+ 2 - 2
packages/ai/test/provider/pdf.recorded.test.ts

@@ -1,6 +1,6 @@
 import { describe, expect } from "bun:test"
 import { Effect, Schema, Stream } from "effect"
-import { LLM, LLMResponse, Message, ToolDefinition, type Model } from "../../src"
+import { LLM, LLMResponse, Message, ToolDefinition, type LanguageModel } from "../../src"
 import { AmazonBedrock, Anthropic, Google, OpenAI, XAI } from "../../src/providers"
 import { LLMClient } from "../../src/route"
 import { Tool } from "../../src/tool"
@@ -28,7 +28,7 @@ const targets: ReadonlyArray<{
   readonly requires: string
   readonly filename: string
   readonly maxTokens: number
-  readonly model: Model
+  readonly model: LanguageModel
 }> = [
   {
     id: "openai",

+ 2 - 2
packages/ai/test/recorded-golden.ts

@@ -1,7 +1,7 @@
 import type { HttpRecorder } from "@opencode-ai/http-recorder"
 import { describe } from "bun:test"
 import { Effect } from "effect"
-import type { Model } from "../src"
+import type { LanguageModel } from "../src"
 import { goldenScenarioTags, goldenScenarioTitle, runGoldenScenario, type GoldenScenarioID } from "./recorded-scenarios"
 import { recordedTests } from "./recorded-test"
 import { kebab } from "./recorded-utils"
@@ -22,7 +22,7 @@ type ScenarioInput =
 
 type TargetInput = {
   readonly name: string
-  readonly model: Model
+  readonly model: LanguageModel
   readonly protocol?: string
   readonly requires?: ReadonlyArray<string>
   readonly transport?: Transport

+ 4 - 4
packages/ai/test/recorded-scenarios.ts

@@ -12,7 +12,7 @@ import {
   toDefinitions,
   type ContentPart,
   type FinishReason,
-  type Model,
+  type LanguageModel,
 } from "../src"
 import { LLMClient } from "../src/route"
 import { Tool } from "../src/tool"
@@ -54,7 +54,7 @@ export const weatherRuntimeTool = Tool.make({
 
 export const weatherToolLoopRequest = (input: {
   readonly id: string
-  readonly model: Model
+  readonly model: LanguageModel
   readonly system?: string
   readonly maxTokens?: number
   readonly temperature?: number | false
@@ -73,7 +73,7 @@ export const weatherToolLoopRequest = (input: {
 
 export const goldenWeatherToolLoopRequest = (input: {
   readonly id: string
-  readonly model: Model
+  readonly model: LanguageModel
   readonly maxTokens?: number
   readonly temperature?: number | false
 }) =>
@@ -163,7 +163,7 @@ export const expectGoldenWeatherToolLoop = (events: ReadonlyArray<LLMEvent>) =>
 
 export interface GoldenScenarioContext {
   readonly id: string
-  readonly model: Model
+  readonly model: LanguageModel
   readonly maxTokens?: number
   readonly temperature?: number | false
 }

+ 28 - 8
packages/ai/test/schema.test.ts

@@ -1,11 +1,21 @@
 import { describe, expect, test } from "bun:test"
-import { Schema } from "effect"
+import { Effect, Schema } from "effect"
 import * as OpenAIChat from "../src/protocols/openai-chat"
 import * as OpenAIResponses from "../src/protocols/openai-responses"
-import { ContentPart, LLMEvent, LLMRequest, Model, ModelID, ProviderID, Usage } from "../src/schema"
+import {
+  AIError,
+  ContentPart,
+  InvalidRequestReason,
+  LLMEvent,
+  LLMRequest,
+  LanguageModel,
+  ModelID,
+  ProviderID,
+  Usage,
+} from "../src/schema"
 import { ProviderShared } from "../src/protocols/shared"
 
-const model = new Model({
+const model = new LanguageModel({
   id: ModelID.make("fake-model"),
   provider: ProviderID.make("fake-provider"),
   route: OpenAIChat.route,
@@ -33,7 +43,7 @@ describe("llm schema", () => {
 
   test("accepts custom route ids", () => {
     const decoded = decodeLLMRequest({
-      model: Model.update(model, { route: OpenAIResponses.route }),
+      model: LanguageModel.update(model, { route: OpenAIResponses.route }),
       system: [],
       messages: [],
       tools: [],
@@ -51,9 +61,7 @@ describe("llm schema", () => {
     expect(
       LLMEvent.stepFinish({ index: 0, reason: { normalized: "stop" }, usage: { inputTokens: 1 } }).usage,
     ).toBeInstanceOf(Usage)
-    expect(LLMEvent.finish({ reason: { normalized: "stop" }, usage: { outputTokens: 2 } }).usage).toBeInstanceOf(
-      Usage,
-    )
+    expect(LLMEvent.finish({ reason: { normalized: "stop" }, usage: { outputTokens: 2 } }).usage).toBeInstanceOf(Usage)
   })
 
   test("content part tagged union exposes guards", () => {
@@ -62,7 +70,7 @@ describe("llm schema", () => {
   })
 })
 
-describe("LLM.Usage", () => {
+describe("AI.Usage", () => {
   test("subtractTokens clamps non-sensical breakdowns to zero", () => {
     // Defense against a provider reporting cached_tokens > prompt_tokens or
     // reasoning_tokens > completion_tokens — the negative would otherwise
@@ -88,3 +96,15 @@ describe("LLM.Usage", () => {
     expect(new Usage({}).visibleOutputTokens).toBe(0)
   })
 })
+
+test("AI errors expose the shared runtime tag", async () => {
+  const error = new AIError({
+    module: "test",
+    method: "call",
+    reason: new InvalidRequestReason({ message: "invalid" }),
+  })
+  expect(error._tag).toBe("AI.Error")
+  expect(
+    await Effect.runPromise(Effect.fail(error).pipe(Effect.catchTag("AI.Error", () => Effect.succeed("caught")))),
+  ).toBe("caught")
+})

+ 2 - 2
packages/ai/test/tool-stream.test.ts

@@ -1,6 +1,6 @@
 import { describe, expect } from "bun:test"
 import { Effect } from "effect"
-import { LLMError } from "../src/schema"
+import { AIError } from "../src/schema"
 import { ToolStream } from "../src/protocols/utils/tool-stream"
 import { it } from "./lib/effect"
 
@@ -67,7 +67,7 @@ describe("ToolStream", () => {
     Effect.gen(function* () {
       const error = ToolStream.appendExisting(ADAPTER, ToolStream.empty<number>(), 0, "{}", "missing tool")
 
-      expect(error).toBeInstanceOf(LLMError)
+      expect(error).toBeInstanceOf(AIError)
       if (ToolStream.isError(error)) expect(error.reason.message).toBe("missing tool")
     }),
   )

+ 9 - 8
packages/core/src/aisdk.ts

@@ -18,8 +18,8 @@ import {
   FinishReason,
   InvalidProviderOutputReason,
   LLMEvent,
-  LLMError,
-  Model,
+  AIError,
+  LanguageModel,
   ProviderID,
   ProviderMetadata,
   ToolResultValue,
@@ -182,7 +182,7 @@ export interface Interface {
   readonly runSDK: (event: SDKEvent) => Effect.Effect<SDKEvent>
   readonly runLanguage: (event: LanguageEvent) => Effect.Effect<LanguageEvent>
   readonly language: (model: Info) => Effect.Effect<LanguageModelV3, InitError>
-  readonly model: (model: Info) => Effect.Effect<Model, InitError>
+  readonly model: (model: Info) => Effect.Effect<LanguageModel, InitError>
 }
 
 export class Service extends Context.Service<Service, Interface>()("@opencode/AISDK") {}
@@ -338,11 +338,12 @@ function modelFromLanguage(info: Info, language: LanguageModelV3) {
       from: (request) => Effect.succeed(callOptions(request)),
     },
     with: () => route,
-    model: (input) => Model.make({ ...input, provider: "provider" in input ? input.provider : info.providerID, route }),
+    model: (input) =>
+      LanguageModel.make({ ...input, provider: "provider" in input ? input.provider : info.providerID, route }),
     prepareTransport: (body) => Effect.succeed(body),
     streamPrepared: (prepared) => streamLanguage(language, prepared as LanguageModelV3CallOptions),
   }
-  return Model.make({
+  return LanguageModel.make({
     id: info.modelID ?? info.id,
     provider: info.providerID,
     route,
@@ -555,7 +556,7 @@ function streamLanguage(language: LanguageModelV3, options: LanguageModelV3CallO
 function streamPartEvents(
   state: { step: number; toolNames: Record<string, string> },
   event: LanguageModelV3StreamPart,
-): Effect.Effect<ReadonlyArray<LLMEvent>, LLMError> {
+): Effect.Effect<ReadonlyArray<LLMEvent>, AIError> {
   switch (event.type) {
     case "stream-start":
     case "response-metadata":
@@ -720,10 +721,10 @@ function messageValue(input: unknown) {
 
 function llmError(method: string, error: unknown) {
   const reason =
-    error instanceof LLMError
+    error instanceof AIError
       ? new InvalidProviderOutputReason({ message: error.message })
       : new UnknownProviderReason({ message: error instanceof Error ? error.message : String(error) })
-  return new LLMError({
+  return new AIError({
     module: "AISDK",
     method,
     reason,

+ 2 - 2
packages/core/src/generate.ts

@@ -1,6 +1,6 @@
 export * as Generate from "./generate"
 
-import { LLM, LLMClient, LLMError } from "@opencode-ai/ai"
+import { LLM, LLMClient, AIError } from "@opencode-ai/ai"
 import { Context, Effect, Layer, Schema } from "effect"
 import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
 import { llmClient } from "./effect/app-node-platform"
@@ -57,7 +57,7 @@ export const layer = Layer.effect(
         })
       const response = yield* llm.generate(LLM.request({ model: resolved.model, prompt: input.prompt })).pipe(
         Effect.mapError(
-          (error: LLMError) =>
+          (error: AIError) =>
             new UnavailableError({
               message: error.message,
               service: resolved.ref.providerID,

+ 5 - 5
packages/core/src/model-resolver.ts

@@ -1,7 +1,7 @@
 export * as ModelResolver from "./model-resolver"
 
 import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
-import { Model } from "@opencode-ai/ai"
+import { LanguageModel } from "@opencode-ai/ai"
 // ast-grep-ignore: no-star-import
 import * as AnthropicMessages from "@opencode-ai/ai/protocols/anthropic-messages"
 // ast-grep-ignore: no-star-import
@@ -50,7 +50,7 @@ export type Error = VariantUnavailableError | UnsupportedPackageError | Integrat
 
 export interface Resolved {
   /** Route-level model for provider requests; its id is the provider API model id, which may differ from the catalog id. */
-  readonly model: Model
+  readonly model: LanguageModel
   /** Selected catalog identity. Durable records and displays must use this, never the API model id. */
   readonly ref: Ref
   /** Catalog capabilities used to shape requests before provider lowering. */
@@ -134,14 +134,14 @@ export const withVariant = (
 
 export interface Dependencies {
   readonly loadPackage?: (specifier: string) => Effect.Effect<Provider.ProviderPackage, Provider.LoadError>
-  readonly loadAISDK?: (model: Info) => Effect.Effect<Model, AISDK.InitError>
+  readonly loadAISDK?: (model: Info) => Effect.Effect<LanguageModel, AISDK.InitError>
 }
 
 export const fromCatalogModel = (
   model: Info,
   credential?: Credential.Value,
   dependencies?: Dependencies,
-): Effect.Effect<Model, UnsupportedPackageError> => {
+): Effect.Effect<LanguageModel, UnsupportedPackageError> => {
   const resolved = produce(model, (draft) => {
     if (draft.settings?.apiKey === "") delete draft.settings.apiKey
     if (credential?.type === "key" && credential.metadata !== undefined)
@@ -207,7 +207,7 @@ export const fromCatalogModel = (
     return yield* Effect.try({
       try: () => {
         const runtime = module.model(resolved.modelID ?? resolved.id, settings)
-        return Model.update(runtime, {
+        return LanguageModel.update(runtime, {
           provider: resolved.providerID,
           compatibility: resolved.compatibility
             ? Object.assign({}, runtime.compatibility, resolved.compatibility)

+ 5 - 5
packages/core/src/session/compaction.ts

@@ -1,6 +1,6 @@
 export * as SessionCompaction from "./compaction"
 
-import { LLM, LLMClient, LLMError, LLMEvent, Message, type LLMRequest, type Model } from "@opencode-ai/ai"
+import { LLM, LLMClient, AIError, LLMEvent, Message, type LLMRequest, type LanguageModel } from "@opencode-ai/ai"
 import { SessionError } from "@opencode-ai/schema/session-error"
 import { Context, Effect, Layer, Stream } from "effect"
 import { Config } from "../config"
@@ -64,7 +64,7 @@ type Dependencies = {
   readonly app: App.Info
   readonly bus: Bus.Interface
   readonly llm: {
-    readonly stream: (request: LLMRequest) => Stream.Stream<LLMEvent, LLMError>
+    readonly stream: (request: LLMRequest) => Stream.Stream<LLMEvent, AIError>
   }
   readonly models: SessionRunnerModel.Interface
   readonly config: Settings
@@ -73,7 +73,7 @@ type Dependencies = {
 export type AutoInput = {
   readonly session: SessionSchema.Info
   readonly messages: readonly SessionMessage.Info[]
-  readonly model: Model
+  readonly model: LanguageModel
   readonly cost: Info["cost"]
 }
 
@@ -85,7 +85,7 @@ export type ManualInput = {
 
 type Plan = {
   readonly session: SessionSchema.Info
-  readonly model: Model
+  readonly model: LanguageModel
   readonly cost: Info["cost"]
   readonly reason: SessionMessage.Compaction["reason"]
   readonly prompt: string
@@ -282,7 +282,7 @@ const make = (dependencies: Dependencies) => {
           }
           return Effect.void
         }),
-        Effect.catchTag("LLM.Error", (error) =>
+        Effect.catchTag("AI.Error", (error) =>
           Effect.sync(() => {
             failure = toSessionError(error)
           }),

+ 2 - 2
packages/core/src/session/generate.ts

@@ -1,13 +1,13 @@
 export * as SessionGenerate from "./generate"
 
-import type { LLMError } from "@opencode-ai/ai"
+import type { AIError } from "@opencode-ai/ai"
 import { Context, type Effect } from "effect"
 import type { Instructions } from "../instructions"
 import type { AgentNotFoundError } from "./error"
 import type { SessionRunnerModel } from "./runner/model"
 import type { SessionSchema } from "./schema"
 
-export type Error = AgentNotFoundError | Instructions.InitializationBlocked | SessionRunnerModel.Error | LLMError
+export type Error = AgentNotFoundError | Instructions.InitializationBlocked | SessionRunnerModel.Error | AIError
 
 export interface Interface {
   /** Generates text from current Session context without mutating the Session. */

+ 2 - 2
packages/core/src/session/runner/index.ts

@@ -1,6 +1,6 @@
 export * as SessionRunner from "./index"
 
-import type { LLMError } from "@opencode-ai/ai"
+import type { AIError } from "@opencode-ai/ai"
 import { Context, Effect } from "effect"
 import { SessionSchema } from "../schema"
 import type { AgentNotFoundError, MessageDecodeError, StepFailedError, UserInterruptedError } from "../error"
@@ -8,7 +8,7 @@ import { SessionRunnerModel } from "./model"
 import type { Instructions } from "../../instructions/index"
 
 export type RunError =
-  | LLMError
+  | AIError
   | SessionRunnerModel.Error
   | MessageDecodeError
   | AgentNotFoundError

+ 27 - 11
packages/core/src/session/runner/llm.ts

@@ -1,6 +1,13 @@
 export * as SessionRunnerLLM from "./llm"
 
-import { LLMClient, LLMError, LLMEvent, isContextOverflowFailure, type ProviderErrorEvent, type ToolCall } from "@opencode-ai/ai"
+import {
+  LLMClient,
+  AIError,
+  LLMEvent,
+  isContextOverflowFailure,
+  type ProviderErrorEvent,
+  type ToolCall,
+} from "@opencode-ai/ai"
 import { Cause, Data, Effect, Exit, Fiber, FiberSet, Layer, Option, Pull, Schedule, Stream } from "effect"
 import { Database } from "../../database/database"
 import { Bus } from "../../bus"
@@ -60,16 +67,20 @@ const classifyToolExits = (
       : [],
   )
   const causes =
-    settled._tag === "Failure" ? [settled.cause] : exits.flatMap((exit) => (exit._tag === "Failure" ? [exit.cause] : []))
+    settled._tag === "Failure"
+      ? [settled.cause]
+      : exits.flatMap((exit) => (exit._tag === "Failure" ? [exit.cause] : []))
   // The first non-interrupt, non-decline failure, rebuilt without decline reasons so the
   // drain's error channel never carries a decline.
-  const failure = causes.flatMap((cause) => {
-    if (Cause.hasInterrupts(cause)) return []
-    const reasons = cause.reasons.flatMap((reason): Array<Cause.Reason<never>> =>
-      Cause.isFailReason(reason) ? [] : [reason],
-    )
-    return reasons.length > 0 ? [Cause.fromReasons(reasons)] : []
-  }).at(0)
+  const failure = causes
+    .flatMap((cause) => {
+      if (Cause.hasInterrupts(cause)) return []
+      const reasons = cause.reasons.flatMap(
+        (reason): Array<Cause.Reason<never>> => (Cause.isFailReason(reason) ? [] : [reason]),
+      )
+      return reasons.length > 0 ? [Cause.fromReasons(reasons)] : []
+    })
+    .at(0)
   return {
     interrupted: causes.some(Cause.hasInterrupts),
     declines,
@@ -356,9 +367,14 @@ const layer = Layer.effect(
           if (overflowFailure) yield* publisher.publish(overflowFailure)
           // A thrown LLM failure not already recorded as the provider error either
           // escapes as a scheduled retry or fails the assistant durably.
-          const llmFailure = streamFailure instanceof LLMError ? streamFailure : undefined
+          const llmFailure = streamFailure instanceof AIError ? streamFailure : undefined
           const llmError = llmFailure && !publisher.record().providerFailed ? toSessionError(llmFailure) : undefined
-          if (llmFailure && llmError && SessionRunnerRetry.isRetryable(llmFailure) && !publisher.record().outputStarted) {
+          if (
+            llmFailure &&
+            llmError &&
+            SessionRunnerRetry.isRetryable(llmFailure) &&
+            !publisher.record().outputStarted
+          ) {
             // RetryScheduled and Step.Failed fold onto an existing assistant message, so
             // Step.Started must be durable before the failure escapes.
             yield* publisher.startAssistant()

+ 2 - 2
packages/core/src/session/runner/model.ts

@@ -1,7 +1,7 @@
 export * as SessionRunnerModel from "./model"
 
 import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
-import { Model } from "@opencode-ai/ai"
+import { LanguageModel } from "@opencode-ai/ai"
 import { Context, Effect, Layer, Schema } from "effect"
 import { Catalog } from "../../catalog"
 import { ModelResolver } from "../../model-resolver"
@@ -42,7 +42,7 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/Se
 
 /** Builds a Resolved whose catalog identity mirrors the route model. Test or embedding seam. */
 export const resolved = (
-  model: Model,
+  model: LanguageModel,
   options: {
     readonly capabilities: Capabilities
     readonly variant?: VariantID

+ 8 - 4
packages/core/src/session/runner/retry.ts

@@ -1,6 +1,6 @@
 export * as SessionRunnerRetry from "./retry"
 
-import { LLMError } from "@opencode-ai/ai"
+import { AIError } from "@opencode-ai/ai"
 import { SessionError } from "@opencode-ai/schema/session-error"
 import { Data, Duration, Effect, Schedule } from "effect"
 import { Bus } from "../../bus"
@@ -9,12 +9,12 @@ import { SessionMessage } from "../message"
 import { SessionSchema } from "../schema"
 
 export class RetryableFailure extends Data.TaggedError("SessionRunner.RetryableFailure")<{
-  readonly cause: LLMError
+  readonly cause: AIError
   readonly error: SessionError.Error
   readonly step: number
 }> {}
 
-export function isRetryable(error: LLMError) {
+export function isRetryable(error: AIError) {
   switch (error.reason._tag) {
     case "RateLimit":
     case "ProviderInternal":
@@ -41,7 +41,11 @@ const retryAfter = (failure: RetryableFailure) => {
   return undefined
 }
 
-export const schedule = (bus: Bus.Interface, sessionID: SessionSchema.ID, assistantMessageID: () => SessionMessage.ID) =>
+export const schedule = (
+  bus: Bus.Interface,
+  sessionID: SessionSchema.ID,
+  assistantMessageID: () => SessionMessage.ID,
+) =>
   Schedule.max([Schedule.exponential("2 seconds"), Schedule.recurs(4)]).pipe(
     Schedule.setInputType<RetryableFailure>(),
     Schedule.modifyDelay(({ input: failure, duration: delay }) => {

+ 3 - 3
packages/core/src/session/title.ts

@@ -1,6 +1,6 @@
 export * as SessionTitle from "./title"
 
-import { LLM, LLMClient, LLMError, LLMEvent, Message, type LLMRequest } from "@opencode-ai/ai"
+import { LLM, LLMClient, AIError, LLMEvent, Message, type LLMRequest } from "@opencode-ai/ai"
 import { Context, DateTime, Effect, Layer, Stream } from "effect"
 import { Agent } from "../agent"
 import { Database } from "../database/database"
@@ -24,7 +24,7 @@ type Dependencies = {
   readonly app: App.Info
   readonly bus: Bus.Interface
   readonly llm: {
-    readonly stream: (request: LLMRequest) => Stream.Stream<LLMEvent, LLMError>
+    readonly stream: (request: LLMRequest) => Stream.Stream<LLMEvent, AIError>
   }
   readonly agents: Agent.Interface
   readonly models: SessionRunnerModel.Interface
@@ -97,7 +97,7 @@ const make = (dependencies: Dependencies) => {
           return Effect.void
         }),
         Effect.as(true),
-        Effect.catchTag("LLM.Error", () => Effect.succeed(false)),
+        Effect.catchTag("AI.Error", () => Effect.succeed(false)),
         Effect.onInterrupt(() => recordUsage.pipe(Effect.asVoid)),
       )
     yield* recordUsage

+ 3 - 3
packages/core/src/session/to-session-error.ts

@@ -1,4 +1,4 @@
-import { LLMError, ToolFailure } from "@opencode-ai/ai"
+import { AIError, ToolFailure } from "@opencode-ai/ai"
 import { Tool } from "@opencode-ai/schema/tool"
 import { SessionError } from "@opencode-ai/schema/session-error"
 import { Permission } from "../permission"
@@ -8,7 +8,7 @@ import { AgentNotFoundError, StepFailedError, UserInterruptedError } from "./err
 import { SessionRunnerModel } from "./runner/model"
 
 export function toSessionError(cause: unknown): SessionError.Error {
-  if (cause instanceof LLMError) {
+  if (cause instanceof AIError) {
     switch (cause.reason._tag) {
       case "RateLimit":
         return providerError("provider.rate-limit", cause.reason)
@@ -59,7 +59,7 @@ export function toSessionError(cause: unknown): SessionError.Error {
   return { type: "unknown", message: cause instanceof Error ? cause.message : String(cause) }
 }
 
-function providerError(type: string, reason: LLMError["reason"]): SessionError.Error {
+function providerError(type: string, reason: AIError["reason"]): SessionError.Error {
   const status =
     ("http" in reason ? reason.http?.response?.status : undefined) ?? ("status" in reason ? reason.status : undefined)
   return { type, message: reason.message, ...(status === undefined ? {} : { status }) }

+ 2 - 2
packages/core/test/aisdk.test.ts

@@ -2,7 +2,7 @@ import type { LanguageModelV3, LanguageModelV3StreamPart } from "@ai-sdk/provide
 import { AISDK } from "@opencode-ai/core/aisdk"
 import { Model } from "@opencode-ai/core/model"
 import { Provider } from "@opencode-ai/core/provider"
-import { LLM, LLMError, LLMEvent, Message } from "@opencode-ai/ai"
+import { LLM, AIError, LLMEvent, Message } from "@opencode-ai/ai"
 import { LLMClient, RequestExecutor } from "@opencode-ai/ai/route"
 import { compileRequest } from "@opencode-ai/ai/route/client"
 import { expect } from "bun:test"
@@ -316,7 +316,7 @@ it.effect("keeps malformed provider-executed AI SDK input terminal", () =>
       Effect.flip,
     )
 
-    expect(error).toBeInstanceOf(LLMError)
+    expect(error).toBeInstanceOf(AIError)
     expect(error.message).toContain("Invalid JSON input for aisdk tool call web_search")
   }),
 )

+ 2 - 2
packages/core/test/generate.test.ts

@@ -1,5 +1,5 @@
 import { expect } from "bun:test"
-import { Model } from "@opencode-ai/ai"
+import { LanguageModel } from "@opencode-ai/ai"
 import { OpenAIChat } from "@opencode-ai/ai/protocols"
 import { TestLLM } from "@opencode-ai/ai/testing"
 import { AISDK } from "@opencode-ai/core/aisdk"
@@ -17,7 +17,7 @@ const selected = Info.make({
   ...Info.default(Provider.ID.make("test-provider"), ID.make("gemini")),
   package: Provider.aisdk("@ai-sdk/mistral"),
 })
-const runtime = Model.make({ id: "gemini", provider: "test-provider", route: OpenAIChat.route })
+const runtime = LanguageModel.make({ id: "gemini", provider: "test-provider", route: OpenAIChat.route })
 
 const catalog = Layer.mock(Catalog.Service, {
   provider: {

+ 6 - 6
packages/core/test/model-resolver.test.ts

@@ -1,5 +1,5 @@
 import { describe, expect } from "bun:test"
-import { LLM, Model } from "@opencode-ai/ai"
+import { LLM, LanguageModel } from "@opencode-ai/ai"
 import { OpenAIChat } from "@opencode-ai/ai/protocols"
 import { compileRequest } from "@opencode-ai/ai/route/client"
 import { Effect } from "effect"
@@ -439,7 +439,7 @@ describe("ModelResolver", () => {
                   body: { custom: true },
                   limits: { context: 100, output: 20 },
                 })
-                return Model.make({ id: modelID, provider: "package-provider", route: native.route })
+                return LanguageModel.make({ id: modelID, provider: "package-provider", route: native.route })
               },
             })
           },
@@ -481,7 +481,7 @@ describe("ModelResolver", () => {
               model: (modelID, settings) => {
                 expect(settings).toMatchObject({ [key]: "oauth-token" })
                 expect(settings).not.toHaveProperty("apiKey")
-                return Model.make({ id: modelID, provider: "package-provider", route: native.route })
+                return LanguageModel.make({ id: modelID, provider: "package-provider", route: native.route })
               },
             }),
         }),
@@ -536,7 +536,7 @@ describe("ModelResolver", () => {
                     limits: { context: 100, output: 20 },
                     providerOptions,
                   })
-                  return Model.make({ id: modelID, provider: "native-provider", route: native.route })
+                  return LanguageModel.make({ id: modelID, provider: "native-provider", route: native.route })
                 },
               })
             },
@@ -571,7 +571,7 @@ describe("ModelResolver", () => {
                 transforms: ["middle-out"],
                 provider: { sort: "price", only: ["anthropic"] },
               })
-              return Model.make({ id: modelID, provider: "openrouter", route: OpenAIChat.route })
+              return LanguageModel.make({ id: modelID, provider: "openrouter", route: OpenAIChat.route })
             },
           }),
       },
@@ -632,7 +632,7 @@ describe("ModelResolver", () => {
                 headers: { "x-aisdk": "header" },
                 body: { custom: true },
               })
-              return Model.make({
+              return LanguageModel.make({
                 id: runtime.modelID ?? runtime.id,
                 provider: runtime.providerID,
                 route: native.route,

+ 2 - 2
packages/core/test/prompt-cache-diagnostics.test.ts

@@ -1,9 +1,9 @@
 import { describe, expect, test } from "bun:test"
-import { GenerationOptions, LLM, LLMRequest, Message, Model, ToolDefinition } from "@opencode-ai/ai"
+import { GenerationOptions, LLM, LLMRequest, Message, LanguageModel, ToolDefinition } from "@opencode-ai/ai"
 import { OpenAIChat } from "@opencode-ai/ai/protocols"
 import { PromptCacheDiagnostics } from "@opencode-ai/core/session/prompt-cache-diagnostics"
 
-const model = Model.make({ id: "test", provider: "test", route: OpenAIChat.route })
+const model = LanguageModel.make({ id: "test", provider: "test", route: OpenAIChat.route })
 const tool = ToolDefinition.make({
   name: "read",
   description: "Read a file",

+ 2 - 2
packages/core/test/session-compact.test.ts

@@ -1,5 +1,5 @@
 import { describe, expect } from "bun:test"
-import { LLMClient, LLMEvent, Model, type LLMRequest } from "@opencode-ai/ai"
+import { LLMClient, LLMEvent, LanguageModel, type LLMRequest } from "@opencode-ai/ai"
 import { OpenAIChat } from "@opencode-ai/ai/protocols"
 import { Config } from "@opencode-ai/core/config"
 import { Database } from "@opencode-ai/core/database/database"
@@ -25,7 +25,7 @@ import { DateTime, Effect, Layer, LayerMap, Stream } from "effect"
 import { testEffect } from "./lib/effect"
 
 const location = Location.Ref.make({ directory: AbsolutePath.make("/project") })
-const model = Model.make({
+const model = LanguageModel.make({
   id: "summary-model",
   provider: "test",
   route: OpenAIChat.route.with({ limits: { context: 10_000, output: 1_000 } }),

+ 3 - 3
packages/core/test/session-compaction.test.ts

@@ -1,5 +1,5 @@
 import { expect, test } from "bun:test"
-import { LLMClient, LLMEvent, Model, type LLMRequest } from "@opencode-ai/ai"
+import { LLMClient, LLMEvent, LanguageModel, type LLMRequest } from "@opencode-ai/ai"
 import { OpenAIChat } from "@opencode-ai/ai/protocols"
 import { Config } from "@opencode-ai/core/config"
 import { Database } from "@opencode-ai/core/database/database"
@@ -28,7 +28,7 @@ import { asc, eq } from "drizzle-orm"
 import { testEffect } from "./lib/effect"
 
 let requests: LLMRequest[] = []
-const model = Model.make({
+const model = LanguageModel.make({
   id: "summary-model",
   provider: "test",
   route: OpenAIChat.route.with({ limits: { context: 10_000, output: 1_000 } }),
@@ -145,7 +145,7 @@ it.effect("auto compaction reserves a buffer below the prompt ceiling", () =>
     })
     const input = (tokens: number, limits: { context: number; input?: number; output: number }) => ({
       session,
-      model: Model.make({
+      model: LanguageModel.make({
         id: "test-model",
         provider: "test-provider",
         route: OpenAIChat.route.with({ limits }),

+ 3 - 3
packages/core/test/session-error.test.ts

@@ -4,7 +4,7 @@ import {
   ContentPolicyReason,
   InvalidProviderOutputReason,
   InvalidRequestReason,
-  LLMError,
+  AIError,
   NoRouteReason,
   ModelID,
   ProviderID,
@@ -23,10 +23,10 @@ import { Tool } from "@opencode-ai/schema/tool"
 import { toSessionError } from "@opencode-ai/core/session/to-session-error"
 import { SessionRunnerRetry } from "@opencode-ai/core/session/runner/retry"
 
-const llm = (reason: LLMError["reason"]) => new LLMError({ module: "test", method: "stream", reason })
+const llm = (reason: AIError["reason"]) => new AIError({ module: "test", method: "stream", reason })
 
 describe("toSessionError", () => {
-  test("maps every LLM reason to the open wire type", () => {
+  test("maps every AI error reason to the open wire type", () => {
     expect(toSessionError(llm(new RateLimitReason({ message: "rate", retryAfterMs: 123 })))).toEqual({
       type: "provider.rate-limit",
       message: "rate",

+ 2 - 2
packages/core/test/session-execution.test.ts

@@ -1,5 +1,5 @@
 import { describe, expect, test } from "bun:test"
-import { LLMError, TransportReason } from "@opencode-ai/ai"
+import { AIError, TransportReason } from "@opencode-ai/ai"
 import { Database } from "@opencode-ai/core/database/database"
 import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
 import { LayerNode } from "@opencode-ai/util/effect/layer-node"
@@ -27,7 +27,7 @@ describe("SessionExecution lifecycle", () => {
     expect(
       SessionExecution.terminal(
         Exit.fail(
-          new LLMError({
+          new AIError({
             module: "test",
             method: "stream",
             reason: new TransportReason({ message: "Disconnected" }),

+ 10 - 2
packages/core/test/session-generate.test.ts

@@ -1,5 +1,13 @@
 import { expect } from "bun:test"
-import { LLMClient, LLMEvent, LLMResponse, Model, SystemPart, ToolDefinition, type LLMRequest } from "@opencode-ai/ai"
+import {
+  LLMClient,
+  LLMEvent,
+  LLMResponse,
+  LanguageModel,
+  SystemPart,
+  ToolDefinition,
+  type LLMRequest,
+} from "@opencode-ai/ai"
 import { OpenAIChat } from "@opencode-ai/ai/protocols"
 import { Agent } from "@opencode-ai/core/agent"
 import { Database } from "@opencode-ai/core/database/database"
@@ -46,7 +54,7 @@ const requests: LLMRequest[] = []
 let instruction: string | Instructions.Unavailable = "Initial context"
 const sessionID = SessionSchema.ID.make("ses_generate_test")
 
-const model = Model.make({ id: "generate-model", provider: "test", route: OpenAIChat.route })
+const model = LanguageModel.make({ id: "generate-model", provider: "test", route: OpenAIChat.route })
 const client = Layer.mock(LLMClient.Service)({
   stream: () => Stream.die(new Error("unused")),
   generate: (request) =>

+ 18 - 20
packages/core/test/session-runner.test.ts

@@ -1,10 +1,10 @@
 import { describe, expect, test } from "bun:test"
 import {
-  LLMError,
+  AIError,
   LLMEvent,
   LLMRequest,
   Message,
-  Model,
+  LanguageModel,
   SystemPart,
   ToolFailure,
   TransportReason,
@@ -130,25 +130,25 @@ const testLLM = TestLLM.layer({
     }),
 })
 const client = TestLLM.clientLayer
-const model = Model.make({ id: "fake-model", provider: "fake", route: OpenAIChat.route })
+const model = LanguageModel.make({ id: "fake-model", provider: "fake", route: OpenAIChat.route })
 const defaultSystem = PROMPT_DEFAULT
-const replacementModel = Model.make({ id: "replacement", provider: "fake", route: OpenAIChat.route })
-const compactModel = Model.make({
+const replacementModel = LanguageModel.make({ id: "replacement", provider: "fake", route: OpenAIChat.route })
+const compactModel = LanguageModel.make({
   id: "compact",
   provider: "fake",
   route: OpenAIChat.route.with({ limits: { context: 4_000, output: 50 } }),
 })
-const fullOutputModel = Model.make({
+const fullOutputModel = LanguageModel.make({
   id: "full-output",
   provider: "fake",
   route: OpenAIChat.route.with({ limits: { context: 262_144, output: 262_144 } }),
 })
-const undersizedContextModel = Model.make({
+const undersizedContextModel = LanguageModel.make({
   id: "undersized-context",
   provider: "fake",
   route: OpenAIChat.route.with({ limits: { context: 1, output: 1_000 } }),
 })
-const recoveryModel = Model.make({
+const recoveryModel = LanguageModel.make({
   id: "recovery",
   provider: "fake",
   route: OpenAIChat.route.with({ limits: { context: 20_000, output: 1_000 } }),
@@ -230,9 +230,7 @@ const permission = Layer.succeed(
 )
 const transformTools = (registry: Tool.Interface, tools: Readonly<Record<string, Info>>, options?: Tool.Options) =>
   registry.transform((draft) =>
-    Object.entries(tools).forEach(([name, tool]) =>
-      draft.add({ ...tool, name, options: options ?? tool.options }),
-    ),
+    Object.entries(tools).forEach(([name, tool]) => draft.add({ ...tool, name, options: options ?? tool.options })),
   )
 const echo = Layer.effectDiscard(
   Tool.Service.use((registry) =>
@@ -509,21 +507,21 @@ const setup = Effect.gen(function* () {
 })
 
 const providerUnavailable = () =>
-  new LLMError({
+  new AIError({
     module: "test",
     method: "stream",
     reason: new TransportReason({ message: "Provider unavailable" }),
   })
 
 const invalidRequest = () =>
-  new LLMError({
+  new AIError({
     module: "test",
     method: "stream",
     reason: new InvalidRequestReason({ message: "Invalid request" }),
   })
 
 const rateLimited = (retryAfterMs?: number) =>
-  new LLMError({
+  new AIError({
     module: "test",
     method: "stream",
     reason: new RateLimitReason({ message: "Rate limited", retryAfterMs }),
@@ -1307,7 +1305,7 @@ describe("SessionRunnerLLM", () => {
   it.effect("uses the selected model family prompt when the agent does not override it", () =>
     Effect.gen(function* () {
       const session = yield* setup
-      currentModel = Model.make({ id: "gpt-5", provider: "openai", route: OpenAIChat.route })
+      currentModel = LanguageModel.make({ id: "gpt-5", provider: "openai", route: OpenAIChat.route })
       yield* admit(session, "First")
 
       yield* TestLLM.push(TestLLM.text("Done", "text-provider-prompt"))
@@ -1323,7 +1321,7 @@ describe("SessionRunnerLLM", () => {
   it.effect("uses the selected model family prompt when the agent system override is empty", () =>
     Effect.gen(function* () {
       const session = yield* setup
-      currentModel = Model.make({ id: "gpt-5", provider: "openai", route: OpenAIChat.route })
+      currentModel = LanguageModel.make({ id: "gpt-5", provider: "openai", route: OpenAIChat.route })
       const agent = yield* Agent.Service
       yield* agent.transform((editor) =>
         editor.update(Agent.ID.make("build"), (agent) => {
@@ -2149,7 +2147,7 @@ describe("SessionRunnerLLM", () => {
       const session = yield* setupOverflowRecovery
       yield* TestLLM.push(
         Stream.fail(
-          new LLMError({
+          new AIError({
             module: "test",
             method: "stream",
             reason: new InvalidRequestReason({
@@ -4076,7 +4074,7 @@ describe("SessionRunnerLLM", () => {
   it.effect("settles malformed streamed tool input before the provider failure", () =>
     Effect.gen(function* () {
       const session = yield* setup
-      const failure = new LLMError({
+      const failure = new AIError({
         module: "test",
         method: "stream",
         reason: new InvalidProviderOutputReason({ message: "Invalid JSON input for tool call echo" }),
@@ -4290,7 +4288,7 @@ describe("SessionRunnerLLM", () => {
   it.effect("records malformed provider-executed input as executed", () =>
     Effect.gen(function* () {
       const session = yield* setup
-      const failure = new LLMError({
+      const failure = new AIError({
         module: "test",
         method: "stream",
         reason: new InvalidProviderOutputReason({ message: "Invalid hosted tool input" }),
@@ -4322,7 +4320,7 @@ describe("SessionRunnerLLM", () => {
   it.effect("records a provider failure after malformed input", () =>
     Effect.gen(function* () {
       const session = yield* setup
-      const failure = new LLMError({
+      const failure = new AIError({
         module: "test",
         method: "stream",
         reason: new InvalidProviderOutputReason({ message: "Provider failed after malformed input" }),

+ 3 - 10
packages/core/test/session-title.test.ts

@@ -1,5 +1,5 @@
 import { expect } from "bun:test"
-import { LLMClient, LLMEvent, Model, type LLMRequest } from "@opencode-ai/ai"
+import { LLMClient, LLMEvent, LanguageModel, type LLMRequest } from "@opencode-ai/ai"
 import { OpenAIChat } from "@opencode-ai/ai/protocols"
 import { Agent } from "@opencode-ai/core/agent"
 import { Database } from "@opencode-ai/core/database/database"
@@ -24,7 +24,7 @@ import { Deferred, Effect, Fiber, Layer, Stream } from "effect"
 import { testEffect } from "./lib/effect"
 
 let requests: LLMRequest[] = []
-const model = Model.make({
+const model = LanguageModel.make({
   id: "title-model",
   provider: "test",
   route: OpenAIChat.route.with({ limits: { context: 10_000, output: 1_000 } }),
@@ -77,14 +77,7 @@ const models = Layer.mock(SessionRunnerModel.Service)({
 })
 const it = testEffect(
   AppNodeBuilder.build(
-    LayerNode.group([
-      Database.node,
-      Bus.node,
-      SessionProjector.node,
-      SessionStore.node,
-      Agent.node,
-      SessionTitle.node,
-    ]),
+    LayerNode.group([Database.node, Bus.node, SessionProjector.node, SessionStore.node, Agent.node, SessionTitle.node]),
     [
       [llmClient, client],
       [SessionRunnerModel.node, models],

+ 4 - 3
packages/core/test/shared-schema.test.ts

@@ -17,6 +17,7 @@ import { Connection } from "@opencode-ai/schema/connection"
 import { Credential } from "@opencode-ai/schema/credential"
 import { FileSystem } from "@opencode-ai/schema/filesystem"
 import { Integration } from "@opencode-ai/schema/integration"
+import { AI } from "@opencode-ai/schema/ai"
 import { LLM } from "@opencode-ai/schema/llm"
 import { Permission } from "@opencode-ai/schema/permission"
 import { Pty } from "@opencode-ai/schema/pty"
@@ -34,7 +35,7 @@ test("Core reuses the canonical shared schemas", async () => {
     coreFileSystem,
     coreIntegration,
     coreLocation,
-    coreLLM,
+    coreAI,
     coreModel,
     corePermission,
     corePermissionV1,
@@ -100,8 +101,8 @@ test("Core reuses the canonical shared schemas", async () => {
     [coreIntegration.Inputs, Integration.Inputs],
     [coreIntegration.Ref, Integration.Ref],
     [coreLocation.Ref, Location.Ref],
-    [coreLLM.ProviderMetadata, LLM.ProviderMetadata],
-    [coreLLM.FinishReason, LLM.FinishReason],
+    [coreAI.ProviderMetadata, AI.ProviderMetadata],
+    [coreAI.FinishReason, LLM.FinishReason],
     [coreModel.ID, Model.ID],
     [coreModel.VariantID, Model.VariantID],
     [coreModel.Ref, Model.Ref],

+ 8 - 0
packages/schema/src/ai.ts

@@ -0,0 +1,8 @@
+export * as AI from "./ai.js"
+
+import { Schema } from "effect"
+
+export const ProviderMetadata = Schema.Record(Schema.String, Schema.Record(Schema.String, Schema.Unknown)).annotate({
+  identifier: "AI.ProviderMetadata",
+})
+export type ProviderMetadata = Schema.Schema.Type<typeof ProviderMetadata>

+ 1 - 0
packages/schema/src/index.ts

@@ -8,6 +8,7 @@ export { FileSystem } from "./filesystem.js"
 export { Form } from "./form.js"
 export { Integration } from "./integration.js"
 export { LLM } from "./llm.js"
+export { AI } from "./ai.js"
 export { Location } from "./location.js"
 export { Mcp } from "./mcp.js"
 export { Model } from "./model.js"

+ 0 - 6
packages/schema/src/llm.ts

@@ -1,12 +1,6 @@
 export * as LLM from "./llm.js"
 
 import { Schema } from "effect"
-import { optional } from "./schema.js"
-
-export const ProviderMetadata = Schema.Record(Schema.String, Schema.Record(Schema.String, Schema.Unknown)).annotate({
-  identifier: "LLM.ProviderMetadata",
-})
-export type ProviderMetadata = Schema.Schema.Type<typeof ProviderMetadata>
 
 export const FinishReason = Schema.Literals(["stop", "length", "tool-calls", "content-filter", "error", "unknown"])
 export type FinishReason = typeof FinishReason.Type