mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-13 19:46:16 +03:00
584 lines
23 KiB
TypeScript
584 lines
23 KiB
TypeScript
export * as ModelResolver from "./model-resolver.js"
|
|
|
|
import { makeLocationNode } from "@opencode/util/effect/app-node"
|
|
import {
|
|
DecisionModel,
|
|
DecisionModelCompatibility,
|
|
DecisionModelSchema,
|
|
HttpOptions,
|
|
ImageModel,
|
|
ImageModelSchema,
|
|
LanguageModel,
|
|
ProviderConfigurationError,
|
|
} from "@opencode/ai/effect"
|
|
import { Auth, mergeHttpOptions } 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
|
|
/** Prompt-cache settings from the provider, model and variant; omitted keeps the provider defaults. */
|
|
readonly cache?: Provider.Cache
|
|
/** Provider transport policy; omitted means HTTP. */
|
|
readonly transport?: Provider.Transport
|
|
/** Milliseconds without streamed data before a WebSocket exchange fails; `false` disables the limit. */
|
|
readonly chunkTimeout?: number | false
|
|
}
|
|
|
|
export type MediaError =
|
|
| VariantUnavailableError
|
|
| UnsupportedPackageError
|
|
| ModelConfigurationError
|
|
| ModelInitializationError
|
|
| UnresolvedProviderVariablesError
|
|
| Integration.AuthorizationError
|
|
|
|
export interface Interface {
|
|
readonly resolve: (requested?: Ref) => Effect.Effect<Resolved | undefined, Error>
|
|
readonly resolveModel: (model: Info, variant?: VariantID) => Effect.Effect<Resolved, Error>
|
|
/** An enabled image model by ref, or undefined when no image model has that ref. */
|
|
readonly resolveImage: (ref: Ref) => Effect.Effect<ImageModel | undefined, MediaError>
|
|
/** An enabled decision model by ref, or undefined when no decision model has that ref. */
|
|
readonly resolveDecision: (ref: Ref) => Effect.Effect<DecisionModel | undefined, MediaError>
|
|
}
|
|
|
|
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,
|
|
// Config accepts unknown variant keys, so drop `cache` here too; a variant can't change the prompt cache.
|
|
settings: Provider.mergeOverlay(
|
|
model.settings,
|
|
Provider.modelSettings(variant.settings && Struct.omit(variant.settings, ["cache"])),
|
|
),
|
|
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,
|
|
) {
|
|
if (Provider.isAISDK(model.package)) {
|
|
const resolved = prepareRuntimeModel(model)
|
|
const loadAISDK = dependencies?.loadAISDK
|
|
if (!loadAISDK) return yield* unsupported(resolved)
|
|
const settings = yield* prepareProviderSettings(
|
|
resolved,
|
|
Provider.mergeOverlay(
|
|
resolved.settings,
|
|
nativeCredentialSettings(resolved, resolved.package ?? "", credential),
|
|
) ?? {},
|
|
)
|
|
return yield* loadAISDK({ ...resolved, settings }).pipe(
|
|
Effect.mapError((error) => initialization(resolved, "init", error.cause)),
|
|
)
|
|
}
|
|
const { resolved, configured, module, settings } = yield* loadCatalogPackage(model, credential, dependencies)
|
|
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,
|
|
// Timeouts and body encoding are transport policy, so they land on the route's HTTP defaults instead of package settings.
|
|
defaults: {
|
|
...runtime.defaults,
|
|
http: mergeHttpOptions(
|
|
runtime.defaults?.http,
|
|
new HttpOptions({ ...Provider.timeouts(configured), encoding: Provider.encoding(configured) }),
|
|
),
|
|
},
|
|
})
|
|
},
|
|
catch: (cause) => constructionError(resolved, cause),
|
|
})
|
|
})
|
|
|
|
/** Settings and package loading shared by every model type: credentials, variables, canonical provider, overlays. */
|
|
const loadCatalogPackage = Effect.fn("ModelResolver.loadCatalogPackage")(function* (
|
|
model: RuntimeInfo,
|
|
credential?: Credential.Value,
|
|
dependencies?: Dependencies,
|
|
) {
|
|
const resolved = prepareRuntimeModel(model)
|
|
const configured = resolved.settings ?? {}
|
|
const specifier = Provider.packageName(resolved.package)
|
|
if (!specifier || Provider.isAISDK(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(resolved, specifier, credential),
|
|
headers: resolved.headers,
|
|
body: resolved.body,
|
|
}
|
|
return { resolved, configured, module, settings }
|
|
})
|
|
|
|
/**
|
|
* Builds an image or decision model from its catalog package. The entrypoint (`providers/<P>/images`) exports the
|
|
* same `model(modelID, settings)` as chat; a package that returns another kind of model cannot serve this type.
|
|
*/
|
|
const fromCatalogMedia = Effect.fn("ModelResolver.fromCatalogMedia")(function* <Runtime>(
|
|
model: RuntimeInfo,
|
|
is: (value: unknown) => value is Runtime,
|
|
credential?: Credential.Value,
|
|
dependencies?: Dependencies,
|
|
) {
|
|
const { resolved, module, settings } = yield* loadCatalogPackage(model, credential, dependencies)
|
|
const runtime: unknown = yield* Effect.try({
|
|
try: () => module.model(resolved.modelID ?? resolved.id, settings),
|
|
catch: (cause) => constructionError(resolved, cause),
|
|
})
|
|
// Catalog and config compatibility tell a decision route how its host takes media, as chat does above.
|
|
const mediaInput = resolved.compatibility?.mediaInput
|
|
const adapted =
|
|
runtime instanceof DecisionModel && mediaInput !== undefined
|
|
? new DecisionModel({
|
|
...runtime,
|
|
compatibility: DecisionModelCompatibility.make({ ...runtime.compatibility, mediaInput }),
|
|
})
|
|
: runtime
|
|
if (!is(adapted)) return yield* unsupported(resolved)
|
|
return adapted
|
|
})
|
|
|
|
const constructionError = (model: RuntimeInfo, cause: unknown) =>
|
|
cause instanceof ProviderConfigurationError
|
|
? new ModelConfigurationError({
|
|
providerID: model.providerID,
|
|
modelID: model.id,
|
|
package: model.package ?? "unknown",
|
|
detail: cause.message,
|
|
})
|
|
: initialization(model, "construct", cause)
|
|
|
|
const isImageModel = Schema.is(ImageModelSchema)
|
|
const isDecisionModel = Schema.is(DecisionModelSchema)
|
|
|
|
function prepareRuntimeModel(model: RuntimeInfo) {
|
|
if (model.settings?.apiKey !== "") return model
|
|
return { ...model, settings: Struct.omit(model.settings, ["apiKey"]) }
|
|
}
|
|
|
|
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, settings).pipe(
|
|
Effect.map((prepared) => (prepared === baseURL ? settings : { ...settings, baseURL: prepared })),
|
|
)
|
|
}
|
|
|
|
function prepareProviderURL(
|
|
model: RuntimeInfo,
|
|
baseURL: string,
|
|
settings: Readonly<Record<string, unknown>>,
|
|
): Effect.Effect<string, UnresolvedProviderVariablesError> {
|
|
if (!baseURL.includes("${")) return Effect.succeed(baseURL)
|
|
// Catalog Bedrock URLs are model settings while `region` is a provider setting, so they only meet here.
|
|
const prepared = baseURL.replace(
|
|
/\$\{([^}]+)\}/g,
|
|
(placeholder, name: string) =>
|
|
(name === "AWS_REGION" && typeof settings.region === "string" ? settings.region : 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 = (model: RuntimeInfo, specifier: string, credential: Credential.Value | undefined) => {
|
|
if (!credential) return {}
|
|
if (credential.type === "key") return { apiKey: credential.key }
|
|
if (credential.type === "oauth") return tokenSettings(specifier, credential.access)
|
|
// The Bedrock plugin projects the saved profile into settings; SigV4 keeps an ambient bearer token from taking over.
|
|
if (specifier.startsWith("@opencode/ai/providers/amazon-bedrock")) return { auth: "sigv4" }
|
|
// The Azure plugin's request hooks replace this with an Entra ID token from the Azure CLI; it only gets the
|
|
// request past the package's own credential check.
|
|
if (model.providerID === Provider.ID.azure) return tokenSettings(specifier, "azure-cli")
|
|
return {}
|
|
}
|
|
|
|
const tokenSettings = (specifier: string, token: string) => {
|
|
if (specifier === "@opencode/ai/providers/anthropic" || specifier === "@opencode/ai/providers/anthropic-compatible")
|
|
return { authToken: token }
|
|
if (
|
|
specifier === "@opencode/ai/providers/google-vertex" ||
|
|
specifier.startsWith("@opencode/ai/providers/google-vertex/")
|
|
)
|
|
return { accessToken: token }
|
|
return { apiKey: token }
|
|
}
|
|
|
|
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 dependencies: Dependencies = {
|
|
loadPackage: (specifier) => Provider.loadPackage(specifier, npm),
|
|
loadAISDK: (model) => aisdk.model(model),
|
|
}
|
|
const prepare = Effect.fn("ModelResolver.prepare")(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)),
|
|
}
|
|
return { provider, credential, runtimeInfo }
|
|
})
|
|
const load = Effect.fn("ModelResolver.resolveModel")(function* (selected: Info, variant?: VariantID) {
|
|
const { provider, credential, runtimeInfo } = yield* prepare(selected, variant)
|
|
const model = yield* fromCatalogModel(runtimeInfo, credential, dependencies)
|
|
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,
|
|
cache: runtimeInfo.settings?.cache,
|
|
transport: provider?.settings?.transport,
|
|
chunkTimeout: Provider.timeout(provider?.settings?.chunkTimeout),
|
|
}
|
|
})
|
|
const loadMedia = Effect.fnUntraced(function* <Runtime>(
|
|
ref: Ref,
|
|
type: Model.Type,
|
|
is: (value: unknown) => value is Runtime,
|
|
) {
|
|
const info = (yield* models.available([type])).find(
|
|
(model) => model.providerID === ref.providerID && model.id === ref.id,
|
|
)
|
|
if (!info) return undefined
|
|
const { credential, runtimeInfo } = yield* prepare(info, ref.variant)
|
|
return yield* fromCatalogMedia(runtimeInfo, is, credential, dependencies)
|
|
})
|
|
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((model) => hasPackage(model) && Model.supportsText(model)),
|
|
),
|
|
),
|
|
)
|
|
if (!selected) return undefined
|
|
return yield* load(selected, requested?.variant)
|
|
}),
|
|
resolveModel: load,
|
|
resolveImage: Effect.fn("ModelResolver.resolveImage")(function* (ref) {
|
|
return yield* loadMedia(ref, "image", isImageModel)
|
|
}),
|
|
resolveDecision: Effect.fn("ModelResolver.resolveDecision")(function* (ref) {
|
|
return yield* loadMedia(ref, "decision", isDecisionModel)
|
|
}),
|
|
})
|
|
}),
|
|
)
|
|
|
|
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/google/interactions" ||
|
|
name === "@opencode/ai/providers/groq" ||
|
|
name === "@opencode/ai/providers/mistral" ||
|
|
name === "@opencode/ai/providers/togetherai" ||
|
|
name === "@opencode/ai/providers/vercel-ai-gateway" ||
|
|
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],
|
|
})
|