From cc1289048e7c9dee7b324b1316117e503504cf4e Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Thu, 30 Jul 2026 18:38:29 -0500 Subject: [PATCH] refactor(core): isolate AI SDK native mappings (#39761) --- packages/ai/src/provider-package.ts | 1 + packages/core/src/aisdk-native.ts | 59 +++++++++++++++++++++++++++++ packages/core/src/model-resolver.ts | 33 ++++------------ 3 files changed, 67 insertions(+), 26 deletions(-) create mode 100644 packages/core/src/aisdk-native.ts diff --git a/packages/ai/src/provider-package.ts b/packages/ai/src/provider-package.ts index 4f7dc0dcad6..ed753ba954f 100644 --- a/packages/ai/src/provider-package.ts +++ b/packages/ai/src/provider-package.ts @@ -1,6 +1,7 @@ import type { Model, ProviderOptions } from "./schema" export interface Settings extends Readonly> { + readonly baseURL?: string readonly headers?: Readonly> readonly body?: Readonly> readonly limits?: { diff --git a/packages/core/src/aisdk-native.ts b/packages/core/src/aisdk-native.ts new file mode 100644 index 00000000000..f2b2c595ef2 --- /dev/null +++ b/packages/core/src/aisdk-native.ts @@ -0,0 +1,59 @@ +export * as AISDKNative from "./aisdk-native" + +export interface Mapping { + readonly package: string + readonly settings: Readonly> +} + +export function map(packageName: string | undefined, settings: Readonly>): Mapping | undefined { + const baseSettings = mapBaseSettings(settings) + switch (packageName) { + case "@ai-sdk/google": + return { + package: "@opencode-ai/ai/providers/google", + settings: { + ...baseSettings, + ...mapAPIKey(settings), + ...mapProviderOptions("gemini", settings), + }, + } + case "@openrouter/ai-sdk-provider": + return { + package: "@opencode-ai/ai/providers/openrouter", + settings: { + ...baseSettings, + ...mapAPIKey(settings), + ...mapProviderOptions("openrouter", settings), + }, + } + case "@ai-sdk/xai": + return { + package: "@opencode-ai/ai/providers/xai", + settings: { + ...baseSettings, + ...mapAPIKey(settings), + ...mapProviderOptions("xai", settings), + }, + } + } +} + +function mapBaseSettings(settings: Readonly>) { + return { + ...(typeof settings.baseURL === "string" ? { baseURL: settings.baseURL } : {}), + } +} + +function mapAPIKey(settings: Readonly>) { + return typeof settings.apiKey === "string" ? { apiKey: settings.apiKey } : {} +} + +function mapProviderOptions(namespace: string, settings: Readonly>) { + const values = Object.fromEntries( + Object.entries(settings).filter( + ([key]) => !["apiKey", "authToken", "baseURL", "chunkTimeout", "fetch", "timeout"].includes(key), + ), + ) + if (Object.keys(values).length === 0) return {} + return { providerOptions: { [namespace]: values } } +} diff --git a/packages/core/src/model-resolver.ts b/packages/core/src/model-resolver.ts index 04c737e914b..1d3396736b0 100644 --- a/packages/core/src/model-resolver.ts +++ b/packages/core/src/model-resolver.ts @@ -12,6 +12,7 @@ import { Auth, type AnyRoute } from "@opencode-ai/ai/route" import { Context, Effect, Layer, Schema } from "effect" import { produce } from "immer" import { AISDK } from "./aisdk" +import { AISDKNative } from "./aisdk-native" import { Catalog } from "./catalog" import { Credential } from "./credential" import { Integration } from "./integration" @@ -182,8 +183,10 @@ export const fromCatalogModel = ( .model({ id: resolved.modelID ?? resolved.id, compatibility: resolved.compatibility }), ) } - const native = Provider.isAISDK(resolved.package) ? nativePackage(packageName) : resolved.package - if (Provider.isAISDK(resolved.package) && !native) { + const configured = { ...resolved.settings, ...credential?.metadata } + const mapping = Provider.isAISDK(resolved.package) ? AISDKNative.map(packageName, configured) : undefined + const native = mapping?.package ?? resolved.package + if (Provider.isAISDK(resolved.package) && !mapping) { if (!dependencies?.loadAISDK) return Effect.fail(unsupported(resolved)) const runtime = produce(resolved, (draft) => { draft.settings = Provider.mergeOverlay(draft.settings, { @@ -201,15 +204,13 @@ export const fromCatalogModel = ( const module = yield* (dependencies?.loadPackage ?? Provider.loadPackage)(specifier).pipe( Effect.mapError(() => unsupported(resolved)), ) - const configured = { ...resolved.settings, ...credential?.metadata } - const providerOptions = nativeProviderOptions(packageName, configured) + const mapped = mapping?.settings ?? configured const settings = { - ...(credential ? withoutNativeAuthSettings(configured) : configured), + ...(credential ? withoutNativeAuthSettings(mapped) : mapped), ...nativeCredentialSettings(specifier, credential), headers: resolved.headers, body: resolved.body, limits: { context: resolved.limit.context, output: resolved.limit.output }, - ...(providerOptions ? { providerOptions } : {}), } return yield* Effect.try({ try: () => { @@ -226,26 +227,6 @@ export const fromCatalogModel = ( }) } -const nativePackage = (packageName: string | undefined) => { - if (packageName === "@ai-sdk/google") return "@opencode-ai/ai/providers/google" - if (packageName === "@openrouter/ai-sdk-provider") return "@opencode-ai/ai/providers/openrouter" - if (packageName === "@ai-sdk/xai") return "@opencode-ai/ai/providers/xai" - return undefined -} - -const nativeProviderOptions = (packageName: string | undefined, settings: Readonly>) => { - const values = Object.fromEntries( - Object.entries(settings).filter( - ([key]) => !["apiKey", "authToken", "baseURL", "chunkTimeout", "fetch", "timeout"].includes(key), - ), - ) - if (Object.keys(values).length === 0) return undefined - if (packageName === "@ai-sdk/google") return { gemini: values } - if (packageName === "@openrouter/ai-sdk-provider") return { openrouter: values } - if (packageName === "@ai-sdk/xai") return { xai: values } - return undefined -} - const isNativeOpenAI = (packageName: string | undefined) => packageName === "@opencode-ai/ai/providers/openai" || packageName?.startsWith("@opencode-ai/ai/providers/openai/") === true