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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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