amazon-bedrock.test.ts 13 KB

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