aisdk-native.test.ts 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  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 Bedrock provider and request options", () => {
  17. expect(
  18. map(
  19. "@ai-sdk/amazon-bedrock",
  20. {
  21. region: "us-east-1",
  22. topP: 0.8,
  23. headers: { "x-test": "value" },
  24. additionalModelRequestFields: {
  25. existing: true,
  26. anthropic_beta: ["existing-beta"],
  27. output_config: { format: "text" },
  28. },
  29. reasoningConfig: { type: "adaptive", display: "summarized", maxReasoningEffort: "high" },
  30. anthropicBeta: ["context-1m-2025-08-07"],
  31. serviceTier: "priority",
  32. },
  33. "anthropic.claude-sonnet-4-6-v1",
  34. ),
  35. ).toEqual({
  36. package: "@opencode-ai/ai/providers/amazon-bedrock",
  37. settings: { region: "us-east-1", topP: 0.8 },
  38. headers: { "x-test": "value" },
  39. body: {
  40. additionalModelRequestFields: {
  41. existing: true,
  42. anthropic_beta: ["existing-beta", "context-1m-2025-08-07"],
  43. thinking: { type: "adaptive", display: "summarized" },
  44. output_config: { format: "text", effort: "high" },
  45. },
  46. serviceTier: { type: "priority" },
  47. },
  48. })
  49. expect(
  50. map(
  51. "@ai-sdk/amazon-bedrock",
  52. { reasoningConfig: { type: "enabled", maxReasoningEffort: "max" } },
  53. "amazon.nova-2-lite-v1:0",
  54. )?.body,
  55. ).toEqual({
  56. additionalModelRequestFields: {
  57. reasoningConfig: { type: "enabled", maxReasoningEffort: "max" },
  58. },
  59. })
  60. expect(
  61. map("@ai-sdk/amazon-bedrock", { reasoningConfig: { maxReasoningEffort: "high" } }, "openai.gpt-oss-120b-1:0")
  62. ?.body,
  63. ).toEqual({ additionalModelRequestFields: { reasoning_effort: "high" } })
  64. })
  65. test("maps Bedrock Mantle models to their supported native APIs", () => {
  66. const settings = {
  67. bearerToken: "token",
  68. region: "us-west-2",
  69. baseURL: "https://mantle.test/v1",
  70. headers: { "x-test": "value" },
  71. reasoningEffort: "high",
  72. reasoningSummary: "auto",
  73. include: ["reasoning.encrypted_content"],
  74. }
  75. expect(map("@ai-sdk/amazon-bedrock/mantle", settings, "openai.gpt-oss-120b")).toEqual({
  76. package: "@opencode-ai/ai/providers/amazon-bedrock/mantle/responses",
  77. settings: {
  78. apiKey: "token",
  79. baseURL: "https://mantle.test/v1",
  80. region: "us-west-2",
  81. providerOptions: {
  82. openai: {
  83. reasoningEffort: "high",
  84. reasoningSummary: "auto",
  85. include: ["reasoning.encrypted_content"],
  86. },
  87. },
  88. },
  89. headers: { "x-test": "value" },
  90. })
  91. expect(map("@ai-sdk/amazon-bedrock/mantle", settings, "openai.gpt-oss-safeguard-20b")?.package).toBe(
  92. "@opencode-ai/ai/providers/amazon-bedrock/mantle/chat",
  93. )
  94. })
  95. test("maps static Bedrock Mantle credentials without leaking connection options", () => {
  96. expect(
  97. map(
  98. "@ai-sdk/amazon-bedrock/mantle",
  99. {
  100. credentials: {
  101. accessKeyId: "key",
  102. secretAccessKey: "secret",
  103. sessionToken: "session",
  104. },
  105. region: "eu-west-1",
  106. profile: "ignored",
  107. credentialProvider: "ignored",
  108. fetch: "ignored",
  109. store: false,
  110. },
  111. "openai.gpt-oss-120b",
  112. ),
  113. ).toEqual({
  114. package: "@opencode-ai/ai/providers/amazon-bedrock/mantle/responses",
  115. settings: {
  116. credentials: {
  117. accessKeyId: "key",
  118. secretAccessKey: "secret",
  119. sessionToken: "session",
  120. region: "eu-west-1",
  121. },
  122. region: "eu-west-1",
  123. providerOptions: { openai: { store: false } },
  124. },
  125. })
  126. })
  127. test("maps the legacy Bedrock endpoint override", () => {
  128. expect(
  129. map(
  130. "@ai-sdk/amazon-bedrock/mantle",
  131. { bearerToken: "token", endpoint: "https://mantle.private/v1", region: "us-east-1" },
  132. "openai.gpt-oss-120b",
  133. ),
  134. ).toMatchObject({ settings: { baseURL: "https://mantle.private/v1" } })
  135. })
  136. test("maps OpenRouter settings to native destinations", () => {
  137. expect(
  138. map("@openrouter/ai-sdk-provider", {
  139. appName: "OpenCode",
  140. appUrl: "https://opencode.ai",
  141. headers: { "x-openrouter-title": "Configured", "x-provider-api-keys": "Configured BYOK" },
  142. api_keys: { anthropic: "provider-key" },
  143. extraBody: { transforms: ["middle-out"] },
  144. models: ["anthropic/claude-sonnet-4.6"],
  145. provider: { only: ["anthropic"], require_parameters: true },
  146. reasoning: { effort: "high" },
  147. promptCacheKey: "session_123",
  148. future_option: { enabled: true },
  149. }),
  150. ).toEqual({
  151. package: "@opencode-ai/ai/providers/openrouter",
  152. settings: {
  153. providerOptions: {
  154. openrouter: {
  155. models: ["anthropic/claude-sonnet-4.6"],
  156. provider: { only: ["anthropic"], require_parameters: true },
  157. reasoning: { effort: "high" },
  158. promptCacheKey: "session_123",
  159. future_option: { enabled: true },
  160. },
  161. },
  162. },
  163. headers: {
  164. "x-openrouter-title": "Configured",
  165. "HTTP-Referer": "https://opencode.ai",
  166. "x-provider-api-keys": "Configured BYOK",
  167. },
  168. body: { transforms: ["middle-out"] },
  169. })
  170. })
  171. test("maps every Google thinking setting", () => {
  172. expect(
  173. map("@ai-sdk/google", {
  174. cachedContent: "cachedContents/example",
  175. safetySettings: [{ category: "HARM_CATEGORY_HARASSMENT", threshold: "BLOCK_NONE" }],
  176. serviceTier: "flex",
  177. thinkingConfig: {
  178. thinkingBudget: 0,
  179. includeThoughts: false,
  180. thinkingLevel: "high",
  181. unknown: true,
  182. },
  183. }),
  184. ).toEqual({
  185. package: "@opencode-ai/ai/providers/google",
  186. settings: {
  187. providerOptions: {
  188. gemini: {
  189. cachedContent: "cachedContents/example",
  190. safetySettings: [{ category: "HARM_CATEGORY_HARASSMENT", threshold: "BLOCK_NONE" }],
  191. serviceTier: "flex",
  192. thinkingConfig: {
  193. thinkingBudget: 0,
  194. includeThoughts: false,
  195. thinkingLevel: "high",
  196. },
  197. },
  198. },
  199. },
  200. })
  201. })
  202. test("maps Google thinking settings independently", () => {
  203. for (const thinkingConfig of [{ thinkingBudget: -1 }, { includeThoughts: true }, { thinkingLevel: "medium" }]) {
  204. expect(map("@ai-sdk/google", { thinkingConfig })).toMatchObject({
  205. settings: { providerOptions: { gemini: { thinkingConfig } } },
  206. })
  207. }
  208. })
  209. test("maps Google request options without thinking settings", () => {
  210. expect(
  211. map("@ai-sdk/google", {
  212. cachedContent: "cachedContents/example",
  213. safetySettings: [{ category: "HARM_CATEGORY_HARASSMENT", threshold: "BLOCK_NONE" }],
  214. serviceTier: "future-tier",
  215. }),
  216. ).toMatchObject({
  217. settings: {
  218. providerOptions: {
  219. gemini: {
  220. cachedContent: "cachedContents/example",
  221. safetySettings: [{ category: "HARM_CATEGORY_HARASSMENT", threshold: "BLOCK_NONE" }],
  222. serviceTier: "future-tier",
  223. },
  224. },
  225. },
  226. })
  227. })
  228. test("maps supported xAI settings", () => {
  229. expect(
  230. map("@ai-sdk/xai", {
  231. apiKey: "secret",
  232. baseURL: "https://xai.example/v1",
  233. reasoningEffort: "custom",
  234. store: true,
  235. promptCacheKey: "cache-key",
  236. }),
  237. ).toEqual({
  238. package: "@opencode-ai/ai/providers/xai",
  239. settings: {
  240. apiKey: "secret",
  241. baseURL: "https://xai.example/v1",
  242. providerOptions: {
  243. xai: {
  244. reasoningEffort: "custom",
  245. store: true,
  246. promptCacheKey: "cache-key",
  247. },
  248. },
  249. },
  250. })
  251. })
  252. test("omits invalid and unsupported xAI settings", () => {
  253. expect(
  254. map("@ai-sdk/xai", {
  255. reasoningEffort: 10,
  256. store: "yes",
  257. include: ["unknown"],
  258. logprobs: true,
  259. topLogprobs: 8,
  260. previousResponseId: "response-id",
  261. searchParameters: { mode: "auto" },
  262. }),
  263. ).toEqual({
  264. package: "@opencode-ai/ai/providers/xai",
  265. settings: {},
  266. })
  267. })
  268. })