diff --git a/packages/core/src/plugin/variant.ts b/packages/core/src/plugin/variant.ts index 0e21f98c902..65aba881e1e 100644 --- a/packages/core/src/plugin/variant.ts +++ b/packages/core/src/plugin/variant.ts @@ -15,11 +15,12 @@ export const Plugin = define({ const generated = generate(draft, record.provider) if (generated.length === 0) return - const explicit = new Map((draft.variants ?? []).map((variant) => [variant.id, variant])) + const variants = draft.variants ?? [] + const explicit = new Map(variants.map((variant) => [variant.id, variant])) const generatedIDs = new Set(generated.map((variant) => variant.id)) draft.variants = [ ...generated.map((variant) => explicit.get(variant.id) ?? variant), - ...(draft.variants ?? []).filter((variant) => !generatedIDs.has(variant.id)), + ...variants.filter((variant) => !generatedIDs.has(variant.id)), ] }) } diff --git a/packages/core/src/provider.ts b/packages/core/src/provider.ts index b9e1928a94e..a08844af994 100644 --- a/packages/core/src/provider.ts +++ b/packages/core/src/provider.ts @@ -11,10 +11,12 @@ export const ID = Provider.ID export type ID = typeof ID.Type export const AISDK_PREFIX = "aisdk:" -export const isAISDK = (packageName: string | undefined) => packageName?.startsWith(AISDK_PREFIX) ?? false -export const aisdk = (packageName: string) => (isAISDK(packageName) ? packageName : `${AISDK_PREFIX}${packageName}`) -export const packageName = (packageName: string | undefined) => - isAISDK(packageName) ? packageName!.slice(AISDK_PREFIX.length) : packageName +export const isAISDK = (value: string | undefined) => value?.startsWith(AISDK_PREFIX) ?? false +export const aisdk = (value: string) => (isAISDK(value) ? value : `${AISDK_PREFIX}${value}`) +export const packageName = (value: string | undefined) => { + if (value === undefined || !isAISDK(value)) return value + return value.slice(AISDK_PREFIX.length) +} export class LoadError extends Schema.TaggedErrorClass()("ProviderV2.LoadError", { package: Schema.String, @@ -25,7 +27,14 @@ export type ProviderPackage = ProviderPackageDefinition const packages = new Map>() export const loadPackage = Effect.fn("ProviderV2.loadPackage")(function* (specifier: string, npm?: Npm.Interface) { - const resolved = yield* resolvePackage(specifier) + const resolved = yield* Effect.sync(() => { + if (specifier.startsWith("file://") || specifier.startsWith("@opencode-ai/llm/")) return specifier + try { + return import.meta.resolve(specifier) + } catch { + return undefined + } + }) if (resolved) return yield* importPackage(specifier, resolved) if (!npm) { return yield* new LoadError({ @@ -33,8 +42,10 @@ export const loadPackage = Effect.fn("ProviderV2.loadPackage")(function* (specif cause: new Error(`Provider package ${specifier} is not installed`), }) } + const parts = specifier.split("/") + const root = specifier.startsWith("@") ? parts.slice(0, 2).join("/") : (parts[0] ?? specifier) const installed = yield* npm - .add(rootPackage(specifier)) + .add(root) .pipe(Effect.mapError((cause) => new LoadError({ package: specifier, cause }))) const entrypoint = yield* Effect.try({ try: () => import.meta.resolve(specifier, pathToFileURL(`${installed.directory}/`).href), @@ -95,16 +106,6 @@ export type Info = Provider.Info export type MutableInfo = DeepMutable -const resolvePackage = (specifier: string) => - Effect.sync(() => { - if (specifier.startsWith("file://") || specifier.startsWith("@opencode-ai/llm/")) return specifier - try { - return import.meta.resolve(specifier) - } catch { - return undefined - } - }) - const importPackage = Effect.fn("ProviderV2.importPackage")(function* (specifier: string, entrypoint: string) { const module = yield* Effect.tryPromise({ try: () => { @@ -116,21 +117,11 @@ const importPackage = Effect.fn("ProviderV2.importPackage")(function* (specifier }, catch: (cause) => new LoadError({ package: specifier, cause }), }) - if (!isProviderPackage(module)) { + if (typeof module !== "object" || module === null || typeof (module as { model?: unknown }).model !== "function") { return yield* new LoadError({ package: specifier, cause: new Error(`Provider package ${specifier} does not export model(modelID, settings)`), }) } - return module + return module as ProviderPackageDefinition }) - -function isProviderPackage(input: unknown): input is ProviderPackageDefinition { - return typeof input === "object" && input !== null && "model" in input && typeof input.model === "function" -} - -function rootPackage(specifier: string) { - const parts = specifier.split("/") - if (specifier.startsWith("@")) return parts.slice(0, 2).join("/") - return parts[0] -} diff --git a/packages/core/src/session/runner/model.ts b/packages/core/src/session/runner/model.ts index 29a81f400a0..09789078ec6 100644 --- a/packages/core/src/session/runner/model.ts +++ b/packages/core/src/session/runner/model.ts @@ -108,19 +108,6 @@ export interface Dependencies { readonly loadAISDK?: (model: ModelV2.Info) => Effect.Effect } -const unsupported = (model: ModelV2.Info, packageName = model.package ?? "unknown") => - new UnsupportedPackageError({ - providerID: model.providerID, - modelID: model.id, - package: packageName, - }) - -const credentialSettings = (credential: Credential.Value | undefined) => ({ - ...(credential?.type === "key" ? { apiKey: credential.key } : {}), - ...(credential?.type === "oauth" ? { apiKey: credential.access } : {}), - ...credential?.metadata, -}) - export const fromCatalogModel = ( model: ModelV2.Info, credential?: Credential.Value, @@ -134,35 +121,72 @@ export const fromCatalogModel = ( }) if (ProviderV2.isAISDK(resolved.package)) { if (!dependencies.loadAISDK) { - return Effect.fail(unsupported(resolved)) + return Effect.fail( + new UnsupportedPackageError({ + providerID: resolved.providerID, + modelID: resolved.id, + package: resolved.package ?? "unknown", + }), + ) } const runtime = produce(resolved, (draft) => { - draft.settings = ProviderV2.mergeOverlay(draft.settings, credentialSettings(credential)) + draft.settings = ProviderV2.mergeOverlay(draft.settings, { + ...(credential?.type === "key" ? { apiKey: credential.key } : {}), + ...(credential?.type === "oauth" ? { apiKey: credential.access } : {}), + ...credential?.metadata, + }) }) return dependencies.loadAISDK(runtime).pipe( - Effect.mapError(() => unsupported(resolved)), + Effect.mapError( + () => + new UnsupportedPackageError({ + providerID: resolved.providerID, + modelID: resolved.id, + package: resolved.package ?? "unknown", + }), + ), ) } if (resolved.package) { const specifier = resolved.package return Effect.gen(function* () { const module = yield* (dependencies.loadPackage ?? ProviderV2.loadPackage)(specifier).pipe( - Effect.mapError(() => unsupported(resolved, specifier)), + Effect.mapError( + () => + new UnsupportedPackageError({ + providerID: resolved.providerID, + modelID: resolved.id, + package: specifier, + }), + ), ) const settings = { ...resolved.settings, - ...credentialSettings(credential), + ...(credential?.type === "key" ? { apiKey: credential.key } : {}), + ...(credential?.type === "oauth" ? { apiKey: credential.access } : {}), + ...credential?.metadata, headers: resolved.headers, body: resolved.body, limits: { context: resolved.limit.context, output: resolved.limit.output }, } return yield* Effect.try({ try: () => Model.update(module.model(resolved.modelID ?? resolved.id, settings), { provider: resolved.providerID }), - catch: () => unsupported(resolved, specifier), + catch: () => + new UnsupportedPackageError({ + providerID: resolved.providerID, + modelID: resolved.id, + package: specifier, + }), }) }) } - return Effect.fail(unsupported(resolved)) + return Effect.fail( + new UnsupportedPackageError({ + providerID: resolved.providerID, + modelID: resolved.id, + package: resolved.package ?? "unknown", + }), + ) } export const resolve = (