+21

![opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com>](/assets/img/avatar_default.png)



![opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>](/assets/img/avatar_default.png)



James Long
Brendan Allan
Kit Langton
opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Affan Ali
affanali2k3
Frank
opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com>
𝓛𝓲𝓽𝓽𝓵𝓮 𝓕𝓻𝓪𝓷𝓴
Aiden Cline
Jay V
Dax Raad
Aarav Sareen
OpeOginni
Luke Parker
Ben Guthrie
Dax
Filip
Max Anderson
Brendan Allan
Jack
Shoubhit Dash
Dustin Deus
starptech
Aiden Cline
usrnk1
Jay
runvip
opencode
Julian Coy
Vladimir Glafirov
8c94e9005f
Co-authored-by: Brendan Allan <14191578+Brendonovich@users.noreply.github.com> Co-authored-by: Kit Langton <kit.langton@gmail.com> Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com> Co-authored-by: Affan Ali <93028901+affanali2k3@users.noreply.github.com> Co-authored-by: affanali2k3 <affanalikhanxx@gmail.com> Co-authored-by: Frank <frank@anoma.ly> Co-authored-by: opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com> Co-authored-by: 𝓛𝓲𝓽𝓽𝓵𝓮 𝓕𝓻𝓪𝓷𝓴 <little-frank@opencord.local> Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Co-authored-by: Jay V <air@live.ca> Co-authored-by: Dax Raad <d@ironbay.co> Co-authored-by: Aarav Sareen <96787824+arvsrn@users.noreply.github.com> Co-authored-by: OpeOginni <107570612+OpeOginni@users.noreply.github.com> Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com> Co-authored-by: Ben Guthrie <benjee.012@gmail.com> Co-authored-by: Dax <mail@thdxr.com> Co-authored-by: Filip <34747899+neriousy@users.noreply.github.com> Co-authored-by: Max Anderson <max.a.anderson95@gmail.com> Co-authored-by: Brendan Allan <git@brendonovich.dev> Co-authored-by: Jack <jack@anoma.ly> Co-authored-by: Shoubhit Dash <shoubhit2005@gmail.com> Co-authored-by: Dustin Deus <deusdustin@gmail.com> Co-authored-by: starptech <starptech@starptechs-MBP.fritz.box> Co-authored-by: Aiden Cline <aidenpcline@gmail.com> Co-authored-by: usrnk1 <7547651+usrnk1@users.noreply.github.com> Co-authored-by: Jay <53023+jayair@users.noreply.github.com> Co-authored-by: runvip <164729189+runvip@users.noreply.github.com> Co-authored-by: opencode <opencode@sst.dev> Co-authored-by: Julian Coy <julian@ex-machina.co> Co-authored-by: Vladimir Glafirov <vglafirov@gitlab.com>
362 lines
12 KiB
TypeScript
362 lines
12 KiB
TypeScript
import { afterEach, describe, expect, test } from "bun:test"
|
|
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
|
import { Effect } from "effect"
|
|
import path from "path"
|
|
import { unlink } from "fs/promises"
|
|
import { Global } from "@opencode-ai/core/global"
|
|
import { Filesystem } from "@/util/filesystem"
|
|
import { Env } from "../../src/env"
|
|
import { Provider } from "@/provider/provider"
|
|
|
|
import { disposeAllInstances } from "../fixture/fixture"
|
|
import { testEffect } from "../lib/effect"
|
|
import { ProviderV2 } from "@opencode-ai/core/provider"
|
|
import { ModelV2 } from "@opencode-ai/core/model"
|
|
|
|
const it = testEffect(LayerNode.compile(LayerNode.group([Provider.node, Env.node])))
|
|
|
|
const originalEnv = new Map<string, string | undefined>()
|
|
|
|
const set = (k: string, v: string) =>
|
|
Effect.gen(function* () {
|
|
if (!originalEnv.has(k)) originalEnv.set(k, process.env[k])
|
|
process.env[k] = v
|
|
yield* Env.use.set(k, v)
|
|
})
|
|
|
|
afterEach(async () => {
|
|
for (const [key, value] of originalEnv) {
|
|
if (value === undefined) delete process.env[key]
|
|
else process.env[key] = value
|
|
}
|
|
originalEnv.clear()
|
|
await disposeAllInstances()
|
|
})
|
|
|
|
const list = Provider.use.list()
|
|
|
|
const mantleModelConfig = {
|
|
provider: { npm: "@ai-sdk/amazon-bedrock/mantle" },
|
|
limit: { context: 272_000, output: 32_000 },
|
|
modalities: {
|
|
input: ["text", "image", "pdf"] as Array<"text" | "image" | "pdf">,
|
|
output: ["text"] as Array<"text">,
|
|
},
|
|
}
|
|
|
|
const withAuthJson = (contents: string) =>
|
|
Effect.acquireRelease(
|
|
Effect.promise(async () => {
|
|
const authPath = path.join(Global.Path.data, "auth.json")
|
|
let original: string | undefined
|
|
try {
|
|
original = await Filesystem.readText(authPath)
|
|
} catch {
|
|
original = undefined
|
|
}
|
|
await Filesystem.write(authPath, contents)
|
|
return { authPath, original }
|
|
}),
|
|
({ authPath, original }) =>
|
|
Effect.promise(async () => {
|
|
if (original !== undefined) {
|
|
await Filesystem.write(authPath, original)
|
|
return
|
|
}
|
|
await unlink(authPath).catch(() => undefined)
|
|
}),
|
|
)
|
|
|
|
it.instance(
|
|
"Bedrock: config region takes precedence over AWS_REGION env var",
|
|
() =>
|
|
Effect.gen(function* () {
|
|
yield* set("AWS_REGION", "us-east-1")
|
|
yield* set("AWS_PROFILE", "default")
|
|
const providers = yield* list
|
|
expect(providers[ProviderV2.ID.amazonBedrock]).toBeDefined()
|
|
expect(providers[ProviderV2.ID.amazonBedrock].options?.region).toBe("eu-west-1")
|
|
}),
|
|
{ config: { provider: { "amazon-bedrock": { options: { region: "eu-west-1" } } } } },
|
|
)
|
|
|
|
it.instance("Bedrock: falls back to AWS_REGION env var when no config region", () =>
|
|
Effect.gen(function* () {
|
|
yield* set("AWS_REGION", "eu-west-1")
|
|
yield* set("AWS_PROFILE", "default")
|
|
const providers = yield* list
|
|
expect(providers[ProviderV2.ID.amazonBedrock]).toBeDefined()
|
|
expect(providers[ProviderV2.ID.amazonBedrock].options?.region).toBe("eu-west-1")
|
|
}),
|
|
)
|
|
|
|
it.instance(
|
|
"Bedrock: loads when bearer token from auth.json is present",
|
|
() =>
|
|
Effect.gen(function* () {
|
|
yield* withAuthJson(JSON.stringify({ "amazon-bedrock": { type: "api", key: "test-bearer-token" } }))
|
|
yield* set("AWS_PROFILE", "")
|
|
yield* set("AWS_ACCESS_KEY_ID", "")
|
|
yield* set("AWS_BEARER_TOKEN_BEDROCK", "")
|
|
const providers = yield* list
|
|
expect(providers[ProviderV2.ID.amazonBedrock]).toBeDefined()
|
|
expect(providers[ProviderV2.ID.amazonBedrock].options?.region).toBe("eu-west-1")
|
|
}),
|
|
{ config: { provider: { "amazon-bedrock": { options: { region: "eu-west-1" } } } } },
|
|
)
|
|
|
|
it.instance(
|
|
"Bedrock Mantle: GPT-5.5 uses Responses API and OpenAI base path",
|
|
() =>
|
|
Effect.gen(function* () {
|
|
yield* set("AWS_REGION", "")
|
|
yield* set("AWS_PROFILE", "")
|
|
yield* set("AWS_ACCESS_KEY_ID", "")
|
|
yield* set("AWS_BEARER_TOKEN_BEDROCK", "")
|
|
const model = yield* Provider.use.getModel(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("openai.gpt-5.5"))
|
|
const language = yield* Provider.use.getLanguage(model)
|
|
expect((language as { provider: string }).provider).toBe("bedrock-mantle.responses")
|
|
expect((language as { modelId: string }).modelId).toBe("openai.gpt-5.5")
|
|
expect(
|
|
(language as unknown as { config: { url: (input: { path: string; modelId: string }) => string } }).config.url({
|
|
path: "/responses",
|
|
modelId: "openai.gpt-5.5",
|
|
}),
|
|
).toBe("https://bedrock-mantle.us-east-2.api.aws/openai/v1/responses")
|
|
}),
|
|
{
|
|
config: {
|
|
provider: {
|
|
"amazon-bedrock": {
|
|
options: { region: "us-east-2", apiKey: "test-bearer-token" },
|
|
models: {
|
|
"openai.gpt-5.5": {
|
|
...mantleModelConfig,
|
|
provider: {
|
|
npm: "@ai-sdk/amazon-bedrock/mantle",
|
|
api: "https://bedrock-mantle.${AWS_REGION}.api.aws/openai/v1",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
)
|
|
|
|
it.instance(
|
|
"Bedrock Mantle: GPT OSS safeguard uses Chat Completions and Mantle base path",
|
|
() =>
|
|
Effect.gen(function* () {
|
|
yield* set("AWS_BEARER_TOKEN_BEDROCK", "test-bearer-token")
|
|
const model = yield* Provider.use.getModel(
|
|
ProviderV2.ID.amazonBedrock,
|
|
ModelV2.ID.make("openai.gpt-oss-safeguard-120b"),
|
|
)
|
|
const language = yield* Provider.use.getLanguage(model)
|
|
expect((language as { provider: string }).provider).toBe("bedrock-mantle.chat")
|
|
expect((language as { modelId: string }).modelId).toBe("openai.gpt-oss-safeguard-120b")
|
|
expect(
|
|
(language as unknown as { config: { url: (input: { path: string; modelId: string }) => string } }).config.url({
|
|
path: "/chat/completions",
|
|
modelId: "openai.gpt-oss-safeguard-120b",
|
|
}),
|
|
).toBe("https://bedrock-mantle.us-east-1.api.aws/v1/chat/completions")
|
|
}),
|
|
{
|
|
config: {
|
|
provider: {
|
|
"amazon-bedrock": {
|
|
options: { region: "us-east-1" },
|
|
models: { "openai.gpt-oss-safeguard-120b": mantleModelConfig },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
)
|
|
|
|
it.instance(
|
|
"Bedrock: config profile takes precedence over AWS_PROFILE env var",
|
|
() =>
|
|
Effect.gen(function* () {
|
|
yield* set("AWS_PROFILE", "default")
|
|
yield* set("AWS_ACCESS_KEY_ID", "test-key-id")
|
|
const providers = yield* list
|
|
expect(providers[ProviderV2.ID.amazonBedrock]).toBeDefined()
|
|
expect(providers[ProviderV2.ID.amazonBedrock].options?.region).toBe("us-east-1")
|
|
}),
|
|
{
|
|
config: {
|
|
provider: { "amazon-bedrock": { options: { profile: "my-custom-profile", region: "us-east-1" } } },
|
|
},
|
|
},
|
|
)
|
|
|
|
it.instance(
|
|
"Bedrock: includes custom endpoint in options when specified",
|
|
() =>
|
|
Effect.gen(function* () {
|
|
yield* set("AWS_PROFILE", "default")
|
|
const providers = yield* list
|
|
expect(providers[ProviderV2.ID.amazonBedrock]).toBeDefined()
|
|
expect(providers[ProviderV2.ID.amazonBedrock].options?.endpoint).toBe(
|
|
"https://bedrock-runtime.us-east-1.vpce-xxxxx.amazonaws.com",
|
|
)
|
|
}),
|
|
{
|
|
config: {
|
|
provider: {
|
|
"amazon-bedrock": {
|
|
options: { endpoint: "https://bedrock-runtime.us-east-1.vpce-xxxxx.amazonaws.com" },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
)
|
|
|
|
it.instance(
|
|
"Bedrock: autoloads when AWS_WEB_IDENTITY_TOKEN_FILE is present",
|
|
() =>
|
|
Effect.gen(function* () {
|
|
yield* set("AWS_WEB_IDENTITY_TOKEN_FILE", "/var/run/secrets/eks.amazonaws.com/serviceaccount/token")
|
|
yield* set("AWS_ROLE_ARN", "arn:aws:iam::123456789012:role/my-eks-role")
|
|
yield* set("AWS_PROFILE", "")
|
|
yield* set("AWS_ACCESS_KEY_ID", "")
|
|
const providers = yield* list
|
|
expect(providers[ProviderV2.ID.amazonBedrock]).toBeDefined()
|
|
expect(providers[ProviderV2.ID.amazonBedrock].options?.region).toBe("us-east-1")
|
|
}),
|
|
{ config: { provider: { "amazon-bedrock": { options: { region: "us-east-1" } } } } },
|
|
)
|
|
|
|
// Cross-region inference profile prefix handling.
|
|
// Models from models.dev may come with prefixes already (e.g. us., eu., global.).
|
|
// These should NOT be double-prefixed when passed to the SDK.
|
|
|
|
it.instance(
|
|
"Bedrock: model with us. prefix should not be double-prefixed",
|
|
() =>
|
|
Effect.gen(function* () {
|
|
yield* set("AWS_PROFILE", "default")
|
|
const providers = yield* list
|
|
expect(providers[ProviderV2.ID.amazonBedrock]).toBeDefined()
|
|
expect(providers[ProviderV2.ID.amazonBedrock].models["us.anthropic.claude-opus-4-5-20251101-v1:0"]).toBeDefined()
|
|
}),
|
|
{
|
|
config: {
|
|
provider: {
|
|
"amazon-bedrock": {
|
|
options: { region: "us-east-1" },
|
|
models: { "us.anthropic.claude-opus-4-5-20251101-v1:0": { name: "Claude Opus 4.5 (US)" } },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
)
|
|
|
|
it.instance(
|
|
"Bedrock: model with global. prefix should not be prefixed",
|
|
() =>
|
|
Effect.gen(function* () {
|
|
yield* set("AWS_PROFILE", "default")
|
|
const providers = yield* list
|
|
expect(providers[ProviderV2.ID.amazonBedrock]).toBeDefined()
|
|
expect(
|
|
providers[ProviderV2.ID.amazonBedrock].models["global.anthropic.claude-opus-4-5-20251101-v1:0"],
|
|
).toBeDefined()
|
|
}),
|
|
{
|
|
config: {
|
|
provider: {
|
|
"amazon-bedrock": {
|
|
options: { region: "us-east-1" },
|
|
models: { "global.anthropic.claude-opus-4-5-20251101-v1:0": { name: "Claude Opus 4.5 (Global)" } },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
)
|
|
|
|
it.instance(
|
|
"Bedrock: model with eu. prefix should not be double-prefixed",
|
|
() =>
|
|
Effect.gen(function* () {
|
|
yield* set("AWS_PROFILE", "default")
|
|
const providers = yield* list
|
|
expect(providers[ProviderV2.ID.amazonBedrock]).toBeDefined()
|
|
expect(providers[ProviderV2.ID.amazonBedrock].models["eu.anthropic.claude-opus-4-5-20251101-v1:0"]).toBeDefined()
|
|
}),
|
|
{
|
|
config: {
|
|
provider: {
|
|
"amazon-bedrock": {
|
|
options: { region: "eu-west-1" },
|
|
models: { "eu.anthropic.claude-opus-4-5-20251101-v1:0": { name: "Claude Opus 4.5 (EU)" } },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
)
|
|
|
|
it.instance(
|
|
"Bedrock: model without prefix in US region should get us. prefix added",
|
|
() =>
|
|
Effect.gen(function* () {
|
|
yield* set("AWS_PROFILE", "default")
|
|
const providers = yield* list
|
|
expect(providers[ProviderV2.ID.amazonBedrock]).toBeDefined()
|
|
expect(providers[ProviderV2.ID.amazonBedrock].models["anthropic.claude-opus-4-5-20251101-v1:0"]).toBeDefined()
|
|
}),
|
|
{
|
|
config: {
|
|
provider: {
|
|
"amazon-bedrock": {
|
|
options: { region: "us-east-1" },
|
|
models: { "anthropic.claude-opus-4-5-20251101-v1:0": { name: "Claude Opus 4.5" } },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
)
|
|
|
|
// Direct unit tests for cross-region inference profile prefix detection.
|
|
describe("Bedrock cross-region prefix detection", () => {
|
|
const crossRegionPrefixes = ["global.", "us.", "eu.", "jp.", "apac.", "au."]
|
|
|
|
test("should detect global. prefix", () => {
|
|
expect(crossRegionPrefixes.some((p) => "global.anthropic.claude-opus-4-5-20251101-v1:0".startsWith(p))).toBe(true)
|
|
})
|
|
|
|
test("should detect us. prefix", () => {
|
|
expect(crossRegionPrefixes.some((p) => "us.anthropic.claude-opus-4-5-20251101-v1:0".startsWith(p))).toBe(true)
|
|
})
|
|
|
|
test("should detect eu. prefix", () => {
|
|
expect(crossRegionPrefixes.some((p) => "eu.anthropic.claude-opus-4-5-20251101-v1:0".startsWith(p))).toBe(true)
|
|
})
|
|
|
|
test("should detect jp. prefix", () => {
|
|
expect(crossRegionPrefixes.some((p) => "jp.anthropic.claude-sonnet-4-20250514-v1:0".startsWith(p))).toBe(true)
|
|
})
|
|
|
|
test("should detect apac. prefix", () => {
|
|
expect(crossRegionPrefixes.some((p) => "apac.anthropic.claude-sonnet-4-20250514-v1:0".startsWith(p))).toBe(true)
|
|
})
|
|
|
|
test("should detect au. prefix", () => {
|
|
expect(crossRegionPrefixes.some((p) => "au.anthropic.claude-sonnet-4-5-20250929-v1:0".startsWith(p))).toBe(true)
|
|
})
|
|
|
|
test("should NOT detect prefix for non-prefixed model", () => {
|
|
expect(crossRegionPrefixes.some((p) => "anthropic.claude-opus-4-5-20251101-v1:0".startsWith(p))).toBe(false)
|
|
})
|
|
|
|
test("should NOT detect prefix for amazon nova models", () => {
|
|
expect(crossRegionPrefixes.some((p) => "amazon.nova-pro-v1:0".startsWith(p))).toBe(false)
|
|
})
|
|
|
|
test("should NOT detect prefix for cohere models", () => {
|
|
expect(crossRegionPrefixes.some((p) => "cohere.command-r-plus-v1:0".startsWith(p))).toBe(false)
|
|
})
|
|
})
|