refactor(ai): align multimodal naming (#40073)
This commit is contained in:
parent
5d729521d3
commit
634b9b21dd
91 changed files with 563 additions and 500 deletions
|
|
@ -1,25 +1,25 @@
|
|||
import { Context, Effect, Layer } from "effect"
|
||||
import { RequestExecutor } from "./route/executor"
|
||||
import type { ImageOptions, ImageRequest, ImageRequestFor, ImageResponse } from "./image"
|
||||
import type { LLMError } from "./schema"
|
||||
import type { AIError } from "./schema"
|
||||
|
||||
export type Execute = RequestExecutor.Interface["execute"]
|
||||
|
||||
export interface Interface {
|
||||
readonly generate: <Options extends ImageOptions>(
|
||||
request: ImageRequestFor<Options>,
|
||||
) => Effect.Effect<ImageResponse, LLMError>
|
||||
) => Effect.Effect<ImageResponse, AIError>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/ImageClient") {}
|
||||
|
||||
export const generate = <Options extends ImageOptions>(
|
||||
request: ImageRequestFor<Options>,
|
||||
): Effect.Effect<ImageResponse, LLMError> =>
|
||||
): Effect.Effect<ImageResponse, AIError> =>
|
||||
Effect.gen(function* () {
|
||||
const client = yield* Service
|
||||
return yield* client.generate(request)
|
||||
}) as Effect.Effect<ImageResponse, LLMError>
|
||||
}) as Effect.Effect<ImageResponse, AIError>
|
||||
|
||||
export const layer: Layer.Layer<Service, never, RequestExecutor.Service> = Layer.effect(
|
||||
Service,
|
||||
|
|
|
|||
|
|
@ -1,13 +1,10 @@
|
|||
import { Effect, Schema } from "effect"
|
||||
import { HttpOptions, InvalidRequestReason, LLMError, ModelID, ProviderID, ProviderMetadata, Usage } from "./schema"
|
||||
import { HttpOptions, InvalidRequestReason, AIError, ModelID, ProviderID, ProviderMetadata, Usage } from "./schema"
|
||||
import { ImageClient, Service, type Execute as ImageExecute } from "./image-client"
|
||||
|
||||
export interface ImageRoute<Options extends ImageOptions = ImageOptions> {
|
||||
readonly id: string
|
||||
readonly generate: (
|
||||
request: ImageRequestFor<Options>,
|
||||
execute: ImageExecute,
|
||||
) => Effect.Effect<ImageResponse, LLMError>
|
||||
readonly generate: (request: ImageRequestFor<Options>, execute: ImageExecute) => Effect.Effect<ImageResponse, AIError>
|
||||
}
|
||||
|
||||
export type ImageOptions = Record<string, unknown>
|
||||
|
|
@ -146,13 +143,13 @@ export function request(input: ImageRequest | ImageRequestInput) {
|
|||
|
||||
export function generate<const Model extends object>(
|
||||
input: ImageRequestInput<Model>,
|
||||
): Effect.Effect<ImageResponse, LLMError, Service>
|
||||
export function generate(input: ImageRequest): Effect.Effect<ImageResponse, LLMError, Service>
|
||||
): Effect.Effect<ImageResponse, AIError, Service>
|
||||
export function generate(input: ImageRequest): Effect.Effect<ImageResponse, AIError, Service>
|
||||
export function generate(input: ImageRequest | ImageRequestInput) {
|
||||
return Effect.try({
|
||||
try: () => (input instanceof ImageRequest ? input : request(input)),
|
||||
catch: (error) =>
|
||||
new LLMError({
|
||||
new AIError({
|
||||
module: "Image",
|
||||
method: "generate",
|
||||
reason: new InvalidRequestReason({ message: error instanceof Error ? error.message : String(error) }),
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -4,33 +4,33 @@ import {
|
|||
GenerationOptions,
|
||||
HttpOptions,
|
||||
InvalidProviderOutputReason,
|
||||
LLMError,
|
||||
AIError,
|
||||
LLMEvent,
|
||||
LLMRequest,
|
||||
LLMResponse,
|
||||
Message,
|
||||
Model,
|
||||
LanguageModel,
|
||||
SystemPart,
|
||||
ToolChoice,
|
||||
ToolDefinition,
|
||||
type ContentPart,
|
||||
type ModelProviderOptions,
|
||||
type LanguageModelProviderOptions,
|
||||
} from "./schema"
|
||||
import { make as makeTool, toDefinitions, type ToolSchema } from "./tool"
|
||||
|
||||
/** Input accepted by `LLM.request`, normalized into the canonical `LLMRequest` class. */
|
||||
export type RequestInput<SelectedModel extends Model = Model> = Omit<
|
||||
export type RequestInput<SelectedLanguageModel extends LanguageModel = LanguageModel> = Omit<
|
||||
ConstructorParameters<typeof LLMRequest>[0],
|
||||
"model" | "system" | "messages" | "tools" | "toolChoice" | "generation" | "http" | "providerOptions"
|
||||
> & {
|
||||
readonly model: SelectedModel
|
||||
readonly model: SelectedLanguageModel
|
||||
readonly system?: string | SystemPart | ReadonlyArray<SystemPart>
|
||||
readonly prompt?: string | ContentPart | ReadonlyArray<ContentPart>
|
||||
readonly messages?: ReadonlyArray<Message | Message.Input>
|
||||
readonly tools?: ReadonlyArray<ToolDefinition.Input>
|
||||
readonly toolChoice?: ToolChoice.Input
|
||||
readonly generation?: GenerationOptions.Input
|
||||
readonly providerOptions?: NoInfer<ModelProviderOptions<SelectedModel>>
|
||||
readonly providerOptions?: NoInfer<LanguageModelProviderOptions<SelectedLanguageModel>>
|
||||
readonly http?: HttpOptions.Input
|
||||
}
|
||||
|
||||
|
|
@ -38,7 +38,9 @@ export const generate = LLMClient.generate
|
|||
|
||||
export const stream = LLMClient.stream
|
||||
|
||||
export const request = <const SelectedModel extends Model>(input: RequestInput<SelectedModel>) => {
|
||||
export const request = <const SelectedLanguageModel extends LanguageModel>(
|
||||
input: RequestInput<SelectedLanguageModel>,
|
||||
) => {
|
||||
const {
|
||||
system: requestSystem,
|
||||
prompt,
|
||||
|
|
@ -66,7 +68,10 @@ const GENERATE_OBJECT_TOOL_NAME = "generate_object"
|
|||
|
||||
const GENERATE_OBJECT_TOOL_DESCRIPTION = "Return the structured result by calling this tool."
|
||||
|
||||
type GenerateObjectBase<SelectedModel extends Model = Model> = Omit<RequestInput<SelectedModel>, "tools" | "toolChoice">
|
||||
type GenerateObjectBase<SelectedLanguageModel extends LanguageModel = LanguageModel> = Omit<
|
||||
RequestInput<SelectedLanguageModel>,
|
||||
"tools" | "toolChoice"
|
||||
>
|
||||
|
||||
export class GenerateObjectResponse<T> {
|
||||
constructor(
|
||||
|
|
@ -83,13 +88,15 @@ export class GenerateObjectResponse<T> {
|
|||
}
|
||||
}
|
||||
|
||||
export interface GenerateObjectOptions<S extends ToolSchema<any>, SelectedModel extends Model = Model>
|
||||
extends GenerateObjectBase<SelectedModel> {
|
||||
export interface GenerateObjectOptions<
|
||||
S extends ToolSchema<any>,
|
||||
SelectedLanguageModel extends LanguageModel = LanguageModel,
|
||||
> extends GenerateObjectBase<SelectedLanguageModel> {
|
||||
readonly schema: S
|
||||
}
|
||||
|
||||
export interface GenerateObjectDynamicOptions<SelectedModel extends Model = Model>
|
||||
extends GenerateObjectBase<SelectedModel> {
|
||||
export interface GenerateObjectDynamicOptions<SelectedLanguageModel extends LanguageModel = LanguageModel>
|
||||
extends GenerateObjectBase<SelectedLanguageModel> {
|
||||
/** Raw JSON Schema object describing the expected output shape. */
|
||||
readonly jsonSchema: JsonSchema.JsonSchema
|
||||
}
|
||||
|
|
@ -108,7 +115,7 @@ const runGenerateObject = Effect.fn("LLM.generateObject")(function* (
|
|||
(event) => LLMEvent.is.toolCall(event) && event.name === GENERATE_OBJECT_TOOL_NAME,
|
||||
)
|
||||
if (!call || !LLMEvent.is.toolCall(call))
|
||||
return yield* new LLMError({
|
||||
return yield* new AIError({
|
||||
module: "LLM",
|
||||
method: "generateObject",
|
||||
reason: new InvalidProviderOutputReason({
|
||||
|
|
@ -118,7 +125,7 @@ const runGenerateObject = Effect.fn("LLM.generateObject")(function* (
|
|||
const object = yield* tool._decode(call.input).pipe(
|
||||
Effect.mapError(
|
||||
(error) =>
|
||||
new LLMError({
|
||||
new AIError({
|
||||
module: "LLM",
|
||||
method: "generateObject",
|
||||
reason: new InvalidProviderOutputReason({
|
||||
|
|
@ -138,16 +145,16 @@ const runGenerateObject = Effect.fn("LLM.generateObject")(function* (
|
|||
* Two input modes:
|
||||
*
|
||||
* 1. `schema: EffectSchema<T>` — `.object` is decoded and typed as `T`.
|
||||
* Decode failures surface as `LLMError`.
|
||||
* Decode failures surface as `AIError`.
|
||||
* 2. `jsonSchema: JsonSchema.JsonSchema` — `.object` is `unknown`. Use when
|
||||
* the schema is only available at runtime (MCP, plugin manifests). Caller validates.
|
||||
*/
|
||||
export function generateObject<const SelectedModel extends Model, S extends ToolSchema<any>>(
|
||||
options: GenerateObjectOptions<S, SelectedModel>,
|
||||
): Effect.Effect<GenerateObjectResponse<Schema.Schema.Type<S>>, LLMError>
|
||||
export function generateObject<const SelectedModel extends Model>(
|
||||
options: GenerateObjectDynamicOptions<SelectedModel>,
|
||||
): Effect.Effect<GenerateObjectResponse<unknown>, LLMError>
|
||||
export function generateObject<const SelectedLanguageModel extends LanguageModel, S extends ToolSchema<any>>(
|
||||
options: GenerateObjectOptions<S, SelectedLanguageModel>,
|
||||
): Effect.Effect<GenerateObjectResponse<Schema.Schema.Type<S>>, AIError>
|
||||
export function generateObject<const SelectedLanguageModel extends LanguageModel>(
|
||||
options: GenerateObjectDynamicOptions<SelectedLanguageModel>,
|
||||
): Effect.Effect<GenerateObjectResponse<unknown>, AIError>
|
||||
export function generateObject(options: GenerateObjectOptions<ToolSchema<any>> | GenerateObjectDynamicOptions) {
|
||||
if ("schema" in options) {
|
||||
const { schema, ...rest } = options
|
||||
|
|
|
|||
|
|
@ -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 }),
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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 } })
|
||||
|
|
|
|||
|
|
@ -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 }),
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 }),
|
||||
|
|
|
|||
|
|
@ -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>) =>
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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: [] }
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { GeneratedImage, ImageModel, ImageResponse, type ImageRequestFor, type I
|
|||
import { Auth, type Definition as AuthDefinition } from "../route/auth"
|
||||
import {
|
||||
InvalidProviderOutputReason,
|
||||
LLMError,
|
||||
AIError,
|
||||
Usage,
|
||||
mergeHttpOptions,
|
||||
mergeJsonRecords,
|
||||
|
|
@ -95,7 +95,7 @@ const nativeOptions = (options: XAIImageOptions | undefined) => {
|
|||
}
|
||||
|
||||
const invalidOutput = (message: string) =>
|
||||
new LLMError({
|
||||
new AIError({
|
||||
module: ADAPTER,
|
||||
method: "generate",
|
||||
reason: new InvalidProviderOutputReason({ message, route: ADAPTER }),
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { Effect, Schema } from "effect"
|
|||
import { Headers, HttpClientRequest } from "effect/unstable/http"
|
||||
import { GeneratedImage, ImageModel, ImageResponse, type ImageRequestFor, type ImageRoute } from "../image"
|
||||
import { Auth, type Definition as AuthDefinition } from "../route/auth"
|
||||
import { InvalidProviderOutputReason, LLMError, mergeHttpOptions, mergeJsonRecords, type HttpOptions } from "../schema"
|
||||
import { InvalidProviderOutputReason, AIError, mergeHttpOptions, mergeJsonRecords, type HttpOptions } from "../schema"
|
||||
import { ProviderShared } from "./shared"
|
||||
import { ImageInputs } from "./utils/image-input"
|
||||
|
||||
|
|
@ -58,7 +58,7 @@ const nativeOptions = (options: ZAIImageOptions | undefined) => {
|
|||
}
|
||||
|
||||
const invalidOutput = (message: string) =>
|
||||
new LLMError({
|
||||
new AIError({
|
||||
module: ADAPTER,
|
||||
method: "generate",
|
||||
reason: new InvalidProviderOutputReason({ message, route: ADAPTER }),
|
||||
|
|
|
|||
|
|
@ -3,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)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { Model, ProviderOptions } from "./schema"
|
||||
import type { LanguageModel, ProviderOptions } from "./schema"
|
||||
|
||||
export interface Settings extends Readonly<Record<string, unknown>> {
|
||||
readonly baseURL?: string
|
||||
|
|
@ -15,7 +15,7 @@ export interface Definition<
|
|||
ProviderSettings extends Settings = Settings,
|
||||
Options extends ProviderOptions = ProviderOptions,
|
||||
> {
|
||||
readonly model: (modelID: string, settings: ProviderSettings) => Model<Options>
|
||||
readonly model: (modelID: string, settings: ProviderSettings) => LanguageModel<Options>
|
||||
}
|
||||
|
||||
export * as ProviderPackage from "./provider-package"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { Model, ModelID, ProviderID } from "./schema"
|
||||
import type { LanguageModel, ModelID, ProviderID } from "./schema"
|
||||
|
||||
export type ModelOptions = Pick<Model.Input, "defaults" | "compatibility">
|
||||
export type LanguageModelOptions = Pick<LanguageModel.Input, "defaults" | "compatibility">
|
||||
|
||||
/**
|
||||
* Advanced structural provider definition helper. Built-in providers should
|
||||
|
|
@ -8,23 +8,23 @@ export type ModelOptions = Pick<Model.Input, "defaults" | "compatibility">
|
|||
* chosen before model selection. The optional `apis` map remains for external
|
||||
* structural providers that expose multiple route selectors behind one provider.
|
||||
*/
|
||||
export type ModelFactory<Options extends ModelOptions = ModelOptions> = (
|
||||
export type LanguageModelFactory<Options extends LanguageModelOptions = LanguageModelOptions> = (
|
||||
id: string | ModelID,
|
||||
options?: Options,
|
||||
) => Model
|
||||
) => LanguageModel
|
||||
|
||||
type AnyModelFactory = (...args: never[]) => Model
|
||||
type AnyLanguageModelFactory = (...args: never[]) => LanguageModel
|
||||
|
||||
export interface Definition<Factory extends AnyModelFactory = ModelFactory> {
|
||||
export interface Definition<Factory extends AnyLanguageModelFactory = LanguageModelFactory> {
|
||||
readonly id: ProviderID
|
||||
readonly model: Factory
|
||||
readonly apis?: Record<string, AnyModelFactory>
|
||||
readonly apis?: Record<string, AnyLanguageModelFactory>
|
||||
}
|
||||
|
||||
type DefinitionShape = {
|
||||
readonly id: ProviderID
|
||||
readonly model: (...args: never[]) => Model
|
||||
readonly apis?: Record<string, (...args: never[]) => Model>
|
||||
readonly model: (...args: never[]) => LanguageModel
|
||||
readonly apis?: Record<string, (...args: never[]) => LanguageModel>
|
||||
}
|
||||
|
||||
type NoExtraFields<Input, Shape> = Input & Record<Exclude<keyof Input, keyof Shape>, never>
|
||||
|
|
|
|||
|
|
@ -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 & {
|
||||
|
|
|
|||
|
|
@ -9,14 +9,14 @@ export const id = ProviderID.make("github-copilot")
|
|||
|
||||
// GitHub Copilot has no canonical public URL — callers (opencode, etc.) must
|
||||
// supply `baseURL` explicitly.
|
||||
export type ModelOptions = Omit<RouteDefaultsInput, "providerOptions"> &
|
||||
export type LanguageModelOptions = Omit<RouteDefaultsInput, "providerOptions"> &
|
||||
ProviderAuthOption<"optional"> & {
|
||||
readonly baseURL: string
|
||||
readonly endpoint?: "chat" | "responses"
|
||||
readonly providerOptions?: OpenAIProviderOptionsInput
|
||||
}
|
||||
|
||||
export const shouldUseResponsesApi = (modelID: string | ModelID, endpoint?: ModelOptions["endpoint"]) => {
|
||||
export const shouldUseResponsesApi = (modelID: string | ModelID, endpoint?: LanguageModelOptions["endpoint"]) => {
|
||||
if (endpoint) return endpoint === "responses"
|
||||
const model = String(modelID)
|
||||
const match = /^gpt-(\d+)/.exec(model)
|
||||
|
|
@ -29,24 +29,24 @@ export const routes = [OpenAIResponses.route, OpenAIChat.route]
|
|||
const chatRoute = OpenAIChat.route.with({ provider: id })
|
||||
const responsesRoute = OpenAIResponses.route.with({ provider: id })
|
||||
|
||||
const defaults = (options: ModelOptions) => {
|
||||
const defaults = (options: LanguageModelOptions) => {
|
||||
const { apiKey: _, auth: _auth, baseURL: _baseURL, endpoint: _endpoint, ...rest } = options
|
||||
return rest
|
||||
}
|
||||
|
||||
const configuredResponsesRoute = (options: ModelOptions) =>
|
||||
const configuredResponsesRoute = (options: LanguageModelOptions) =>
|
||||
responsesRoute.with({
|
||||
endpoint: { baseURL: options.baseURL },
|
||||
auth: AuthOptions.bearer(options, []),
|
||||
})
|
||||
|
||||
const configuredChatRoute = (options: ModelOptions) =>
|
||||
const configuredChatRoute = (options: LanguageModelOptions) =>
|
||||
chatRoute.with({
|
||||
endpoint: { baseURL: options.baseURL },
|
||||
auth: AuthOptions.bearer(options, []),
|
||||
})
|
||||
|
||||
export const configure = (options: ModelOptions) => {
|
||||
export const configure = (options: LanguageModelOptions) => {
|
||||
const responsesRoute = configuredResponsesRoute(options)
|
||||
const chatRoute = configuredChatRoute(options)
|
||||
const responses = (modelID: string | ModelID) =>
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ export type OpenRouterProviderOptionsInput = ProviderOptions & {
|
|||
readonly openrouter?: OpenRouterOptions
|
||||
}
|
||||
|
||||
export type ModelOptions = Omit<RouteDefaultsInput, "providerOptions"> &
|
||||
export type LanguageModelOptions = Omit<RouteDefaultsInput, "providerOptions"> &
|
||||
ProviderAuthOption<"optional"> & {
|
||||
readonly baseURL?: string
|
||||
readonly providerOptions?: OpenRouterProviderOptionsInput
|
||||
|
|
@ -175,7 +175,7 @@ export const route = Route.make({
|
|||
|
||||
export const routes = [route]
|
||||
|
||||
const configuredRoute = (input: ModelOptions) => {
|
||||
const configuredRoute = (input: LanguageModelOptions) => {
|
||||
const { apiKey: _, auth: _auth, baseURL, ...rest } = input
|
||||
return route.with({
|
||||
...rest,
|
||||
|
|
@ -184,7 +184,7 @@ const configuredRoute = (input: ModelOptions) => {
|
|||
})
|
||||
}
|
||||
|
||||
export const configure = (input: ModelOptions = {}) => {
|
||||
export const configure = (input: LanguageModelOptions = {}) => {
|
||||
const route = configuredRoute(input)
|
||||
return {
|
||||
id,
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ export type XAIProviderOptionsInput = ProviderOptions & {
|
|||
readonly xai?: OpenAIOptionsInput
|
||||
}
|
||||
|
||||
export type ModelOptions = Omit<RouteDefaultsInput, "providerOptions"> &
|
||||
export type LanguageModelOptions = Omit<RouteDefaultsInput, "providerOptions"> &
|
||||
ProviderAuthOption<"optional"> & {
|
||||
readonly baseURL?: string
|
||||
readonly providerOptions?: XAIProviderOptionsInput
|
||||
|
|
@ -53,7 +53,7 @@ export const routes = [responsesRoute, chatRoute]
|
|||
|
||||
const auth = (options: ProviderAuthOption<"optional">) => AuthOptions.bearer(options, "XAI_API_KEY")
|
||||
|
||||
const configuredResponsesRoute = (input: ModelOptions) => {
|
||||
const configuredResponsesRoute = (input: LanguageModelOptions) => {
|
||||
const { apiKey: _, auth: _auth, baseURL, ...rest } = input
|
||||
return responsesRoute.with({
|
||||
...rest,
|
||||
|
|
@ -62,7 +62,7 @@ const configuredResponsesRoute = (input: ModelOptions) => {
|
|||
})
|
||||
}
|
||||
|
||||
const configuredChatRoute = (input: ModelOptions) => {
|
||||
const configuredChatRoute = (input: LanguageModelOptions) => {
|
||||
const { apiKey: _, auth: _auth, baseURL, ...rest } = input
|
||||
return chatRoute.with({
|
||||
...rest,
|
||||
|
|
@ -71,7 +71,7 @@ const configuredChatRoute = (input: ModelOptions) => {
|
|||
})
|
||||
}
|
||||
|
||||
export const configure = (input: ModelOptions = {}) => {
|
||||
export const configure = (input: LanguageModelOptions = {}) => {
|
||||
const responsesRoute = configuredResponsesRoute(input)
|
||||
const chatRoute = configuredChatRoute(input)
|
||||
const responses = (modelID: string | ModelID) => responsesRoute.model<XAIProviderOptionsInput>({ id: modelID })
|
||||
|
|
|
|||
|
|
@ -22,13 +22,17 @@ export type ProviderAuthOption<Mode extends ApiKeyMode> =
|
|||
| AuthOverride
|
||||
| (Mode extends "optional" ? OptionalApiKeyAuth : RequiredApiKeyAuth)
|
||||
|
||||
export type ModelOptions<Base, Mode extends ApiKeyMode> = Omit<Base, "apiKey" | "auth"> & ProviderAuthOption<Mode>
|
||||
export type LanguageModelOptions<Base, Mode extends ApiKeyMode> = Omit<Base, "apiKey" | "auth"> &
|
||||
ProviderAuthOption<Mode>
|
||||
|
||||
export type ModelArgs<Base, Mode extends ApiKeyMode> = Mode extends "optional"
|
||||
? readonly [options?: ModelOptions<Base, Mode>]
|
||||
: readonly [options: ModelOptions<Base, Mode>]
|
||||
export type LanguageModelArgs<Base, Mode extends ApiKeyMode> = Mode extends "optional"
|
||||
? readonly [options?: LanguageModelOptions<Base, Mode>]
|
||||
: readonly [options: LanguageModelOptions<Base, Mode>]
|
||||
|
||||
export type ModelFactory<Base, Mode extends ApiKeyMode, Model> = (id: string, ...args: ModelArgs<Base, Mode>) => Model
|
||||
export type LanguageModelFactory<Base, Mode extends ApiKeyMode, LanguageModel> = (
|
||||
id: string,
|
||||
...args: LanguageModelArgs<Base, Mode>
|
||||
) => LanguageModel
|
||||
|
||||
/**
|
||||
* Require at least one of the keys in `T`. Use for option shapes where any
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { Config, Effect, Redacted } from "effect"
|
||||
import { Headers } from "effect/unstable/http"
|
||||
import { AuthenticationReason, InvalidRequestReason, LLMError, type HttpOptions } from "../schema"
|
||||
import { AuthenticationReason, InvalidRequestReason, AIError, type HttpOptions } from "../schema"
|
||||
|
||||
export class MissingCredentialError extends Error {
|
||||
readonly _tag = "MissingCredentialError"
|
||||
|
|
@ -11,7 +11,7 @@ export class MissingCredentialError extends Error {
|
|||
}
|
||||
|
||||
export type CredentialError = MissingCredentialError | Config.ConfigError
|
||||
export type AuthError = CredentialError | LLMError
|
||||
export type AuthError = CredentialError | AIError
|
||||
type Secret = string | Redacted.Redacted | Config.Config<string | Redacted.Redacted>
|
||||
|
||||
export interface AuthInput {
|
||||
|
|
@ -100,7 +100,7 @@ export const headers = (input: Headers.Input) =>
|
|||
|
||||
export const remove = (name: string) => auth((input) => Effect.succeed(Headers.remove(input.headers, name)))
|
||||
|
||||
export const custom = (apply: (input: AuthInput) => Effect.Effect<Headers.Headers, LLMError>) => auth(apply)
|
||||
export const custom = (apply: (input: AuthInput) => Effect.Effect<Headers.Headers, AIError>) => auth(apply)
|
||||
|
||||
export const passthrough = none
|
||||
|
||||
|
|
@ -134,9 +134,9 @@ export function bearerHeader(name: string, source?: Secret | Credential) {
|
|||
return render(source)
|
||||
}
|
||||
|
||||
const toLLMError = (error: AuthError): LLMError => {
|
||||
const toAIError = (error: AuthError): AIError => {
|
||||
if (error instanceof MissingCredentialError || error instanceof Config.ConfigError) {
|
||||
return new LLMError({
|
||||
return new AIError({
|
||||
module: "Auth",
|
||||
method: "apply",
|
||||
reason:
|
||||
|
|
@ -150,7 +150,7 @@ const toLLMError = (error: AuthError): LLMError => {
|
|||
|
||||
export const toEffect =
|
||||
(input: Definition) =>
|
||||
(authInput: AuthInput): Effect.Effect<Headers.Headers, LLMError> =>
|
||||
input.apply(authInput).pipe(Effect.mapError(toLLMError))
|
||||
(authInput: AuthInput): Effect.Effect<Headers.Headers, AIError> =>
|
||||
input.apply(authInput).pipe(Effect.mapError(toAIError))
|
||||
|
||||
export * as Auth from "./auth"
|
||||
|
|
|
|||
|
|
@ -10,15 +10,15 @@ import { WebSocketExecutor } from "./transport"
|
|||
import type { Protocol } from "./protocol"
|
||||
import { applyCachePolicy } from "../cache-policy"
|
||||
import * as ProviderShared from "../protocols/shared"
|
||||
import type { LLMError, ProtocolID, ProviderOptions } from "../schema"
|
||||
import type { ProtocolID, ProviderOptions } from "../schema"
|
||||
import {
|
||||
AIError,
|
||||
GenerationOptions,
|
||||
HttpOptions,
|
||||
LLMRequest,
|
||||
LLMResponse,
|
||||
Model,
|
||||
ModelLimits,
|
||||
LLMError as LLMErrorClass,
|
||||
LanguageModel,
|
||||
LanguageModelLimits,
|
||||
LLMEvent,
|
||||
ProviderID,
|
||||
mergeGenerationOptions,
|
||||
|
|
@ -30,7 +30,7 @@ export interface RouteBody<Body> {
|
|||
/** Schema for the validated provider-native body sent as the JSON request. */
|
||||
readonly schema: Schema.Codec<Body, unknown>
|
||||
/** Build the provider-native body from a common `LLMRequest`. */
|
||||
readonly from: (request: LLMRequest) => Effect.Effect<Body, LLMError>
|
||||
readonly from: (request: LLMRequest) => Effect.Effect<Body, AIError>
|
||||
}
|
||||
|
||||
export interface Route<Body, Prepared = unknown> {
|
||||
|
|
@ -45,17 +45,19 @@ export interface Route<Body, Prepared = unknown> {
|
|||
readonly defaults: RouteDefaults
|
||||
readonly body: RouteBody<Body>
|
||||
readonly with: (patch: RoutePatch<Body, Prepared>) => Route<Body, Prepared>
|
||||
readonly model: <Options extends ProviderOptions = ProviderOptions>(input: RouteMappedModelInput) => Model<Options>
|
||||
readonly model: <Options extends ProviderOptions = ProviderOptions>(
|
||||
input: RouteMappedLanguageModelInput,
|
||||
) => LanguageModel<Options>
|
||||
readonly prepareTransport: (
|
||||
body: Body,
|
||||
request: LLMRequest,
|
||||
options?: StreamOptions,
|
||||
) => Effect.Effect<Prepared, LLMError>
|
||||
) => Effect.Effect<Prepared, AIError>
|
||||
readonly streamPrepared: (
|
||||
prepared: Prepared,
|
||||
request: LLMRequest,
|
||||
runtime: TransportRuntime,
|
||||
) => Stream.Stream<LLMEvent, LLMError>
|
||||
) => Stream.Stream<LLMEvent, AIError>
|
||||
}
|
||||
|
||||
// Route registries intentionally erase body generics after construction.
|
||||
|
|
@ -66,13 +68,13 @@ export type AnyRoute = Route<any, any>
|
|||
|
||||
export type HttpOptionsInput = HttpOptions.Input
|
||||
|
||||
export type RouteModelInput = Omit<Model.Input, "provider" | "route">
|
||||
export type RouteLanguageModelInput = Omit<LanguageModel.Input, "provider" | "route">
|
||||
|
||||
export type RouteRoutedModelInput = Omit<Model.Input, "route">
|
||||
export type RouteRoutedLanguageModelInput = Omit<LanguageModel.Input, "route">
|
||||
|
||||
export interface RouteDefaults {
|
||||
readonly headers?: Record<string, string>
|
||||
readonly limits?: ModelLimits
|
||||
readonly limits?: LanguageModelLimits
|
||||
readonly generation?: GenerationOptions
|
||||
readonly providerOptions?: ProviderOptions
|
||||
readonly http?: HttpOptions
|
||||
|
|
@ -80,7 +82,7 @@ export interface RouteDefaults {
|
|||
|
||||
export interface RouteDefaultsInput {
|
||||
readonly headers?: Record<string, string>
|
||||
readonly limits?: ModelLimits.Input
|
||||
readonly limits?: LanguageModelLimits.Input
|
||||
readonly generation?: GenerationOptions.Input
|
||||
readonly providerOptions?: ProviderOptions
|
||||
readonly http?: HttpOptions.Input
|
||||
|
|
@ -94,14 +96,17 @@ export interface RoutePatch<Body, Prepared> extends RouteDefaultsInput {
|
|||
readonly endpoint?: EndpointPatch<Body>
|
||||
}
|
||||
|
||||
type RouteMappedModelInput = RouteModelInput | RouteRoutedModelInput
|
||||
type RouteMappedLanguageModelInput = RouteLanguageModelInput | RouteRoutedLanguageModelInput
|
||||
|
||||
const makeRouteModel = <Options extends ProviderOptions = ProviderOptions>(route: AnyRoute, mapped: RouteMappedModelInput) => {
|
||||
const makeRouteLanguageModel = <Options extends ProviderOptions = ProviderOptions>(
|
||||
route: AnyRoute,
|
||||
mapped: RouteMappedLanguageModelInput,
|
||||
) => {
|
||||
const provider = route.provider ?? ("provider" in mapped ? mapped.provider : undefined)
|
||||
if (!provider) throw new Error(`Route.model(${route.id}) requires a provider`)
|
||||
if (!endpointBaseURL(route.endpoint))
|
||||
throw new Error(`Route.model(${route.id}) requires an endpoint baseURL — configure it on the route first`)
|
||||
return Model.make<Options>({
|
||||
return LanguageModel.make<Options>({
|
||||
...mapped,
|
||||
provider,
|
||||
route,
|
||||
|
|
@ -114,7 +119,7 @@ const mergeRouteDefaults = (base: RouteDefaults | undefined, patch: RouteDefault
|
|||
...base,
|
||||
...patch,
|
||||
headers,
|
||||
limits: patch.limits === undefined ? base?.limits : ModelLimits.make(patch.limits),
|
||||
limits: patch.limits === undefined ? base?.limits : LanguageModelLimits.make(patch.limits),
|
||||
generation: mergeGenerationOptions(generationOptions(base?.generation), generationOptions(patch.generation)),
|
||||
providerOptions: mergeProviderOptions(base?.providerOptions, patch.providerOptions),
|
||||
http: mergeHttpOptions(
|
||||
|
|
@ -154,11 +159,11 @@ export interface StreamOptions {
|
|||
}
|
||||
|
||||
export interface StreamMethod {
|
||||
(request: LLMRequest, options?: StreamOptions): Stream.Stream<LLMEvent, LLMError>
|
||||
(request: LLMRequest, options?: StreamOptions): Stream.Stream<LLMEvent, AIError>
|
||||
}
|
||||
|
||||
export interface GenerateMethod {
|
||||
(request: LLMRequest, options?: StreamOptions): Effect.Effect<LLMResponse, LLMError>
|
||||
(request: LLMRequest, options?: StreamOptions): Effect.Effect<LLMResponse, AIError>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/LLMClient") {}
|
||||
|
|
@ -222,11 +227,11 @@ export interface MakeTransportInput<Body, Prepared, Frame, Event, State> {
|
|||
|
||||
const streamError = (route: string, message: string, cause: Cause.Cause<unknown>) => {
|
||||
const failed = cause.reasons.find(Cause.isFailReason)?.error
|
||||
if (failed instanceof LLMErrorClass) return failed
|
||||
if (failed instanceof AIError) return failed
|
||||
return ProviderShared.eventError(route, message, Cause.pretty(cause))
|
||||
}
|
||||
|
||||
const requireTerminalEvent = (route: string) => (events: Stream.Stream<LLMEvent, LLMError>) =>
|
||||
const requireTerminalEvent = (route: string) => (events: Stream.Stream<LLMEvent, AIError>) =>
|
||||
Stream.suspend(() => {
|
||||
let terminal = false
|
||||
return events.pipe(
|
||||
|
|
@ -292,8 +297,8 @@ function makeFromTransport<Body, Prepared, Frame, Event, State>(
|
|||
defaults: mergeRouteDefaults(route.defaults, defaults),
|
||||
})
|
||||
},
|
||||
model: <Options extends ProviderOptions = ProviderOptions>(input: RouteMappedModelInput) =>
|
||||
makeRouteModel<Options>(route, input),
|
||||
model: <Options extends ProviderOptions = ProviderOptions>(input: RouteMappedLanguageModelInput) =>
|
||||
makeRouteLanguageModel<Options>(route, input),
|
||||
prepareTransport: (body, request, options) =>
|
||||
routeInput.transport.prepare({
|
||||
body,
|
||||
|
|
@ -417,18 +422,18 @@ const generateWith = (stream: Interface["stream"]) =>
|
|||
)
|
||||
})
|
||||
|
||||
export function stream(request: LLMRequest, options?: StreamOptions): Stream.Stream<LLMEvent, LLMError> {
|
||||
export function stream(request: LLMRequest, options?: StreamOptions): Stream.Stream<LLMEvent, AIError> {
|
||||
return Stream.unwrap(
|
||||
Effect.gen(function* () {
|
||||
return (yield* Service).stream(request, options)
|
||||
}),
|
||||
) as Stream.Stream<LLMEvent, LLMError>
|
||||
) as Stream.Stream<LLMEvent, AIError>
|
||||
}
|
||||
|
||||
export function generate(request: LLMRequest, options?: StreamOptions): Effect.Effect<LLMResponse, LLMError> {
|
||||
export function generate(request: LLMRequest, options?: StreamOptions): Effect.Effect<LLMResponse, AIError> {
|
||||
return Effect.gen(function* () {
|
||||
return yield* (yield* Service).generate(request, options)
|
||||
}) as Effect.Effect<LLMResponse, LLMError>
|
||||
}) as Effect.Effect<LLMResponse, AIError>
|
||||
}
|
||||
|
||||
export const streamRequest = (request: LLMRequest, options?: StreamOptions) =>
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import {
|
|||
HttpRateLimitDetails,
|
||||
HttpRequestDetails,
|
||||
HttpResponseDetails,
|
||||
LLMError,
|
||||
AIError,
|
||||
TransportReason,
|
||||
} from "../schema"
|
||||
import { classifyProviderFailure } from "../provider-error"
|
||||
|
|
@ -20,10 +20,10 @@ import { classifyProviderFailure } from "../provider-error"
|
|||
export interface Interface {
|
||||
readonly execute: (
|
||||
request: HttpClientRequest.HttpClientRequest,
|
||||
) => Effect.Effect<HttpClientResponse.HttpClientResponse, LLMError>
|
||||
) => Effect.Effect<HttpClientResponse.HttpClientResponse, AIError>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/LLM/RequestExecutor") {}
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/AI/RequestExecutor") {}
|
||||
|
||||
const BODY_LIMIT = 16_384
|
||||
const REDACTED = "<redacted>"
|
||||
|
|
@ -220,7 +220,7 @@ const statusError =
|
|||
const retryAfter = retryAfterMs(headers)
|
||||
const rateLimit = rateLimitDetails(headers, retryAfter)
|
||||
const details = responseBody(body, request)
|
||||
return yield* new LLMError({
|
||||
return yield* new AIError({
|
||||
module: "RequestExecutor",
|
||||
method: "execute",
|
||||
reason: classifyProviderFailure({
|
||||
|
|
@ -246,7 +246,7 @@ const toHttpError = (redactedNames: ReadonlyArray<string | RegExp>) => (error: u
|
|||
readonly kind?: string | undefined
|
||||
readonly request?: HttpClientRequest.HttpClientRequest | undefined
|
||||
}) =>
|
||||
new LLMError({
|
||||
new AIError({
|
||||
module: "RequestExecutor",
|
||||
method: "execute",
|
||||
reason: new TransportReason({
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { Stream } from "effect"
|
||||
import * as ProviderShared from "../protocols/shared"
|
||||
import type { LLMError } from "../schema"
|
||||
import type { AIError } from "../schema"
|
||||
|
||||
/**
|
||||
* Decode a streaming HTTP response body into provider-protocol frames.
|
||||
|
|
@ -18,7 +18,7 @@ import type { LLMError } from "../schema"
|
|||
*/
|
||||
export interface Definition<Frame> {
|
||||
readonly id: string
|
||||
readonly frame: (bytes: Stream.Stream<Uint8Array, LLMError>) => Stream.Stream<Frame, LLMError>
|
||||
readonly frame: (bytes: Stream.Stream<Uint8Array, AIError>) => Stream.Stream<Frame, AIError>
|
||||
}
|
||||
|
||||
/** Server-Sent Events framing. Used by every JSON-streaming HTTP provider. */
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
export { Route, LLMClient } from "./client"
|
||||
export type {
|
||||
Route as RouteShape,
|
||||
RouteModelInput,
|
||||
RouteRoutedModelInput,
|
||||
RouteLanguageModelInput,
|
||||
RouteRoutedLanguageModelInput,
|
||||
RouteDefaults,
|
||||
RouteDefaultsInput,
|
||||
AnyRoute,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Schema, type Effect } from "effect"
|
||||
import type { LLMError, LLMEvent, LLMRequest, ProtocolID } from "../schema"
|
||||
import type { AIError, LLMEvent, LLMRequest, ProtocolID } from "../schema"
|
||||
|
||||
/**
|
||||
* The semantic API contract of one model server family.
|
||||
|
|
@ -47,7 +47,7 @@ export interface ProtocolBody<Body> {
|
|||
/** Schema for the validated provider-native body sent as the JSON request. */
|
||||
readonly schema: Schema.Codec<Body, unknown>
|
||||
/** Build the provider-native body from a common `LLMRequest`. */
|
||||
readonly from: (request: LLMRequest) => Effect.Effect<Body, LLMError>
|
||||
readonly from: (request: LLMRequest) => Effect.Effect<Body, AIError>
|
||||
}
|
||||
|
||||
export interface ProtocolStream<Frame, Event, State> {
|
||||
|
|
@ -56,7 +56,7 @@ export interface ProtocolStream<Frame, Event, State> {
|
|||
/** Initial parser state. Called once per response with the resolved request. */
|
||||
readonly initial: (request: LLMRequest) => State
|
||||
/** Translate one event into emitted `LLMEvent`s plus the next state. */
|
||||
readonly step: (state: State, event: Event) => Effect.Effect<readonly [State, ReadonlyArray<LLMEvent>], LLMError>
|
||||
readonly step: (state: State, event: Event) => Effect.Effect<readonly [State, ReadonlyArray<LLMEvent>], AIError>
|
||||
/** Optional request-completion signal for transports that do not end naturally. */
|
||||
readonly terminal?: (event: Event) => boolean
|
||||
/** Optional flush emitted when the framed stream ends. */
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { Endpoint } from "../endpoint"
|
|||
import { Auth } from "../auth"
|
||||
import type { Interface as RequestExecutorInterface } from "../executor"
|
||||
import type { Interface as WebSocketExecutorInterface } from "./websocket"
|
||||
import type { LLMError, LLMRequest } from "../../schema"
|
||||
import type { AIError, LLMRequest } from "../../schema"
|
||||
|
||||
export interface TransportRuntime {
|
||||
readonly http: RequestExecutorInterface
|
||||
|
|
@ -21,12 +21,8 @@ export type HttpRequestTransform = (request: HttpRequest) => Effect.Effect<void>
|
|||
|
||||
export interface Transport<Body, Prepared, Frame> {
|
||||
readonly id: string
|
||||
readonly prepare: (input: TransportPrepareInput<Body>) => Effect.Effect<Prepared, LLMError>
|
||||
readonly frames: (
|
||||
prepared: Prepared,
|
||||
request: LLMRequest,
|
||||
runtime: TransportRuntime,
|
||||
) => Stream.Stream<Frame, LLMError>
|
||||
readonly prepare: (input: TransportPrepareInput<Body>) => Effect.Effect<Prepared, AIError>
|
||||
readonly frames: (prepared: Prepared, request: LLMRequest, runtime: TransportRuntime) => Stream.Stream<Frame, AIError>
|
||||
}
|
||||
|
||||
export interface TransportPrepareInput<Body> {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { Cause, Context, Effect, Layer, Queue, Stream } from "effect"
|
||||
import { Headers } from "effect/unstable/http"
|
||||
import { LLMError, TransportReason } from "../../schema"
|
||||
import { AIError, TransportReason } from "../../schema"
|
||||
import * as HttpTransport from "./http"
|
||||
import type { Transport } from "./index"
|
||||
|
||||
|
|
@ -10,13 +10,13 @@ export interface WebSocketRequest {
|
|||
}
|
||||
|
||||
export interface WebSocketConnection {
|
||||
readonly sendText: (message: string) => Effect.Effect<void, LLMError>
|
||||
readonly messages: Stream.Stream<string | Uint8Array, LLMError>
|
||||
readonly sendText: (message: string) => Effect.Effect<void, AIError>
|
||||
readonly messages: Stream.Stream<string | Uint8Array, AIError>
|
||||
readonly close: Effect.Effect<void, never>
|
||||
}
|
||||
|
||||
export interface Interface {
|
||||
readonly open: (input: WebSocketRequest) => Effect.Effect<WebSocketConnection, LLMError>
|
||||
readonly open: (input: WebSocketRequest) => Effect.Effect<WebSocketConnection, AIError>
|
||||
}
|
||||
|
||||
type WebSocketConstructorWithHeaders = new (
|
||||
|
|
@ -24,14 +24,14 @@ type WebSocketConstructorWithHeaders = new (
|
|||
options?: { readonly headers?: Headers.Headers },
|
||||
) => globalThis.WebSocket
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/LLM/WebSocketExecutor") {}
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/AI/WebSocketExecutor") {}
|
||||
|
||||
const transportError = (
|
||||
method: string,
|
||||
message: string,
|
||||
input: { readonly url?: string; readonly kind?: string } = {},
|
||||
) =>
|
||||
new LLMError({
|
||||
new AIError({
|
||||
module: "WebSocketExecutor",
|
||||
method,
|
||||
reason: new TransportReason({ message, url: input.url, kind: input.kind }),
|
||||
|
|
@ -59,7 +59,7 @@ const waitOpen = (ws: globalThis.WebSocket, input: WebSocketRequest) => {
|
|||
}),
|
||||
)
|
||||
}
|
||||
return Effect.callback<void, LLMError>((resume, signal) => {
|
||||
return Effect.callback<void, AIError>((resume, signal) => {
|
||||
const cleanup = () => {
|
||||
ws.removeEventListener("open", onOpen)
|
||||
ws.removeEventListener("error", onError)
|
||||
|
|
@ -138,10 +138,10 @@ export const layer: Layer.Layer<Service> = Layer.succeed(Service, Service.of({ o
|
|||
export const fromWebSocket = (
|
||||
ws: globalThis.WebSocket,
|
||||
input: WebSocketRequest,
|
||||
): Effect.Effect<WebSocketConnection, LLMError> =>
|
||||
): Effect.Effect<WebSocketConnection, AIError> =>
|
||||
Effect.gen(function* () {
|
||||
yield* waitOpen(ws, input)
|
||||
const messages = yield* Queue.bounded<string | Uint8Array, LLMError | Cause.Done<void>>(128)
|
||||
const messages = yield* Queue.bounded<string | Uint8Array, AIError | Cause.Done<void>>(128)
|
||||
|
||||
const onMessage = (event: MessageEvent) => {
|
||||
if (typeof event.data === "string") return Queue.offerUnsafe(messages, event.data)
|
||||
|
|
@ -213,7 +213,7 @@ export interface JsonPrepared {
|
|||
}
|
||||
|
||||
export interface JsonInput<Body, Message> {
|
||||
readonly toMessage: (body: Body | Record<string, unknown>) => Effect.Effect<Message, LLMError>
|
||||
readonly toMessage: (body: Body | Record<string, unknown>) => Effect.Effect<Message, AIError>
|
||||
readonly encodeMessage: (message: Message) => string
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,25 +5,25 @@ import { ModelID, ProviderID, ProviderMetadata, RouteID } from "./ids"
|
|||
export const ProviderFailureClassification = Schema.Literals(["context-overflow", "payload-too-large"])
|
||||
export type ProviderFailureClassification = typeof ProviderFailureClassification.Type
|
||||
|
||||
export class HttpRequestDetails extends Schema.Class<HttpRequestDetails>("LLM.HttpRequestDetails")({
|
||||
export class HttpRequestDetails extends Schema.Class<HttpRequestDetails>("AI.HttpRequestDetails")({
|
||||
method: Schema.String,
|
||||
url: Schema.String,
|
||||
headers: Schema.Record(Schema.String, Schema.String),
|
||||
}) {}
|
||||
|
||||
export class HttpResponseDetails extends Schema.Class<HttpResponseDetails>("LLM.HttpResponseDetails")({
|
||||
export class HttpResponseDetails extends Schema.Class<HttpResponseDetails>("AI.HttpResponseDetails")({
|
||||
status: Schema.Number,
|
||||
headers: Schema.Record(Schema.String, Schema.String),
|
||||
}) {}
|
||||
|
||||
export class HttpRateLimitDetails extends Schema.Class<HttpRateLimitDetails>("LLM.HttpRateLimitDetails")({
|
||||
export class HttpRateLimitDetails extends Schema.Class<HttpRateLimitDetails>("AI.HttpRateLimitDetails")({
|
||||
retryAfterMs: Schema.optional(Schema.Number),
|
||||
limit: Schema.optional(Schema.Record(Schema.String, Schema.String)),
|
||||
remaining: Schema.optional(Schema.Record(Schema.String, Schema.String)),
|
||||
reset: Schema.optional(Schema.Record(Schema.String, Schema.String)),
|
||||
}) {}
|
||||
|
||||
export class HttpContext extends Schema.Class<HttpContext>("LLM.HttpContext")({
|
||||
export class HttpContext extends Schema.Class<HttpContext>("AI.HttpContext")({
|
||||
request: HttpRequestDetails,
|
||||
response: Schema.optional(HttpResponseDetails),
|
||||
body: Schema.optional(Schema.String),
|
||||
|
|
@ -32,7 +32,7 @@ export class HttpContext extends Schema.Class<HttpContext>("LLM.HttpContext")({
|
|||
rateLimit: Schema.optional(HttpRateLimitDetails),
|
||||
}) {}
|
||||
|
||||
export class InvalidRequestReason extends Schema.Class<InvalidRequestReason>("LLM.Error.InvalidRequest")({
|
||||
export class InvalidRequestReason extends Schema.Class<InvalidRequestReason>("AI.Error.InvalidRequest")({
|
||||
_tag: Schema.tag("InvalidRequest"),
|
||||
message: Schema.String,
|
||||
parameter: Schema.optional(Schema.String),
|
||||
|
|
@ -41,18 +41,18 @@ export class InvalidRequestReason extends Schema.Class<InvalidRequestReason>("LL
|
|||
http: Schema.optional(HttpContext),
|
||||
}) {}
|
||||
|
||||
export class NoRouteReason extends Schema.Class<NoRouteReason>("LLM.Error.NoRoute")({
|
||||
export class NoRouteReason extends Schema.Class<NoRouteReason>("AI.Error.NoRoute")({
|
||||
_tag: Schema.tag("NoRoute"),
|
||||
route: RouteID,
|
||||
provider: ProviderID,
|
||||
model: ModelID,
|
||||
}) {
|
||||
get message() {
|
||||
return `No LLM route for ${this.provider}/${this.model} using ${this.route}`
|
||||
return `No AI route for ${this.provider}/${this.model} using ${this.route}`
|
||||
}
|
||||
}
|
||||
|
||||
export class AuthenticationReason extends Schema.Class<AuthenticationReason>("LLM.Error.Authentication")({
|
||||
export class AuthenticationReason extends Schema.Class<AuthenticationReason>("AI.Error.Authentication")({
|
||||
_tag: Schema.tag("Authentication"),
|
||||
message: Schema.String,
|
||||
kind: Schema.Literals(["missing", "invalid", "expired", "insufficient-permissions", "unknown"]),
|
||||
|
|
@ -60,7 +60,7 @@ export class AuthenticationReason extends Schema.Class<AuthenticationReason>("LL
|
|||
http: Schema.optional(HttpContext),
|
||||
}) {}
|
||||
|
||||
export class RateLimitReason extends Schema.Class<RateLimitReason>("LLM.Error.RateLimit")({
|
||||
export class RateLimitReason extends Schema.Class<RateLimitReason>("AI.Error.RateLimit")({
|
||||
_tag: Schema.tag("RateLimit"),
|
||||
message: Schema.String,
|
||||
retryAfterMs: Schema.optional(Schema.Number),
|
||||
|
|
@ -69,21 +69,21 @@ export class RateLimitReason extends Schema.Class<RateLimitReason>("LLM.Error.Ra
|
|||
http: Schema.optional(HttpContext),
|
||||
}) {}
|
||||
|
||||
export class QuotaExceededReason extends Schema.Class<QuotaExceededReason>("LLM.Error.QuotaExceeded")({
|
||||
export class QuotaExceededReason extends Schema.Class<QuotaExceededReason>("AI.Error.QuotaExceeded")({
|
||||
_tag: Schema.tag("QuotaExceeded"),
|
||||
message: Schema.String,
|
||||
providerMetadata: Schema.optional(ProviderMetadata),
|
||||
http: Schema.optional(HttpContext),
|
||||
}) {}
|
||||
|
||||
export class ContentPolicyReason extends Schema.Class<ContentPolicyReason>("LLM.Error.ContentPolicy")({
|
||||
export class ContentPolicyReason extends Schema.Class<ContentPolicyReason>("AI.Error.ContentPolicy")({
|
||||
_tag: Schema.tag("ContentPolicy"),
|
||||
message: Schema.String,
|
||||
providerMetadata: Schema.optional(ProviderMetadata),
|
||||
http: Schema.optional(HttpContext),
|
||||
}) {}
|
||||
|
||||
export class ProviderInternalReason extends Schema.Class<ProviderInternalReason>("LLM.Error.ProviderInternal")({
|
||||
export class ProviderInternalReason extends Schema.Class<ProviderInternalReason>("AI.Error.ProviderInternal")({
|
||||
_tag: Schema.tag("ProviderInternal"),
|
||||
message: Schema.String,
|
||||
status: Schema.optional(Schema.Number),
|
||||
|
|
@ -92,7 +92,7 @@ export class ProviderInternalReason extends Schema.Class<ProviderInternalReason>
|
|||
http: Schema.optional(HttpContext),
|
||||
}) {}
|
||||
|
||||
export class TransportReason extends Schema.Class<TransportReason>("LLM.Error.Transport")({
|
||||
export class TransportReason extends Schema.Class<TransportReason>("AI.Error.Transport")({
|
||||
_tag: Schema.tag("Transport"),
|
||||
message: Schema.String,
|
||||
kind: Schema.optional(Schema.String),
|
||||
|
|
@ -101,7 +101,7 @@ export class TransportReason extends Schema.Class<TransportReason>("LLM.Error.Tr
|
|||
}) {}
|
||||
|
||||
export class InvalidProviderOutputReason extends Schema.Class<InvalidProviderOutputReason>(
|
||||
"LLM.Error.InvalidProviderOutput",
|
||||
"AI.Error.InvalidProviderOutput",
|
||||
)({
|
||||
_tag: Schema.tag("InvalidProviderOutput"),
|
||||
message: Schema.String,
|
||||
|
|
@ -110,7 +110,7 @@ export class InvalidProviderOutputReason extends Schema.Class<InvalidProviderOut
|
|||
providerMetadata: Schema.optional(ProviderMetadata),
|
||||
}) {}
|
||||
|
||||
export class UnknownProviderReason extends Schema.Class<UnknownProviderReason>("LLM.Error.UnknownProvider")({
|
||||
export class UnknownProviderReason extends Schema.Class<UnknownProviderReason>("AI.Error.UnknownProvider")({
|
||||
_tag: Schema.tag("UnknownProvider"),
|
||||
message: Schema.String,
|
||||
status: Schema.optional(Schema.Number),
|
||||
|
|
@ -118,7 +118,7 @@ export class UnknownProviderReason extends Schema.Class<UnknownProviderReason>("
|
|||
http: Schema.optional(HttpContext),
|
||||
}) {}
|
||||
|
||||
export const LLMErrorReason = Schema.Union([
|
||||
export const AIErrorReason = Schema.Union([
|
||||
InvalidRequestReason,
|
||||
NoRouteReason,
|
||||
AuthenticationReason,
|
||||
|
|
@ -130,12 +130,12 @@ export const LLMErrorReason = Schema.Union([
|
|||
InvalidProviderOutputReason,
|
||||
UnknownProviderReason,
|
||||
]).pipe(Schema.toTaggedUnion("_tag"))
|
||||
export type LLMErrorReason = Schema.Schema.Type<typeof LLMErrorReason>
|
||||
export type AIErrorReason = Schema.Schema.Type<typeof AIErrorReason>
|
||||
|
||||
export class LLMError extends Schema.TaggedErrorClass<LLMError>()("LLM.Error", {
|
||||
export class AIError extends Schema.TaggedErrorClass<AIError>()("AI.Error", {
|
||||
module: Schema.String,
|
||||
method: Schema.String,
|
||||
reason: LLMErrorReason,
|
||||
reason: AIErrorReason,
|
||||
}) {
|
||||
override readonly cause = this.reason
|
||||
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ import { ProviderFailureClassification } from "./errors"
|
|||
* — for fields we don't normalize and for billing-level audit trails.
|
||||
* Matches the same escape-hatch field on `LLMEvent`.
|
||||
*/
|
||||
export class Usage extends Schema.Class<Usage>("LLM.Usage")({
|
||||
export class Usage extends Schema.Class<Usage>("AI.Usage")({
|
||||
inputTokens: Schema.optional(Schema.Number),
|
||||
outputTokens: Schema.optional(Schema.Number),
|
||||
nonCachedInputTokens: Schema.optional(Schema.Number),
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { Schema } from "effect"
|
||||
import { LLM, ProviderMetadata } from "@opencode-ai/schema/llm"
|
||||
import { ProviderMetadata } from "@opencode-ai/schema/ai"
|
||||
import { LLM } from "@opencode-ai/schema/llm"
|
||||
|
||||
export { ProviderMetadata }
|
||||
|
||||
|
|
@ -11,10 +12,10 @@ export type ProtocolID = Schema.Schema.Type<typeof ProtocolID>
|
|||
export const RouteID = Schema.String
|
||||
export type RouteID = Schema.Schema.Type<typeof RouteID>
|
||||
|
||||
export const ModelID = Schema.String.pipe(Schema.brand("LLM.ModelID"))
|
||||
export const ModelID = Schema.String.pipe(Schema.brand("AI.ModelID"))
|
||||
export type ModelID = typeof ModelID.Type
|
||||
|
||||
export const ProviderID = Schema.String.pipe(Schema.brand("LLM.ProviderID"))
|
||||
export const ProviderID = Schema.String.pipe(Schema.brand("AI.ProviderID"))
|
||||
export type ProviderID = typeof ProviderID.Type
|
||||
|
||||
export const ResponseID = Schema.String
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { Schema } from "effect"
|
||||
import { Tool } from "@opencode-ai/schema/tool"
|
||||
import { JsonSchema, MessageRole, ProviderMetadata } from "./ids"
|
||||
import { CacheHint, CachePolicy, GenerationOptions, HttpOptions, ModelSchema, ProviderOptions } from "./options"
|
||||
import { CacheHint, CachePolicy, GenerationOptions, HttpOptions, LanguageModelSchema, ProviderOptions } from "./options"
|
||||
import { isRecord } from "../utils/record"
|
||||
|
||||
const systemPartSchema = Schema.Struct({
|
||||
|
|
@ -263,7 +263,7 @@ export namespace ToolChoice {
|
|||
|
||||
export class LLMRequest extends Schema.Class<LLMRequest>("LLM.Request")({
|
||||
id: Schema.optional(Schema.String),
|
||||
model: ModelSchema,
|
||||
model: LanguageModelSchema,
|
||||
system: Schema.Array(SystemPart),
|
||||
messages: Schema.Array(Message),
|
||||
tools: Schema.Array(ToolDefinition),
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ export const mergeProviderOptions = (
|
|||
return Object.keys(result).length === 0 ? undefined : result
|
||||
}
|
||||
|
||||
export class HttpOptions extends Schema.Class<HttpOptions>("LLM.HttpOptions")({
|
||||
export class HttpOptions extends Schema.Class<HttpOptions>("AI.HttpOptions")({
|
||||
body: Schema.optional(JsonSchema),
|
||||
headers: Schema.optional(Schema.Record(Schema.String, Schema.String)),
|
||||
query: Schema.optional(Schema.Record(Schema.String, Schema.String)),
|
||||
|
|
@ -121,32 +121,32 @@ export const mergeGenerationOptions = (...items: ReadonlyArray<GenerationOptions
|
|||
return Object.values(result).some((value) => value !== undefined) ? result : undefined
|
||||
}
|
||||
|
||||
export class ModelLimits extends Schema.Class<ModelLimits>("LLM.ModelLimits")({
|
||||
export class LanguageModelLimits extends Schema.Class<LanguageModelLimits>("LLM.LanguageModelLimits")({
|
||||
context: Schema.optional(Schema.Number),
|
||||
input: Schema.optional(Schema.Number),
|
||||
output: Schema.optional(Schema.Number),
|
||||
}) {}
|
||||
|
||||
export namespace ModelLimits {
|
||||
export type Input = ModelLimits | ConstructorParameters<typeof ModelLimits>[0]
|
||||
export namespace LanguageModelLimits {
|
||||
export type Input = LanguageModelLimits | ConstructorParameters<typeof LanguageModelLimits>[0]
|
||||
|
||||
/** Normalize model limit input into the canonical `ModelLimits` class. */
|
||||
/** Normalize model limit input into the canonical `LanguageModelLimits` class. */
|
||||
export const make = (input: Input | undefined) =>
|
||||
input instanceof ModelLimits ? input : new ModelLimits(input ?? {})
|
||||
input instanceof LanguageModelLimits ? input : new LanguageModelLimits(input ?? {})
|
||||
}
|
||||
|
||||
export class ModelDefaults extends Schema.Class<ModelDefaults>("LLM.ModelDefaults")({
|
||||
limits: Schema.optional(ModelLimits),
|
||||
export class LanguageModelDefaults extends Schema.Class<LanguageModelDefaults>("LLM.LanguageModelDefaults")({
|
||||
limits: Schema.optional(LanguageModelLimits),
|
||||
generation: Schema.optional(GenerationOptions),
|
||||
providerOptions: Schema.optional(ProviderOptions),
|
||||
http: Schema.optional(HttpOptions),
|
||||
}) {}
|
||||
|
||||
export namespace ModelDefaults {
|
||||
export namespace LanguageModelDefaults {
|
||||
export type Input =
|
||||
| ModelDefaults
|
||||
| LanguageModelDefaults
|
||||
| {
|
||||
readonly limits?: ModelLimits.Input
|
||||
readonly limits?: LanguageModelLimits.Input
|
||||
readonly generation?: GenerationOptions.Input
|
||||
readonly providerOptions?: ProviderOptions
|
||||
readonly http?: HttpOptions.Input
|
||||
|
|
@ -154,9 +154,9 @@ export namespace ModelDefaults {
|
|||
|
||||
/** Normalize selected-model request defaults without applying precedence. */
|
||||
export const make = (input: Input) => {
|
||||
if (input instanceof ModelDefaults) return input
|
||||
return new ModelDefaults({
|
||||
limits: input.limits === undefined ? undefined : ModelLimits.make(input.limits),
|
||||
if (input instanceof LanguageModelDefaults) return input
|
||||
return new LanguageModelDefaults({
|
||||
limits: input.limits === undefined ? undefined : LanguageModelLimits.make(input.limits),
|
||||
generation: input.generation === undefined ? undefined : GenerationOptions.make(input.generation),
|
||||
providerOptions: input.providerOptions,
|
||||
http: input.http === undefined ? undefined : HttpOptions.make(input.http),
|
||||
|
|
@ -164,34 +164,39 @@ export namespace ModelDefaults {
|
|||
}
|
||||
}
|
||||
|
||||
export const ModelToolSchemaCompatibility = Schema.Literals(["gemini", "moonshot"])
|
||||
export type ModelToolSchemaCompatibility = Schema.Schema.Type<typeof ModelToolSchemaCompatibility>
|
||||
export const LanguageModelToolSchemaCompatibility = Schema.Literals(["gemini", "moonshot"])
|
||||
export type LanguageModelToolSchemaCompatibility = Schema.Schema.Type<typeof LanguageModelToolSchemaCompatibility>
|
||||
|
||||
export const ModelMaxTokensFieldCompatibility = Schema.Literals(["max_completion_tokens", "max_tokens"])
|
||||
export type ModelMaxTokensFieldCompatibility = Schema.Schema.Type<typeof ModelMaxTokensFieldCompatibility>
|
||||
export const LanguageModelMaxTokensFieldCompatibility = Schema.Literals(["max_completion_tokens", "max_tokens"])
|
||||
export type LanguageModelMaxTokensFieldCompatibility = Schema.Schema.Type<
|
||||
typeof LanguageModelMaxTokensFieldCompatibility
|
||||
>
|
||||
|
||||
export class ModelCompatibility extends Schema.Class<ModelCompatibility>("LLM.ModelCompatibility")({
|
||||
toolSchema: Schema.optional(ModelToolSchemaCompatibility),
|
||||
export class LanguageModelCompatibility extends Schema.Class<LanguageModelCompatibility>(
|
||||
"LLM.LanguageModelCompatibility",
|
||||
)({
|
||||
toolSchema: Schema.optional(LanguageModelToolSchemaCompatibility),
|
||||
reasoningField: Schema.optional(Schema.String),
|
||||
maxTokensField: Schema.optional(ModelMaxTokensFieldCompatibility),
|
||||
maxTokensField: Schema.optional(LanguageModelMaxTokensFieldCompatibility),
|
||||
}) {}
|
||||
|
||||
export namespace ModelCompatibility {
|
||||
export type Input = ModelCompatibility | ConstructorParameters<typeof ModelCompatibility>[0]
|
||||
export namespace LanguageModelCompatibility {
|
||||
export type Input = LanguageModelCompatibility | ConstructorParameters<typeof LanguageModelCompatibility>[0]
|
||||
|
||||
/** Normalize model/upstream compatibility metadata without projecting requests. */
|
||||
export const make = (input: Input) => (input instanceof ModelCompatibility ? input : new ModelCompatibility(input))
|
||||
export const make = (input: Input) =>
|
||||
input instanceof LanguageModelCompatibility ? input : new LanguageModelCompatibility(input)
|
||||
}
|
||||
|
||||
export class Model<Options extends ProviderOptions = ProviderOptions> {
|
||||
export class LanguageModel<Options extends ProviderOptions = ProviderOptions> {
|
||||
declare protected readonly _ProviderOptions: Options
|
||||
readonly id: ModelID
|
||||
readonly provider: ProviderID
|
||||
readonly route: AnyRoute
|
||||
readonly defaults?: ModelDefaults
|
||||
readonly compatibility?: ModelCompatibility
|
||||
readonly defaults?: LanguageModelDefaults
|
||||
readonly compatibility?: LanguageModelCompatibility
|
||||
|
||||
constructor(input: Model.ConstructorInput) {
|
||||
constructor(input: LanguageModel.ConstructorInput) {
|
||||
this.id = input.id
|
||||
this.provider = input.provider
|
||||
this.route = input.route
|
||||
|
|
@ -199,17 +204,18 @@ export class Model<Options extends ProviderOptions = ProviderOptions> {
|
|||
this.compatibility = input.compatibility
|
||||
}
|
||||
|
||||
static make<Options extends ProviderOptions = ProviderOptions>(input: Model.Input) {
|
||||
return new Model<Options>({
|
||||
static make<Options extends ProviderOptions = ProviderOptions>(input: LanguageModel.Input) {
|
||||
return new LanguageModel<Options>({
|
||||
id: ModelID.make(input.id),
|
||||
provider: ProviderID.make(input.provider),
|
||||
route: input.route,
|
||||
defaults: input.defaults === undefined ? undefined : ModelDefaults.make(input.defaults),
|
||||
compatibility: input.compatibility === undefined ? undefined : ModelCompatibility.make(input.compatibility),
|
||||
defaults: input.defaults === undefined ? undefined : LanguageModelDefaults.make(input.defaults),
|
||||
compatibility:
|
||||
input.compatibility === undefined ? undefined : LanguageModelCompatibility.make(input.compatibility),
|
||||
})
|
||||
}
|
||||
|
||||
static input<Options extends ProviderOptions>(model: Model<Options>): Model.ConstructorInput {
|
||||
static input<Options extends ProviderOptions>(model: LanguageModel<Options>): LanguageModel.ConstructorInput {
|
||||
return {
|
||||
id: model.id,
|
||||
provider: model.provider,
|
||||
|
|
@ -219,37 +225,40 @@ export class Model<Options extends ProviderOptions = ProviderOptions> {
|
|||
}
|
||||
}
|
||||
|
||||
static update<Options extends ProviderOptions>(model: Model<Options>, patch: Partial<Model.Input>) {
|
||||
static update<Options extends ProviderOptions>(model: LanguageModel<Options>, patch: Partial<LanguageModel.Input>) {
|
||||
if (Object.keys(patch).length === 0) return model
|
||||
return Model.make<Options>({
|
||||
...Model.input(model),
|
||||
return LanguageModel.make<Options>({
|
||||
...LanguageModel.input(model),
|
||||
...patch,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export namespace Model {
|
||||
export namespace LanguageModel {
|
||||
export type ConstructorInput = {
|
||||
readonly id: ModelID
|
||||
readonly provider: ProviderID
|
||||
readonly route: AnyRoute
|
||||
readonly defaults?: ModelDefaults
|
||||
readonly compatibility?: ModelCompatibility
|
||||
readonly defaults?: LanguageModelDefaults
|
||||
readonly compatibility?: LanguageModelCompatibility
|
||||
}
|
||||
|
||||
export type Input = Omit<ConstructorInput, "id" | "provider" | "defaults" | "compatibility"> & {
|
||||
readonly id: string | ModelID
|
||||
readonly provider: string | ProviderID
|
||||
readonly defaults?: ModelDefaults.Input
|
||||
readonly compatibility?: ModelCompatibility.Input
|
||||
readonly defaults?: LanguageModelDefaults.Input
|
||||
readonly compatibility?: LanguageModelCompatibility.Input
|
||||
}
|
||||
}
|
||||
|
||||
export type ModelInput = Model.Input
|
||||
export type LanguageModelInput = LanguageModel.Input
|
||||
|
||||
export type ModelProviderOptions<SelectedModel> = SelectedModel extends Model<infer Options> ? Options : never
|
||||
export type LanguageModelProviderOptions<SelectedModel> =
|
||||
SelectedModel extends LanguageModel<infer Options> ? Options : never
|
||||
|
||||
export const ModelSchema = Schema.declare((value): value is Model => value instanceof Model, { expected: "LLM.Model" })
|
||||
export const LanguageModelSchema = Schema.declare((value): value is LanguageModel => value instanceof LanguageModel, {
|
||||
expected: "LLM.LanguageModel",
|
||||
})
|
||||
|
||||
export class CacheHint extends Schema.Class<CacheHint>("LLM.CacheHint")({
|
||||
type: Schema.Literals(["ephemeral", "persistent"]),
|
||||
|
|
|
|||
|
|
@ -5,13 +5,13 @@ import {
|
|||
LLMEvent,
|
||||
LLMResponse,
|
||||
type FinishReasonDetails,
|
||||
type LLMError,
|
||||
type AIError,
|
||||
type LLMRequest,
|
||||
type UsageInput,
|
||||
} from "./schema"
|
||||
import { Context, Deferred, Effect, Latch, Layer, Queue, Scope, Stream } from "effect"
|
||||
|
||||
export type Response = readonly LLMEvent[] | Stream.Stream<LLMEvent, LLMError>
|
||||
export type Response = readonly LLMEvent[] | Stream.Stream<LLMEvent, AIError>
|
||||
|
||||
export type Gate = Readonly<{ started: Effect.Effect<void>; release: Effect.Effect<void> }>
|
||||
|
||||
|
|
@ -63,7 +63,7 @@ export const textWithUsage = (value: string, id: string, inputTokens: number) =>
|
|||
|
||||
export const tool = (id: string, name: string, input: unknown) => toolCalls(LLMEvent.toolCall({ id, name, input }))
|
||||
|
||||
export const failAfter = (error: LLMError, ...events: readonly LLMEvent[]) =>
|
||||
export const failAfter = (error: AIError, ...events: readonly LLMEvent[]) =>
|
||||
Stream.fromIterable(events).pipe(Stream.concat(Stream.fail(error)))
|
||||
|
||||
export const hangAfter = (...events: readonly LLMEvent[]) => Stream.concat(Stream.fromIterable(events), Stream.never)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue