provider-amazon-bedrock.test.ts 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608
  1. import { AISDK } from "@opencode-ai/core/aisdk"
  2. import { describe, expect } from "bun:test"
  3. import type { LanguageModelV3 } from "@ai-sdk/provider"
  4. import { Effect } from "effect"
  5. import { Catalog } from "@opencode-ai/core/catalog"
  6. import { ModelV2 } from "@opencode-ai/core/model"
  7. import { PluginV2 } from "@opencode-ai/core/plugin"
  8. import { PluginHost } from "@opencode-ai/core/plugin/host"
  9. import { AmazonBedrockPlugin } from "@opencode-ai/core/plugin/provider/amazon-bedrock"
  10. import { ProviderV2 } from "@opencode-ai/core/provider"
  11. import { testEffect } from "../lib/effect"
  12. import { PluginTestLayer } from "./fixture"
  13. const it = testEffect(PluginTestLayer)
  14. const addPlugin = Effect.fn(function* () {
  15. const plugin = yield* PluginV2.Service
  16. const aisdk = yield* AISDK.Service
  17. const host = yield* PluginHost.make(plugin)
  18. yield* AmazonBedrockPlugin.effect(host)
  19. })
  20. function required<T>(value: T | undefined): T {
  21. if (value === undefined) throw new Error("Expected value")
  22. return value
  23. }
  24. function withEnv<A, E, R>(vars: Record<string, string | undefined>, fx: () => Effect.Effect<A, E, R>) {
  25. return Effect.acquireUseRelease(
  26. Effect.sync(() => {
  27. const previous = Object.fromEntries(Object.keys(vars).map((key) => [key, process.env[key]]))
  28. Object.entries(vars).forEach(([key, value]) => {
  29. if (value === undefined) delete process.env[key]
  30. else process.env[key] = value
  31. })
  32. return previous
  33. }),
  34. fx,
  35. (previous) =>
  36. Effect.sync(() => {
  37. Object.entries(previous).forEach(([key, value]) => {
  38. if (value === undefined) delete process.env[key]
  39. else process.env[key] = value
  40. })
  41. }),
  42. )
  43. }
  44. function fakeSelectorSdk(calls: string[]) {
  45. const make = (method: string) => (id: string) => {
  46. calls.push(`${method}:${id}`)
  47. return { modelId: id, provider: method, specificationVersion: "v3" } as unknown as LanguageModelV3
  48. }
  49. return {
  50. responses: make("responses"),
  51. messages: make("messages"),
  52. chat: make("chat"),
  53. languageModel: make("languageModel"),
  54. }
  55. }
  56. function bedrockBaseURL(sdk: unknown, modelID = "anthropic.claude-sonnet-4-5") {
  57. const language = (sdk as { languageModel: (id: string) => unknown }).languageModel(modelID)
  58. return (language as { config: { baseUrl: () => string } }).config.baseUrl()
  59. }
  60. function bedrockFetch(sdk: unknown, modelID = "anthropic.claude-sonnet-4-5") {
  61. const language = (sdk as { languageModel: (id: string) => unknown }).languageModel(modelID)
  62. return (
  63. language as { config: { fetch: (input: Parameters<typeof fetch>[0], init?: RequestInit) => Promise<Response> } }
  64. ).config.fetch
  65. }
  66. function openAIUrl(language: unknown, path: string, modelId: string) {
  67. return (language as { config: { url: (input: { path: string; modelId: string }) => string } }).config.url({
  68. path,
  69. modelId,
  70. })
  71. }
  72. describe("AmazonBedrockPlugin", () => {
  73. it.effect("moves endpoint setting to baseURL", () =>
  74. Effect.gen(function* () {
  75. const catalog = yield* Catalog.Service
  76. yield* catalog.transform((catalog) => {
  77. const bedrock = ProviderV2.Info.make({
  78. ...ProviderV2.Info.empty(ProviderV2.ID.amazonBedrock),
  79. package: ProviderV2.aisdk("@ai-sdk/amazon-bedrock"),
  80. settings: { endpoint: "https://bedrock.example" },
  81. })
  82. catalog.provider.update(bedrock.id, (item) => {
  83. item.package = bedrock.package
  84. item.settings = { endpoint: "https://bedrock.example" }
  85. })
  86. })
  87. yield* addPlugin()
  88. const result = required(yield* catalog.provider.get(ProviderV2.ID.amazonBedrock))
  89. expect(result.package).toBe(ProviderV2.aisdk("@ai-sdk/amazon-bedrock"))
  90. expect(result.settings).toEqual({ baseURL: "https://bedrock.example" })
  91. }),
  92. )
  93. it.effect("prefers endpoint over baseURL for SDK base URL", () =>
  94. withEnv({ AWS_BEARER_TOKEN_BEDROCK: undefined, AWS_PROFILE: undefined, AWS_ACCESS_KEY_ID: undefined }, () =>
  95. Effect.gen(function* () {
  96. const plugin = yield* PluginV2.Service
  97. const aisdk = yield* AISDK.Service
  98. yield* addPlugin()
  99. const result = yield* aisdk.runSDK({
  100. model: ModelV2.Info.make({
  101. ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
  102. modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
  103. package: ProviderV2.aisdk("test-provider"),
  104. }),
  105. package: "@ai-sdk/amazon-bedrock",
  106. options: {
  107. name: "amazon-bedrock",
  108. bearerToken: "token",
  109. baseURL: "https://base.example",
  110. endpoint: "https://endpoint.example",
  111. region: "us-east-1",
  112. },
  113. })
  114. expect(bedrockBaseURL(result.sdk)).toBe("https://endpoint.example")
  115. }),
  116. ),
  117. )
  118. it.effect("uses baseURL as SDK base URL", () =>
  119. withEnv({ AWS_BEARER_TOKEN_BEDROCK: undefined, AWS_PROFILE: undefined, AWS_ACCESS_KEY_ID: undefined }, () =>
  120. Effect.gen(function* () {
  121. const plugin = yield* PluginV2.Service
  122. const aisdk = yield* AISDK.Service
  123. yield* addPlugin()
  124. const result = yield* aisdk.runSDK({
  125. model: ModelV2.Info.make({
  126. ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
  127. modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
  128. package: ProviderV2.aisdk("test-provider"),
  129. }),
  130. package: "@ai-sdk/amazon-bedrock",
  131. options: {
  132. name: "amazon-bedrock",
  133. bearerToken: "token",
  134. baseURL: "https://base.example",
  135. region: "us-east-1",
  136. },
  137. })
  138. expect(bedrockBaseURL(result.sdk)).toBe("https://base.example")
  139. }),
  140. ),
  141. )
  142. it.effect("creates SDK without explicit credential env so the default AWS chain can resolve credentials", () =>
  143. withEnv(
  144. {
  145. AWS_ACCESS_KEY_ID: undefined,
  146. AWS_BEARER_TOKEN_BEDROCK: undefined,
  147. AWS_CONTAINER_CREDENTIALS_FULL_URI: undefined,
  148. AWS_CONTAINER_CREDENTIALS_RELATIVE_URI: undefined,
  149. AWS_PROFILE: undefined,
  150. AWS_REGION: undefined,
  151. AWS_WEB_IDENTITY_TOKEN_FILE: undefined,
  152. },
  153. () =>
  154. Effect.gen(function* () {
  155. const plugin = yield* PluginV2.Service
  156. const aisdk = yield* AISDK.Service
  157. yield* addPlugin()
  158. const result = yield* aisdk.runSDK({
  159. model: ModelV2.Info.make({
  160. ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
  161. modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
  162. package: ProviderV2.aisdk("test-provider"),
  163. }),
  164. package: "@ai-sdk/amazon-bedrock",
  165. options: { name: "amazon-bedrock" },
  166. })
  167. expect(result.sdk).toBeDefined()
  168. expect(bedrockBaseURL(result.sdk)).toBe("https://bedrock-runtime.us-east-1.amazonaws.com")
  169. }),
  170. ),
  171. )
  172. it.effect("uses config region over AWS_REGION for SDK base URL", () =>
  173. withEnv({ AWS_BEARER_TOKEN_BEDROCK: "token", AWS_REGION: "us-east-1" }, () =>
  174. Effect.gen(function* () {
  175. const plugin = yield* PluginV2.Service
  176. const aisdk = yield* AISDK.Service
  177. yield* addPlugin()
  178. const result = yield* aisdk.runSDK({
  179. model: ModelV2.Info.make({
  180. ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
  181. modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
  182. package: ProviderV2.aisdk("test-provider"),
  183. }),
  184. package: "@ai-sdk/amazon-bedrock",
  185. options: { name: "amazon-bedrock", region: "eu-west-1" },
  186. })
  187. expect(bedrockBaseURL(result.sdk)).toBe("https://bedrock-runtime.eu-west-1.amazonaws.com")
  188. }),
  189. ),
  190. )
  191. it.effect("uses AWS_REGION for SDK base URL when config region is absent", () =>
  192. withEnv({ AWS_BEARER_TOKEN_BEDROCK: "token", AWS_REGION: "eu-west-1" }, () =>
  193. Effect.gen(function* () {
  194. const plugin = yield* PluginV2.Service
  195. const aisdk = yield* AISDK.Service
  196. yield* addPlugin()
  197. const result = yield* aisdk.runSDK({
  198. model: ModelV2.Info.make({
  199. ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
  200. modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
  201. package: ProviderV2.aisdk("test-provider"),
  202. }),
  203. package: "@ai-sdk/amazon-bedrock",
  204. options: { name: "amazon-bedrock" },
  205. })
  206. expect(bedrockBaseURL(result.sdk)).toBe("https://bedrock-runtime.eu-west-1.amazonaws.com")
  207. }),
  208. ),
  209. )
  210. it.effect("defaults SDK region to us-east-1", () =>
  211. withEnv({ AWS_BEARER_TOKEN_BEDROCK: "token", AWS_REGION: undefined }, () =>
  212. Effect.gen(function* () {
  213. const plugin = yield* PluginV2.Service
  214. const aisdk = yield* AISDK.Service
  215. yield* addPlugin()
  216. const result = yield* aisdk.runSDK({
  217. model: ModelV2.Info.make({
  218. ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
  219. modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
  220. package: ProviderV2.aisdk("test-provider"),
  221. }),
  222. package: "@ai-sdk/amazon-bedrock",
  223. options: { name: "amazon-bedrock" },
  224. })
  225. expect(bedrockBaseURL(result.sdk)).toBe("https://bedrock-runtime.us-east-1.amazonaws.com")
  226. }),
  227. ),
  228. )
  229. it.effect("loads bearer token option into env and uses bearer auth", () =>
  230. withEnv({ AWS_ACCESS_KEY_ID: undefined, AWS_BEARER_TOKEN_BEDROCK: undefined, AWS_PROFILE: undefined }, () =>
  231. Effect.gen(function* () {
  232. const plugin = yield* PluginV2.Service
  233. const aisdk = yield* AISDK.Service
  234. const headers: Array<string | null> = []
  235. yield* addPlugin()
  236. const result = yield* aisdk.runSDK({
  237. model: ModelV2.Info.make({
  238. ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
  239. modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
  240. package: ProviderV2.aisdk("test-provider"),
  241. }),
  242. package: "@ai-sdk/amazon-bedrock",
  243. options: {
  244. name: "amazon-bedrock",
  245. bearerToken: "option-token",
  246. fetch: async (_input: Parameters<typeof fetch>[0], init?: RequestInit) => {
  247. headers.push(new Headers(init?.headers).get("Authorization"))
  248. return new Response("{}")
  249. },
  250. },
  251. })
  252. yield* Effect.promise(() => bedrockFetch(result.sdk)("https://bedrock.example", { method: "POST" }))
  253. expect(process.env.AWS_BEARER_TOKEN_BEDROCK).toBe("option-token")
  254. expect(headers).toEqual(["Bearer option-token"])
  255. }),
  256. ),
  257. )
  258. it.effect("prefers bearer token env over bearer token option", () =>
  259. withEnv({ AWS_BEARER_TOKEN_BEDROCK: "env-token" }, () =>
  260. Effect.gen(function* () {
  261. const plugin = yield* PluginV2.Service
  262. const aisdk = yield* AISDK.Service
  263. const headers: Array<string | null> = []
  264. yield* addPlugin()
  265. const result = yield* aisdk.runSDK({
  266. model: ModelV2.Info.make({
  267. ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
  268. modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
  269. package: ProviderV2.aisdk("test-provider"),
  270. }),
  271. package: "@ai-sdk/amazon-bedrock",
  272. options: {
  273. name: "amazon-bedrock",
  274. bearerToken: "option-token",
  275. fetch: async (_input: Parameters<typeof fetch>[0], init?: RequestInit) => {
  276. headers.push(new Headers(init?.headers).get("Authorization"))
  277. return new Response("{}")
  278. },
  279. },
  280. })
  281. yield* Effect.promise(() => bedrockFetch(result.sdk)("https://bedrock.example", { method: "POST" }))
  282. expect(process.env.AWS_BEARER_TOKEN_BEDROCK).toBe("env-token")
  283. expect(headers).toEqual(["Bearer env-token"])
  284. }),
  285. ),
  286. )
  287. it.effect("creates Mantle SDK with GPT-5 OpenAI base path", () =>
  288. withEnv({ AWS_BEARER_TOKEN_BEDROCK: undefined, AWS_PROFILE: undefined, AWS_ACCESS_KEY_ID: undefined }, () =>
  289. Effect.gen(function* () {
  290. const plugin = yield* PluginV2.Service
  291. const aisdk = yield* AISDK.Service
  292. yield* addPlugin()
  293. const result = yield* aisdk.runSDK({
  294. model: ModelV2.Info.make({
  295. ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("openai.gpt-5.5")),
  296. modelID: ModelV2.ID.make("openai.gpt-5.5"),
  297. package: ProviderV2.aisdk("@ai-sdk/amazon-bedrock/mantle"),
  298. }),
  299. package: "@ai-sdk/amazon-bedrock/mantle",
  300. options: {
  301. name: "amazon-bedrock",
  302. bearerToken: "token",
  303. baseURL: "https://bedrock-mantle.us-east-2.api.aws/openai/v1",
  304. region: "us-east-2",
  305. },
  306. })
  307. const language = result.sdk.responses("openai.gpt-5.5")
  308. expect(openAIUrl(language, "/responses", "openai.gpt-5.5")).toBe(
  309. "https://bedrock-mantle.us-east-2.api.aws/openai/v1/responses",
  310. )
  311. }),
  312. ),
  313. )
  314. it.effect("selects Mantle APIs without Bedrock cross-region prefixes", () =>
  315. Effect.gen(function* () {
  316. const plugin = yield* PluginV2.Service
  317. const aisdk = yield* AISDK.Service
  318. const calls: string[] = []
  319. yield* addPlugin()
  320. yield* aisdk.runLanguage({
  321. model: ModelV2.Info.make({
  322. ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("openai.gpt-5.5")),
  323. modelID: ModelV2.ID.make("openai.gpt-5.5"),
  324. package: ProviderV2.aisdk("@ai-sdk/amazon-bedrock/mantle"),
  325. }),
  326. sdk: fakeSelectorSdk(calls),
  327. options: { baseURL: "https://bedrock-mantle.us-east-2.api.aws/openai/v1", region: "us-east-2" },
  328. })
  329. yield* aisdk.runLanguage({
  330. model: ModelV2.Info.make({
  331. ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("openai.gpt-oss-safeguard-120b")),
  332. modelID: ModelV2.ID.make("openai.gpt-oss-safeguard-120b"),
  333. package: ProviderV2.aisdk("@ai-sdk/amazon-bedrock/mantle"),
  334. }),
  335. sdk: fakeSelectorSdk(calls),
  336. options: { region: "us-east-1" },
  337. })
  338. expect(calls).toEqual(["responses:openai.gpt-5.5", "chat:openai.gpt-oss-safeguard-120b"])
  339. }),
  340. )
  341. it.effect("ignores other Bedrock provider subpaths", () =>
  342. Effect.gen(function* () {
  343. const plugin = yield* PluginV2.Service
  344. const aisdk = yield* AISDK.Service
  345. yield* addPlugin()
  346. const result = yield* aisdk.runSDK({
  347. model: ModelV2.Info.make({
  348. ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
  349. modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
  350. package: ProviderV2.aisdk("@ai-sdk/amazon-bedrock/anthropic"),
  351. }),
  352. package: "@ai-sdk/amazon-bedrock/anthropic",
  353. options: { name: "amazon-bedrock" },
  354. })
  355. expect(result.sdk).toBeUndefined()
  356. }),
  357. )
  358. it.effect("uses SigV4 credential env when bearer token is absent", () =>
  359. withEnv(
  360. {
  361. AWS_ACCESS_KEY_ID: "test-access-key",
  362. AWS_BEARER_TOKEN_BEDROCK: undefined,
  363. AWS_REGION: "us-east-1",
  364. AWS_SECRET_ACCESS_KEY: "test-secret-key",
  365. AWS_SESSION_TOKEN: "test-session-token",
  366. },
  367. () =>
  368. Effect.gen(function* () {
  369. const plugin = yield* PluginV2.Service
  370. const aisdk = yield* AISDK.Service
  371. const headers: Array<string | null> = []
  372. yield* addPlugin()
  373. const result = yield* aisdk.runSDK({
  374. model: ModelV2.Info.make({
  375. ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
  376. modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
  377. package: ProviderV2.aisdk("test-provider"),
  378. }),
  379. package: "@ai-sdk/amazon-bedrock",
  380. options: {
  381. name: "amazon-bedrock",
  382. fetch: async (_input: Parameters<typeof fetch>[0], init?: RequestInit) => {
  383. headers.push(new Headers(init?.headers).get("Authorization"))
  384. return new Response("{}")
  385. },
  386. },
  387. })
  388. yield* Effect.promise(() =>
  389. bedrockFetch(result.sdk)("https://bedrock-runtime.us-east-1.amazonaws.com/model/test/invoke", {
  390. body: "{}",
  391. method: "POST",
  392. }),
  393. )
  394. expect(headers[0]?.startsWith("AWS4-HMAC-SHA256 ")).toBe(true)
  395. }),
  396. ),
  397. )
  398. it.effect("applies legacy cross-region inference prefixes", () =>
  399. Effect.gen(function* () {
  400. const plugin = yield* PluginV2.Service
  401. const aisdk = yield* AISDK.Service
  402. const calls: string[] = []
  403. yield* addPlugin()
  404. yield* aisdk.runLanguage({
  405. model: ModelV2.Info.make({
  406. ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
  407. modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
  408. package: ProviderV2.aisdk("test-provider"),
  409. }),
  410. sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
  411. options: {},
  412. })
  413. yield* aisdk.runLanguage({
  414. model: ModelV2.Info.make({
  415. ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
  416. modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
  417. package: ProviderV2.aisdk("test-provider"),
  418. }),
  419. sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
  420. options: { region: "eu-west-1" },
  421. })
  422. yield* aisdk.runLanguage({
  423. model: ModelV2.Info.make({
  424. ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("global.anthropic.claude-sonnet-4-5")),
  425. modelID: ModelV2.ID.make("global.anthropic.claude-sonnet-4-5"),
  426. package: ProviderV2.aisdk("test-provider"),
  427. }),
  428. sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
  429. options: { region: "eu-west-1" },
  430. })
  431. yield* aisdk.runLanguage({
  432. model: ModelV2.Info.make({
  433. ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
  434. modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
  435. package: ProviderV2.aisdk("test-provider"),
  436. }),
  437. sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
  438. options: { region: "ap-northeast-1" },
  439. })
  440. yield* aisdk.runLanguage({
  441. model: ModelV2.Info.make({
  442. ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
  443. modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
  444. package: ProviderV2.aisdk("test-provider"),
  445. }),
  446. sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
  447. options: { region: "ap-southeast-2" },
  448. })
  449. expect(calls).toEqual([
  450. "languageModel:us.anthropic.claude-sonnet-4-5",
  451. "languageModel:eu.anthropic.claude-sonnet-4-5",
  452. "languageModel:global.anthropic.claude-sonnet-4-5",
  453. "languageModel:jp.anthropic.claude-sonnet-4-5",
  454. "languageModel:au.anthropic.claude-sonnet-4-5",
  455. ])
  456. }),
  457. )
  458. it.effect("uses AWS_REGION for language prefixes when region option is absent", () =>
  459. withEnv({ AWS_REGION: "eu-west-1" }, () =>
  460. Effect.gen(function* () {
  461. const plugin = yield* PluginV2.Service
  462. const aisdk = yield* AISDK.Service
  463. const calls: string[] = []
  464. yield* addPlugin()
  465. yield* aisdk.runLanguage({
  466. model: ModelV2.Info.make({
  467. ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
  468. modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
  469. package: ProviderV2.aisdk("test-provider"),
  470. }),
  471. sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
  472. options: {},
  473. })
  474. expect(calls).toEqual(["languageModel:eu.anthropic.claude-sonnet-4-5"])
  475. }),
  476. ),
  477. )
  478. it.effect("applies the full legacy cross-region prefix matrix", () =>
  479. Effect.gen(function* () {
  480. const plugin = yield* PluginV2.Service
  481. const aisdk = yield* AISDK.Service
  482. const calls: string[] = []
  483. const cases = [
  484. { region: "us-east-1", modelID: "amazon.nova-micro-v1:0", expected: "us.amazon.nova-micro-v1:0" },
  485. { region: "us-east-1", modelID: "amazon.nova-lite-v1:0", expected: "us.amazon.nova-lite-v1:0" },
  486. { region: "us-east-1", modelID: "amazon.nova-pro-v1:0", expected: "us.amazon.nova-pro-v1:0" },
  487. { region: "us-east-1", modelID: "amazon.nova-premier-v1:0", expected: "us.amazon.nova-premier-v1:0" },
  488. { region: "us-east-1", modelID: "amazon.nova-2-lite-v1:0", expected: "us.amazon.nova-2-lite-v1:0" },
  489. { region: "us-east-1", modelID: "anthropic.claude-sonnet-4-5", expected: "us.anthropic.claude-sonnet-4-5" },
  490. { region: "us-east-1", modelID: "deepseek.r1-v1:0", expected: "us.deepseek.r1-v1:0" },
  491. { region: "us-gov-west-1", modelID: "anthropic.claude-sonnet-4-5", expected: "anthropic.claude-sonnet-4-5" },
  492. { region: "us-east-1", modelID: "cohere.command-r-plus-v1:0", expected: "cohere.command-r-plus-v1:0" },
  493. { region: "eu-west-1", modelID: "anthropic.claude-sonnet-4-5", expected: "eu.anthropic.claude-sonnet-4-5" },
  494. { region: "eu-west-2", modelID: "amazon.nova-lite-v1:0", expected: "eu.amazon.nova-lite-v1:0" },
  495. { region: "eu-west-3", modelID: "amazon.nova-micro-v1:0", expected: "eu.amazon.nova-micro-v1:0" },
  496. {
  497. region: "eu-north-1",
  498. modelID: "meta.llama3-70b-instruct-v1:0",
  499. expected: "eu.meta.llama3-70b-instruct-v1:0",
  500. },
  501. { region: "eu-central-1", modelID: "mistral.pixtral-large-v1:0", expected: "eu.mistral.pixtral-large-v1:0" },
  502. { region: "eu-south-1", modelID: "anthropic.claude-sonnet-4-5", expected: "eu.anthropic.claude-sonnet-4-5" },
  503. { region: "eu-south-2", modelID: "anthropic.claude-sonnet-4-5", expected: "eu.anthropic.claude-sonnet-4-5" },
  504. { region: "eu-central-2", modelID: "anthropic.claude-sonnet-4-5", expected: "anthropic.claude-sonnet-4-5" },
  505. { region: "eu-west-1", modelID: "cohere.command-r-plus-v1:0", expected: "cohere.command-r-plus-v1:0" },
  506. {
  507. region: "ap-southeast-2",
  508. modelID: "anthropic.claude-sonnet-4-5",
  509. expected: "au.anthropic.claude-sonnet-4-5",
  510. },
  511. {
  512. region: "ap-southeast-4",
  513. modelID: "anthropic.claude-haiku-v1:0",
  514. expected: "au.anthropic.claude-haiku-v1:0",
  515. },
  516. { region: "ap-southeast-2", modelID: "anthropic.claude-opus-4", expected: "apac.anthropic.claude-opus-4" },
  517. {
  518. region: "ap-northeast-1",
  519. modelID: "anthropic.claude-sonnet-4-5",
  520. expected: "jp.anthropic.claude-sonnet-4-5",
  521. },
  522. { region: "ap-northeast-1", modelID: "amazon.nova-pro-v1:0", expected: "jp.amazon.nova-pro-v1:0" },
  523. { region: "ap-south-1", modelID: "anthropic.claude-sonnet-4-5", expected: "apac.anthropic.claude-sonnet-4-5" },
  524. { region: "ap-south-1", modelID: "amazon.nova-lite-v1:0", expected: "apac.amazon.nova-lite-v1:0" },
  525. { region: "ca-central-1", modelID: "anthropic.claude-sonnet-4-5", expected: "anthropic.claude-sonnet-4-5" },
  526. {
  527. region: "us-east-1",
  528. modelID: "global.anthropic.claude-sonnet-4-5",
  529. expected: "global.anthropic.claude-sonnet-4-5",
  530. },
  531. { region: "us-east-1", modelID: "us.anthropic.claude-sonnet-4-5", expected: "us.anthropic.claude-sonnet-4-5" },
  532. { region: "eu-west-1", modelID: "eu.anthropic.claude-sonnet-4-5", expected: "eu.anthropic.claude-sonnet-4-5" },
  533. {
  534. region: "ap-northeast-1",
  535. modelID: "jp.anthropic.claude-sonnet-4-5",
  536. expected: "jp.anthropic.claude-sonnet-4-5",
  537. },
  538. {
  539. region: "ap-south-1",
  540. modelID: "apac.anthropic.claude-sonnet-4-5",
  541. expected: "apac.anthropic.claude-sonnet-4-5",
  542. },
  543. {
  544. region: "ap-southeast-2",
  545. modelID: "au.anthropic.claude-sonnet-4-5",
  546. expected: "au.anthropic.claude-sonnet-4-5",
  547. },
  548. ]
  549. yield* addPlugin()
  550. for (const item of cases) {
  551. yield* aisdk.runLanguage({
  552. model: ModelV2.Info.make({
  553. ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make(item.modelID)),
  554. modelID: ModelV2.ID.make(item.modelID),
  555. package: ProviderV2.aisdk("test-provider"),
  556. }),
  557. sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
  558. options: { region: item.region },
  559. })
  560. }
  561. expect(calls).toEqual(cases.map((item) => `languageModel:${item.expected}`))
  562. }),
  563. )
  564. it.effect("ignores non-Bedrock providers for language selection", () =>
  565. Effect.gen(function* () {
  566. const plugin = yield* PluginV2.Service
  567. const aisdk = yield* AISDK.Service
  568. const calls: string[] = []
  569. yield* addPlugin()
  570. const result = yield* aisdk.runLanguage({
  571. model: ModelV2.Info.make({
  572. ...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
  573. modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
  574. package: ProviderV2.aisdk("test-provider"),
  575. }),
  576. sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
  577. options: { region: "eu-west-1" },
  578. })
  579. expect(calls).toEqual([])
  580. expect(result.language).toBeUndefined()
  581. }),
  582. )
  583. })