diff --git a/packages/ai/AGENTS.md b/packages/ai/AGENTS.md index b1e9fcbfa03..15927996f44 100644 --- a/packages/ai/AGENTS.md +++ b/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/ diff --git a/packages/ai/DESIGN.md b/packages/ai/DESIGN.md index 5629c13408d..91cd8818e4f 100644 --- a/packages/ai/DESIGN.md +++ b/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`. +`LLM.stream` returns an Effect `Stream`. 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 diff --git a/packages/ai/README.md b/packages/ai/README.md index 6d89fa317be..4eb4e045ab1 100644 --- a/packages/ai/README.md +++ b/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`. diff --git a/packages/ai/example/call-sites.md b/packages/ai/example/call-sites.md index 397e0cc09f8..06b766cb28b 100644 --- a/packages/ai/example/call-sites.md +++ b/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 = { readonly id: ProviderID - readonly model: (id: string) => Model + readonly model: (id: string) => LanguageModel readonly configure: (input?: Config) => ProviderFacade } & 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(...)`. diff --git a/packages/ai/src/image-client.ts b/packages/ai/src/image-client.ts index 79db227725e..581047e14c6 100644 --- a/packages/ai/src/image-client.ts +++ b/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: ( request: ImageRequestFor, - ) => Effect.Effect + ) => Effect.Effect } export class Service extends Context.Service()("@opencode/ImageClient") {} export const generate = ( request: ImageRequestFor, -): Effect.Effect => +): Effect.Effect => Effect.gen(function* () { const client = yield* Service return yield* client.generate(request) - }) as Effect.Effect + }) as Effect.Effect export const layer: Layer.Layer = Layer.effect( Service, diff --git a/packages/ai/src/image.ts b/packages/ai/src/image.ts index ee3ab6202ad..cb2e9310e6a 100644 --- a/packages/ai/src/image.ts +++ b/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 { readonly id: string - readonly generate: ( - request: ImageRequestFor, - execute: ImageExecute, - ) => Effect.Effect + readonly generate: (request: ImageRequestFor, execute: ImageExecute) => Effect.Effect } export type ImageOptions = Record @@ -146,13 +143,13 @@ export function request(input: ImageRequest | ImageRequestInput) { export function generate( input: ImageRequestInput, -): Effect.Effect -export function generate(input: ImageRequest): Effect.Effect +): Effect.Effect +export function generate(input: ImageRequest): Effect.Effect 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) }), diff --git a/packages/ai/src/index.ts b/packages/ai/src/index.ts index c6c30f76c3a..1962ea9ff5b 100644 --- a/packages/ai/src/index.ts +++ b/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" diff --git a/packages/ai/src/llm.ts b/packages/ai/src/llm.ts index 77ec766e9af..97ce92bf7e2 100644 --- a/packages/ai/src/llm.ts +++ b/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 = Omit< +export type RequestInput = Omit< ConstructorParameters[0], "model" | "system" | "messages" | "tools" | "toolChoice" | "generation" | "http" | "providerOptions" > & { - readonly model: SelectedModel + readonly model: SelectedLanguageModel readonly system?: string | SystemPart | ReadonlyArray readonly prompt?: string | ContentPart | ReadonlyArray readonly messages?: ReadonlyArray readonly tools?: ReadonlyArray readonly toolChoice?: ToolChoice.Input readonly generation?: GenerationOptions.Input - readonly providerOptions?: NoInfer> + readonly providerOptions?: NoInfer> readonly http?: HttpOptions.Input } @@ -38,7 +38,9 @@ export const generate = LLMClient.generate export const stream = LLMClient.stream -export const request = (input: RequestInput) => { +export const request = ( + input: RequestInput, +) => { 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 = Omit, "tools" | "toolChoice"> +type GenerateObjectBase = Omit< + RequestInput, + "tools" | "toolChoice" +> export class GenerateObjectResponse { constructor( @@ -83,13 +88,15 @@ export class GenerateObjectResponse { } } -export interface GenerateObjectOptions, SelectedModel extends Model = Model> - extends GenerateObjectBase { +export interface GenerateObjectOptions< + S extends ToolSchema, + SelectedLanguageModel extends LanguageModel = LanguageModel, +> extends GenerateObjectBase { readonly schema: S } -export interface GenerateObjectDynamicOptions - extends GenerateObjectBase { +export interface GenerateObjectDynamicOptions + extends GenerateObjectBase { /** 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` — `.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>( - options: GenerateObjectOptions, -): Effect.Effect>, LLMError> -export function generateObject( - options: GenerateObjectDynamicOptions, -): Effect.Effect, LLMError> +export function generateObject>( + options: GenerateObjectOptions, +): Effect.Effect>, AIError> +export function generateObject( + options: GenerateObjectDynamicOptions, +): Effect.Effect, AIError> export function generateObject(options: GenerateObjectOptions> | GenerateObjectDynamicOptions) { if ("schema" in options) { const { schema, ...rest } = options diff --git a/packages/ai/src/protocols/anthropic-messages.ts b/packages/ai/src/protocols/anthropic-messages.ts index 6041500be64..2edfcce975f 100644 --- a/packages/ai/src/protocols/anthropic-messages.ts +++ b/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 }), diff --git a/packages/ai/src/protocols/bedrock-converse.ts b/packages/ai/src/protocols/bedrock-converse.ts index 6dda74d4171..865df6f460c 100644 --- a/packages/ai/src/protocols/bedrock-converse.ts +++ b/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, ): 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({ diff --git a/packages/ai/src/protocols/google-images.ts b/packages/ai/src/protocols/google-images.ts index ce5132809da..bad287d0f8b 100644 --- a/packages/ai/src/protocols/google-images.ts +++ b/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({ id: input.id, provider: "google", route, http: input.http }) } -const googleImagePart = (image: ImageInput): Effect.Effect, LLMError> => { +const googleImagePart = (image: ImageInput): Effect.Effect, 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 } }) diff --git a/packages/ai/src/protocols/open-responses.ts b/packages/ai/src/protocols/open-responses.ts index 97ad0846141..c34f509bbb8 100644 --- a/packages/ai/src/protocols/open-responses.ts +++ b/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>( (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 }), diff --git a/packages/ai/src/protocols/openai-chat.ts b/packages/ai/src/protocols/openai-chat.ts index e61f39aae25..f758cdc260b 100644 --- a/packages/ai/src/protocols/openai-chat.ts +++ b/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({ diff --git a/packages/ai/src/protocols/openai-compatible-chat.ts b/packages/ai/src/protocols/openai-compatible-chat.ts index 9ae9a53b437..dff4e21a586 100644 --- a/packages/ai/src/protocols/openai-compatible-chat.ts +++ b/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 diff --git a/packages/ai/src/protocols/openai-compatible-responses.ts b/packages/ai/src/protocols/openai-compatible-responses.ts index 0278c7941e4..a2b0bedfa52 100644 --- a/packages/ai/src/protocols/openai-compatible-responses.ts +++ b/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 diff --git a/packages/ai/src/protocols/openai-images.ts b/packages/ai/src/protocols/openai-images.ts index 12b0f162c38..2cf70bfa2a7 100644 --- a/packages/ai/src/protocols/openai-images.ts +++ b/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 }), diff --git a/packages/ai/src/protocols/shared.ts b/packages/ai/src/protocols/shared.ts index 9d8f98a1ad2..6dc24481dc8 100644 --- a/packages/ai/src/protocols/shared.ts +++ b/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 } 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): Stream.Stream => +export const sseFraming = (bytes: Stream.Stream): Stream.Stream => bytes.pipe( Stream.decodeText(), Stream.pipeThroughChannel(Sse.decode()), @@ -257,7 +257,7 @@ export const sseFraming = (bytes: Stream.Stream): 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 = (decode: (input: I) => Effect.Effect) => diff --git a/packages/ai/src/protocols/utils/image-input.ts b/packages/ai/src/protocols/utils/image-input.ts index 7b16191f191..4f45868a0d0 100644 --- a/packages/ai/src/protocols/utils/image-input.ts +++ b/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) 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")) diff --git a/packages/ai/src/protocols/utils/tool-schema.ts b/packages/ai/src/protocols/utils/tool-schema.ts index 473942939ce..75025187ca8 100644 --- a/packages/ai/src/protocols/utils/tool-schema.ts +++ b/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) { diff --git a/packages/ai/src/protocols/utils/tool-stream.ts b/packages/ai/src/protocols/utils/tool-stream.ts index fbea376e709..aeb6b5573de 100644 --- a/packages/ai/src/protocols/utils/tool-stream.ts +++ b/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 = ( } } -export const isError = (result: AppendOutcome | LLMError): result is LLMError => - result instanceof LLMError +export const isError = (result: AppendOutcome | 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 = ( key: K, delta: { readonly id?: string; readonly name?: string; readonly text: string }, missingToolMessage: string, -): AppendOutcome | LLMError => { +): AppendOutcome | AIError => { const current = tools[key] const id = current?.id ?? delta.id const name = current?.name ?? delta.name @@ -167,7 +167,7 @@ export const appendExisting = ( key: K, text: string, missingToolMessage: string, -): AppendOutcome | LLMError => { +): AppendOutcome | AIError => { const current = tools[key] if (!current) return eventError(route, missingToolMessage) if (text.length === 0) return { tools, tool: current, events: [] } diff --git a/packages/ai/src/protocols/xai-images.ts b/packages/ai/src/protocols/xai-images.ts index f297fe66ee8..2350ffde941 100644 --- a/packages/ai/src/protocols/xai-images.ts +++ b/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 }), diff --git a/packages/ai/src/protocols/zai-images.ts b/packages/ai/src/protocols/zai-images.ts index 670c9c6c014..6d3b510323d 100644 --- a/packages/ai/src/protocols/zai-images.ts +++ b/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 }), diff --git a/packages/ai/src/provider-error.ts b/packages/ai/src/provider-error.ts index 37b0f2a587a..34290007125 100644 --- a/packages/ai/src/provider-error.ts +++ b/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) diff --git a/packages/ai/src/provider-package.ts b/packages/ai/src/provider-package.ts index 149c5a85b6e..3d47cd48c12 100644 --- a/packages/ai/src/provider-package.ts +++ b/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> { 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 + readonly model: (modelID: string, settings: ProviderSettings) => LanguageModel } export * as ProviderPackage from "./provider-package" diff --git a/packages/ai/src/provider.ts b/packages/ai/src/provider.ts index c0406f3da02..6ff1f58c198 100644 --- a/packages/ai/src/provider.ts +++ b/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 +export type LanguageModelOptions = Pick /** * Advanced structural provider definition helper. Built-in providers should @@ -8,23 +8,23 @@ export type ModelOptions = Pick * chosen before model selection. The optional `apis` map remains for external * structural providers that expose multiple route selectors behind one provider. */ -export type ModelFactory = ( +export type LanguageModelFactory = ( id: string | ModelID, options?: Options, -) => Model +) => LanguageModel -type AnyModelFactory = (...args: never[]) => Model +type AnyLanguageModelFactory = (...args: never[]) => LanguageModel -export interface Definition { +export interface Definition { readonly id: ProviderID readonly model: Factory - readonly apis?: Record + readonly apis?: Record } type DefinitionShape = { readonly id: ProviderID - readonly model: (...args: never[]) => Model - readonly apis?: Record Model> + readonly model: (...args: never[]) => LanguageModel + readonly apis?: Record LanguageModel> } type NoExtraFields = Input & Record, never> diff --git a/packages/ai/src/providers/azure.ts b/packages/ai/src/providers/azure.ts index d4c916a5889..cf17808f9cc 100644 --- a/packages/ai/src/providers/azure.ts +++ b/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 & { diff --git a/packages/ai/src/providers/github-copilot.ts b/packages/ai/src/providers/github-copilot.ts index ff75d0a776e..ec7a64ea206 100644 --- a/packages/ai/src/providers/github-copilot.ts +++ b/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 & +export type LanguageModelOptions = Omit & 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) => diff --git a/packages/ai/src/providers/openrouter.ts b/packages/ai/src/providers/openrouter.ts index bacb355e1ad..3fe7bf13585 100644 --- a/packages/ai/src/providers/openrouter.ts +++ b/packages/ai/src/providers/openrouter.ts @@ -76,7 +76,7 @@ export type OpenRouterProviderOptionsInput = ProviderOptions & { readonly openrouter?: OpenRouterOptions } -export type ModelOptions = Omit & +export type LanguageModelOptions = Omit & 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, diff --git a/packages/ai/src/providers/xai.ts b/packages/ai/src/providers/xai.ts index 7c50afe00c2..56377e25dd3 100644 --- a/packages/ai/src/providers/xai.ts +++ b/packages/ai/src/providers/xai.ts @@ -16,7 +16,7 @@ export type XAIProviderOptionsInput = ProviderOptions & { readonly xai?: OpenAIOptionsInput } -export type ModelOptions = Omit & +export type LanguageModelOptions = Omit & 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({ id: modelID }) diff --git a/packages/ai/src/route/auth-options.ts b/packages/ai/src/route/auth-options.ts index 957ae9b3111..d1005351f8a 100644 --- a/packages/ai/src/route/auth-options.ts +++ b/packages/ai/src/route/auth-options.ts @@ -22,13 +22,17 @@ export type ProviderAuthOption = | AuthOverride | (Mode extends "optional" ? OptionalApiKeyAuth : RequiredApiKeyAuth) -export type ModelOptions = Omit & ProviderAuthOption +export type LanguageModelOptions = Omit & + ProviderAuthOption -export type ModelArgs = Mode extends "optional" - ? readonly [options?: ModelOptions] - : readonly [options: ModelOptions] +export type LanguageModelArgs = Mode extends "optional" + ? readonly [options?: LanguageModelOptions] + : readonly [options: LanguageModelOptions] -export type ModelFactory = (id: string, ...args: ModelArgs) => Model +export type LanguageModelFactory = ( + id: string, + ...args: LanguageModelArgs +) => LanguageModel /** * Require at least one of the keys in `T`. Use for option shapes where any diff --git a/packages/ai/src/route/auth.ts b/packages/ai/src/route/auth.ts index 6dc87d3a91f..ff8231b35a1 100644 --- a/packages/ai/src/route/auth.ts +++ b/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 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) => auth(apply) +export const custom = (apply: (input: AuthInput) => Effect.Effect) => 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 => - input.apply(authInput).pipe(Effect.mapError(toLLMError)) + (authInput: AuthInput): Effect.Effect => + input.apply(authInput).pipe(Effect.mapError(toAIError)) export * as Auth from "./auth" diff --git a/packages/ai/src/route/client.ts b/packages/ai/src/route/client.ts index 5021ae958f4..d35c21d0b6b 100644 --- a/packages/ai/src/route/client.ts +++ b/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 { /** Schema for the validated provider-native body sent as the JSON request. */ readonly schema: Schema.Codec /** Build the provider-native body from a common `LLMRequest`. */ - readonly from: (request: LLMRequest) => Effect.Effect + readonly from: (request: LLMRequest) => Effect.Effect } export interface Route { @@ -45,17 +45,19 @@ export interface Route { readonly defaults: RouteDefaults readonly body: RouteBody readonly with: (patch: RoutePatch) => Route - readonly model: (input: RouteMappedModelInput) => Model + readonly model: ( + input: RouteMappedLanguageModelInput, + ) => LanguageModel readonly prepareTransport: ( body: Body, request: LLMRequest, options?: StreamOptions, - ) => Effect.Effect + ) => Effect.Effect readonly streamPrepared: ( prepared: Prepared, request: LLMRequest, runtime: TransportRuntime, - ) => Stream.Stream + ) => Stream.Stream } // Route registries intentionally erase body generics after construction. @@ -66,13 +68,13 @@ export type AnyRoute = Route export type HttpOptionsInput = HttpOptions.Input -export type RouteModelInput = Omit +export type RouteLanguageModelInput = Omit -export type RouteRoutedModelInput = Omit +export type RouteRoutedLanguageModelInput = Omit export interface RouteDefaults { readonly headers?: Record - 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 - 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 extends RouteDefaultsInput { readonly endpoint?: EndpointPatch } -type RouteMappedModelInput = RouteModelInput | RouteRoutedModelInput +type RouteMappedLanguageModelInput = RouteLanguageModelInput | RouteRoutedLanguageModelInput -const makeRouteModel = (route: AnyRoute, mapped: RouteMappedModelInput) => { +const makeRouteLanguageModel = ( + 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({ + return LanguageModel.make({ ...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 + (request: LLMRequest, options?: StreamOptions): Stream.Stream } export interface GenerateMethod { - (request: LLMRequest, options?: StreamOptions): Effect.Effect + (request: LLMRequest, options?: StreamOptions): Effect.Effect } export class Service extends Context.Service()("@opencode/LLMClient") {} @@ -222,11 +227,11 @@ export interface MakeTransportInput { const streamError = (route: string, message: string, cause: Cause.Cause) => { 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) => +const requireTerminalEvent = (route: string) => (events: Stream.Stream) => Stream.suspend(() => { let terminal = false return events.pipe( @@ -292,8 +297,8 @@ function makeFromTransport( defaults: mergeRouteDefaults(route.defaults, defaults), }) }, - model: (input: RouteMappedModelInput) => - makeRouteModel(route, input), + model: (input: RouteMappedLanguageModelInput) => + makeRouteLanguageModel(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 { +export function stream(request: LLMRequest, options?: StreamOptions): Stream.Stream { return Stream.unwrap( Effect.gen(function* () { return (yield* Service).stream(request, options) }), - ) as Stream.Stream + ) as Stream.Stream } -export function generate(request: LLMRequest, options?: StreamOptions): Effect.Effect { +export function generate(request: LLMRequest, options?: StreamOptions): Effect.Effect { return Effect.gen(function* () { return yield* (yield* Service).generate(request, options) - }) as Effect.Effect + }) as Effect.Effect } export const streamRequest = (request: LLMRequest, options?: StreamOptions) => diff --git a/packages/ai/src/route/executor.ts b/packages/ai/src/route/executor.ts index ab97bdaba89..f7a0fb465b3 100644 --- a/packages/ai/src/route/executor.ts +++ b/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 + ) => Effect.Effect } -export class Service extends Context.Service()("@opencode/LLM/RequestExecutor") {} +export class Service extends Context.Service()("@opencode/AI/RequestExecutor") {} const BODY_LIMIT = 16_384 const 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) => (error: u readonly kind?: string | undefined readonly request?: HttpClientRequest.HttpClientRequest | undefined }) => - new LLMError({ + new AIError({ module: "RequestExecutor", method: "execute", reason: new TransportReason({ diff --git a/packages/ai/src/route/framing.ts b/packages/ai/src/route/framing.ts index f4ec86cbf9e..95e12b73fb9 100644 --- a/packages/ai/src/route/framing.ts +++ b/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 { readonly id: string - readonly frame: (bytes: Stream.Stream) => Stream.Stream + readonly frame: (bytes: Stream.Stream) => Stream.Stream } /** Server-Sent Events framing. Used by every JSON-streaming HTTP provider. */ diff --git a/packages/ai/src/route/index.ts b/packages/ai/src/route/index.ts index 0f382326180..8b85cfc3f76 100644 --- a/packages/ai/src/route/index.ts +++ b/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, diff --git a/packages/ai/src/route/protocol.ts b/packages/ai/src/route/protocol.ts index c7340ac063e..77cc2684435 100644 --- a/packages/ai/src/route/protocol.ts +++ b/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 { /** Schema for the validated provider-native body sent as the JSON request. */ readonly schema: Schema.Codec /** Build the provider-native body from a common `LLMRequest`. */ - readonly from: (request: LLMRequest) => Effect.Effect + readonly from: (request: LLMRequest) => Effect.Effect } export interface ProtocolStream { @@ -56,7 +56,7 @@ export interface ProtocolStream { /** 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], LLMError> + readonly step: (state: State, event: Event) => Effect.Effect], 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. */ diff --git a/packages/ai/src/route/transport/index.ts b/packages/ai/src/route/transport/index.ts index 588ea9c8969..16624cce563 100644 --- a/packages/ai/src/route/transport/index.ts +++ b/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 export interface Transport { readonly id: string - readonly prepare: (input: TransportPrepareInput) => Effect.Effect - readonly frames: ( - prepared: Prepared, - request: LLMRequest, - runtime: TransportRuntime, - ) => Stream.Stream + readonly prepare: (input: TransportPrepareInput) => Effect.Effect + readonly frames: (prepared: Prepared, request: LLMRequest, runtime: TransportRuntime) => Stream.Stream } export interface TransportPrepareInput { diff --git a/packages/ai/src/route/transport/websocket.ts b/packages/ai/src/route/transport/websocket.ts index 310121420c4..ec02d2dcf4b 100644 --- a/packages/ai/src/route/transport/websocket.ts +++ b/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 - readonly messages: Stream.Stream + readonly sendText: (message: string) => Effect.Effect + readonly messages: Stream.Stream readonly close: Effect.Effect } export interface Interface { - readonly open: (input: WebSocketRequest) => Effect.Effect + readonly open: (input: WebSocketRequest) => Effect.Effect } type WebSocketConstructorWithHeaders = new ( @@ -24,14 +24,14 @@ type WebSocketConstructorWithHeaders = new ( options?: { readonly headers?: Headers.Headers }, ) => globalThis.WebSocket -export class Service extends Context.Service()("@opencode/LLM/WebSocketExecutor") {} +export class Service extends Context.Service()("@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((resume, signal) => { + return Effect.callback((resume, signal) => { const cleanup = () => { ws.removeEventListener("open", onOpen) ws.removeEventListener("error", onError) @@ -138,10 +138,10 @@ export const layer: Layer.Layer = Layer.succeed(Service, Service.of({ o export const fromWebSocket = ( ws: globalThis.WebSocket, input: WebSocketRequest, -): Effect.Effect => +): Effect.Effect => Effect.gen(function* () { yield* waitOpen(ws, input) - const messages = yield* Queue.bounded>(128) + const messages = yield* Queue.bounded>(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 { - readonly toMessage: (body: Body | Record) => Effect.Effect + readonly toMessage: (body: Body | Record) => Effect.Effect readonly encodeMessage: (message: Message) => string } diff --git a/packages/ai/src/schema/errors.ts b/packages/ai/src/schema/errors.ts index 0bb6bfe74d0..a7ca7f75ec1 100644 --- a/packages/ai/src/schema/errors.ts +++ b/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("LLM.HttpRequestDetails")({ +export class HttpRequestDetails extends Schema.Class("AI.HttpRequestDetails")({ method: Schema.String, url: Schema.String, headers: Schema.Record(Schema.String, Schema.String), }) {} -export class HttpResponseDetails extends Schema.Class("LLM.HttpResponseDetails")({ +export class HttpResponseDetails extends Schema.Class("AI.HttpResponseDetails")({ status: Schema.Number, headers: Schema.Record(Schema.String, Schema.String), }) {} -export class HttpRateLimitDetails extends Schema.Class("LLM.HttpRateLimitDetails")({ +export class HttpRateLimitDetails extends Schema.Class("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("LLM.HttpContext")({ +export class HttpContext extends Schema.Class("AI.HttpContext")({ request: HttpRequestDetails, response: Schema.optional(HttpResponseDetails), body: Schema.optional(Schema.String), @@ -32,7 +32,7 @@ export class HttpContext extends Schema.Class("LLM.HttpContext")({ rateLimit: Schema.optional(HttpRateLimitDetails), }) {} -export class InvalidRequestReason extends Schema.Class("LLM.Error.InvalidRequest")({ +export class InvalidRequestReason extends Schema.Class("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("LL http: Schema.optional(HttpContext), }) {} -export class NoRouteReason extends Schema.Class("LLM.Error.NoRoute")({ +export class NoRouteReason extends Schema.Class("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("LLM.Error.Authentication")({ +export class AuthenticationReason extends Schema.Class("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("LL http: Schema.optional(HttpContext), }) {} -export class RateLimitReason extends Schema.Class("LLM.Error.RateLimit")({ +export class RateLimitReason extends Schema.Class("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("LLM.Error.Ra http: Schema.optional(HttpContext), }) {} -export class QuotaExceededReason extends Schema.Class("LLM.Error.QuotaExceeded")({ +export class QuotaExceededReason extends Schema.Class("AI.Error.QuotaExceeded")({ _tag: Schema.tag("QuotaExceeded"), message: Schema.String, providerMetadata: Schema.optional(ProviderMetadata), http: Schema.optional(HttpContext), }) {} -export class ContentPolicyReason extends Schema.Class("LLM.Error.ContentPolicy")({ +export class ContentPolicyReason extends Schema.Class("AI.Error.ContentPolicy")({ _tag: Schema.tag("ContentPolicy"), message: Schema.String, providerMetadata: Schema.optional(ProviderMetadata), http: Schema.optional(HttpContext), }) {} -export class ProviderInternalReason extends Schema.Class("LLM.Error.ProviderInternal")({ +export class ProviderInternalReason extends Schema.Class("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 http: Schema.optional(HttpContext), }) {} -export class TransportReason extends Schema.Class("LLM.Error.Transport")({ +export class TransportReason extends Schema.Class("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("LLM.Error.Tr }) {} export class InvalidProviderOutputReason extends Schema.Class( - "LLM.Error.InvalidProviderOutput", + "AI.Error.InvalidProviderOutput", )({ _tag: Schema.tag("InvalidProviderOutput"), message: Schema.String, @@ -110,7 +110,7 @@ export class InvalidProviderOutputReason extends Schema.Class("LLM.Error.UnknownProvider")({ +export class UnknownProviderReason extends Schema.Class("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(" 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 +export type AIErrorReason = Schema.Schema.Type -export class LLMError extends Schema.TaggedErrorClass()("LLM.Error", { +export class AIError extends Schema.TaggedErrorClass()("AI.Error", { module: Schema.String, method: Schema.String, - reason: LLMErrorReason, + reason: AIErrorReason, }) { override readonly cause = this.reason diff --git a/packages/ai/src/schema/events.ts b/packages/ai/src/schema/events.ts index e1b9251c023..49e9f334a87 100644 --- a/packages/ai/src/schema/events.ts +++ b/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("LLM.Usage")({ +export class Usage extends Schema.Class("AI.Usage")({ inputTokens: Schema.optional(Schema.Number), outputTokens: Schema.optional(Schema.Number), nonCachedInputTokens: Schema.optional(Schema.Number), diff --git a/packages/ai/src/schema/ids.ts b/packages/ai/src/schema/ids.ts index 4775caf2ef9..006a78fa4de 100644 --- a/packages/ai/src/schema/ids.ts +++ b/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 export const RouteID = Schema.String export type RouteID = Schema.Schema.Type -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 diff --git a/packages/ai/src/schema/messages.ts b/packages/ai/src/schema/messages.ts index d3689dc0e34..94b12206fb4 100644 --- a/packages/ai/src/schema/messages.ts +++ b/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("LLM.Request")({ id: Schema.optional(Schema.String), - model: ModelSchema, + model: LanguageModelSchema, system: Schema.Array(SystemPart), messages: Schema.Array(Message), tools: Schema.Array(ToolDefinition), diff --git a/packages/ai/src/schema/options.ts b/packages/ai/src/schema/options.ts index 62c606056d0..1b5c41a4d82 100644 --- a/packages/ai/src/schema/options.ts +++ b/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("LLM.HttpOptions")({ +export class HttpOptions extends Schema.Class("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 value !== undefined) ? result : undefined } -export class ModelLimits extends Schema.Class("LLM.ModelLimits")({ +export class LanguageModelLimits extends Schema.Class("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[0] +export namespace LanguageModelLimits { + export type Input = LanguageModelLimits | ConstructorParameters[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("LLM.ModelDefaults")({ - limits: Schema.optional(ModelLimits), +export class LanguageModelDefaults extends Schema.Class("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 +export const LanguageModelToolSchemaCompatibility = Schema.Literals(["gemini", "moonshot"]) +export type LanguageModelToolSchemaCompatibility = Schema.Schema.Type -export const ModelMaxTokensFieldCompatibility = Schema.Literals(["max_completion_tokens", "max_tokens"]) -export type ModelMaxTokensFieldCompatibility = Schema.Schema.Type +export const LanguageModelMaxTokensFieldCompatibility = Schema.Literals(["max_completion_tokens", "max_tokens"]) +export type LanguageModelMaxTokensFieldCompatibility = Schema.Schema.Type< + typeof LanguageModelMaxTokensFieldCompatibility +> -export class ModelCompatibility extends Schema.Class("LLM.ModelCompatibility")({ - toolSchema: Schema.optional(ModelToolSchemaCompatibility), +export class LanguageModelCompatibility extends Schema.Class( + "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[0] +export namespace LanguageModelCompatibility { + export type Input = LanguageModelCompatibility | ConstructorParameters[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 { +export class LanguageModel { 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 { this.compatibility = input.compatibility } - static make(input: Model.Input) { - return new Model({ + static make(input: LanguageModel.Input) { + return new LanguageModel({ 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(model: Model): Model.ConstructorInput { + static input(model: LanguageModel): LanguageModel.ConstructorInput { return { id: model.id, provider: model.provider, @@ -219,37 +225,40 @@ export class Model { } } - static update(model: Model, patch: Partial) { + static update(model: LanguageModel, patch: Partial) { if (Object.keys(patch).length === 0) return model - return Model.make({ - ...Model.input(model), + return LanguageModel.make({ + ...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 & { 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 extends Model ? Options : never +export type LanguageModelProviderOptions = + SelectedModel extends LanguageModel ? 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("LLM.CacheHint")({ type: Schema.Literals(["ephemeral", "persistent"]), diff --git a/packages/ai/src/testing.ts b/packages/ai/src/testing.ts index dbddb8d9888..475569a24ec 100644 --- a/packages/ai/src/testing.ts +++ b/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 +export type Response = readonly LLMEvent[] | Stream.Stream export type Gate = Readonly<{ started: Effect.Effect; release: Effect.Effect }> @@ -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) diff --git a/packages/ai/test/adapter.test.ts b/packages/ai/test/adapter.test.ts index 9409d04a173..b705a9859f8 100644 --- a/packages/ai/test/adapter.test.ts +++ b/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.update(model, patch) +const updateModel = (model: LanguageModel, patch: Partial) => 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, diff --git a/packages/ai/test/auth-options.types.ts b/packages/ai/test/auth-options.types.ts index 4572a0aee54..36dd9855803 100644 --- a/packages/ai/test/auth-options.types.ts +++ b/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 } -type Model = { +type LanguageModel = { readonly id: string } declare const auth: Auth.Definition -declare const optionalAuthModel: ModelFactory -declare const requiredAuthModel: ModelFactory +declare const optionalAuthModel: LanguageModelFactory +declare const requiredAuthModel: LanguageModelFactory const configApiKey = Config.redacted("OPENAI_API_KEY") OpenAIChat.route.model({ id: "gpt-4.1-mini" }) diff --git a/packages/ai/test/auth.test.ts b/packages/ai/test/auth.test.ts index 1c7148dbbb4..548613ccfd1 100644 --- a/packages/ai/test/auth.test.ts +++ b/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", }) diff --git a/packages/ai/test/continuation-scenarios.ts b/packages/ai/test/continuation-scenarios.ts index 1bb1848b557..b0f1bfb9673 100644 --- a/packages/ai/test/continuation-scenarios.ts +++ b/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 readonly image?: string }) { diff --git a/packages/ai/test/endpoint.test.ts b/packages/ai/test/endpoint.test.ts index 504c9843c1b..98de4afd23a 100644 --- a/packages/ai/test/endpoint.test.ts +++ b/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, diff --git a/packages/ai/test/executor.test.ts b/packages/ai/test/executor.test.ts index 07119cfc5a2..91417c8f08f 100644 --- a/packages/ai/test/executor.test.ts +++ b/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, 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("") expect(errorHttp(error)?.response?.headers["x-safe"]).toBe("") }).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":""') expect(errorHttp(error)?.body).toContain("api_key=") 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 ") expect(errorHttp(error)?.body).toContain("authorization ") 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) }), diff --git a/packages/ai/test/exports.test.ts b/packages/ai/test/exports.test.ts index d036392278f..b0003b55d7b 100644 --- a/packages/ai/test/exports.test.ts +++ b/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) diff --git a/packages/ai/test/llm-option-types.types.ts b/packages/ai/test/llm-option-types.types.ts index 8171a7b2e99..49dba9b0127 100644 --- a/packages/ai/test/llm-option-types.types.ts +++ b/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 = { example: { mode: "fast" } } +const options: LanguageModelProviderOptions = { example: { mode: "fast" } } void options diff --git a/packages/ai/test/llm.test.ts b/packages/ai/test/llm.test.ts index e5588cf95d6..84bd8c02f79 100644 --- a/packages/ai/test/llm.test.ts +++ b/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], }) diff --git a/packages/ai/test/provider.types.ts b/packages/ai/test/provider.types.ts index f8b46e37539..7ce0c5014c7 100644 --- a/packages/ai/test/provider.types.ts +++ b/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"), diff --git a/packages/ai/test/provider/anthropic-messages.recorded.test.ts b/packages/ai/test/provider/anthropic-messages.recorded.test.ts index 9b552550add..203fcb49628 100644 --- a/packages/ai/test/provider/anthropic-messages.recorded.test.ts +++ b/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") }), diff --git a/packages/ai/test/provider/anthropic-messages.test.ts b/packages/ai/test/provider/anthropic-messages.test.ts index 920978258c2..5765d2b1b9e 100644 --- a/packages/ai/test/provider/anthropic-messages.test.ts +++ b/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") }), diff --git a/packages/ai/test/provider/gemini.test.ts b/packages/ai/test/provider/gemini.test.ts index 1548df5fe69..239b53f6cbd 100644 --- a/packages/ai/test/provider/gemini.test.ts +++ b/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") }), diff --git a/packages/ai/test/provider/openai-chat-reasoning.recorded.test.ts b/packages/ai/test/provider/openai-chat-reasoning.recorded.test.ts index 4c881d20fb7..8cfc7ae607c 100644 --- a/packages/ai/test/provider/openai-chat-reasoning.recorded.test.ts +++ b/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", diff --git a/packages/ai/test/provider/openai-chat.test.ts b/packages/ai/test/provider/openai-chat.test.ts index 27d1f296ddc..340abf3f5d8 100644 --- a/packages/ai/test/provider/openai-chat.test.ts +++ b/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") }), diff --git a/packages/ai/test/provider/openai-responses.test.ts b/packages/ai/test/provider/openai-responses.test.ts index 3b9a0b76aa1..02722c5cc98 100644 --- a/packages/ai/test/provider/openai-responses.test.ts +++ b/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") }), diff --git a/packages/ai/test/provider/pdf.recorded.test.ts b/packages/ai/test/provider/pdf.recorded.test.ts index 1a4c65b7356..5f9221607d0 100644 --- a/packages/ai/test/provider/pdf.recorded.test.ts +++ b/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", diff --git a/packages/ai/test/recorded-golden.ts b/packages/ai/test/recorded-golden.ts index 404a5028b2a..cca25c299f4 100644 --- a/packages/ai/test/recorded-golden.ts +++ b/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 readonly transport?: Transport diff --git a/packages/ai/test/recorded-scenarios.ts b/packages/ai/test/recorded-scenarios.ts index d0f043cca74..f9650e0e1c1 100644 --- a/packages/ai/test/recorded-scenarios.ts +++ b/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) => export interface GoldenScenarioContext { readonly id: string - readonly model: Model + readonly model: LanguageModel readonly maxTokens?: number readonly temperature?: number | false } diff --git a/packages/ai/test/schema.test.ts b/packages/ai/test/schema.test.ts index 4fdada7d143..bf5c94dfd96 100644 --- a/packages/ai/test/schema.test.ts +++ b/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") +}) diff --git a/packages/ai/test/tool-stream.test.ts b/packages/ai/test/tool-stream.test.ts index 72160ed2294..51c2c6fcfc2 100644 --- a/packages/ai/test/tool-stream.test.ts +++ b/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(), 0, "{}", "missing tool") - expect(error).toBeInstanceOf(LLMError) + expect(error).toBeInstanceOf(AIError) if (ToolStream.isError(error)) expect(error.reason.message).toBe("missing tool") }), ) diff --git a/packages/core/src/aisdk.ts b/packages/core/src/aisdk.ts index 4114f4aaea4..492428426a6 100644 --- a/packages/core/src/aisdk.ts +++ b/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 readonly runLanguage: (event: LanguageEvent) => Effect.Effect readonly language: (model: Info) => Effect.Effect - readonly model: (model: Info) => Effect.Effect + readonly model: (model: Info) => Effect.Effect } export class Service extends Context.Service()("@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 }, event: LanguageModelV3StreamPart, -): Effect.Effect, LLMError> { +): Effect.Effect, 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, diff --git a/packages/core/src/generate.ts b/packages/core/src/generate.ts index ceec4de4449..5244f51cc84 100644 --- a/packages/core/src/generate.ts +++ b/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, diff --git a/packages/core/src/model-resolver.ts b/packages/core/src/model-resolver.ts index bcfa5aae507..b334d5cd07c 100644 --- a/packages/core/src/model-resolver.ts +++ b/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 - readonly loadAISDK?: (model: Info) => Effect.Effect + readonly loadAISDK?: (model: Info) => Effect.Effect } export const fromCatalogModel = ( model: Info, credential?: Credential.Value, dependencies?: Dependencies, -): Effect.Effect => { +): Effect.Effect => { 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) diff --git a/packages/core/src/session/compaction.ts b/packages/core/src/session/compaction.ts index b36d997727e..d523f38abf4 100644 --- a/packages/core/src/session/compaction.ts +++ b/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 + readonly stream: (request: LLMRequest) => Stream.Stream } 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) }), diff --git a/packages/core/src/session/generate.ts b/packages/core/src/session/generate.ts index a8f38b10c82..64124f1fb23 100644 --- a/packages/core/src/session/generate.ts +++ b/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. */ diff --git a/packages/core/src/session/runner/index.ts b/packages/core/src/session/runner/index.ts index db45c0cd31f..3771c41008c 100644 --- a/packages/core/src/session/runner/index.ts +++ b/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 diff --git a/packages/core/src/session/runner/llm.ts b/packages/core/src/session/runner/llm.ts index 2879bab4872..6586c659d0b 100644 --- a/packages/core/src/session/runner/llm.ts +++ b/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.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.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() diff --git a/packages/core/src/session/runner/model.ts b/packages/core/src/session/runner/model.ts index 3b842b72eb8..383e8fe1939 100644 --- a/packages/core/src/session/runner/model.ts +++ b/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()("@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 diff --git a/packages/core/src/session/runner/retry.ts b/packages/core/src/session/runner/retry.ts index 3e22554d60f..ff09fbc6769 100644 --- a/packages/core/src/session/runner/retry.ts +++ b/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(), Schedule.modifyDelay(({ input: failure, duration: delay }) => { diff --git a/packages/core/src/session/title.ts b/packages/core/src/session/title.ts index 0f2d611f3d7..689a02cd0fc 100644 --- a/packages/core/src/session/title.ts +++ b/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 + readonly stream: (request: LLMRequest) => Stream.Stream } 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 diff --git a/packages/core/src/session/to-session-error.ts b/packages/core/src/session/to-session-error.ts index 771deeb6043..0a57bb296db 100644 --- a/packages/core/src/session/to-session-error.ts +++ b/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 }) } diff --git a/packages/core/test/aisdk.test.ts b/packages/core/test/aisdk.test.ts index e3dfbe8d803..693d790685c 100644 --- a/packages/core/test/aisdk.test.ts +++ b/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") }), ) diff --git a/packages/core/test/generate.test.ts b/packages/core/test/generate.test.ts index 52decda55db..68d56235a00 100644 --- a/packages/core/test/generate.test.ts +++ b/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: { diff --git a/packages/core/test/model-resolver.test.ts b/packages/core/test/model-resolver.test.ts index 55c64112321..9a1956d6fb6 100644 --- a/packages/core/test/model-resolver.test.ts +++ b/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, diff --git a/packages/core/test/prompt-cache-diagnostics.test.ts b/packages/core/test/prompt-cache-diagnostics.test.ts index 4049c99465f..092f23b7da4 100644 --- a/packages/core/test/prompt-cache-diagnostics.test.ts +++ b/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", diff --git a/packages/core/test/session-compact.test.ts b/packages/core/test/session-compact.test.ts index 61bb24a622b..a1e3ca203f1 100644 --- a/packages/core/test/session-compact.test.ts +++ b/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 } }), diff --git a/packages/core/test/session-compaction.test.ts b/packages/core/test/session-compaction.test.ts index b32f453285a..689acdd6250 100644 --- a/packages/core/test/session-compaction.test.ts +++ b/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 }), diff --git a/packages/core/test/session-error.test.ts b/packages/core/test/session-error.test.ts index 9e2d18659a3..72f8ebf85ed 100644 --- a/packages/core/test/session-error.test.ts +++ b/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", diff --git a/packages/core/test/session-execution.test.ts b/packages/core/test/session-execution.test.ts index 09920eae7c9..b51881cf38b 100644 --- a/packages/core/test/session-execution.test.ts +++ b/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" }), diff --git a/packages/core/test/session-generate.test.ts b/packages/core/test/session-generate.test.ts index 9a0d1efa3f5..eba19650838 100644 --- a/packages/core/test/session-generate.test.ts +++ b/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) => diff --git a/packages/core/test/session-runner.test.ts b/packages/core/test/session-runner.test.ts index 110d1505a05..f7bca091004 100644 --- a/packages/core/test/session-runner.test.ts +++ b/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>, 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" }), diff --git a/packages/core/test/session-title.test.ts b/packages/core/test/session-title.test.ts index ac4e4f60979..9a537f02f55 100644 --- a/packages/core/test/session-title.test.ts +++ b/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], diff --git a/packages/core/test/shared-schema.test.ts b/packages/core/test/shared-schema.test.ts index 1e22da6c289..9f30aca2faf 100644 --- a/packages/core/test/shared-schema.test.ts +++ b/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], diff --git a/packages/schema/src/ai.ts b/packages/schema/src/ai.ts new file mode 100644 index 00000000000..d8284802f6c --- /dev/null +++ b/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 diff --git a/packages/schema/src/index.ts b/packages/schema/src/index.ts index 0cbb430add2..51805211175 100644 --- a/packages/schema/src/index.ts +++ b/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" diff --git a/packages/schema/src/llm.ts b/packages/schema/src/llm.ts index 3d4e1514321..6e8886ba3c0 100644 --- a/packages/schema/src/llm.ts +++ b/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 export const FinishReason = Schema.Literals(["stop", "length", "tool-calls", "content-filter", "error", "unknown"]) export type FinishReason = typeof FinishReason.Type