amazon-bedrock.test.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445
  1. import { test, expect, describe } from "bun:test"
  2. import path from "path"
  3. import { unlink } from "fs/promises"
  4. import { tmpdir } from "../fixture/fixture"
  5. import { Instance } from "../../src/project/instance"
  6. import { Provider } from "../../src/provider/provider"
  7. import { Env } from "../../src/env"
  8. import { Global } from "../../src/global"
  9. test("Bedrock: config region takes precedence over AWS_REGION env var", async () => {
  10. await using tmp = await tmpdir({
  11. init: async (dir) => {
  12. await Bun.write(
  13. path.join(dir, "opencode.json"),
  14. JSON.stringify({
  15. $schema: "https://opencode.ai/config.json",
  16. provider: {
  17. "amazon-bedrock": {
  18. options: {
  19. region: "eu-west-1",
  20. },
  21. },
  22. },
  23. }),
  24. )
  25. },
  26. })
  27. await Instance.provide({
  28. directory: tmp.path,
  29. init: async () => {
  30. Env.set("AWS_REGION", "us-east-1")
  31. Env.set("AWS_PROFILE", "default")
  32. },
  33. fn: async () => {
  34. const providers = await Provider.list()
  35. expect(providers["amazon-bedrock"]).toBeDefined()
  36. expect(providers["amazon-bedrock"].options?.region).toBe("eu-west-1")
  37. },
  38. })
  39. })
  40. test("Bedrock: falls back to AWS_REGION env var when no config region", async () => {
  41. await using tmp = await tmpdir({
  42. init: async (dir) => {
  43. await Bun.write(
  44. path.join(dir, "opencode.json"),
  45. JSON.stringify({
  46. $schema: "https://opencode.ai/config.json",
  47. }),
  48. )
  49. },
  50. })
  51. await Instance.provide({
  52. directory: tmp.path,
  53. init: async () => {
  54. Env.set("AWS_REGION", "eu-west-1")
  55. Env.set("AWS_PROFILE", "default")
  56. },
  57. fn: async () => {
  58. const providers = await Provider.list()
  59. expect(providers["amazon-bedrock"]).toBeDefined()
  60. expect(providers["amazon-bedrock"].options?.region).toBe("eu-west-1")
  61. },
  62. })
  63. })
  64. test("Bedrock: loads when bearer token from auth.json is present", async () => {
  65. await using tmp = await tmpdir({
  66. init: async (dir) => {
  67. await Bun.write(
  68. path.join(dir, "opencode.json"),
  69. JSON.stringify({
  70. $schema: "https://opencode.ai/config.json",
  71. provider: {
  72. "amazon-bedrock": {
  73. options: {
  74. region: "eu-west-1",
  75. },
  76. },
  77. },
  78. }),
  79. )
  80. },
  81. })
  82. const authPath = path.join(Global.Path.data, "auth.json")
  83. // Save original auth.json if it exists
  84. let originalAuth: string | undefined
  85. try {
  86. originalAuth = await Bun.file(authPath).text()
  87. } catch {
  88. // File doesn't exist, that's fine
  89. }
  90. try {
  91. // Write test auth.json
  92. await Bun.write(
  93. authPath,
  94. JSON.stringify({
  95. "amazon-bedrock": {
  96. type: "api",
  97. key: "test-bearer-token",
  98. },
  99. }),
  100. )
  101. await Instance.provide({
  102. directory: tmp.path,
  103. init: async () => {
  104. Env.set("AWS_PROFILE", "")
  105. Env.set("AWS_ACCESS_KEY_ID", "")
  106. Env.set("AWS_BEARER_TOKEN_BEDROCK", "")
  107. },
  108. fn: async () => {
  109. const providers = await Provider.list()
  110. expect(providers["amazon-bedrock"]).toBeDefined()
  111. expect(providers["amazon-bedrock"].options?.region).toBe("eu-west-1")
  112. },
  113. })
  114. } finally {
  115. // Restore original or delete
  116. if (originalAuth !== undefined) {
  117. await Bun.write(authPath, originalAuth)
  118. } else {
  119. try {
  120. await unlink(authPath)
  121. } catch {
  122. // Ignore errors if file doesn't exist
  123. }
  124. }
  125. }
  126. })
  127. test("Bedrock: config profile takes precedence over AWS_PROFILE env var", async () => {
  128. await using tmp = await tmpdir({
  129. init: async (dir) => {
  130. await Bun.write(
  131. path.join(dir, "opencode.json"),
  132. JSON.stringify({
  133. $schema: "https://opencode.ai/config.json",
  134. provider: {
  135. "amazon-bedrock": {
  136. options: {
  137. profile: "my-custom-profile",
  138. region: "us-east-1",
  139. },
  140. },
  141. },
  142. }),
  143. )
  144. },
  145. })
  146. await Instance.provide({
  147. directory: tmp.path,
  148. init: async () => {
  149. Env.set("AWS_PROFILE", "default")
  150. Env.set("AWS_ACCESS_KEY_ID", "test-key-id")
  151. },
  152. fn: async () => {
  153. const providers = await Provider.list()
  154. expect(providers["amazon-bedrock"]).toBeDefined()
  155. expect(providers["amazon-bedrock"].options?.region).toBe("us-east-1")
  156. },
  157. })
  158. })
  159. test("Bedrock: includes custom endpoint in options when specified", async () => {
  160. await using tmp = await tmpdir({
  161. init: async (dir) => {
  162. await Bun.write(
  163. path.join(dir, "opencode.json"),
  164. JSON.stringify({
  165. $schema: "https://opencode.ai/config.json",
  166. provider: {
  167. "amazon-bedrock": {
  168. options: {
  169. endpoint: "https://bedrock-runtime.us-east-1.vpce-xxxxx.amazonaws.com",
  170. },
  171. },
  172. },
  173. }),
  174. )
  175. },
  176. })
  177. await Instance.provide({
  178. directory: tmp.path,
  179. init: async () => {
  180. Env.set("AWS_PROFILE", "default")
  181. },
  182. fn: async () => {
  183. const providers = await Provider.list()
  184. expect(providers["amazon-bedrock"]).toBeDefined()
  185. expect(providers["amazon-bedrock"].options?.endpoint).toBe(
  186. "https://bedrock-runtime.us-east-1.vpce-xxxxx.amazonaws.com",
  187. )
  188. },
  189. })
  190. })
  191. test("Bedrock: autoloads when AWS_WEB_IDENTITY_TOKEN_FILE is present", async () => {
  192. await using tmp = await tmpdir({
  193. init: async (dir) => {
  194. await Bun.write(
  195. path.join(dir, "opencode.json"),
  196. JSON.stringify({
  197. $schema: "https://opencode.ai/config.json",
  198. provider: {
  199. "amazon-bedrock": {
  200. options: {
  201. region: "us-east-1",
  202. },
  203. },
  204. },
  205. }),
  206. )
  207. },
  208. })
  209. await Instance.provide({
  210. directory: tmp.path,
  211. init: async () => {
  212. Env.set("AWS_WEB_IDENTITY_TOKEN_FILE", "/var/run/secrets/eks.amazonaws.com/serviceaccount/token")
  213. Env.set("AWS_ROLE_ARN", "arn:aws:iam::123456789012:role/my-eks-role")
  214. Env.set("AWS_PROFILE", "")
  215. Env.set("AWS_ACCESS_KEY_ID", "")
  216. },
  217. fn: async () => {
  218. const providers = await Provider.list()
  219. expect(providers["amazon-bedrock"]).toBeDefined()
  220. expect(providers["amazon-bedrock"].options?.region).toBe("us-east-1")
  221. },
  222. })
  223. })
  224. // Tests for cross-region inference profile prefix handling
  225. // Models from models.dev may come with prefixes already (e.g., us., eu., global.)
  226. // These should NOT be double-prefixed when passed to the SDK
  227. test("Bedrock: model with us. prefix should not be double-prefixed", async () => {
  228. await using tmp = await tmpdir({
  229. init: async (dir) => {
  230. await Bun.write(
  231. path.join(dir, "opencode.json"),
  232. JSON.stringify({
  233. $schema: "https://opencode.ai/config.json",
  234. provider: {
  235. "amazon-bedrock": {
  236. options: {
  237. region: "us-east-1",
  238. },
  239. models: {
  240. "us.anthropic.claude-opus-4-5-20251101-v1:0": {
  241. name: "Claude Opus 4.5 (US)",
  242. },
  243. },
  244. },
  245. },
  246. }),
  247. )
  248. },
  249. })
  250. await Instance.provide({
  251. directory: tmp.path,
  252. init: async () => {
  253. Env.set("AWS_PROFILE", "default")
  254. },
  255. fn: async () => {
  256. const providers = await Provider.list()
  257. expect(providers["amazon-bedrock"]).toBeDefined()
  258. // The model should exist with the us. prefix
  259. expect(providers["amazon-bedrock"].models["us.anthropic.claude-opus-4-5-20251101-v1:0"]).toBeDefined()
  260. },
  261. })
  262. })
  263. test("Bedrock: model with global. prefix should not be prefixed", async () => {
  264. await using tmp = await tmpdir({
  265. init: async (dir) => {
  266. await Bun.write(
  267. path.join(dir, "opencode.json"),
  268. JSON.stringify({
  269. $schema: "https://opencode.ai/config.json",
  270. provider: {
  271. "amazon-bedrock": {
  272. options: {
  273. region: "us-east-1",
  274. },
  275. models: {
  276. "global.anthropic.claude-opus-4-5-20251101-v1:0": {
  277. name: "Claude Opus 4.5 (Global)",
  278. },
  279. },
  280. },
  281. },
  282. }),
  283. )
  284. },
  285. })
  286. await Instance.provide({
  287. directory: tmp.path,
  288. init: async () => {
  289. Env.set("AWS_PROFILE", "default")
  290. },
  291. fn: async () => {
  292. const providers = await Provider.list()
  293. expect(providers["amazon-bedrock"]).toBeDefined()
  294. expect(providers["amazon-bedrock"].models["global.anthropic.claude-opus-4-5-20251101-v1:0"]).toBeDefined()
  295. },
  296. })
  297. })
  298. test("Bedrock: model with eu. prefix should not be double-prefixed", async () => {
  299. await using tmp = await tmpdir({
  300. init: async (dir) => {
  301. await Bun.write(
  302. path.join(dir, "opencode.json"),
  303. JSON.stringify({
  304. $schema: "https://opencode.ai/config.json",
  305. provider: {
  306. "amazon-bedrock": {
  307. options: {
  308. region: "eu-west-1",
  309. },
  310. models: {
  311. "eu.anthropic.claude-opus-4-5-20251101-v1:0": {
  312. name: "Claude Opus 4.5 (EU)",
  313. },
  314. },
  315. },
  316. },
  317. }),
  318. )
  319. },
  320. })
  321. await Instance.provide({
  322. directory: tmp.path,
  323. init: async () => {
  324. Env.set("AWS_PROFILE", "default")
  325. },
  326. fn: async () => {
  327. const providers = await Provider.list()
  328. expect(providers["amazon-bedrock"]).toBeDefined()
  329. expect(providers["amazon-bedrock"].models["eu.anthropic.claude-opus-4-5-20251101-v1:0"]).toBeDefined()
  330. },
  331. })
  332. })
  333. test("Bedrock: model without prefix in US region should get us. prefix added", async () => {
  334. await using tmp = await tmpdir({
  335. init: async (dir) => {
  336. await Bun.write(
  337. path.join(dir, "opencode.json"),
  338. JSON.stringify({
  339. $schema: "https://opencode.ai/config.json",
  340. provider: {
  341. "amazon-bedrock": {
  342. options: {
  343. region: "us-east-1",
  344. },
  345. models: {
  346. "anthropic.claude-opus-4-5-20251101-v1:0": {
  347. name: "Claude Opus 4.5",
  348. },
  349. },
  350. },
  351. },
  352. }),
  353. )
  354. },
  355. })
  356. await Instance.provide({
  357. directory: tmp.path,
  358. init: async () => {
  359. Env.set("AWS_PROFILE", "default")
  360. },
  361. fn: async () => {
  362. const providers = await Provider.list()
  363. expect(providers["amazon-bedrock"]).toBeDefined()
  364. // Non-prefixed model should still be registered
  365. expect(providers["amazon-bedrock"].models["anthropic.claude-opus-4-5-20251101-v1:0"]).toBeDefined()
  366. },
  367. })
  368. })
  369. // Direct unit tests for cross-region inference profile prefix handling
  370. // These test the prefix detection logic used in getModel
  371. describe("Bedrock cross-region prefix detection", () => {
  372. const crossRegionPrefixes = ["global.", "us.", "eu.", "jp.", "apac.", "au."]
  373. test("should detect global. prefix", () => {
  374. const modelID = "global.anthropic.claude-opus-4-5-20251101-v1:0"
  375. const hasPrefix = crossRegionPrefixes.some((prefix) => modelID.startsWith(prefix))
  376. expect(hasPrefix).toBe(true)
  377. })
  378. test("should detect us. prefix", () => {
  379. const modelID = "us.anthropic.claude-opus-4-5-20251101-v1:0"
  380. const hasPrefix = crossRegionPrefixes.some((prefix) => modelID.startsWith(prefix))
  381. expect(hasPrefix).toBe(true)
  382. })
  383. test("should detect eu. prefix", () => {
  384. const modelID = "eu.anthropic.claude-opus-4-5-20251101-v1:0"
  385. const hasPrefix = crossRegionPrefixes.some((prefix) => modelID.startsWith(prefix))
  386. expect(hasPrefix).toBe(true)
  387. })
  388. test("should detect jp. prefix", () => {
  389. const modelID = "jp.anthropic.claude-sonnet-4-20250514-v1:0"
  390. const hasPrefix = crossRegionPrefixes.some((prefix) => modelID.startsWith(prefix))
  391. expect(hasPrefix).toBe(true)
  392. })
  393. test("should detect apac. prefix", () => {
  394. const modelID = "apac.anthropic.claude-sonnet-4-20250514-v1:0"
  395. const hasPrefix = crossRegionPrefixes.some((prefix) => modelID.startsWith(prefix))
  396. expect(hasPrefix).toBe(true)
  397. })
  398. test("should detect au. prefix", () => {
  399. const modelID = "au.anthropic.claude-sonnet-4-5-20250929-v1:0"
  400. const hasPrefix = crossRegionPrefixes.some((prefix) => modelID.startsWith(prefix))
  401. expect(hasPrefix).toBe(true)
  402. })
  403. test("should NOT detect prefix for non-prefixed model", () => {
  404. const modelID = "anthropic.claude-opus-4-5-20251101-v1:0"
  405. const hasPrefix = crossRegionPrefixes.some((prefix) => modelID.startsWith(prefix))
  406. expect(hasPrefix).toBe(false)
  407. })
  408. test("should NOT detect prefix for amazon nova models", () => {
  409. const modelID = "amazon.nova-pro-v1:0"
  410. const hasPrefix = crossRegionPrefixes.some((prefix) => modelID.startsWith(prefix))
  411. expect(hasPrefix).toBe(false)
  412. })
  413. test("should NOT detect prefix for cohere models", () => {
  414. const modelID = "cohere.command-r-plus-v1:0"
  415. const hasPrefix = crossRegionPrefixes.some((prefix) => modelID.startsWith(prefix))
  416. expect(hasPrefix).toBe(false)
  417. })
  418. })