fix(core): centralize native provider mapping (#43468)
This commit is contained in:
@@ -19,6 +19,7 @@ export interface Settings extends ProviderPackage.Settings {
|
||||
readonly apiKey?: string
|
||||
readonly baseURL: string
|
||||
readonly provider?: string
|
||||
readonly providerOptions?: OpenAIProviderOptionsInput
|
||||
}
|
||||
|
||||
export type FamilyModelOptions = Omit<RouteDefaultsInput, "providerOptions"> &
|
||||
@@ -75,6 +76,7 @@ export const model: ProviderPackage.Definition<Settings, OpenAIProviderOptionsIn
|
||||
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
|
||||
limits: settings.limits,
|
||||
provider: settings.provider,
|
||||
providerOptions: settings.providerOptions,
|
||||
}).model(modelID)
|
||||
|
||||
export const baseten = define(profiles.baseten)
|
||||
|
||||
@@ -14,11 +14,22 @@ export interface MapInput {
|
||||
readonly packageName: string | undefined
|
||||
readonly settings: Readonly<Record<string, unknown>>
|
||||
readonly modelID: string
|
||||
readonly providerID: string
|
||||
}
|
||||
|
||||
export function map(input: MapInput): Mapping | undefined {
|
||||
const baseSettings = mapBaseSettings(input.settings)
|
||||
switch (input.packageName) {
|
||||
case "@ai-sdk/anthropic":
|
||||
return {
|
||||
package: "@opencode-ai/ai/providers/anthropic",
|
||||
settings: {
|
||||
...baseSettings,
|
||||
...mapAPIKey(input.settings),
|
||||
...(typeof input.settings.authToken === "string" ? { authToken: input.settings.authToken } : {}),
|
||||
...mapAnthropicOptions(input.settings),
|
||||
},
|
||||
}
|
||||
case "@ai-sdk/amazon-bedrock":
|
||||
return {
|
||||
package: "@opencode-ai/ai/providers/amazon-bedrock",
|
||||
@@ -88,6 +99,35 @@ export function map(input: MapInput): Mapping | undefined {
|
||||
},
|
||||
...(isStringRecord(input.settings.headers) ? { headers: input.settings.headers } : {}),
|
||||
}
|
||||
case "@ai-sdk/openai":
|
||||
return {
|
||||
package: "@opencode-ai/ai/providers/openai",
|
||||
settings: {
|
||||
...baseSettings,
|
||||
...mapAPIKey(input.settings),
|
||||
...(typeof input.settings.organization === "string" ? { organization: input.settings.organization } : {}),
|
||||
...(typeof input.settings.project === "string" ? { project: input.settings.project } : {}),
|
||||
...(isStringRecord(input.settings.queryParams) ? { queryParams: input.settings.queryParams } : {}),
|
||||
...mapProviderOptions(input.settings, "openai", [
|
||||
"apiKey",
|
||||
"baseURL",
|
||||
"organization",
|
||||
"project",
|
||||
"queryParams",
|
||||
]),
|
||||
},
|
||||
}
|
||||
case "@ai-sdk/openai-compatible":
|
||||
if (typeof input.settings.baseURL !== "string") return
|
||||
return {
|
||||
package: "@opencode-ai/ai/providers/openai-compatible",
|
||||
settings: {
|
||||
...baseSettings,
|
||||
...mapAPIKey(input.settings),
|
||||
provider: input.providerID,
|
||||
...mapProviderOptions(input.settings, "openai", ["apiKey", "baseURL"]),
|
||||
},
|
||||
}
|
||||
case "@openrouter/ai-sdk-provider":
|
||||
return mapOpenRouter(input.settings, baseSettings)
|
||||
case "@ai-sdk/xai":
|
||||
@@ -102,6 +142,20 @@ export function map(input: MapInput): Mapping | undefined {
|
||||
}
|
||||
}
|
||||
|
||||
function mapAnthropicOptions(settings: Readonly<Record<string, unknown>>) {
|
||||
return mapProviderOptions(settings, "anthropic", ["apiKey", "authToken", "baseURL"])
|
||||
}
|
||||
|
||||
function mapProviderOptions(
|
||||
settings: Readonly<Record<string, unknown>>,
|
||||
key: string,
|
||||
excluded: ReadonlyArray<string>,
|
||||
) {
|
||||
const options = Object.fromEntries(Object.entries(settings).filter(([name]) => !excluded.includes(name)))
|
||||
if (Object.keys(options).length === 0) return {}
|
||||
return { providerOptions: { [key]: options } }
|
||||
}
|
||||
|
||||
function mapBedrockMantle(input: MapInput, baseSettings: Readonly<Record<string, unknown>>): Mapping | undefined {
|
||||
const settings = input.settings
|
||||
const chat = input.modelID === "openai.gpt-oss-safeguard-20b" || input.modelID === "openai.gpt-oss-safeguard-120b"
|
||||
@@ -256,6 +310,7 @@ function mapGoogleOptions(settings: Readonly<Record<string, unknown>>, extra: Re
|
||||
}
|
||||
const options = {
|
||||
...(typeof settings.cachedContent === "string" ? { cachedContent: settings.cachedContent } : {}),
|
||||
...(isStringRecord(settings.labels) ? { labels: settings.labels } : {}),
|
||||
...(Array.isArray(settings.safetySettings) ? { safetySettings: settings.safetySettings } : {}),
|
||||
...(typeof settings.serviceTier === "string" ? { serviceTier: settings.serviceTier } : {}),
|
||||
...(Object.keys(thinkingConfig).length > 0 ? { thinkingConfig } : {}),
|
||||
|
||||
@@ -2,13 +2,7 @@ export * as ModelResolver from "./model-resolver.js"
|
||||
|
||||
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
|
||||
import { LanguageModel } from "@opencode-ai/ai"
|
||||
// ast-grep-ignore: no-star-import
|
||||
import * as AnthropicMessages from "@opencode-ai/ai/protocols/anthropic-messages"
|
||||
// ast-grep-ignore: no-star-import
|
||||
import * as OpenAICompatibleChat from "@opencode-ai/ai/protocols/openai-compatible-chat"
|
||||
// ast-grep-ignore: no-star-import
|
||||
import * as OpenAIResponses from "@opencode-ai/ai/protocols/openai-responses"
|
||||
import { Auth, type AnyRoute } from "@opencode-ai/ai/route"
|
||||
import { Auth } from "@opencode-ai/ai/route"
|
||||
import { Context, Effect, Layer, Schema } from "effect"
|
||||
import { produce } from "immer"
|
||||
import { AISDK } from "./aisdk.js"
|
||||
@@ -83,47 +77,6 @@ export interface Interface {
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/ModelResolver") {}
|
||||
|
||||
const apiKey = (model: Info, credential?: Credential.Value) => {
|
||||
if (credential?.type === "key") return Auth.value(credential.key)
|
||||
if (credential?.type === "oauth") return Auth.value(credential.access)
|
||||
const value = model.settings?.apiKey
|
||||
if (typeof value === "string") return Auth.value(value)
|
||||
return undefined
|
||||
}
|
||||
|
||||
const withDefaults = (model: Info, route: AnyRoute) =>
|
||||
route.with({
|
||||
provider: model.providerID,
|
||||
endpoint: typeof model.settings?.baseURL === "string" ? { baseURL: model.settings.baseURL } : undefined,
|
||||
headers: providerHeaders(model),
|
||||
providerOptions: providerOptions(model),
|
||||
http: model.body === undefined ? undefined : { body: model.body },
|
||||
limits: { context: model.limit.context, input: model.limit.input, output: model.limit.output },
|
||||
})
|
||||
|
||||
const providerHeaders = (model: Info) => {
|
||||
const packageName = Provider.packageName(model.package)
|
||||
const generated = new Map<string, string>()
|
||||
if (packageName === "@ai-sdk/openai" && typeof model.settings?.organization === "string")
|
||||
generated.set("OpenAI-Organization", model.settings.organization)
|
||||
if (packageName === "@ai-sdk/openai" && typeof model.settings?.project === "string")
|
||||
generated.set("OpenAI-Project", model.settings.project)
|
||||
if (packageName === "@ai-sdk/anthropic" && typeof model.settings?.authToken === "string")
|
||||
generated.set("Authorization", `Bearer ${model.settings.authToken}`)
|
||||
return Provider.mergeHeaders(generated.size === 0 ? undefined : Object.fromEntries(generated), model.headers)
|
||||
}
|
||||
|
||||
const providerOptions = (model: Info): { readonly [key: string]: { readonly [key: string]: unknown } } | undefined => {
|
||||
if (!Provider.isAISDK(model.package) || model.settings === undefined) return undefined
|
||||
const { apiKey: _, baseURL: _baseURL, ...settings } = model.settings
|
||||
if (Object.keys(settings).length === 0) return undefined
|
||||
const packageName = Provider.packageName(model.package)
|
||||
if (packageName === "@ai-sdk/openai") return { openai: settings }
|
||||
if (packageName === "@ai-sdk/anthropic") return { anthropic: settings }
|
||||
if (packageName === "@ai-sdk/openai-compatible") return { openai: settings }
|
||||
return undefined
|
||||
}
|
||||
|
||||
export const withVariant = (
|
||||
model: Info,
|
||||
variantID: VariantID | undefined,
|
||||
@@ -170,37 +123,14 @@ const resolveCatalogModel = Effect.fn("ModelResolver.resolveCatalogModel")(funct
|
||||
) {
|
||||
const resolved = prepareRuntimeModel(model, credential)
|
||||
const packageName = Provider.packageName(resolved.package)
|
||||
const key = apiKey(resolved, credential)
|
||||
const configuration = credential?.type === "key" ? credential.configuration : undefined
|
||||
|
||||
if (Provider.isAISDK(resolved.package) && packageName === "@ai-sdk/openai") {
|
||||
const runtime = yield* prepareProviderModel(resolved)
|
||||
return withDefaults(runtime, OpenAIResponses.route)
|
||||
.with({ auth: key === undefined ? Auth.none : Auth.bearer(key) })
|
||||
.model({ id: runtime.modelID ?? runtime.id, compatibility: runtime.compatibility })
|
||||
}
|
||||
if (Provider.isAISDK(resolved.package) && packageName === "@ai-sdk/anthropic") {
|
||||
const runtime = yield* prepareProviderModel(resolved)
|
||||
return withDefaults(runtime, AnthropicMessages.route)
|
||||
.with({ auth: key === undefined ? Auth.none : Auth.header("x-api-key", key) })
|
||||
.model({ id: runtime.modelID ?? runtime.id, compatibility: runtime.compatibility })
|
||||
}
|
||||
if (
|
||||
Provider.isAISDK(resolved.package) &&
|
||||
packageName === "@ai-sdk/openai-compatible" &&
|
||||
typeof resolved.settings?.baseURL === "string"
|
||||
) {
|
||||
const runtime = yield* prepareProviderModel(resolved)
|
||||
return withDefaults(runtime, OpenAICompatibleChat.route)
|
||||
.with({ auth: key === undefined ? Auth.none : Auth.bearer(key) })
|
||||
.model({ id: runtime.modelID ?? runtime.id, compatibility: runtime.compatibility })
|
||||
}
|
||||
const configured = { ...resolved.settings, ...credential?.metadata, ...configuration }
|
||||
const mapping = Provider.isAISDK(resolved.package)
|
||||
? AISDKNative.map({
|
||||
packageName,
|
||||
settings: configured,
|
||||
modelID: resolved.modelID ?? resolved.id,
|
||||
providerID: resolved.providerID,
|
||||
})
|
||||
: undefined
|
||||
const native = mapping?.package ?? resolved.package
|
||||
@@ -267,19 +197,6 @@ function validateProviderVariables(
|
||||
return failure ? Effect.fail(failure) : Effect.succeed(resolved)
|
||||
}
|
||||
|
||||
function prepareProviderModel(model: Info): Effect.Effect<Info, UnresolvedProviderVariablesError> {
|
||||
if (!model.settings) return Effect.succeed(model)
|
||||
return prepareProviderSettings(model, model.settings).pipe(
|
||||
Effect.map((settings) =>
|
||||
settings === model.settings
|
||||
? model
|
||||
: produce(model, (draft) => {
|
||||
draft.settings = settings
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
function prepareProviderSettings(
|
||||
model: Info,
|
||||
settings: Readonly<Record<string, unknown>>,
|
||||
|
||||
@@ -190,13 +190,6 @@ export const OpenAIPlugin = define({
|
||||
})
|
||||
yield* load()
|
||||
yield* ctx.catalog.transform((evt) => {
|
||||
for (const item of evt.provider.list()) {
|
||||
if (!Provider.isAISDK(item.provider.package)) continue
|
||||
if (Provider.packageName(item.provider.package) !== "@ai-sdk/openai") continue
|
||||
evt.provider.update(item.provider.id, (provider) => {
|
||||
provider.package = "@opencode-ai/ai/providers/openai"
|
||||
})
|
||||
}
|
||||
if (!chatgpt) return
|
||||
const item = evt.provider.get(Provider.ID.openai)
|
||||
if (!item) return
|
||||
|
||||
@@ -2,9 +2,91 @@ import { describe, expect, test } from "bun:test"
|
||||
import { AISDKNative } from "@opencode-ai/core/aisdk-native"
|
||||
|
||||
const map = (packageName: string, settings: Readonly<Record<string, unknown>>, modelID = "test-model") =>
|
||||
AISDKNative.map({ packageName, settings, modelID })
|
||||
AISDKNative.map({ packageName, settings, modelID, providerID: "test-provider" })
|
||||
|
||||
describe("AISDKNative", () => {
|
||||
test("maps OpenAI-family packages and request options to native providers", () => {
|
||||
expect(
|
||||
map("@ai-sdk/openai", {
|
||||
apiKey: "secret",
|
||||
baseURL: "https://api.meta.ai/v1",
|
||||
organization: "org",
|
||||
reasoningEffort: "xhigh",
|
||||
reasoningSummary: "auto",
|
||||
include: ["reasoning.encrypted_content"],
|
||||
instructions: "Follow the repository instructions.",
|
||||
truncation: "auto",
|
||||
}),
|
||||
).toEqual({
|
||||
package: "@opencode-ai/ai/providers/openai",
|
||||
settings: {
|
||||
apiKey: "secret",
|
||||
baseURL: "https://api.meta.ai/v1",
|
||||
organization: "org",
|
||||
providerOptions: {
|
||||
openai: {
|
||||
reasoningEffort: "xhigh",
|
||||
reasoningSummary: "auto",
|
||||
include: ["reasoning.encrypted_content"],
|
||||
instructions: "Follow the repository instructions.",
|
||||
truncation: "auto",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
expect(map("@ai-sdk/openai-compatible", { baseURL: "https://example.com/v1", reasoningEffort: "high" })).toEqual({
|
||||
package: "@opencode-ai/ai/providers/openai-compatible",
|
||||
settings: {
|
||||
baseURL: "https://example.com/v1",
|
||||
provider: "test-provider",
|
||||
providerOptions: { openai: { reasoningEffort: "high" } },
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("maps Anthropic settings and request options to the native provider", () => {
|
||||
expect(
|
||||
map("@ai-sdk/anthropic", {
|
||||
authToken: "token",
|
||||
baseURL: "https://anthropic.example/v1",
|
||||
thinking: { type: "adaptive", display: "summarized" },
|
||||
effort: "high",
|
||||
}),
|
||||
).toEqual({
|
||||
package: "@opencode-ai/ai/providers/anthropic",
|
||||
settings: {
|
||||
authToken: "token",
|
||||
baseURL: "https://anthropic.example/v1",
|
||||
providerOptions: {
|
||||
anthropic: {
|
||||
thinking: { type: "adaptive", display: "summarized" },
|
||||
effort: "high",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("maps Google Vertex settings to the native provider", () => {
|
||||
expect(
|
||||
map("@ai-sdk/google-vertex", {
|
||||
project: "project",
|
||||
location: "us-central1",
|
||||
labels: { environment: "test" },
|
||||
thinkingConfig: { thinkingLevel: "high" },
|
||||
}),
|
||||
).toEqual({
|
||||
package: "@opencode-ai/ai/providers/google-vertex",
|
||||
settings: {
|
||||
project: "project",
|
||||
location: "us-central1",
|
||||
providerOptions: {
|
||||
gemini: { labels: { environment: "test" }, thinkingConfig: { thinkingLevel: "high" } },
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("maps both models.dev Bedrock packages to native providers", () => {
|
||||
expect(map("@ai-sdk/amazon-bedrock", { region: "us-east-1" })).toEqual({
|
||||
package: "@opencode-ai/ai/providers/amazon-bedrock",
|
||||
|
||||
@@ -219,7 +219,10 @@ describe("ModelResolver", () => {
|
||||
settings: { baseURL: "https://openai.example/v1" },
|
||||
limit: { context: 100, input: 80, output: 20 },
|
||||
})
|
||||
const resolved = yield* ModelResolver.fromCatalogModel(catalog)
|
||||
const resolved = yield* ModelResolver.fromCatalogModel(
|
||||
catalog,
|
||||
Credential.Key.make({ type: "key", key: "secret" }),
|
||||
)
|
||||
|
||||
expect(catalog.id).toBe(ID.make("test-model"))
|
||||
expect(resolved).toMatchObject({ id: "api-test-model", provider: "test-provider" })
|
||||
@@ -254,22 +257,24 @@ describe("ModelResolver", () => {
|
||||
)
|
||||
|
||||
it.effect("treats an empty configured API key as omitted", () =>
|
||||
Effect.gen(function* () {
|
||||
const resolved = yield* ModelResolver.fromCatalogModel(
|
||||
model(Provider.aisdk("@ai-sdk/openai"), {
|
||||
settings: { apiKey: "", baseURL: "https://openai.example/v1" },
|
||||
}),
|
||||
)
|
||||
const headers = yield* resolved.route.auth.apply({
|
||||
request: LLM.request({ model: resolved, prompt: "Hello" }),
|
||||
method: "POST",
|
||||
url: "https://openai.example/v1/responses",
|
||||
body: "{}",
|
||||
headers: Headers.empty,
|
||||
})
|
||||
withEnv({ OPENAI_API_KEY: "environment-key" }, () =>
|
||||
Effect.gen(function* () {
|
||||
const resolved = yield* ModelResolver.fromCatalogModel(
|
||||
model(Provider.aisdk("@ai-sdk/openai"), {
|
||||
settings: { apiKey: "", baseURL: "https://openai.example/v1" },
|
||||
}),
|
||||
)
|
||||
const headers = yield* resolved.route.auth.apply({
|
||||
request: LLM.request({ model: resolved, prompt: "Hello" }),
|
||||
method: "POST",
|
||||
url: "https://openai.example/v1/responses",
|
||||
body: "{}",
|
||||
headers: Headers.empty,
|
||||
})
|
||||
|
||||
expect(headers.authorization).toBeUndefined()
|
||||
}),
|
||||
expect(headers.authorization).toBe("Bearer environment-key")
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
it.effect("uses no native API-key auth for an explicitly enabled provider without credentials", () => {
|
||||
@@ -457,8 +462,12 @@ describe("ModelResolver", () => {
|
||||
settings: { baseURL: "https://openai.example/v1" },
|
||||
variants: [
|
||||
{
|
||||
id: VariantID.make("high"),
|
||||
settings: { reasoningEffort: "high" },
|
||||
id: VariantID.make("xhigh"),
|
||||
settings: {
|
||||
reasoningEffort: "xhigh",
|
||||
reasoningSummary: "auto",
|
||||
include: ["reasoning.encrypted_content"],
|
||||
},
|
||||
headers: { "x-variant": "high" },
|
||||
body: {
|
||||
store: false,
|
||||
@@ -468,7 +477,7 @@ describe("ModelResolver", () => {
|
||||
},
|
||||
],
|
||||
})
|
||||
const resolved = yield* ModelResolver.resolveModel(catalog, VariantID.make("high"))
|
||||
const resolved = yield* ModelResolver.resolveModel(catalog, VariantID.make("xhigh"))
|
||||
|
||||
expect(resolved.route.defaults.headers).toMatchObject({ "x-test": "header", "x-variant": "high" })
|
||||
expect(resolved.route.defaults.http?.body).toEqual({
|
||||
@@ -478,7 +487,17 @@ describe("ModelResolver", () => {
|
||||
temperature: 0.2,
|
||||
})
|
||||
expect(resolved.route.defaults.providerOptions).toEqual({
|
||||
openai: { store: false, reasoningEffort: "high" },
|
||||
openai: {
|
||||
store: false,
|
||||
reasoningEffort: "xhigh",
|
||||
reasoningSummary: "auto",
|
||||
include: ["reasoning.encrypted_content"],
|
||||
},
|
||||
})
|
||||
const prepared = yield* compileRequest(LLM.request({ model: resolved, prompt: "Hello" }))
|
||||
expect(prepared.body).toMatchObject({
|
||||
include: ["reasoning.encrypted_content"],
|
||||
reasoning: { effort: "xhigh", summary: "auto" },
|
||||
})
|
||||
}),
|
||||
)
|
||||
@@ -815,12 +834,46 @@ describe("ModelResolver", () => {
|
||||
Effect.gen(function* () {
|
||||
const native = yield* ModelResolver.fromCatalogModel(model(Provider.aisdk("@ai-sdk/openai")))
|
||||
const packages = [
|
||||
[
|
||||
"@ai-sdk/openai",
|
||||
"@opencode-ai/ai/providers/openai",
|
||||
{
|
||||
reasoningEffort: "xhigh",
|
||||
reasoningSummary: "auto",
|
||||
include: ["reasoning.encrypted_content"],
|
||||
},
|
||||
{
|
||||
openai: {
|
||||
reasoningEffort: "xhigh",
|
||||
reasoningSummary: "auto",
|
||||
include: ["reasoning.encrypted_content"],
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
"@ai-sdk/anthropic",
|
||||
"@opencode-ai/ai/providers/anthropic",
|
||||
{ thinking: { type: "adaptive", display: "summarized" }, effort: "high" },
|
||||
{ anthropic: { thinking: { type: "adaptive", display: "summarized" }, effort: "high" } },
|
||||
],
|
||||
[
|
||||
"@ai-sdk/openai-compatible",
|
||||
"@opencode-ai/ai/providers/openai-compatible",
|
||||
{ reasoningEffort: "high" },
|
||||
{ openai: { reasoningEffort: "high" } },
|
||||
],
|
||||
[
|
||||
"@ai-sdk/google",
|
||||
"@opencode-ai/ai/providers/google",
|
||||
{ thinkingConfig: { thinkingLevel: "high" } },
|
||||
{ gemini: { thinkingConfig: { thinkingLevel: "high" } } },
|
||||
],
|
||||
[
|
||||
"@ai-sdk/google-vertex",
|
||||
"@opencode-ai/ai/providers/google-vertex",
|
||||
{ thinkingConfig: { thinkingLevel: "high" } },
|
||||
{ gemini: { thinkingConfig: { thinkingLevel: "high" } } },
|
||||
],
|
||||
[
|
||||
"@openrouter/ai-sdk-provider",
|
||||
"@opencode-ai/ai/providers/openrouter",
|
||||
@@ -869,6 +922,51 @@ describe("ModelResolver", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("never loads the AI SDK for packages with native implementations", () =>
|
||||
Effect.gen(function* () {
|
||||
const packages = [
|
||||
["@ai-sdk/anthropic", "@opencode-ai/ai/providers/anthropic", "api-model"],
|
||||
["@ai-sdk/amazon-bedrock", "@opencode-ai/ai/providers/amazon-bedrock", "api-model"],
|
||||
[
|
||||
"@ai-sdk/amazon-bedrock/mantle",
|
||||
"@opencode-ai/ai/providers/amazon-bedrock/mantle/responses",
|
||||
"openai.gpt-oss-120b",
|
||||
],
|
||||
["@ai-sdk/azure", "@opencode-ai/ai/providers/azure/responses", "api-model"],
|
||||
["@ai-sdk/google", "@opencode-ai/ai/providers/google", "api-model"],
|
||||
["@ai-sdk/google-vertex", "@opencode-ai/ai/providers/google-vertex", "api-model"],
|
||||
[
|
||||
"@ai-sdk/google-vertex/anthropic",
|
||||
"@opencode-ai/ai/providers/google-vertex/messages",
|
||||
"claude-sonnet-4-6",
|
||||
],
|
||||
["@ai-sdk/openai", "@opencode-ai/ai/providers/openai", "api-model"],
|
||||
["@ai-sdk/openai-compatible", "@opencode-ai/ai/providers/openai-compatible", "api-model"],
|
||||
["@openrouter/ai-sdk-provider", "@opencode-ai/ai/providers/openrouter", "api-model"],
|
||||
["@ai-sdk/xai", "@opencode-ai/ai/providers/xai", "api-model"],
|
||||
] as const
|
||||
|
||||
yield* Effect.forEach(packages, ([catalogPackage, nativePackage, modelID]) =>
|
||||
ModelResolver.fromCatalogModel(
|
||||
model(Provider.aisdk(catalogPackage), {
|
||||
modelID,
|
||||
settings: { baseURL: "https://provider.example/v1", region: "us-east-1" },
|
||||
}),
|
||||
undefined,
|
||||
{
|
||||
loadPackage: (specifier) => {
|
||||
expect(specifier).toBe(nativePackage)
|
||||
return Effect.succeed({
|
||||
model: (id) => LanguageModel.make({ id, provider: "native-provider", route: OpenAIChat.route }),
|
||||
})
|
||||
},
|
||||
loadAISDK: () => Effect.die(`AI SDK loader called for ${catalogPackage}`),
|
||||
},
|
||||
),
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("routes Vertex Anthropic catalog models through native Messages", () =>
|
||||
Effect.gen(function* () {
|
||||
const native = yield* ModelResolver.fromCatalogModel(model(Provider.aisdk("@ai-sdk/openai")))
|
||||
|
||||
@@ -137,7 +137,7 @@ describe("OpenAIPlugin", () => {
|
||||
const proxy = yield* request(Provider.ID.openai, "https://proxy.example/v1?region=us")
|
||||
|
||||
const provider = required(yield* catalog.provider.get(Provider.ID.openai))
|
||||
expect(provider.package).toBe("@opencode-ai/ai/providers/openai")
|
||||
expect(provider.package).toBe(Provider.aisdk("@ai-sdk/openai"))
|
||||
expect(provider.settings).toMatchObject({ baseURL: "https://chatgpt.com/backend-api/codex" })
|
||||
expect(provider.headers).toMatchObject({ originator: "opencode", "chatgpt-account-id": "acct_123" })
|
||||
expect(direct.baseURL).toBe("https://chatgpt.com/backend-api/codex")
|
||||
@@ -147,7 +147,7 @@ describe("OpenAIPlugin", () => {
|
||||
expect(proxy.baseURL).toBe("https://proxy.example/v1?region=us")
|
||||
expect(proxy.headers).toMatchObject({ originator: "opencode", "session-id": "ses_test" })
|
||||
const eligible = required(yield* catalog.model.get(Provider.ID.openai, Model.ID.make("gpt-5.5")))
|
||||
expect(eligible.package).toBe("@opencode-ai/ai/providers/openai")
|
||||
expect(eligible.package).toBe(Provider.aisdk("@ai-sdk/openai"))
|
||||
expect(eligible.headers).toMatchObject({ originator: "opencode", "chatgpt-account-id": "acct_123" })
|
||||
expect(eligible.cost).toEqual([])
|
||||
expect(eligible.limit).toEqual({ context: 400_000, input: 272_000, output: 128_000 })
|
||||
@@ -194,7 +194,7 @@ describe("OpenAIPlugin", () => {
|
||||
|
||||
const provider = required(yield* catalog.provider.get(Provider.ID.openai))
|
||||
const model = required(yield* catalog.model.get(Provider.ID.openai, Model.ID.make("gpt-5.5")))
|
||||
expect(model.package).toBe("@opencode-ai/ai/providers/openai")
|
||||
expect(model.package).toBe(Provider.aisdk("@ai-sdk/openai"))
|
||||
expect(model.enabled).toBe(true)
|
||||
expect(model.limit).toEqual({ context: 1_050_000, input: 922_000, output: 128_000 })
|
||||
expect(direct.headers).not.toHaveProperty("originator")
|
||||
|
||||
Reference in New Issue
Block a user