aisdk-native.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314
  1. export * as AISDKNative from "./aisdk-native.js"
  2. import { isRecord } from "@opencode-ai/ai/utils/record"
  3. import { Provider } from "./provider.js"
  4. export interface Mapping {
  5. readonly package: string
  6. readonly settings: Readonly<Record<string, unknown>>
  7. readonly headers?: Readonly<Record<string, string>>
  8. readonly body?: Readonly<Record<string, unknown>>
  9. }
  10. export interface MapInput {
  11. readonly packageName: string | undefined
  12. readonly settings: Readonly<Record<string, unknown>>
  13. readonly modelID: string
  14. }
  15. export function map(input: MapInput): Mapping | undefined {
  16. const baseSettings = mapBaseSettings(input.settings)
  17. switch (input.packageName) {
  18. case "@ai-sdk/amazon-bedrock":
  19. return {
  20. package: "@opencode-ai/ai/providers/amazon-bedrock",
  21. settings: mapBedrockSettings(input.settings, baseSettings),
  22. ...mapBedrockRequest(input),
  23. }
  24. case "@ai-sdk/amazon-bedrock/mantle":
  25. return mapBedrockMantle(input, baseSettings)
  26. case "@ai-sdk/azure":
  27. return {
  28. package: `@opencode-ai/ai/providers/azure/${input.settings.useCompletionUrls === true ? "chat" : "responses"}`,
  29. settings: {
  30. ...baseSettings,
  31. ...mapAPIKey(input.settings),
  32. ...(typeof input.settings.resourceName === "string" ? { resourceName: input.settings.resourceName } : {}),
  33. ...(typeof input.settings.apiVersion === "string" ? { apiVersion: input.settings.apiVersion } : {}),
  34. ...(isStringRecord(input.settings.queryParams) ? { queryParams: input.settings.queryParams } : {}),
  35. ...(typeof input.settings.useDeploymentBasedUrls === "boolean"
  36. ? { useDeploymentBasedUrls: input.settings.useDeploymentBasedUrls }
  37. : {}),
  38. ...mapOpenAIOptions(input.settings),
  39. },
  40. }
  41. case "@ai-sdk/google":
  42. return {
  43. package: "@opencode-ai/ai/providers/google",
  44. settings: {
  45. ...baseSettings,
  46. ...mapAPIKey(input.settings),
  47. ...mapGoogleOptions(input.settings),
  48. },
  49. }
  50. case "@ai-sdk/google-vertex/anthropic":
  51. return {
  52. package: "@opencode-ai/ai/providers/google-vertex/messages",
  53. settings: {
  54. ...baseSettings,
  55. ...(typeof input.settings.accessToken === "string" ? { accessToken: input.settings.accessToken } : {}),
  56. ...(typeof input.settings.location === "string" ? { location: input.settings.location } : {}),
  57. ...(typeof input.settings.project === "string" ? { project: input.settings.project } : {}),
  58. ...(isRecord(input.settings.thinking) || typeof input.settings.effort === "string"
  59. ? {
  60. providerOptions: {
  61. anthropic: {
  62. ...(isRecord(input.settings.thinking) ? { thinking: input.settings.thinking } : {}),
  63. ...(typeof input.settings.effort === "string" ? { effort: input.settings.effort } : {}),
  64. },
  65. },
  66. }
  67. : {}),
  68. },
  69. ...(isStringRecord(input.settings.headers) ? { headers: input.settings.headers } : {}),
  70. }
  71. case "@openrouter/ai-sdk-provider":
  72. return mapOpenRouter(input.settings, baseSettings)
  73. case "@ai-sdk/xai":
  74. return {
  75. package: "@opencode-ai/ai/providers/xai",
  76. settings: {
  77. ...baseSettings,
  78. ...mapAPIKey(input.settings),
  79. ...mapXAIOptions(input.settings),
  80. },
  81. }
  82. }
  83. }
  84. function mapBedrockMantle(input: MapInput, baseSettings: Readonly<Record<string, unknown>>): Mapping | undefined {
  85. const settings = input.settings
  86. const chat = input.modelID === "openai.gpt-oss-safeguard-20b" || input.modelID === "openai.gpt-oss-safeguard-120b"
  87. return {
  88. package: `@opencode-ai/ai/providers/amazon-bedrock/mantle/${chat ? "chat" : "responses"}`,
  89. settings: {
  90. ...mapBedrockSettings(settings, baseSettings),
  91. ...mapOpenAIOptions(settings),
  92. },
  93. ...(isStringRecord(settings.headers) ? { headers: settings.headers } : {}),
  94. }
  95. }
  96. function mapBedrockSettings(
  97. settings: Readonly<Record<string, unknown>>,
  98. baseSettings: Readonly<Record<string, unknown>>,
  99. ) {
  100. const apiKey =
  101. typeof settings.apiKey === "string"
  102. ? settings.apiKey
  103. : typeof settings.bearerToken === "string"
  104. ? settings.bearerToken
  105. : undefined
  106. const region = bedrockRegion(settings)
  107. const credentials = mapBedrockCredentials(settings, region)
  108. return {
  109. ...baseSettings,
  110. ...(typeof baseSettings.baseURL === "string" && region !== undefined
  111. ? { baseURL: baseSettings.baseURL.replaceAll("${AWS_REGION}", region) }
  112. : {}),
  113. ...(typeof settings.baseURL !== "string" && typeof settings.endpoint === "string"
  114. ? { baseURL: settings.endpoint }
  115. : {}),
  116. ...(apiKey === undefined ? {} : { apiKey }),
  117. ...(credentials === undefined ? {} : { credentials }),
  118. ...(typeof settings.region === "string" ? { region: settings.region } : {}),
  119. ...(typeof settings.topP === "number" ? { topP: settings.topP } : {}),
  120. }
  121. }
  122. function mapBedrockRequest(input: MapInput): Pick<Mapping, "headers" | "body"> {
  123. const settings = input.settings
  124. const headers = isStringRecord(settings.headers) ? settings.headers : undefined
  125. const additional = isRecord(settings.additionalModelRequestFields) ? settings.additionalModelRequestFields : {}
  126. const reasoning = isRecord(settings.reasoningConfig) ? settings.reasoningConfig : undefined
  127. const anthropic = input.modelID.includes("anthropic")
  128. const openai = input.modelID.startsWith("openai.")
  129. const effort = typeof reasoning?.maxReasoningEffort === "string" ? reasoning.maxReasoningEffort : undefined
  130. const type = typeof reasoning?.type === "string" ? reasoning.type : undefined
  131. const budget = typeof reasoning?.budgetTokens === "number" ? reasoning.budgetTokens : undefined
  132. const display = typeof reasoning?.display === "string" ? reasoning.display : undefined
  133. const betas = Array.isArray(settings.anthropicBeta)
  134. ? settings.anthropicBeta.filter((item): item is string => typeof item === "string")
  135. : []
  136. const existingBetas = Array.isArray(additional.anthropic_beta)
  137. ? additional.anthropic_beta.filter((item): item is string => typeof item === "string")
  138. : []
  139. const fields = Provider.mergeOverlay(additional, {
  140. ...(betas.length > 0 ? { anthropic_beta: [...existingBetas, ...betas] } : {}),
  141. ...(anthropic && type === "enabled" && budget !== undefined
  142. ? { thinking: { type: "enabled", budget_tokens: budget } }
  143. : {}),
  144. ...(anthropic && type === "adaptive"
  145. ? { thinking: { type: "adaptive", ...(display === undefined ? {} : { display }) } }
  146. : {}),
  147. ...(anthropic && effort !== undefined
  148. ? {
  149. output_config: {
  150. ...(isRecord(additional.output_config) ? additional.output_config : {}),
  151. effort,
  152. },
  153. }
  154. : {}),
  155. ...(!anthropic && openai && effort !== undefined ? { reasoning_effort: effort } : {}),
  156. ...(!anthropic && !openai && effort !== undefined
  157. ? {
  158. reasoningConfig: {
  159. ...(type === undefined || type === "adaptive" ? {} : { type }),
  160. ...(budget === undefined ? {} : { budgetTokens: budget }),
  161. maxReasoningEffort: effort,
  162. },
  163. }
  164. : {}),
  165. })
  166. const body = {
  167. ...(fields && Object.keys(fields).length > 0 ? { additionalModelRequestFields: fields } : {}),
  168. ...(typeof settings.serviceTier === "string" ? { serviceTier: { type: settings.serviceTier } } : {}),
  169. }
  170. return {
  171. ...(headers === undefined ? {} : { headers }),
  172. ...(Object.keys(body).length === 0 ? {} : { body }),
  173. }
  174. }
  175. function mapBedrockCredentials(settings: Readonly<Record<string, unknown>>, region: string | undefined) {
  176. const credentials = isRecord(settings.credentials) ? settings.credentials : settings
  177. if (
  178. region === undefined ||
  179. typeof credentials.accessKeyId !== "string" ||
  180. typeof credentials.secretAccessKey !== "string"
  181. )
  182. return undefined
  183. return {
  184. region,
  185. accessKeyId: credentials.accessKeyId,
  186. secretAccessKey: credentials.secretAccessKey,
  187. ...(typeof credentials.sessionToken === "string" ? { sessionToken: credentials.sessionToken } : {}),
  188. }
  189. }
  190. function bedrockRegion(settings: Readonly<Record<string, unknown>>) {
  191. const credentials = isRecord(settings.credentials) ? settings.credentials : settings
  192. return typeof settings.region === "string"
  193. ? settings.region
  194. : typeof credentials.region === "string"
  195. ? credentials.region
  196. : undefined
  197. }
  198. function mapOpenAIOptions(settings: Readonly<Record<string, unknown>>) {
  199. const options = {
  200. ...(typeof settings.reasoningEffort === "string" ? { reasoningEffort: settings.reasoningEffort } : {}),
  201. ...(typeof settings.reasoningSummary === "string" ? { reasoningSummary: settings.reasoningSummary } : {}),
  202. ...(Array.isArray(settings.include) ? { include: settings.include } : {}),
  203. ...(typeof settings.store === "boolean" ? { store: settings.store } : {}),
  204. ...(typeof settings.promptCacheKey === "string" ? { promptCacheKey: settings.promptCacheKey } : {}),
  205. ...(typeof settings.textVerbosity === "string" ? { textVerbosity: settings.textVerbosity } : {}),
  206. ...(typeof settings.serviceTier === "string" ? { serviceTier: settings.serviceTier } : {}),
  207. }
  208. if (Object.keys(options).length === 0) return {}
  209. return { providerOptions: { openai: options } }
  210. }
  211. function mapBaseSettings(settings: Readonly<Record<string, unknown>>) {
  212. return {
  213. ...(typeof settings.baseURL === "string" ? { baseURL: settings.baseURL } : {}),
  214. }
  215. }
  216. function mapAPIKey(settings: Readonly<Record<string, unknown>>) {
  217. return typeof settings.apiKey === "string" ? { apiKey: settings.apiKey } : {}
  218. }
  219. function mapGoogleOptions(settings: Readonly<Record<string, unknown>>) {
  220. const input = settings.thinkingConfig
  221. const thinkingConfig = {
  222. ...(isRecord(input) && typeof input.thinkingBudget === "number" ? { thinkingBudget: input.thinkingBudget } : {}),
  223. ...(isRecord(input) && typeof input.includeThoughts === "boolean"
  224. ? { includeThoughts: input.includeThoughts }
  225. : {}),
  226. ...(isRecord(input) && typeof input.thinkingLevel === "string" ? { thinkingLevel: input.thinkingLevel } : {}),
  227. }
  228. const options = {
  229. ...(typeof settings.cachedContent === "string" ? { cachedContent: settings.cachedContent } : {}),
  230. ...(Array.isArray(settings.safetySettings) ? { safetySettings: settings.safetySettings } : {}),
  231. ...(typeof settings.serviceTier === "string" ? { serviceTier: settings.serviceTier } : {}),
  232. ...(Object.keys(thinkingConfig).length > 0 ? { thinkingConfig } : {}),
  233. }
  234. if (Object.keys(options).length === 0) return {}
  235. return { providerOptions: { gemini: options } }
  236. }
  237. function mapOpenRouter(
  238. settings: Readonly<Record<string, unknown>>,
  239. baseSettings: Readonly<Record<string, unknown>>,
  240. ): Mapping {
  241. const headers =
  242. Provider.mergeHeaders(
  243. {
  244. ...(typeof settings.appName === "string" ? { "X-OpenRouter-Title": settings.appName } : {}),
  245. ...(typeof settings.appUrl === "string" ? { "HTTP-Referer": settings.appUrl } : {}),
  246. ...(isStringRecord(settings.api_keys) && Object.keys(settings.api_keys).length > 0
  247. ? { "X-Provider-API-Keys": JSON.stringify(settings.api_keys) }
  248. : {}),
  249. },
  250. isStringRecord(settings.headers) ? settings.headers : undefined,
  251. ) ?? {}
  252. return {
  253. package: "@opencode-ai/ai/providers/openrouter",
  254. settings: {
  255. ...baseSettings,
  256. ...mapAPIKey(settings),
  257. ...mapOpenRouterOptions(settings),
  258. },
  259. ...(Object.keys(headers).length > 0 ? { headers } : {}),
  260. ...(isRecord(settings.extraBody) ? { body: settings.extraBody } : {}),
  261. }
  262. }
  263. function mapOpenRouterOptions(settings: Readonly<Record<string, unknown>>) {
  264. const options = Object.fromEntries(
  265. Object.entries(settings).filter(
  266. ([key]) =>
  267. ![
  268. "apiKey",
  269. "api_keys",
  270. "appName",
  271. "appUrl",
  272. "authToken",
  273. "baseURL",
  274. "chunkTimeout",
  275. "compatibility",
  276. "extraBody",
  277. "fetch",
  278. "headers",
  279. "promptCacheKey",
  280. "timeout",
  281. ].includes(key),
  282. ),
  283. )
  284. if (Object.keys(options).length === 0) return {}
  285. return { providerOptions: { openrouter: options } }
  286. }
  287. function isStringRecord(value: unknown): value is Readonly<Record<string, string>> {
  288. return isRecord(value) && Object.values(value).every((item) => typeof item === "string")
  289. }
  290. function mapXAIOptions(settings: Readonly<Record<string, unknown>>) {
  291. const options = {
  292. ...(typeof settings.reasoningEffort === "string" ? { reasoningEffort: settings.reasoningEffort } : {}),
  293. ...(typeof settings.store === "boolean" ? { store: settings.store } : {}),
  294. }
  295. if (Object.keys(options).length === 0) return {}
  296. return { providerOptions: { xai: options } }
  297. }