provider-amazon-bedrock.test.ts 26 KB

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