provider-amazon-bedrock.test.ts 27 KB

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