Compare commits

...
Author SHA1 Message Date
Aiden Cline a42f173b47 fix(ai): resolve Bedrock regions without implicit defaults 2026-08-26 17:12:01 -05:00
15 changed files with 311 additions and 37 deletions
+2
View File
@@ -335,6 +335,8 @@ OpenAI Responses has one semantic route and uses HTTP by default. Advanced calle
Vertex Gemini, Vertex Chat, Vertex Responses, and Vertex Messages are separate API entrypoints. All accept `project`, `location`, and an optional `accessToken`; when no explicit token or auth override is supplied they lazily use Google Application Default Credentials. Vertex Gemini instead selects express mode when `apiKey` or `GOOGLE_VERTEX_API_KEY` is present. Vertex Chat targets MaaS models through the OpenAI-compatible Chat Completions endpoint, while Vertex Responses targets Grok models and defaults `store` to `false` as required by Vertex. `providers/google-vertex` remains the default alias for `providers/google-vertex/gemini`.
Bedrock Converse and Mantle resolve the region from explicit `region`, then existing `credentials.region`, then `AWS_REGION`; there is no default region. Missing or blank regions fail with a typed configuration error when selecting a model, not when importing the package or creating an unconfigured facade. A custom `baseURL` with bearer `apiKey` authentication can omit the region because neither endpoint construction nor signing needs it. SigV4 still requires a region, and the resolved region is used for both the generated endpoint and signing. AWS profile-region discovery and credential refresh are not provided by this resolution step.
Tuned Vertex Gemini deployments use model ids shaped like `endpoints/1234567890` and require OAuth or ADC; Vertex express-mode API keys support publisher models only.
```ts
@@ -6,6 +6,7 @@ import { OpenAIResponses } from "../protocols/openai-responses.js"
import { BedrockAuth, type Credentials } from "../protocols/utils/bedrock-auth.js"
import { ProviderID, type ModelID } from "../schema/index.js"
import { withOpenAIOptions, type OpenAIProviderOptionsInput } from "./openai-options.js"
import { BedrockRegion } from "./amazon-bedrock-region.js"
export const id = ProviderID.make("amazon-bedrock")
@@ -13,6 +14,7 @@ export type Config = RouteDefaultsInput & {
readonly apiKey?: string
readonly baseURL?: string
readonly credentials?: Credentials
/** AWS region. Falls back to `credentials.region`, then `AWS_REGION`. */
readonly region?: string
readonly providerOptions?: OpenAIProviderOptionsInput
}
@@ -47,8 +49,11 @@ const chatRoute = OpenAIChat.route.with({
export const routes = [responsesRoute, chatRoute]
const configuredRoute = <Body, Prepared>(route: Route<Body, Prepared>, input: Config) => {
const region = input.region ?? input.credentials?.region ?? "us-east-1"
const credentials = input.credentials === undefined ? undefined : { ...input.credentials, region }
const region = BedrockRegion.resolve(input.region, input.credentials?.region)
if (region !== undefined || input.baseURL === undefined || input.apiKey === undefined)
BedrockRegion.require(region, "Amazon Bedrock Mantle")
const credentials =
input.credentials === undefined ? undefined : { ...input.credentials, region: region ?? input.credentials.region }
return route.with({
endpoint: { baseURL: input.baseURL ?? `https://bedrock-mantle.${region}.api.aws/v1` },
auth:
@@ -64,15 +69,13 @@ const defaults = (input: Config) => {
}
export const configure = (input: Config = {}) => {
const configuredResponsesRoute = configuredRoute(responsesRoute, input)
const configuredChatRoute = configuredRoute(chatRoute, input)
const modelDefaults = defaults(input)
const responses = (modelID: string | ModelID) =>
configuredResponsesRoute
configuredRoute(responsesRoute, input)
.with(withOpenAIOptions(modelID, modelDefaults))
.model<OpenAIProviderOptionsInput>({ id: modelID })
const chat = (modelID: string | ModelID) =>
configuredChatRoute
configuredRoute(chatRoute, input)
.with(withOpenAIOptions(modelID, modelDefaults))
.model<OpenAIProviderOptionsInput>({ id: modelID })
@@ -0,0 +1,18 @@
import { AIError, InvalidRequestReason } from "../schema/errors.js"
export const resolve = (region?: string, credentialRegion?: string) =>
(region ?? credentialRegion ?? process.env.AWS_REGION)?.trim()
export const require = (region: string | undefined, provider: string) => {
if (region) return region
throw new AIError({
module: provider,
method: "configure",
reason: new InvalidRequestReason({
parameter: "region",
message: `${provider} requires an AWS region. Set region or AWS_REGION.`,
}),
})
}
export * as BedrockRegion from "./amazon-bedrock-region.js"
+13 -8
View File
@@ -4,6 +4,7 @@ import type { ProviderPackage } from "../provider-package.js"
import { ProviderID, type ModelID } from "../schema/index.js"
import * as BedrockConverse from "../protocols/bedrock-converse.js"
import type { BedrockCredentials } from "../protocols/bedrock-converse.js"
import { BedrockRegion } from "./amazon-bedrock-region.js"
export const id = ProviderID.make("amazon-bedrock")
@@ -11,7 +12,7 @@ export type Config = RouteDefaultsInput & {
readonly apiKey?: string
readonly headers?: Record<string, string>
readonly credentials?: BedrockCredentials
/** AWS region. Defaults to `us-east-1` when neither this nor `credentials.region` is set. */
/** AWS region. Falls back to `credentials.region`, then `AWS_REGION`. */
readonly region?: string
/** Override the computed `https://bedrock-runtime.<region>.amazonaws.com` URL. */
readonly baseURL?: string
@@ -27,25 +28,29 @@ export interface Settings extends ProviderPackage.Settings {
}
export const routes = [BedrockConverse.route]
const bedrockBaseURL = (region: string) => `https://bedrock-runtime.${region}.amazonaws.com`
const configuredRoute = (input: Config) => {
const { apiKey, credentials, region, baseURL, ...rest } = input
const resolvedRegion = region ?? credentials?.region ?? "us-east-1"
const resolvedRegion = BedrockRegion.resolve(region, credentials?.region)
if (resolvedRegion !== undefined || baseURL === undefined || apiKey === undefined)
BedrockRegion.require(resolvedRegion, "Amazon Bedrock")
return BedrockConverse.route.with({
...rest,
provider: id,
providerMetadataKey: "bedrock",
endpoint: { baseURL: baseURL ?? bedrockBaseURL(resolvedRegion) },
auth: apiKey === undefined ? BedrockConverse.sigV4Auth(credentials) : Auth.bearer(apiKey),
endpoint: { baseURL: baseURL ?? `https://bedrock-runtime.${resolvedRegion}.amazonaws.com` },
auth:
apiKey === undefined
? BedrockConverse.sigV4Auth(
credentials === undefined ? undefined : { ...credentials, region: resolvedRegion ?? credentials.region },
)
: Auth.bearer(apiKey),
})
}
export const configure = (input: Config = {}) => {
const route = configuredRoute(input)
return {
id,
model: (modelID: string | ModelID) => route.model({ id: modelID }),
model: (modelID: string | ModelID) => configuredRoute(input).model({ id: modelID }),
configure,
}
}
+1 -1
View File
@@ -36,7 +36,7 @@ describe("provider package entrypoints", () => {
expect(modules[0].model).toBe(modules[1].model)
expect(modules[8].model).toBe(modules[9].model)
expect(modules[12].model).toBe(modules[13].model)
expect(modules[19].model).toBe(modules[20].model)
expect(modules[19].model).toBe(modules[21].model)
})
test("maps DeepInfra package settings onto its native executable model", async () => {
@@ -858,6 +858,7 @@ describe("Bedrock Converse route", () => {
Effect.gen(function* () {
const unsignedModel = AmazonBedrock.configure({
baseURL: "https://bedrock-runtime.test",
region: "us-east-1",
}).model("anthropic.claude-3-5-sonnet-20240620-v1:0")
const error = yield* LLMClient.generate(LLMRequest.update(baseRequest, { model: unsignedModel })).pipe(
Effect.provide(fixedBytes(eventStreamBody(["messageStop", { stopReason: "end_turn" }]))),
@@ -47,7 +47,7 @@ describe("Amazon Bedrock Mantle provider", () => {
it.effect("preserves configured top-p generation defaults for Chat and Responses", () =>
Effect.gen(function* () {
const settings = { apiKey: "test-key", topP: 0.8 }
const settings = { apiKey: "test-key", region: "us-east-1", topP: 0.8 }
const chat = yield* compileRequest(
LLM.request({ model: AmazonBedrockMantle.chatModel("openai.gpt-oss-safeguard-20b", settings), prompt: "Hi" }),
)
@@ -109,7 +109,9 @@ describe("Amazon Bedrock Mantle provider", () => {
it.effect("replays reasoning with Mantle's message-prefixed item ids", () =>
Effect.gen(function* () {
const model = AmazonBedrockMantle.configure({ apiKey: "test-key" }).responses("openai.gpt-oss-120b")
const model = AmazonBedrockMantle.configure({ apiKey: "test-key", region: "us-east-1" }).responses(
"openai.gpt-oss-120b",
)
const item = { type: "reasoning", id: "msg_95d4d0af4350432a", encrypted_content: "mantle-state" }
const response = yield* LLMClient.generate(LLM.request({ model, prompt: "Think." })).pipe(
Effect.provide(
@@ -0,0 +1,113 @@
import { describe, expect } from "bun:test"
import { Effect } from "effect"
import { Headers } from "effect/unstable/http"
import { AIError, LLM } from "../../src/index.js"
import { AmazonBedrock, AmazonBedrockMantle } from "../../src/providers.js"
import { it } from "../lib/effect.js"
const providers = [AmazonBedrock, AmazonBedrockMantle]
const credentials = {
region: "us-east-2",
accessKeyId: "test-access-key",
secretAccessKey: "test-secret-key",
}
function withRegion<A, E, R>(region: string | undefined, effect: () => Effect.Effect<A, E, R>) {
return Effect.acquireUseRelease(
Effect.sync(() => {
const previous = process.env.AWS_REGION
if (region === undefined) delete process.env.AWS_REGION
else process.env.AWS_REGION = region
return previous
}),
effect,
(previous) =>
Effect.sync(() => {
if (previous === undefined) delete process.env.AWS_REGION
else process.env.AWS_REGION = previous
}),
)
}
describe("Bedrock region resolution", () => {
it.effect("uses AWS_REGION for bearer endpoints and lets an explicit region override it", () =>
withRegion("eu-west-1", () =>
Effect.gen(function* () {
for (const provider of providers) {
for (const region of [undefined, "us-west-2"]) {
const model = provider.configure({ apiKey: "test", region }).model("model")
expect(model.route.endpoint.baseURL).toContain(`.${region ?? "eu-west-1"}.`)
}
}
}),
),
)
it.effect("uses the same resolved region for the endpoint and signing without mutating credentials", () =>
withRegion("eu-west-1", () =>
Effect.gen(function* () {
for (const provider of providers) {
for (const region of [undefined, "us-west-2"]) {
const model = provider.configure({ credentials, region }).model("model")
const expected = region ?? credentials.region
expect(model.route.endpoint.baseURL).toContain(`.${expected}.`)
const headers = yield* model.route.auth.apply({
request: LLM.request({ model, prompt: "Hi" }),
method: "POST",
url: `${model.route.endpoint.baseURL}/test`,
body: "{}",
headers: Headers.empty,
})
expect(headers.authorization).toContain(`/${expected}/bedrock`)
expect(credentials.region).toBe("us-east-2")
}
}
}),
),
)
it.effect("allows unconfigured facades but rejects model initialization without a region", () =>
withRegion(undefined, () =>
Effect.gen(function* () {
for (const provider of providers) {
expect(() => provider.configure()).not.toThrow()
expect(() => provider.provider.model("model")).toThrow(AIError)
expect(() => provider.configure({ apiKey: "test" }).model("model")).toThrow("Set region or AWS_REGION")
expect(() => provider.configure({ baseURL: "https://gateway.test/v1" }).model("model")).toThrow(AIError)
expect(() => provider.model("model", { apiKey: "test" })).toThrow(AIError)
}
expect(() => AmazonBedrockMantle.configure({ apiKey: "test" }).chat("model")).toThrow(AIError)
}),
),
)
it.effect("does not require a region for custom endpoints with bearer auth", () =>
withRegion(undefined, () =>
Effect.gen(function* () {
for (const provider of providers) {
const model = provider.configure({ apiKey: "test", baseURL: "https://gateway.test/v1" }).model("model")
expect(model.route.endpoint.baseURL).toBe("https://gateway.test/v1")
}
}),
),
)
it.effect("rejects blank configured and environment regions rather than inventing a default", () =>
withRegion(" ", () =>
Effect.gen(function* () {
for (const provider of providers) {
expect(() => provider.configure({ apiKey: "test" }).model("model")).toThrow(AIError)
expect(
provider.configure({ apiKey: "test", region: " us-west-2 " }).model("model").route.endpoint.baseURL,
).toContain(".us-west-2.")
}
yield* withRegion("eu-west-1", () =>
Effect.sync(() => {
for (const provider of providers)
expect(() => provider.configure({ apiKey: "test", region: "" }).model("model")).toThrow(AIError)
}),
)
}),
),
)
})
@@ -39,9 +39,9 @@ describe("native OpenAI-compatible providers", () => {
[OpenAI.configure({ apiKey: "test" }).responses("model"), "openai"],
[Azure.configure({ resourceName: "resource", apiKey: "test" }).chat("model"), "azure"],
[Azure.configure({ resourceName: "resource", apiKey: "test" }).responses("model"), "azure"],
[AmazonBedrock.configure({ apiKey: "test" }).model("model"), "bedrock"],
[AmazonBedrockMantle.configure({ apiKey: "test" }).chat("model"), "mantle"],
[AmazonBedrockMantle.configure({ apiKey: "test" }).responses("model"), "mantle"],
[AmazonBedrock.configure({ apiKey: "test", region: "us-east-1" }).model("model"), "bedrock"],
[AmazonBedrockMantle.configure({ apiKey: "test", region: "us-east-1" }).chat("model"), "mantle"],
[AmazonBedrockMantle.configure({ apiKey: "test", region: "us-east-1" }).responses("model"), "mantle"],
[Google.configure({ apiKey: "test" }).model("model"), "google"],
[GoogleVertex.configure(vertex).model("model"), "vertex"],
[GoogleVertexChat.configure(vertex).model("model"), "vertex"],
+9 -11
View File
@@ -1,6 +1,7 @@
export * as AISDKNative from "./aisdk-native.js"
import { isRecord } from "@opencode-ai/ai/utils/record"
import { BedrockRegion } from "@opencode-ai/ai/providers/amazon-bedrock-region"
import { Provider } from "./provider.js"
export interface Mapping {
@@ -178,17 +179,15 @@ function mapBedrockSettings(
: undefined
const region = bedrockRegion(settings)
const credentials = mapBedrockCredentials(settings, region)
const baseURL = typeof baseSettings.baseURL === "string" ? baseSettings.baseURL : settings.endpoint
return {
...baseSettings,
...(typeof baseSettings.baseURL === "string" && region !== undefined
? { baseURL: baseSettings.baseURL.replaceAll("${AWS_REGION}", region) }
: {}),
...(typeof settings.baseURL !== "string" && typeof settings.endpoint === "string"
? { baseURL: settings.endpoint }
...(typeof baseURL === "string"
? { baseURL: region === undefined ? baseURL : baseURL.replaceAll("${AWS_REGION}", region) }
: {}),
...(apiKey === undefined ? {} : { apiKey }),
...(credentials === undefined ? {} : { credentials }),
...(typeof settings.region === "string" ? { region: settings.region } : {}),
...(region === undefined ? {} : { region }),
...(typeof settings.topP === "number" ? { topP: settings.topP } : {}),
}
}
@@ -265,11 +264,10 @@ function mapBedrockCredentials(settings: Readonly<Record<string, unknown>>, regi
function bedrockRegion(settings: Readonly<Record<string, unknown>>) {
const credentials = isRecord(settings.credentials) ? settings.credentials : settings
return typeof settings.region === "string"
? settings.region
: typeof credentials.region === "string"
? credentials.region
: undefined
return BedrockRegion.resolve(
typeof settings.region === "string" ? settings.region : undefined,
typeof credentials.region === "string" ? credentials.region : undefined,
)
}
function mapOpenAIOptions(settings: Readonly<Record<string, unknown>>) {
+1
View File
@@ -38,6 +38,7 @@ export const layer = Layer.effect(
const runText = Effect.fn("Generate.text")(function* (input: TextInput) {
const resolved = yield* resolver.resolve(input.model).pipe(
Effect.catchTags({
"AI.Error": (error) => new UnavailableError({ message: error.message, service: input.model?.providerID }),
"SessionRunnerModel.VariantUnavailableError": (error) =>
input.model
? new ModelSelectionError({ message: error.message })
+15 -4
View File
@@ -1,8 +1,10 @@
export * as ModelResolver from "./model-resolver.js"
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
import { LanguageModel } from "@opencode-ai/ai"
import { AIError, LanguageModel } from "@opencode-ai/ai"
import { BedrockRegion } from "@opencode-ai/ai/providers/amazon-bedrock-region"
import { Auth } from "@opencode-ai/ai/route"
import { isRecord } from "@opencode-ai/ai/utils/record"
import { Context, Effect, Layer, Schema } from "effect"
import { produce } from "immer"
import { AISDK } from "./aisdk.js"
@@ -54,6 +56,7 @@ export class UnresolvedProviderVariablesError extends Schema.TaggedError<Unresol
}
export type Error =
| AIError
| VariantUnavailableError
| UnsupportedPackageError
| UnresolvedProviderVariablesError
@@ -113,7 +116,7 @@ export const fromCatalogModel = (
model: Info,
credential?: Credential.Value,
dependencies?: Dependencies,
): Effect.Effect<LanguageModel, UnsupportedPackageError | UnresolvedProviderVariablesError> =>
): Effect.Effect<LanguageModel, AIError | UnsupportedPackageError | UnresolvedProviderVariablesError> =>
resolveCatalogModel(model, credential, dependencies).pipe(
Effect.flatMap((resolved) => validateProviderVariables(model, resolved)),
)
@@ -175,7 +178,7 @@ const resolveCatalogModel = Effect.fn("ModelResolver.resolveCatalogModel")(funct
: runtime.compatibility,
})
},
catch: () => unsupported(resolved),
catch: (error) => (error instanceof AIError ? error : unsupported(resolved)),
})
})
@@ -204,7 +207,15 @@ function prepareProviderSettings(
): Effect.Effect<Readonly<Record<string, unknown>>, UnresolvedProviderVariablesError> {
const baseURL = settings.baseURL
if (typeof baseURL !== "string") return Effect.succeed(settings)
return prepareProviderURL(model, baseURL).pipe(
const region = model.package?.startsWith("@opencode-ai/ai/providers/amazon-bedrock")
? BedrockRegion.resolve(
typeof settings.region === "string" ? settings.region : undefined,
isRecord(settings.credentials) && typeof settings.credentials.region === "string"
? settings.credentials.region
: undefined,
)
: undefined
return prepareProviderURL(model, region === undefined ? baseURL : baseURL.replaceAll("${AWS_REGION}", region)).pipe(
Effect.map((prepared) => (prepared === baseURL ? settings : { ...settings, baseURL: prepared })),
)
}
+1
View File
@@ -268,6 +268,7 @@ describe("AISDKNative", () => {
region: "eu-west-1",
},
baseURL: "https://bedrock-mantle.eu-west-1.api.aws/v1",
region: "eu-west-1",
providerOptions: { store: false },
},
})
+22 -1
View File
@@ -18,6 +18,11 @@ const selected = Info.make({
package: Provider.aisdk("@ai-sdk/mistral"),
})
const runtime = LanguageModel.make({ id: "gemini", provider: "test-provider", route: OpenAIChat.route })
const bedrock = Info.make({
...Info.default(Provider.ID.amazonBedrock, ID.make("bedrock-model")),
package: Provider.aisdk("@ai-sdk/amazon-bedrock"),
settings: { region: "" },
})
const catalog = Layer.mock(Catalog.Service, {
provider: {
@@ -26,7 +31,7 @@ const catalog = Layer.mock(Catalog.Service, {
available: () => Effect.die("unused"),
},
model: {
get: () => Effect.succeed(selected),
get: (_providerID, modelID) => Effect.succeed(modelID === bedrock.id ? bedrock : selected),
all: () => Effect.die("unused"),
available: () => Effect.die("unused"),
default: () => Effect.die("unused"),
@@ -97,3 +102,19 @@ resolverIt.effect("resolves dynamic models with their catalog metadata", () =>
})
}),
)
it.effect("reports missing Bedrock region as a configuration failure rather than an unsupported package", () =>
Effect.gen(function* () {
const generate = yield* Generate.Service
const error = yield* generate
.text({
prompt: "Hi",
model: Ref.make({ providerID: bedrock.providerID, id: bedrock.id }),
})
.pipe(Effect.flip)
expect(error).toBeInstanceOf(Generate.UnavailableError)
expect(error.message).toContain("Set region or AWS_REGION")
expect(error.message).not.toContain("Unsupported package")
}),
)
+99 -1
View File
@@ -1,5 +1,5 @@
import { describe, expect } from "bun:test"
import { LLM, LanguageModel, Message } from "@opencode-ai/ai"
import { AIError, LLM, LanguageModel, Message } from "@opencode-ai/ai"
import { OpenAIChat } from "@opencode-ai/ai/protocols"
import { compileRequest } from "@opencode-ai/ai/route/client"
import { ConfigProvider, Effect, Layer } from "effect"
@@ -9,6 +9,7 @@ import { Integration } from "@opencode-ai/core/integration"
import { Compatibility, ID, Info, VariantID } from "@opencode-ai/core/model"
import { Provider } from "@opencode-ai/core/provider"
import { ModelResolver } from "@opencode-ai/core/model-resolver"
import { toSessionError } from "@opencode-ai/core/session/to-session-error"
import { Catalog } from "@opencode-ai/core/catalog"
import { AISDK } from "@opencode-ai/core/aisdk"
import { Npm } from "@opencode-ai/util/npm"
@@ -217,6 +218,103 @@ describe("ModelResolver", () => {
),
)
it.effect("resolves Bedrock environment regions before mapping credentials and endpoint templates", () =>
withEnv({ AWS_REGION: "eu-west-1" }, () =>
Effect.gen(function* () {
for (const packageName of ["@ai-sdk/amazon-bedrock", "@ai-sdk/amazon-bedrock/mantle"]) {
for (const field of ["baseURL", "endpoint"]) {
for (const region of [undefined, "us-west-2"]) {
const resolved = yield* ModelResolver.fromCatalogModel(
model(Provider.aisdk(packageName), {
settings: {
...(region === undefined ? {} : { region }),
[field]: "https://gateway.${AWS_REGION}.test/v1",
accessKeyId: "test-access-key",
secretAccessKey: "test-secret-key",
},
}),
)
const expected = region ?? "eu-west-1"
expect(resolved.route.endpoint.baseURL).toBe(`https://gateway.${expected}.test/v1`)
const headers = yield* resolved.route.auth.apply({
request: LLM.request({ model: resolved, prompt: "Hi" }),
method: "POST",
url: `${resolved.route.endpoint.baseURL}/test`,
body: "{}",
headers: Headers.empty,
})
expect(headers.authorization).toContain(`/${expected}/bedrock`)
}
}
}
}),
),
)
it.effect("uses explicit and credential regions for direct native Bedrock endpoint templates", () =>
Effect.gen(function* () {
for (const environment of [undefined, "eu-west-1"]) {
yield* withEnv({ AWS_REGION: environment }, () =>
Effect.gen(function* () {
for (const packageName of [
"@opencode-ai/ai/providers/amazon-bedrock",
"@opencode-ai/ai/providers/amazon-bedrock/mantle",
]) {
for (const region of [undefined, "us-west-2"]) {
const resolved = yield* ModelResolver.fromCatalogModel(
model(packageName, {
settings: {
...(region === undefined ? {} : { region }),
baseURL: "https://gateway.${AWS_REGION}.test/v1",
credentials: {
region: "us-east-2",
accessKeyId: "test-access-key",
secretAccessKey: "test-secret-key",
},
},
}),
)
const expected = region ?? "us-east-2"
expect(resolved.route.endpoint.baseURL).toBe(`https://gateway.${expected}.test/v1`)
const headers = yield* resolved.route.auth.apply({
request: LLM.request({ model: resolved, prompt: "Hi" }),
method: "POST",
url: `${resolved.route.endpoint.baseURL}/test`,
body: "{}",
headers: Headers.empty,
})
expect(headers.authorization).toContain(`/${expected}/bedrock`)
}
}
}),
)
}
}),
)
it.effect("preserves missing-region initialization errors through native resolution and session display", () =>
withEnv({ AWS_REGION: undefined }, () =>
Effect.gen(function* () {
for (const packageName of [
Provider.aisdk("@ai-sdk/amazon-bedrock"),
Provider.aisdk("@ai-sdk/amazon-bedrock/mantle"),
"@opencode-ai/ai/providers/amazon-bedrock",
"@opencode-ai/ai/providers/amazon-bedrock/mantle",
]) {
const error = yield* ModelResolver.fromCatalogModel(
model(packageName),
Credential.Key.make({ type: "key", key: "test" }),
).pipe(Effect.flip)
expect(error).toBeInstanceOf(AIError)
expect(toSessionError(error)).toMatchObject({
type: "provider.invalid-request",
message: expect.stringContaining("Set region or AWS_REGION"),
})
}
}),
),
)
it.effect("uses the API modelID instead of the catalog ID for native OpenAI routes", () =>
Effect.gen(function* () {
const catalog = model(Provider.aisdk("@ai-sdk/openai"), {