aisdk-native.test.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344
  1. import { describe, expect, test } from "bun:test"
  2. import { AISDKNative } from "@opencode-ai/core/aisdk-native"
  3. const map = (packageName: string, settings: Readonly<Record<string, unknown>>, modelID = "test-model") =>
  4. AISDKNative.map({ packageName, settings, modelID })
  5. describe("AISDKNative", () => {
  6. test("maps both models.dev Bedrock packages to native providers", () => {
  7. expect(map("@ai-sdk/amazon-bedrock", { region: "us-east-1" })).toEqual({
  8. package: "@opencode-ai/ai/providers/amazon-bedrock",
  9. settings: { region: "us-east-1" },
  10. })
  11. expect(map("@ai-sdk/amazon-bedrock/mantle", { region: "us-east-1" }, "openai.gpt-oss-120b")).toEqual({
  12. package: "@opencode-ai/ai/providers/amazon-bedrock/mantle/responses",
  13. settings: { region: "us-east-1" },
  14. })
  15. })
  16. test("maps Azure deployments and settings to native routes", () => {
  17. const settings = {
  18. apiKey: "secret",
  19. resourceName: "resource",
  20. apiVersion: "2025-01-01-preview",
  21. queryParams: { feature: "enabled" },
  22. useDeploymentBasedUrls: true,
  23. reasoningEffort: "high",
  24. }
  25. expect(map("@ai-sdk/azure", settings, "deployment")).toEqual({
  26. package: "@opencode-ai/ai/providers/azure/responses",
  27. settings: {
  28. apiKey: "secret",
  29. resourceName: "resource",
  30. apiVersion: "2025-01-01-preview",
  31. queryParams: { feature: "enabled" },
  32. useDeploymentBasedUrls: true,
  33. providerOptions: { openai: { reasoningEffort: "high" } },
  34. },
  35. })
  36. expect(map("@ai-sdk/azure", { ...settings, useCompletionUrls: true }, "custom-deployment")?.package).toBe(
  37. "@opencode-ai/ai/providers/azure/chat",
  38. )
  39. })
  40. test("maps Bedrock provider and request options", () => {
  41. expect(
  42. map(
  43. "@ai-sdk/amazon-bedrock",
  44. {
  45. region: "us-east-1",
  46. topP: 0.8,
  47. headers: { "x-test": "value" },
  48. additionalModelRequestFields: {
  49. existing: true,
  50. anthropic_beta: ["existing-beta"],
  51. output_config: { format: "text" },
  52. },
  53. reasoningConfig: { type: "adaptive", display: "summarized", maxReasoningEffort: "high" },
  54. anthropicBeta: ["context-1m-2025-08-07"],
  55. serviceTier: "priority",
  56. },
  57. "anthropic.claude-sonnet-4-6-v1",
  58. ),
  59. ).toEqual({
  60. package: "@opencode-ai/ai/providers/amazon-bedrock",
  61. settings: { region: "us-east-1", topP: 0.8 },
  62. headers: { "x-test": "value" },
  63. body: {
  64. additionalModelRequestFields: {
  65. existing: true,
  66. anthropic_beta: ["existing-beta", "context-1m-2025-08-07"],
  67. thinking: { type: "adaptive", display: "summarized" },
  68. output_config: { format: "text", effort: "high" },
  69. },
  70. serviceTier: { type: "priority" },
  71. },
  72. })
  73. expect(
  74. map(
  75. "@ai-sdk/amazon-bedrock",
  76. { reasoningConfig: { type: "enabled", maxReasoningEffort: "max" } },
  77. "amazon.nova-2-lite-v1:0",
  78. )?.body,
  79. ).toEqual({
  80. additionalModelRequestFields: {
  81. reasoningConfig: { type: "enabled", maxReasoningEffort: "max" },
  82. },
  83. })
  84. expect(
  85. map("@ai-sdk/amazon-bedrock", { reasoningConfig: { maxReasoningEffort: "high" } }, "openai.gpt-oss-120b-1:0")
  86. ?.body,
  87. ).toEqual({ additionalModelRequestFields: { reasoning_effort: "high" } })
  88. })
  89. test("maps Bedrock Mantle models to their supported native APIs", () => {
  90. const settings = {
  91. bearerToken: "token",
  92. region: "us-west-2",
  93. baseURL: "https://mantle.test/v1",
  94. headers: { "x-test": "value" },
  95. reasoningEffort: "high",
  96. reasoningSummary: "auto",
  97. include: ["reasoning.encrypted_content"],
  98. }
  99. expect(map("@ai-sdk/amazon-bedrock/mantle", settings, "openai.gpt-oss-120b")).toEqual({
  100. package: "@opencode-ai/ai/providers/amazon-bedrock/mantle/responses",
  101. settings: {
  102. apiKey: "token",
  103. baseURL: "https://mantle.test/v1",
  104. region: "us-west-2",
  105. providerOptions: {
  106. openai: {
  107. reasoningEffort: "high",
  108. reasoningSummary: "auto",
  109. include: ["reasoning.encrypted_content"],
  110. },
  111. },
  112. },
  113. headers: { "x-test": "value" },
  114. })
  115. expect(map("@ai-sdk/amazon-bedrock/mantle", settings, "openai.gpt-oss-safeguard-20b")?.package).toBe(
  116. "@opencode-ai/ai/providers/amazon-bedrock/mantle/chat",
  117. )
  118. expect(
  119. map(
  120. "@ai-sdk/amazon-bedrock/mantle",
  121. {
  122. region: "us-west-2",
  123. baseURL: "https://bedrock-mantle.${AWS_REGION}.api.aws/openai/v1",
  124. },
  125. "openai.gpt-5.5",
  126. ),
  127. ).toMatchObject({ settings: { baseURL: "https://bedrock-mantle.us-west-2.api.aws/openai/v1" } })
  128. })
  129. test("maps static Bedrock Mantle credentials without leaking connection options", () => {
  130. expect(
  131. map(
  132. "@ai-sdk/amazon-bedrock/mantle",
  133. {
  134. credentials: {
  135. accessKeyId: "key",
  136. secretAccessKey: "secret",
  137. sessionToken: "session",
  138. region: "eu-west-1",
  139. },
  140. baseURL: "https://bedrock-mantle.${AWS_REGION}.api.aws/v1",
  141. profile: "ignored",
  142. credentialProvider: "ignored",
  143. fetch: "ignored",
  144. store: false,
  145. },
  146. "openai.gpt-oss-120b",
  147. ),
  148. ).toEqual({
  149. package: "@opencode-ai/ai/providers/amazon-bedrock/mantle/responses",
  150. settings: {
  151. credentials: {
  152. accessKeyId: "key",
  153. secretAccessKey: "secret",
  154. sessionToken: "session",
  155. region: "eu-west-1",
  156. },
  157. baseURL: "https://bedrock-mantle.eu-west-1.api.aws/v1",
  158. providerOptions: { openai: { store: false } },
  159. },
  160. })
  161. })
  162. test("maps the legacy Bedrock endpoint override", () => {
  163. expect(
  164. map(
  165. "@ai-sdk/amazon-bedrock/mantle",
  166. { bearerToken: "token", endpoint: "https://mantle.private/v1", region: "us-east-1" },
  167. "openai.gpt-oss-120b",
  168. ),
  169. ).toMatchObject({ settings: { baseURL: "https://mantle.private/v1" } })
  170. })
  171. test("maps OpenRouter settings to native destinations", () => {
  172. expect(
  173. map("@openrouter/ai-sdk-provider", {
  174. appName: "OpenCode",
  175. appUrl: "https://opencode.ai",
  176. headers: { "x-openrouter-title": "Configured", "x-provider-api-keys": "Configured BYOK" },
  177. api_keys: { anthropic: "provider-key" },
  178. extraBody: { transforms: ["middle-out"] },
  179. models: ["anthropic/claude-sonnet-4.6"],
  180. provider: { only: ["anthropic"], require_parameters: true },
  181. reasoning: { effort: "high" },
  182. future_option: { enabled: true },
  183. }),
  184. ).toEqual({
  185. package: "@opencode-ai/ai/providers/openrouter",
  186. settings: {
  187. providerOptions: {
  188. openrouter: {
  189. models: ["anthropic/claude-sonnet-4.6"],
  190. provider: { only: ["anthropic"], require_parameters: true },
  191. reasoning: { effort: "high" },
  192. future_option: { enabled: true },
  193. },
  194. },
  195. },
  196. headers: {
  197. "x-openrouter-title": "Configured",
  198. "HTTP-Referer": "https://opencode.ai",
  199. "x-provider-api-keys": "Configured BYOK",
  200. },
  201. body: { transforms: ["middle-out"] },
  202. })
  203. })
  204. test("maps every Google thinking setting", () => {
  205. expect(
  206. map("@ai-sdk/google", {
  207. cachedContent: "cachedContents/example",
  208. safetySettings: [{ category: "HARM_CATEGORY_HARASSMENT", threshold: "BLOCK_NONE" }],
  209. serviceTier: "flex",
  210. thinkingConfig: {
  211. thinkingBudget: 0,
  212. includeThoughts: false,
  213. thinkingLevel: "high",
  214. unknown: true,
  215. },
  216. }),
  217. ).toEqual({
  218. package: "@opencode-ai/ai/providers/google",
  219. settings: {
  220. providerOptions: {
  221. gemini: {
  222. cachedContent: "cachedContents/example",
  223. safetySettings: [{ category: "HARM_CATEGORY_HARASSMENT", threshold: "BLOCK_NONE" }],
  224. serviceTier: "flex",
  225. thinkingConfig: {
  226. thinkingBudget: 0,
  227. includeThoughts: false,
  228. thinkingLevel: "high",
  229. },
  230. },
  231. },
  232. },
  233. })
  234. })
  235. test("maps Google thinking settings independently", () => {
  236. for (const thinkingConfig of [{ thinkingBudget: -1 }, { includeThoughts: true }, { thinkingLevel: "medium" }]) {
  237. expect(map("@ai-sdk/google", { thinkingConfig })).toMatchObject({
  238. settings: { providerOptions: { gemini: { thinkingConfig } } },
  239. })
  240. }
  241. })
  242. test("maps Google request options without thinking settings", () => {
  243. expect(
  244. map("@ai-sdk/google", {
  245. cachedContent: "cachedContents/example",
  246. safetySettings: [{ category: "HARM_CATEGORY_HARASSMENT", threshold: "BLOCK_NONE" }],
  247. serviceTier: "future-tier",
  248. }),
  249. ).toMatchObject({
  250. settings: {
  251. providerOptions: {
  252. gemini: {
  253. cachedContent: "cachedContents/example",
  254. safetySettings: [{ category: "HARM_CATEGORY_HARASSMENT", threshold: "BLOCK_NONE" }],
  255. serviceTier: "future-tier",
  256. },
  257. },
  258. },
  259. })
  260. })
  261. test("maps Vertex Anthropic settings to native Messages", () => {
  262. expect(
  263. map("@ai-sdk/google-vertex/anthropic", {
  264. accessToken: "vertex-token",
  265. baseURL: "https://vertex.example/v1",
  266. headers: { "x-test": "value" },
  267. location: "eu",
  268. project: "vertex-project",
  269. thinking: { type: "adaptive", display: "summarized" },
  270. effort: "high",
  271. }),
  272. ).toEqual({
  273. package: "@opencode-ai/ai/providers/google-vertex/messages",
  274. settings: {
  275. accessToken: "vertex-token",
  276. baseURL: "https://vertex.example/v1",
  277. location: "eu",
  278. project: "vertex-project",
  279. providerOptions: {
  280. anthropic: {
  281. thinking: { type: "adaptive", display: "summarized" },
  282. effort: "high",
  283. },
  284. },
  285. },
  286. headers: { "x-test": "value" },
  287. })
  288. })
  289. test("maps supported xAI settings", () => {
  290. expect(
  291. map("@ai-sdk/xai", {
  292. apiKey: "secret",
  293. baseURL: "https://xai.example/v1",
  294. reasoningEffort: "custom",
  295. store: true,
  296. }),
  297. ).toEqual({
  298. package: "@opencode-ai/ai/providers/xai",
  299. settings: {
  300. apiKey: "secret",
  301. baseURL: "https://xai.example/v1",
  302. providerOptions: {
  303. xai: {
  304. reasoningEffort: "custom",
  305. store: true,
  306. },
  307. },
  308. },
  309. })
  310. })
  311. test("omits invalid and unsupported xAI settings", () => {
  312. expect(
  313. map("@ai-sdk/xai", {
  314. reasoningEffort: 10,
  315. store: "yes",
  316. include: ["unknown"],
  317. logprobs: true,
  318. topLogprobs: 8,
  319. previousResponseId: "response-id",
  320. searchParameters: { mode: "auto" },
  321. }),
  322. ).toEqual({
  323. package: "@opencode-ai/ai/providers/xai",
  324. settings: {},
  325. })
  326. })
  327. })