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

This commit is contained in:
Aiden Cline 2026-08-01 21:57:05 -05:00 committed by GitHub
commit 634b9b21dd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
91 changed files with 563 additions and 500 deletions

View file

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

View file

@ -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,

View file

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

View file

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

View file

@ -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. */

View file

@ -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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 }) }