mirror of
https://github.com/anomalyco/opencode.git
synced 2026-09-08 09:56:24 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a42f173b47 |
@@ -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"
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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>>) {
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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 })),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 },
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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")
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -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"), {
|
||||
|
||||
Reference in New Issue
Block a user