Something went wrong. Try again.
opencode v2 mirror
Something went wrong. Try again.
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463export * as ModelResolver from "./model-resolver.js"
import { makeLocationNode } from "@opencode/util/effect/app-node"import { LanguageModel, ProviderConfigurationError } from "@opencode/ai"import { Auth } from "@opencode/ai/route"import { Context, Effect, Layer, Schema, Struct } from "effect"import { AISDK } from "./aisdk.js"import { Credential } from "./credential.js"import { Integration } from "./integration.js"import { Capabilities, ID, Info, Model, Ref, VariantID } from "./model.js"import type { RuntimeInfo } from "./model.js"import { Npm } from "@opencode/util/npm"import { Provider } from "./provider.js"
export class VariantUnavailableError extends Schema.TaggedError<VariantUnavailableError>()( "SessionRunnerModel.VariantUnavailableError", { providerID: Provider.ID, modelID: ID, variant: VariantID, },) { override get message() { return `Variant unavailable for ${this.providerID}/${this.modelID}: ${this.variant}` }}
export class UnsupportedPackageError extends Schema.TaggedError<UnsupportedPackageError>()( "SessionRunnerModel.UnsupportedPackageError", { providerID: Provider.ID, modelID: ID, package: Schema.String, },) { override get message() { return `Unsupported package for ${this.providerID}/${this.modelID}: ${this.package}` }}
export const InitializationPhase = Schema.Literals(["load", "init", "construct"])export type InitializationPhase = typeof InitializationPhase.Type
/** Provider settings are missing, conflicting, or unsupported; the provider's own message tells the user what to fix. */export class ModelConfigurationError extends Schema.TaggedError<ModelConfigurationError>()( "SessionRunnerModel.ModelConfigurationError", { providerID: Provider.ID, modelID: ID, package: Schema.String, detail: Schema.String, },) { override get message() { return `Cannot initialize ${this.providerID}/${this.modelID}: ${this.detail}` }}
/** A supported package failed unexpectedly while loading or constructing the model. */export class ModelInitializationError extends Schema.TaggedError<ModelInitializationError>()( "SessionRunnerModel.ModelInitializationError", { providerID: Provider.ID, modelID: ID, package: Schema.String, phase: InitializationPhase, detail: Schema.String, },) { override get message() { return `Cannot initialize ${this.providerID}/${this.modelID}: ${this.detail}` }}
export class UnresolvedProviderVariablesError extends Schema.TaggedError<UnresolvedProviderVariablesError>()( "SessionRunnerModel.UnresolvedProviderVariablesError", { providerID: Provider.ID, modelID: ID, variables: Schema.Array(Schema.String), },) { override get message() { return `Cannot initialize ${this.providerID}/${this.modelID}: ${this.variables.join(", ")} ${this.variables.length === 1 ? "is" : "are"} required to resolve the provider endpoint` }}
export class UnsupportedCompactionError extends Schema.TaggedError<UnsupportedCompactionError>()( "SessionRunnerModel.UnsupportedCompactionError", { providerID: Provider.ID, modelID: ID, route: Schema.String, },) { override get message() { return `Provider compaction is not supported by ${this.providerID}/${this.modelID} (${this.route})` }}
export type Error = | VariantUnavailableError | UnsupportedPackageError | ModelConfigurationError | ModelInitializationError | UnresolvedProviderVariablesError | UnsupportedCompactionError | Integration.AuthorizationError
export interface Resolved { /** Route-level model for provider requests; its id is the provider API model id, which may differ from the catalog id. */ readonly model: LanguageModel /** Selected catalog identity. Durable records and displays must use this, never the API model id. */ readonly ref: Ref /** Catalog capabilities used to shape requests before provider lowering. */ readonly capabilities: Capabilities /** Catalog pricing in dollars per million tokens. */ readonly cost: Info["cost"] /** Catalog token limits used by Core for context management. */ readonly limit: Info["limit"] /** Model policy overrides the provider policy; omitted means summary compaction. */ readonly compaction?: Provider.Compaction /** Provider transport policy; omitted means HTTP. */ readonly transport?: Provider.Transport}
export interface Interface { readonly resolve: (requested?: Ref) => Effect.Effect<Resolved | undefined, Error> readonly resolveModel: (model: Info, variant?: VariantID) => Effect.Effect<Resolved, Error>}
export class Service extends Context.Service<Service, Interface>()("@opencode/ModelResolver") {}
// Variant resolution adds request-local overlays without changing the committed model snapshot.export const withVariant = ( model: Info, variantID: VariantID | undefined,): Effect.Effect<Info, VariantUnavailableError> => { const id = variantID === "default" ? undefined : variantID const variant = model.variants?.find((item) => item.id === id) if (!variant && id !== undefined) return Effect.fail( new VariantUnavailableError({ providerID: model.providerID, modelID: model.id, variant: id, }), ) return Effect.succeed( variant ? { ...model, settings: Provider.mergeOverlay(model.settings, Provider.modelSettings(variant.settings)), headers: Provider.mergeHeaders(model.headers, variant.headers), body: Provider.mergeOverlay(model.body, variant.body), } : model, )}
export interface Dependencies { readonly loadPackage?: (specifier: string) => Effect.Effect<Provider.ProviderPackage, Provider.LoadError> readonly loadAISDK?: (model: RuntimeInfo) => Effect.Effect<LanguageModel, AISDK.InitError>}
export const fromCatalogModel = ( model: RuntimeInfo, credential?: Credential.Value, dependencies?: Dependencies,): Effect.Effect< LanguageModel, | UnsupportedPackageError | ModelConfigurationError | ModelInitializationError | UnresolvedProviderVariablesError | UnsupportedCompactionError> => resolveCatalogModel(model, credential, dependencies).pipe( Effect.flatMap((resolved) => validateProviderVariables(model, resolved)), Effect.flatMap((resolved) => { // Reject provider compaction policies up front so the misconfiguration surfaces before any step runs. if ( model.settings?.compaction?.type !== "native" || resolved.route.compact?.trigger || resolved.route.compact?.endpoint ) return Effect.succeed(resolved) return Effect.fail( new UnsupportedCompactionError({ providerID: model.providerID, modelID: model.id, route: resolved.route.id }), ) }), )
const resolveCatalogModel = Effect.fn("ModelResolver.resolveCatalogModel")(function* ( model: RuntimeInfo, credential?: Credential.Value, dependencies?: Dependencies,) { const resolved = prepareRuntimeModel(model, credential) const configuration = credential?.type === "key" ? credential.configuration : undefined const configured = { ...resolved.settings, ...credential?.metadata, ...configuration } if (Provider.isAISDK(resolved.package)) { const loadAISDK = dependencies?.loadAISDK if (!loadAISDK) return yield* unsupported(resolved) const settings = yield* prepareProviderSettings( resolved, Provider.mergeOverlay(resolved.settings, { ...nativeCredentialSettings(resolved.package ?? "", credential), ...credential?.metadata, ...configuration, }) ?? {}, ) return yield* loadAISDK({ ...resolved, settings }).pipe( Effect.mapError((error) => initialization(resolved, "init", error.cause)), ) } const specifier = Provider.packageName(resolved.package) if (!specifier) return yield* unsupported(resolved) const mapped = yield* prepareProviderSettings(resolved, Provider.nativeSettings(configured)) const module = yield* (dependencies?.loadPackage ?? Provider.loadPackage)(specifier).pipe( Effect.mapError((error) => initialization(resolved, "load", error.cause)), ) const settings = { ...(credential ? Struct.omit(mapped, ["accessToken", "apiKey", "authToken"]) : mapped), ...(resolved.canonical === undefined ? {} : { provider: resolved.canonical }), ...nativeCredentialSettings(specifier, credential), headers: resolved.headers, body: resolved.body, } return yield* Effect.try({ try: () => { const runtime = module.model(resolved.modelID ?? resolved.id, settings) return LanguageModel.update(runtime, { provider: resolved.canonical ?? resolved.providerID, compatibility: resolved.compatibility ? Object.assign({}, runtime.compatibility, resolved.compatibility) : runtime.compatibility, }) }, catch: (cause) => cause instanceof ProviderConfigurationError ? new ModelConfigurationError({ providerID: resolved.providerID, modelID: resolved.id, package: resolved.package ?? "unknown", detail: cause.message, }) : initialization(resolved, "construct", cause), })})
function prepareRuntimeModel(model: RuntimeInfo, credential: Credential.Value | undefined) { if (model.settings?.apiKey !== "" && (credential?.type !== "key" || credential.metadata === undefined)) return model return { ...model, ...(model.settings?.apiKey === "" ? { settings: Struct.omit(model.settings, ["apiKey"]) } : {}), ...(credential?.type === "key" && credential.metadata !== undefined ? { body: Provider.mergeOverlay(model.body, credential.metadata) } : {}), }}
function validateProviderVariables( model: RuntimeInfo, resolved: LanguageModel,): Effect.Effect<LanguageModel, UnresolvedProviderVariablesError> { const baseURL = resolved.route.endpoint.baseURL if (typeof baseURL !== "string") return Effect.succeed(resolved) const failure = unresolvedProviderVariables(model, baseURL) return failure ? Effect.fail(failure) : Effect.succeed(resolved)}
function prepareProviderSettings( model: RuntimeInfo, settings: Readonly<Record<string, unknown>>,): Effect.Effect<Readonly<Record<string, unknown>>, UnresolvedProviderVariablesError> { const baseURL = settings.baseURL if (typeof baseURL !== "string") return Effect.succeed(settings) return prepareProviderURL(model, baseURL).pipe( Effect.map((prepared) => (prepared === baseURL ? settings : { ...settings, baseURL: prepared })), )}
function prepareProviderURL(model: RuntimeInfo, baseURL: string): Effect.Effect<string, UnresolvedProviderVariablesError> { if (!baseURL.includes("${")) return Effect.succeed(baseURL) const prepared = baseURL.replace(/\$\{([^}]+)\}/g, (placeholder, name: string) => process.env[name] ?? placeholder) const failure = unresolvedProviderVariables(model, prepared) return failure ? Effect.fail(failure) : Effect.succeed(prepared)}
function unresolvedProviderVariables(model: RuntimeInfo, baseURL: string) { const variables = new Set(Array.from(baseURL.matchAll(/\$\{([^}]+)\}/g), (match) => match[1])) if (variables.size === 0) return return new UnresolvedProviderVariablesError({ providerID: model.providerID, modelID: model.id, variables: Array.from(variables), })}
const nativeCredentialSettings = (specifier: string, credential: Credential.Value | undefined) => { if (!credential) return {} if (credential.type === "key") return { apiKey: credential.key } if (specifier === "@opencode/ai/providers/anthropic" || specifier === "@opencode/ai/providers/anthropic-compatible") return { authToken: credential.access } if ( specifier === "@opencode/ai/providers/google-vertex" || specifier.startsWith("@opencode/ai/providers/google-vertex/") ) return { accessToken: credential.access } return { apiKey: credential.access }}
const unsupported = (model: RuntimeInfo) => new UnsupportedPackageError({ providerID: model.providerID, modelID: model.id, package: model.package ?? "unknown", })
const initialization = (model: RuntimeInfo, phase: InitializationPhase, cause: unknown) => new ModelInitializationError({ providerID: model.providerID, modelID: model.id, package: model.package ?? "unknown", phase, detail: causeMessage(cause) ?? `${phase} failed for ${model.package ?? "unknown"}`, })
// Unexpected throws still carry the most useful diagnosis in their message; a stack or an unknown value does not.const causeMessage = (cause: unknown): string | undefined => { if (typeof cause === "string") return cause.trim() || undefined if (!(cause instanceof globalThis.Error)) return undefined const message = cause.message.trim() if (message) return message return causeMessage(cause.cause)}
export const resolveModel = ( model: Info, variant: VariantID | undefined, credential?: Credential.Value, dependencies?: Dependencies,) => withVariant(model, variant).pipe(Effect.flatMap((model) => fromCatalogModel(model, credential, dependencies)))
export const hasPackage = (model: Info) => Boolean(model.package)
/** Resolves catalog selections into runtime models for the current Location. */export const layer = Layer.effect( Service, Effect.gen(function* () { const providers = yield* Provider.Service const models = yield* Model.Service const integrations = yield* Integration.Service const npm = yield* Npm.Service const aisdk = yield* AISDK.Service const load = Effect.fn("ModelResolver.resolveModel")(function* (selected: Info, variant?: VariantID) { const provider = yield* providers.get(selected.providerID) const connection = yield* integrations.connection.active( provider?.integrationID ?? Integration.ID.make(selected.providerID), ) const credential = connection ? yield* integrations.connection.resolve(connection) : undefined const selectedVariant = yield* withVariant(selected, variant) const runtimeInfo: RuntimeInfo = { ...selectedVariant, settings: Provider.mergeOverlay(provider?.settings, Provider.modelSettings(selectedVariant.settings)), } const model = yield* fromCatalogModel(runtimeInfo, credential, { loadPackage: (specifier) => Provider.loadPackage(specifier, npm), loadAISDK: (model) => aisdk.model(model), }) const runtime = provider?.activation === "enabled" && credential === undefined && !hasConfiguredAuth(runtimeInfo) && usesAPIKeyAuth(runtimeInfo.package) ? LanguageModel.update(model, { route: model.route.with({ auth: Auth.none }) }) : model return { model: runtime, ref: Ref.make({ id: selected.id, providerID: selected.providerID, ...(variant === undefined ? {} : { variant }), }), capabilities: selected.capabilities, cost: selected.cost, limit: selected.limit, compaction: runtimeInfo.settings?.compaction, transport: provider?.settings?.transport, } }) return Service.of({ resolve: Effect.fn("ModelResolver.resolve")(function* (requested) { const selected = requested ? yield* models.get(requested.providerID, requested.id) : yield* models .default() .pipe( Effect.flatMap((model) => model && hasPackage(model) ? Effect.succeed(model) : Effect.map(models.available(), (models) => models.find(hasPackage)), ), ) if (!selected) return undefined return yield* load(selected, requested?.variant) }), resolveModel: load, }) }),)
function hasConfiguredAuth(model: RuntimeInfo) { return [model.settings?.apiKey, model.settings?.authToken, model.settings?.accessToken].some( (value) => typeof value === "string" && value !== "", )}
function usesAPIKeyAuth(packageName: string | undefined) { const name = Provider.packageName(packageName) return ( name === "@ai-sdk/openai" || name === "@ai-sdk/anthropic" || name === "@ai-sdk/cerebras" || name === "@ai-sdk/deepinfra" || name === "@ai-sdk/openai-compatible" || name === "@ai-sdk/google" || name === "@ai-sdk/groq" || name === "@ai-sdk/mistral" || name === "@ai-sdk/togetherai" || name === "@ai-sdk/xai" || name === "@openrouter/ai-sdk-provider" || name === "@ai-sdk/azure" || name === "@opencode/ai/providers/openai" || name?.startsWith("@opencode/ai/providers/openai/") === true || name === "@opencode/ai/providers/anthropic" || name === "@opencode/ai/providers/anthropic-compatible" || name === "@opencode/ai/providers/baseten" || name === "@opencode/ai/providers/cerebras" || name === "@opencode/ai/providers/cloudflare-ai-gateway" || name === "@opencode/ai/providers/cloudflare-workers-ai" || name === "@opencode/ai/providers/deepinfra" || name === "@opencode/ai/providers/deepseek" || name === "@opencode/ai/providers/fireworks" || name === "@opencode/ai/providers/openai-compatible" || name === "@opencode/ai/providers/google" || name === "@opencode/ai/providers/groq" || name === "@opencode/ai/providers/mistral" || name === "@opencode/ai/providers/togetherai" || name === "@opencode/ai/providers/xai" || name === "@opencode/ai/providers/openrouter" || name === "@opencode/ai/providers/azure" || name?.startsWith("@opencode/ai/providers/azure/") === true )}
export const node = makeLocationNode({ service: Service, layer, deps: [Provider.node, Model.node, Integration.node, Npm.node, AISDK.node],})