Compare commits

..
7 Commits
58 changed files with 476 additions and 642 deletions
+17 -48
View File
@@ -191,7 +191,7 @@ jobs:
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: opencode-preview-cli
name: opencode-preview-cli-macos
path: packages/cli/dist/cli-*
if-no-files-found: error
@@ -199,7 +199,7 @@ jobs:
needs: version
runs-on: blacksmith-4vcpu-ubuntu-2404
timeout-minutes: 30
if: github.repository == 'anomalyco/opencode'
if: github.repository == 'anomalyco/opencode' && !(github.ref_name == 'v2' && (inputs.bump || inputs.version))
steps:
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
@@ -221,7 +221,7 @@ jobs:
needs:
- version
- build-node-app-archive
if: github.repository == 'anomalyco/opencode'
if: github.repository == 'anomalyco/opencode' && !(github.ref_name == 'v2' && (inputs.bump || inputs.version))
strategy:
fail-fast: false
matrix:
@@ -276,10 +276,9 @@ jobs:
sign-cli-windows:
needs:
- build-cli
- version
- sign-cli-macos
runs-on: blacksmith-4vcpu-windows-2025
if: github.repository == 'anomalyco/opencode' && github.ref_name != 'v2' && github.ref_name != 'beta'
if: github.repository == 'anomalyco/opencode' && (github.ref_name == 'v2' || github.ref_name == 'beta')
env:
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
@@ -292,15 +291,8 @@ jobs:
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: opencode-cli-windows
path: packages/opencode/dist
- name: Setup git committer
id: committer
uses: ./.github/actions/setup-git-committer
with:
opencode-app-id: ${{ vars.OPENCODE_APP_ID }}
opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }}
name: opencode-preview-cli-macos
path: packages/cli/dist
- name: Azure login
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0
@@ -315,9 +307,9 @@ jobs:
signing-account-name: ${{ env.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
certificate-profile-name: ${{ env.AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE }}
files: |
${{ github.workspace }}\packages\opencode\dist\opencode-windows-arm64\bin\opencode.exe
${{ github.workspace }}\packages\opencode\dist\opencode-windows-x64\bin\opencode.exe
${{ github.workspace }}\packages\opencode\dist\opencode-windows-x64-baseline\bin\opencode.exe
${{ github.workspace }}\packages\cli\dist\cli-windows-arm64\bin\opencode.exe
${{ github.workspace }}\packages\cli\dist\cli-windows-x64\bin\opencode.exe
${{ github.workspace }}\packages\cli\dist\cli-windows-x64-baseline\bin\opencode.exe
exclude-environment-credential: true
exclude-workload-identity-credential: true
exclude-managed-identity-credential: true
@@ -333,9 +325,9 @@ jobs:
shell: pwsh
run: |
$files = @(
"${{ github.workspace }}\packages\opencode\dist\opencode-windows-arm64\bin\opencode.exe",
"${{ github.workspace }}\packages\opencode\dist\opencode-windows-x64\bin\opencode.exe",
"${{ github.workspace }}\packages\opencode\dist\opencode-windows-x64-baseline\bin\opencode.exe"
"${{ github.workspace }}\packages\cli\dist\cli-windows-arm64\bin\opencode.exe",
"${{ github.workspace }}\packages\cli\dist\cli-windows-x64\bin\opencode.exe",
"${{ github.workspace }}\packages\cli\dist\cli-windows-x64-baseline\bin\opencode.exe"
)
foreach ($file in $files) {
@@ -345,39 +337,16 @@ jobs:
}
}
- name: Repack Windows CLI archives
working-directory: packages/opencode/dist
shell: pwsh
run: |
Compress-Archive -Path "opencode-windows-arm64\bin\*" -DestinationPath "opencode-windows-arm64.zip" -Force
Compress-Archive -Path "opencode-windows-x64\bin\*" -DestinationPath "opencode-windows-x64.zip" -Force
Compress-Archive -Path "opencode-windows-x64-baseline\bin\*" -DestinationPath "opencode-windows-x64-baseline.zip" -Force
- name: Upload signed Windows CLI release assets
if: needs.version.outputs.release != ''
shell: pwsh
env:
GH_TOKEN: ${{ steps.committer.outputs.token }}
run: |
gh release upload "v${{ needs.version.outputs.version }}" `
"${{ github.workspace }}\packages\opencode\dist\opencode-windows-arm64.zip" `
"${{ github.workspace }}\packages\opencode\dist\opencode-windows-x64.zip" `
"${{ github.workspace }}\packages\opencode\dist\opencode-windows-x64-baseline.zip" `
--clobber `
--repo "${{ needs.version.outputs.repo }}"
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: opencode-cli-signed-windows
path: |
packages/opencode/dist/opencode-windows-arm64
packages/opencode/dist/opencode-windows-x64
packages/opencode/dist/opencode-windows-x64-baseline
name: opencode-preview-cli
path: packages/cli/dist/cli-*
if-no-files-found: error
build-electron:
needs:
- version
- sign-cli-macos
- sign-cli-windows
if: github.repository == 'anomalyco/opencode' && (github.ref_name != 'v2' || needs.version.outputs.release != '')
continue-on-error: false
env:
+3 -4
View File
@@ -366,15 +366,14 @@ install_legacy_shim() {
if [[ "$shim_os" == "windows" ]]; then
cat > "$INSTALL_DIR/opencode2.cmd" <<'EOF'
@echo off
echo opencode2 is now just opencode. run opencode
exit /b 1
"%~dp0opencode.exe" %*
exit /b %errorlevel%
EOF
return
fi
cat > "$INSTALL_DIR/opencode2" <<'EOF'
#!/bin/sh
echo 'opencode2 is now just opencode. run opencode'
exit 1
exec "$(dirname "$0")/opencode" "$@"
EOF
chmod 755 "$INSTALL_DIR/opencode2"
}
+3 -3
View File
@@ -5,12 +5,12 @@ ENV BUN_RUNTIME_TRANSPILER_CACHE_PATH=${BUN_RUNTIME_TRANSPILER_CACHE_PATH}
RUN apk add --no-cache libgcc libstdc++ ripgrep
FROM base AS build-amd64
COPY dist/opencode-linux-x64-baseline-musl/bin/opencode /usr/local/bin/opencode
COPY dist/cli-linux-x64-baseline-musl/bin/opencode /usr/local/bin/opencode
FROM base AS build-arm64
COPY dist/opencode-linux-arm64-musl/bin/opencode /usr/local/bin/opencode
COPY dist/cli-linux-arm64-musl/bin/opencode /usr/local/bin/opencode
ARG TARGETARCH
FROM build-${TARGETARCH}
RUN opencode --version
RUN ln -s opencode /usr/local/bin/opencode2 && opencode --version && opencode2 --version
ENTRYPOINT ["opencode"]
+1 -2
View File
@@ -1,4 +1,3 @@
#!/usr/bin/env node
console.log("opencode2 is now just opencode. run opencode")
process.exit(1)
require("./opencode.cjs")
+3 -1
View File
@@ -17,7 +17,9 @@ const outdir = path.join(root, `aur-${name}`)
const dryRun = process.argv.includes("--dry-run")
const pkgver = Script.version.replaceAll("-", ".")
const license = Bun.file(path.join(dir, "..", "..", "LICENSE"))
const shim = "#!/bin/sh\necho 'opencode2 is now just opencode. run opencode'\nexit 1\n"
const shim = `#!/bin/sh
exec "$(dirname "$0")/opencode" "$@"
`
await rm(outdir, { recursive: true, force: true })
await mkdir(path.dirname(outdir), { recursive: true })
+2 -6
View File
@@ -60,17 +60,13 @@ async function publishDistribution(input: {
"",
].join("\n"),
)
if (input.legacyCommand)
await Bun.file(`${input.root}/${input.name}/bin/${input.legacyCommand}.cjs`).write(
`#!/usr/bin/env node\n\nconsole.log(${JSON.stringify(`opencode2 is now just opencode. run ${input.command}`)})\nprocess.exit(1)\n`,
)
await Bun.file(`${input.root}/${input.name}/package.json`).write(
JSON.stringify(
{
name: input.name,
bin: {
[input.command]: `./bin/${input.command}.exe`,
...(input.legacyCommand ? { [input.legacyCommand]: `./bin/${input.legacyCommand}.cjs` } : {}),
...(input.legacyCommand ? { [input.legacyCommand]: `./bin/${input.command}.exe` } : {}),
},
...(input.command !== input.binary ? { opencodeSourceBinary: input.binary } : {}),
scripts: { postinstall: "node ./postinstall.mjs" },
@@ -136,7 +132,7 @@ await publishDistribution({
packagePrefix: "@opencode/cli-",
artifact: "cli",
})
if (existsSync(path.join(root, "node"))) {
if (Script.channel !== "latest" && existsSync(path.join(root, "node"))) {
await publishDistribution({
root: path.join(root, "node"),
name: "@opencode/cli-node",
+67 -119
View File
@@ -3,87 +3,18 @@ export * as AISDKNative from "./aisdk-native.js"
import { Effect, Option, Schema, Struct } from "effect"
import { Provider } from "./provider.js"
/** A provider, model, or variant overlay written in legacy AI SDK vocabulary. */
export interface Overlay {
readonly settings?: Provider.Settings
export interface Mapping {
readonly package: string
readonly settings: Provider.Settings
readonly headers?: Readonly<Record<string, string>>
readonly body?: Readonly<Record<string, unknown>>
}
/** Fields the rewrite touches; catalog records and migrated config entries both fit. */
export interface Target {
package?: string
settings?: Provider.Settings
headers?: Readonly<Record<string, string>>
body?: Readonly<Record<string, unknown>>
}
export interface ModelTarget extends Target {
readonly id: string
readonly modelID?: string
variants?: ReadonlyArray<{ readonly id: string } & Overlay>
}
/** Rewrites a provider and its models in place so they name native packages and native settings wherever one exists. */
export function rewrite(provider: Target, models: Iterable<ModelTarget>) {
const inherited = Provider.isAISDK(provider.package)
? nativePackage(Provider.packageName(provider.package), undefined, provider.settings)
: undefined
// Converse request settings depend on the model, so provider-level ones move onto each model first.
const converse = inherited === BEDROCK ? Struct.pick(provider.settings ?? {}, CONVERSE_KEYS) : {}
if (inherited === BEDROCK && provider.settings) provider.settings = Struct.omit(provider.settings, CONVERSE_KEYS)
for (const model of models) {
const legacy = model.package ?? provider.package
if (!Provider.isAISDK(legacy)) continue
if (model.package === undefined) model.settings = Provider.mergeOverlay(converse, model.settings)
const modelID = model.modelID ?? model.id
const native = nativePackage(
Provider.packageName(legacy),
modelID,
Provider.mergeOverlay(provider.settings, model.settings),
)
if (!native) continue
assign(model, translate(native, model, modelID))
model.variants = model.variants?.map((variant) => ({ id: variant.id, ...translate(native, variant, modelID) }))
model.package = model.package !== undefined || native !== inherited ? native : undefined
}
if (!inherited) return
assign(provider, translate(inherited, provider))
provider.package = inherited
}
function assign(target: Target, overlay: Overlay) {
target.settings = overlay.settings
target.headers = overlay.headers
target.body = overlay.body
}
/** Native package replacing a legacy AI SDK package, or undefined when the AI SDK runtime must run it. */
export function nativePackage(npm: string | undefined, modelID = "", settings?: Provider.Settings) {
switch (npm) {
case "@ai-sdk/anthropic":
case "@ai-sdk/cerebras":
case "@ai-sdk/deepinfra":
case "@ai-sdk/google":
case "@ai-sdk/google-vertex":
case "@ai-sdk/groq":
case "@ai-sdk/mistral":
case "@ai-sdk/openai":
case "@ai-sdk/togetherai":
case "@ai-sdk/xai":
case "@ai-sdk/amazon-bedrock":
return `@opencode/ai/providers/${npm.slice("@ai-sdk/".length)}`
case "@ai-sdk/amazon-bedrock/mantle":
return `@opencode/ai/providers/amazon-bedrock/mantle/${modelID.includes("gpt-oss") ? "chat" : "responses"}`
case "@ai-sdk/azure":
return `@opencode/ai/providers/azure/${settings?.useCompletionUrls === true ? "chat" : "responses"}`
case "@ai-sdk/google-vertex/anthropic":
return "@opencode/ai/providers/google-vertex/messages"
case "@ai-sdk/openai-compatible":
return "@opencode/ai/providers/openai-compatible"
case "@openrouter/ai-sdk-provider":
return "@opencode/ai/providers/openrouter"
}
export interface MapInput {
readonly packageName: string | undefined
readonly settings: Provider.Settings
readonly modelID: string
readonly providerID: string
}
// A wrongly typed legacy value is dropped rather than failing the whole decode.
@@ -144,36 +75,51 @@ const Legacy = Schema.StructWithRest(
type Legacy = typeof Legacy.Type
const decode = Schema.decodeUnknownSync(Legacy)
const BEDROCK = "@opencode/ai/providers/amazon-bedrock"
/** Translates one legacy overlay into the spelling `native` reads. Explicit headers and body win over translated ones. */
export function translate(native: string, overlay: Overlay, modelID = ""): Overlay {
const settings = decode(overlay.settings ?? {})
const converse = native === BEDROCK
// Wrongly typed legacy values decode to undefined; drop them rather than forward them.
const mapped = Object.fromEntries(
Object.entries(Struct.omit(settings, ["headers", "extraBody", "useCompletionUrls", ...OPENROUTER_KEYS])).filter(
([, value]) => value !== undefined,
),
)
return defined({
settings: native.startsWith(BEDROCK) ? bedrockSettings(mapped, converse) : mapped,
headers: Provider.mergeHeaders(
native === "@opencode/ai/providers/openrouter" ? openRouterHeaders(settings) : settings.headers,
overlay.headers,
),
body: Provider.mergeOverlay(
Provider.mergeOverlay(settings.extraBody, converse ? bedrockBody(modelID, settings) : undefined),
overlay.body,
),
})
/** Maps a legacy AI SDK package onto the native package that replaces it. */
export function map(input: MapInput): Mapping | undefined {
const settings = decode(input.settings)
const native = mapPackage(input.packageName, input.modelID, settings)
if (!native) return
const converse = native === "@opencode/ai/providers/amazon-bedrock"
const mapped = {
...Struct.omit(settings, ["headers", "extraBody", "useCompletionUrls", ...OPENROUTER_KEYS]),
...(native === "@opencode/ai/providers/openai-compatible" ? { provider: input.providerID } : {}),
}
return {
package: native,
settings: native.startsWith("@opencode/ai/providers/amazon-bedrock") ? bedrockSettings(mapped, converse) : mapped,
...(settings.headers === undefined ? {} : { headers: settings.headers }),
...(settings.extraBody === undefined ? {} : { body: settings.extraBody }),
...(converse ? bedrockRequest(input.modelID, settings) : {}),
...(native === "@opencode/ai/providers/openrouter" ? openRouterRequest(settings) : {}),
}
}
// Keeps catalog records free of empty overlays.
function defined(overlay: Overlay): Overlay {
return Object.fromEntries(
Object.entries(overlay).filter(([, value]) => value !== undefined && Object.keys(value).length > 0),
)
function mapPackage(packageName: string | undefined, modelID: string, settings: Legacy) {
switch (packageName) {
case "@ai-sdk/anthropic":
case "@ai-sdk/cerebras":
case "@ai-sdk/deepinfra":
case "@ai-sdk/google":
case "@ai-sdk/google-vertex":
case "@ai-sdk/groq":
case "@ai-sdk/mistral":
case "@ai-sdk/openai":
case "@ai-sdk/togetherai":
case "@ai-sdk/xai":
case "@ai-sdk/amazon-bedrock":
return `@opencode/ai/providers/${packageName.slice("@ai-sdk/".length)}`
case "@ai-sdk/amazon-bedrock/mantle":
return `@opencode/ai/providers/amazon-bedrock/mantle/${modelID.includes("gpt-oss") ? "chat" : "responses"}`
case "@ai-sdk/azure":
return `@opencode/ai/providers/azure/${settings.useCompletionUrls === true ? "chat" : "responses"}`
case "@ai-sdk/google-vertex/anthropic":
return "@opencode/ai/providers/google-vertex/messages"
case "@ai-sdk/openai-compatible":
return settings.baseURL === undefined ? undefined : "@opencode/ai/providers/openai-compatible"
case "@openrouter/ai-sdk-provider":
return "@opencode/ai/providers/openrouter"
}
}
// AI SDK spellings the native Bedrock packages do not read.
@@ -186,7 +132,7 @@ const BEDROCK_KEYS = [
"secretAccessKey",
"sessionToken",
]
// Request settings Converse takes in the body; translated by `bedrockBody`.
// Request settings Converse takes in the body; translated by `bedrockRequest`.
const CONVERSE_KEYS = ["additionalModelRequestFields", "reasoningConfig", "anthropicBeta", "serviceTier"]
function bedrockSettings(settings: Legacy, converse: boolean) {
@@ -212,7 +158,7 @@ function bedrockSettings(settings: Legacy, converse: boolean) {
}
}
function bedrockBody(modelID: string, settings: Legacy) {
function bedrockRequest(modelID: string, settings: Legacy): Pick<Mapping, "body"> {
const additional = settings.additionalModelRequestFields ?? {}
const reasoning = settings.reasoningConfig
const anthropic = modelID.includes("anthropic")
@@ -251,22 +197,24 @@ function bedrockBody(modelID: string, settings: Legacy) {
...(fields && Object.keys(fields).length > 0 ? { additionalModelRequestFields: fields } : {}),
...(settings.serviceTier === undefined ? {} : { serviceTier: { type: settings.serviceTier } }),
}
return Object.keys(body).length === 0 ? undefined : body
return Object.keys(body).length === 0 ? {} : { body }
}
// Constructor options the native OpenRouter package takes as headers, plus `compatibility`, which the
// native package would otherwise forward to the request body.
const OPENROUTER_KEYS = ["appName", "appUrl", "api_keys", "compatibility"] as const
function openRouterHeaders(settings: Legacy) {
return Provider.mergeHeaders(
{
...(settings.appName === undefined ? {} : { "X-OpenRouter-Title": settings.appName }),
...(settings.appUrl === undefined ? {} : { "HTTP-Referer": settings.appUrl }),
...(settings.api_keys === undefined || Object.keys(settings.api_keys).length === 0
? {}
: { "X-Provider-API-Keys": JSON.stringify(settings.api_keys) }),
},
settings.headers,
)
function openRouterRequest(settings: Legacy): Pick<Mapping, "headers"> {
const headers =
Provider.mergeHeaders(
{
...(settings.appName === undefined ? {} : { "X-OpenRouter-Title": settings.appName }),
...(settings.appUrl === undefined ? {} : { "HTTP-Referer": settings.appUrl }),
...(settings.api_keys === undefined || Object.keys(settings.api_keys).length === 0
? {}
: { "X-Provider-API-Keys": JSON.stringify(settings.api_keys) }),
},
settings.headers,
) ?? {}
return Object.keys(headers).length === 0 ? {} : { headers }
}
+1 -1
View File
@@ -149,7 +149,7 @@ function build(id: Model.ID, remote: UsableModel, baseURL: string, previous?: Mo
providerID: Provider.ID.githubCopilot,
family: previous?.family ?? Model.Family.make(remote.capabilities.family),
name: previous?.name ?? remote.name,
package: messages ? "@opencode/ai/providers/anthropic" : Provider.aisdk("@ai-sdk/github-copilot"),
package: Provider.aisdk(messages ? "@ai-sdk/anthropic" : "@ai-sdk/github-copilot"),
settings: Provider.mergeOverlay(previous?.settings, {
baseURL: messages ? `${baseURL}/v1` : baseURL,
...(endpoint ? { endpoint } : {}),
+1 -1
View File
@@ -97,7 +97,7 @@ function build(id: Model.ID, remote: RemoteModel, baseURL: string, previous?: Mo
remote.interleaved === undefined
? previous?.compatibility
: (Model.compatibility(remote.interleaved) ?? previous?.compatibility),
package: "@opencode/ai/providers/openai-compatible",
package: Provider.aisdk("@ai-sdk/openai-compatible"),
settings: Provider.mergeOverlay(previous?.settings, { baseURL }),
headers: previous?.headers,
body: previous?.body,
+31 -11
View File
@@ -5,6 +5,7 @@ import { LanguageModel, ProviderConfigurationError } from "@opencode/ai"
import { Auth } from "@opencode/ai/route"
import { Context, Effect, Layer, Schema, Struct } from "effect"
import { AISDK } from "./aisdk.js"
import { AISDKNative } from "./aisdk-native.js"
import { Catalog } from "./catalog.js"
import { Credential } from "./credential.js"
import { Integration } from "./integration.js"
@@ -194,15 +195,25 @@ const resolveCatalogModel = Effect.fn("ModelResolver.resolveCatalogModel")(funct
dependencies?: Dependencies,
) {
const resolved = prepareRuntimeModel(model, credential)
const packageName = Provider.packageName(resolved.package)
const configuration = credential?.type === "key" ? credential.configuration : undefined
const configured = { ...resolved.settings, ...credential?.metadata, ...configuration }
if (Provider.isAISDK(resolved.package)) {
const mapping = Provider.isAISDK(resolved.package)
? AISDKNative.map({
packageName,
settings: configured,
modelID: resolved.modelID ?? resolved.id,
providerID: resolved.canonical ?? resolved.providerID,
})
: undefined
const native = mapping?.package ?? packageName
if (Provider.isAISDK(resolved.package) && !mapping) {
const loadAISDK = dependencies?.loadAISDK
if (!loadAISDK) return yield* unsupported(resolved)
const settings = yield* prepareProviderSettings(
resolved,
Provider.mergeOverlay(resolved.settings, {
...nativeCredentialSettings(resolved.package, credential),
...nativeCredentialSettings(resolved.package ?? "", credential),
...credential?.metadata,
...configuration,
}) ?? {},
@@ -211,22 +222,19 @@ const resolveCatalogModel = Effect.fn("ModelResolver.resolveCatalogModel")(funct
Effect.mapError((error) => initialization(resolved, "init", error.cause)),
)
}
const specifier = Provider.packageName(resolved.package)
if (!specifier) return yield* unsupported(resolved)
if (!native) return yield* unsupported(resolved)
const mapped = yield* prepareProviderSettings(resolved, Provider.nativeSettings(configured))
const specifier = native
const mapped = yield* prepareProviderSettings(resolved, Provider.nativeSettings(mapping?.settings ?? configured))
const module = yield* (dependencies?.loadPackage ?? Provider.loadPackage)(specifier).pipe(
Effect.mapError((error) => initialization(resolved, "load", error.cause)),
)
// The generic OpenAI-compatible package takes the catalog provider as its identity; others carry their own.
const provider =
resolved.canonical ?? (specifier === "@opencode/ai/providers/openai-compatible" ? resolved.providerID : undefined)
const settings = {
...(credential ? Struct.omit(mapped, ["accessToken", "apiKey", "authToken"]) : mapped),
...(provider === undefined ? {} : { provider }),
...(resolved.canonical === undefined ? {} : { provider: resolved.canonical }),
...nativeCredentialSettings(specifier, credential),
headers: resolved.headers,
body: resolved.body,
headers: Provider.mergeHeaders(mapping?.headers, resolved.headers),
body: Provider.mergeOverlay(mapping?.body, resolved.body),
}
return yield* Effect.try({
try: () => {
@@ -416,6 +424,18 @@ function hasConfiguredAuth(model: Info) {
function usesAPIKeyAuth(packageName: string | undefined) {
const name = Provider.packageName(packageName)
return (
name === "@ai-sdk/openai" ||
name === "@ai-sdk/anthropic" ||
name === "@ai-sdk/cerebras" ||
name === "@ai-sdk/deepinfra" ||
name === "@ai-sdk/openai-compatible" ||
name === "@ai-sdk/google" ||
name === "@ai-sdk/groq" ||
name === "@ai-sdk/mistral" ||
name === "@ai-sdk/togetherai" ||
name === "@ai-sdk/xai" ||
name === "@openrouter/ai-sdk-provider" ||
name === "@ai-sdk/azure" ||
name === "@opencode/ai/providers/openai" ||
name?.startsWith("@opencode/ai/providers/openai/") === true ||
name === "@opencode/ai/providers/anthropic" ||
+183 -194
View File
@@ -2,7 +2,6 @@ import { Cause, Context, Duration, Effect, Layer, Option, Schedule, Schema, Sema
import { HttpClient, HttpClientRequest } from "effect/unstable/http"
import { ModelsDev } from "@opencode/schema/models-dev"
import { Money } from "@opencode/schema/money"
import { AISDKNative } from "./aisdk-native.js"
import { App } from "./app.js"
import { Hash } from "@opencode/util/hash"
import { FSUtil } from "@opencode/util/fs-util"
@@ -88,30 +87,23 @@ function normalize(input: Record<string, SourceProvider>): readonly Snapshot[] {
id: providerID,
name: item.name,
activation: "auto",
...packageInfo(item.npm, undefined, item.api),
package: Provider.aisdk(item.npm),
...(item.api ? { settings: { baseURL: item.api } } : {}),
} satisfies Provider.Info
const models: Model.Info[] = []
for (const model of Object.values(item.models)) {
const baseCost = cost(model.cost)
const npm = model.provider?.npm ?? item.npm
const own = packageInfo(npm, model.id, model.provider?.api)
const request = {
...own,
// Models inherit the provider package unless theirs differs, e.g. Mantle gpt-oss selecting chat.
package: own.package === info.package ? undefined : own.package,
variants: reasoningVariants(own.package, model),
}
const variants = reasoningVariants(item, model)
const id = Model.ID.make(model.id)
models.push(modelInfo(providerID, id, model, { ...request, cost: baseCost }))
models.push(modelInfo(providerID, id, model, { cost: baseCost, variants }))
for (const [mode, options] of Object.entries(model.experimental?.modes ?? {})) {
const modeID = Model.ID.make(`${model.id}-${mode}`)
models.push(
modelInfo(providerID, modeID, model, {
...request,
name: modeName(model, mode),
cost: mergeCost(baseCost, options.cost),
headers: Provider.mergeHeaders(own.headers, options.provider?.headers),
body: Provider.mergeOverlay(own.body, options.provider?.body),
request: options.provider,
variants,
}),
)
}
@@ -121,18 +113,6 @@ function normalize(input: Record<string, SourceProvider>): readonly Snapshot[] {
return providers
}
/** The catalog package for a models.dev npm package: native where one exists, otherwise the AI SDK runtime. */
function packageInfo(
npm: string,
modelID: string | undefined,
api: string | undefined,
): { readonly package: string } & AISDKNative.Overlay {
const native = AISDKNative.nativePackage(npm, modelID)
const overlay = api === undefined ? {} : { settings: { baseURL: api } }
if (!native) return { package: Provider.aisdk(npm), ...overlay }
return { package: native, ...AISDKNative.translate(native, overlay, modelID) }
}
function released(date: string) {
const time = Date.parse(date)
return Number.isFinite(time) ? time : 0
@@ -207,45 +187,14 @@ function mergeCost(base: Model.Info["cost"], override: SourceModel["cost"] | und
const OPENAI_INCLUDE_ENCRYPTED_REASONING = ["reasoning.encrypted_content"]
const OUTPUT_TOKEN_MAX = 32_000
type Variant = NonNullable<Model.Info["variants"]>[number]
type Overlay = Omit<Variant, "id">
const NATIVE = "@opencode/ai/providers/"
const OPENAI_FAMILY = [
`${NATIVE}openai`,
`${NATIVE}azure/responses`,
`${NATIVE}amazon-bedrock/mantle/chat`,
`${NATIVE}amazon-bedrock/mantle/responses`,
]
const CHAT_FAMILY = [
`${NATIVE}openai-compatible`,
`${NATIVE}xai`,
`${NATIVE}mistral`,
`${NATIVE}groq`,
`${NATIVE}cerebras`,
`${NATIVE}deepinfra`,
`${NATIVE}togetherai`,
Provider.aisdk("venice-ai-sdk-provider"),
Provider.aisdk("ai-gateway-provider"),
]
// The Vercel gateway runs on the AI SDK and spells upstream options the AI SDK way, so its upstream keys stay legacy.
const ANTHROPIC_FAMILY = [`${NATIVE}anthropic`, `${NATIVE}google-vertex/messages`, Provider.aisdk("@ai-sdk/anthropic")]
const GEMINI_FAMILY = [`${NATIVE}google`, `${NATIVE}google-vertex`, Provider.aisdk("@ai-sdk/google")]
const BEDROCK = `${NATIVE}amazon-bedrock`
const LEGACY_BEDROCK = Provider.aisdk("@ai-sdk/amazon-bedrock")
const OPENROUTER = `${NATIVE}openrouter`
const GATEWAY = Provider.aisdk("@ai-sdk/gateway")
const SAP = Provider.aisdk("@jerome-benoit/sap-ai-provider-v2")
const ALIBABA = Provider.aisdk("@ai-sdk/alibaba")
const COHERE = Provider.aisdk("@ai-sdk/cohere")
function reasoningVariants(pkg: string, model: SourceModel): NonNullable<Model.Info["variants"]> {
function reasoningVariants(provider: SourceProvider, model: SourceModel): NonNullable<Model.Info["variants"]> {
const npm = model.provider?.npm ?? provider.npm
const options = model.reasoning_options
if (!options?.length) return []
const toggle = options.some((option) => option.type === "toggle")
const effort = options.find((option) => option.type === "effort")
if (effort?.type === "effort") {
const off = toggle ? toggleVariants(pkg, model.id).filter((variant) => variant.id === "none") : []
const off = toggle ? toggleVariants(npm, model.id).filter((variant) => variant.id === "none") : []
const variants = [
...off,
...effort.values.flatMap((value) => {
@@ -253,8 +202,8 @@ function reasoningVariants(pkg: string, model: SourceModel): NonNullable<Model.I
const id = typeof raw === "string" && raw !== "null" ? raw : undefined
if (id === undefined) return []
if (id === "none" && off.length > 0) return []
const overlay = effortVariant(pkg, model.id, id)
return overlay ? [{ id: Model.VariantID.make(id), ...overlay }] : []
const settings = settingsForEffort(npm, model.id, id)
return settings ? [{ id: Model.VariantID.make(id), settings }] : []
}),
]
return [...new Map(variants.map((variant) => [variant.id, variant])).values()]
@@ -262,78 +211,80 @@ function reasoningVariants(pkg: string, model: SourceModel): NonNullable<Model.I
const budget = options.find((option) => option.type === "budget_tokens")
if (budget?.type === "budget_tokens")
return [
...(toggle ? toggleVariants(pkg, model.id).filter((variant) => variant.id === "none") : []),
...budgetVariants(pkg, model, budget),
...(toggle ? toggleVariants(npm, model.id).filter((variant) => variant.id === "none") : []),
...budgetVariants(npm, model, budget),
]
if (toggle) return toggleVariants(pkg, model.id)
if (toggle) return toggleVariants(npm, model.id)
return []
}
function effortVariant(pkg: string, modelID: string, effort: string): Overlay | undefined {
if (pkg === OPENROUTER) return { settings: { reasoning: { effort } } }
if (ANTHROPIC_FAMILY.includes(pkg))
return anthropicManualThinking(modelID)
? { settings: { effort } }
: { settings: { thinking: { type: "adaptive", display: "summarized" }, effort } }
if (GEMINI_FAMILY.includes(pkg))
return { settings: { thinkingConfig: { includeThoughts: true, thinkingLevel: effort } } }
if (pkg === BEDROCK) return { body: { additionalModelRequestFields: bedrockEffort(modelID, effort) } }
if (pkg === LEGACY_BEDROCK)
function settingsForEffort(npm: string, modelID: string, effort: string): Provider.Settings | undefined {
if (npm === "@openrouter/ai-sdk-provider") return { reasoning: { effort } }
if (npm === "@ai-sdk/anthropic" || npm === "@ai-sdk/google-vertex/anthropic") {
if (anthropicManualThinking(modelID)) return { effort }
return {
settings: {
reasoningConfig: modelID.includes("anthropic")
? {
...(anthropicManualThinking(modelID) ? {} : { type: "adaptive", display: "summarized" }),
maxReasoningEffort: effort,
}
: { type: "enabled", maxReasoningEffort: effort },
},
thinking: { type: "adaptive", display: "summarized" },
effort,
}
if (pkg === GATEWAY) {
const upstream = gatewayPackage(modelID)
if (upstream) return effortVariant(upstream, modelID, effort)
return { settings: { reasoningEffort: effort } }
}
if (OPENAI_FAMILY.includes(pkg))
return {
settings: { reasoningEffort: effort, reasoningSummary: "auto", include: OPENAI_INCLUDE_ENCRYPTED_REASONING },
}
if (pkg === SAP) {
if (npm === "@ai-sdk/google" || npm === "@ai-sdk/google-vertex")
return { thinkingConfig: { includeThoughts: true, thinkingLevel: effort } }
if (npm === "@ai-sdk/amazon-bedrock") {
if (modelID.includes("anthropic"))
return {
settings: {
modelParams: {
additionalModelRequestFields: {
...(anthropicManualThinking(modelID) ? {} : { thinking: { type: "adaptive", display: "summarized" } }),
output_config: { effort },
},
reasoningConfig: {
...(anthropicManualThinking(modelID) ? {} : { type: "adaptive", display: "summarized" }),
maxReasoningEffort: effort,
},
}
return { reasoningConfig: { type: "enabled", maxReasoningEffort: effort } }
}
if (npm === "@ai-sdk/gateway") {
const upstream = gatewayPackage(modelID)
if (upstream) return settingsForEffort(upstream, modelID, effort)
return { reasoningEffort: effort }
}
if (npm === "@ai-sdk/github-copilot") {
if (modelID.includes("gemini")) return
if (modelID.includes("claude")) return { reasoningEffort: effort }
return { reasoningEffort: effort, reasoningSummary: "auto", include: OPENAI_INCLUDE_ENCRYPTED_REASONING }
}
if (npm === "@ai-sdk/openai" || npm === "@ai-sdk/amazon-bedrock/mantle" || npm === "@ai-sdk/azure")
return { reasoningEffort: effort, reasoningSummary: "auto", include: OPENAI_INCLUDE_ENCRYPTED_REASONING }
if (npm === "@jerome-benoit/sap-ai-provider-v2") {
if (modelID.includes("anthropic"))
return {
modelParams: {
additionalModelRequestFields: {
...(anthropicManualThinking(modelID) ? {} : { thinking: { type: "adaptive", display: "summarized" } }),
output_config: { effort },
},
},
}
if (modelID.includes("gemini"))
return { settings: { modelParams: { thinkingConfig: { includeThoughts: true, thinkingLevel: effort } } } }
return { modelParams: { thinkingConfig: { includeThoughts: true, thinkingLevel: effort } } }
if (modelID.includes("amazon--nova"))
return { settings: { modelParams: { additionalModelRequestFields: { output_config: { effort } } } } }
return { settings: { modelParams: { reasoning_effort: effort } } }
return { modelParams: { additionalModelRequestFields: { output_config: { effort } } } }
return { modelParams: { reasoning_effort: effort } }
}
if (CHAT_FAMILY.includes(pkg)) return { settings: { reasoningEffort: effort } }
}
// Converse spells reasoning per model family inside `additionalModelRequestFields`.
function bedrockEffort(modelID: string, effort: string) {
if (modelID.includes("anthropic"))
return {
...(anthropicManualThinking(modelID) ? {} : { thinking: { type: "adaptive", display: "summarized" } }),
output_config: { effort },
}
// gpt-oss (Harmony) takes the flat chat-completions `reasoning_effort`; GPT-5.6+ take Responses-style `reasoning.effort`.
if (modelID.includes("openai.gpt-oss")) return { reasoning_effort: effort }
if (modelID.includes("openai.")) return { reasoning: { effort } }
return { reasoningConfig: { type: "enabled", maxReasoningEffort: effort } }
if (
[
"@ai-sdk/openai-compatible",
"@ai-sdk/xai",
"@ai-sdk/mistral",
"@ai-sdk/groq",
"@ai-sdk/cerebras",
"@ai-sdk/deepinfra",
"@ai-sdk/togetherai",
"venice-ai-sdk-provider",
"ai-gateway-provider",
].includes(npm)
)
return { reasoningEffort: effort }
}
function budgetVariants(
pkg: string,
npm: string,
model: SourceModel,
option: Extract<NonNullable<SourceModel["reasoning_options"]>[number], { type: "budget_tokens" }>,
): NonNullable<Model.Info["variants"]> {
@@ -344,89 +295,133 @@ function budgetVariants(
{ id: "high", budget: high },
{ id: "max", budget: maximum },
].flatMap((item) => {
const overlay = budgetVariant(pkg, model.id, item.budget)
return overlay ? [{ id: Model.VariantID.make(item.id), ...overlay }] : []
const settings = settingsForBudget(npm, model.id, item.budget)
return settings ? [{ id: Model.VariantID.make(item.id), settings }] : []
})
}
function toggleVariants(pkg: string, modelID: string): NonNullable<Model.Info["variants"]> {
const none = Model.VariantID.make("none")
const thinking = Model.VariantID.make("thinking")
if (pkg === GATEWAY) {
function toggleVariants(npm: string, modelID: string): NonNullable<Model.Info["variants"]> {
if (npm === "@ai-sdk/gateway") {
const upstream = gatewayPackage(modelID)
if (upstream) return toggleVariants(upstream, modelID)
return [
{ id: none, settings: { reasoning: { enabled: false } } },
{ id: thinking, settings: { reasoning: { enabled: true } } },
{
id: Model.VariantID.make("none"),
settings: { reasoning: { enabled: false } },
},
{
id: Model.VariantID.make("thinking"),
settings: { reasoning: { enabled: true } },
},
]
}
if (pkg === OPENROUTER)
if (npm === "@openrouter/ai-sdk-provider")
return [
{ id: none, settings: { reasoning: { enabled: false } } },
{ id: thinking, settings: { reasoning: { enabled: true } } },
{ id: Model.VariantID.make("none"), settings: { reasoning: { enabled: false } } },
{ id: Model.VariantID.make("thinking"), settings: { reasoning: { enabled: true } } },
]
if (ANTHROPIC_FAMILY.includes(pkg))
if (npm === "@ai-sdk/anthropic" || npm === "@ai-sdk/google-vertex/anthropic")
return [
{ id: none, settings: { thinking: { type: "disabled" } } },
{ id: thinking, settings: { thinking: { type: "adaptive", display: "summarized" } } },
{ id: Model.VariantID.make("none"), settings: { thinking: { type: "disabled" } } },
{
id: Model.VariantID.make("thinking"),
settings: {
thinking: { type: "adaptive", display: "summarized" },
},
},
]
if (GEMINI_FAMILY.includes(pkg))
if (npm === "@ai-sdk/google" || npm === "@ai-sdk/google-vertex")
return [
{ id: none, settings: { thinkingConfig: { includeThoughts: false, thinkingBudget: 0 } } },
{ id: thinking, settings: { thinkingConfig: { includeThoughts: true, thinkingBudget: -1 } } },
{
id: Model.VariantID.make("none"),
settings: { thinkingConfig: { includeThoughts: false, thinkingBudget: 0 } },
},
{
id: Model.VariantID.make("thinking"),
settings: { thinkingConfig: { includeThoughts: true, thinkingBudget: -1 } },
},
]
if (pkg === BEDROCK || pkg === LEGACY_BEDROCK) {
if (npm === "@ai-sdk/amazon-bedrock") {
const anthropic = modelID.includes("anthropic")
const off = anthropic ? { thinking: { type: "disabled" } } : { reasoningConfig: { type: "disabled" } }
const on = anthropic
? { thinking: { type: "adaptive", display: "summarized" } }
: { reasoningConfig: { type: "enabled" } }
if (pkg === LEGACY_BEDROCK)
return [
{ id: none, settings: { additionalModelRequestFields: off } },
{ id: thinking, settings: { additionalModelRequestFields: on } },
]
return [
{ id: none, body: { additionalModelRequestFields: off } },
{ id: thinking, body: { additionalModelRequestFields: on } },
{
id: Model.VariantID.make("none"),
settings: {
additionalModelRequestFields: anthropic
? { thinking: { type: "disabled" } }
: { reasoningConfig: { type: "disabled" } },
},
},
{
id: Model.VariantID.make("thinking"),
settings: {
additionalModelRequestFields: anthropic
? { thinking: { type: "adaptive", display: "summarized" } }
: { reasoningConfig: { type: "enabled" } },
},
},
]
}
if (pkg === ALIBABA)
if (npm === "@ai-sdk/alibaba")
return [
{ id: none, settings: { enableThinking: false } },
{ id: thinking, settings: { enableThinking: true } },
{ id: Model.VariantID.make("none"), settings: { enableThinking: false } },
{ id: Model.VariantID.make("thinking"), settings: { enableThinking: true } },
]
if (pkg === COHERE)
if (npm === "@ai-sdk/cohere")
return [
{ id: none, settings: { thinking: { type: "disabled" } } },
{ id: thinking, settings: { thinking: { type: "enabled" } } },
{ id: Model.VariantID.make("none"), settings: { thinking: { type: "disabled" } } },
{ id: Model.VariantID.make("thinking"), settings: { thinking: { type: "enabled" } } },
]
if (pkg === SAP) {
if (npm === "@jerome-benoit/sap-ai-provider-v2") {
if (modelID.includes("gemini"))
return [
{ id: none, settings: { modelParams: { thinkingConfig: { includeThoughts: false, thinkingBudget: 0 } } } },
{ id: thinking, settings: { modelParams: { thinkingConfig: { includeThoughts: true, thinkingBudget: -1 } } } },
{
id: Model.VariantID.make("none"),
settings: { modelParams: { thinkingConfig: { includeThoughts: false, thinkingBudget: 0 } } },
},
{
id: Model.VariantID.make("thinking"),
settings: { modelParams: { thinkingConfig: { includeThoughts: true, thinkingBudget: -1 } } },
},
]
if (modelID.includes("cohere"))
return [
{ id: none, settings: { modelParams: { thinking: { type: "disabled" } } } },
{ id: thinking, settings: { modelParams: { thinking: { type: "enabled" } } } },
{
id: Model.VariantID.make("none"),
settings: { modelParams: { thinking: { type: "disabled" } } },
},
{
id: Model.VariantID.make("thinking"),
settings: { modelParams: { thinking: { type: "enabled" } } },
},
]
if (modelID.includes("amazon--nova"))
return [
{ id: none, settings: { modelParams: { additionalModelRequestFields: { thinking: { type: "disabled" } } } } },
{
id: thinking,
id: Model.VariantID.make("none"),
settings: { modelParams: { additionalModelRequestFields: { thinking: { type: "disabled" } } } },
},
{
id: Model.VariantID.make("thinking"),
settings: { modelParams: { additionalModelRequestFields: { thinking: { type: "enabled" } } } },
},
]
if (modelID.includes("anthropic"))
return [
{ id: none, settings: { modelParams: { additionalModelRequestFields: { thinking: { type: "disabled" } } } } },
{
id: thinking,
id: Model.VariantID.make("none"),
settings: {
modelParams: { additionalModelRequestFields: { thinking: { type: "adaptive", display: "summarized" } } },
modelParams: { additionalModelRequestFields: { thinking: { type: "disabled" } } },
},
},
{
id: Model.VariantID.make("thinking"),
settings: {
modelParams: {
additionalModelRequestFields: {
thinking: { type: "adaptive", display: "summarized" },
},
},
},
},
]
@@ -434,46 +429,40 @@ function toggleVariants(pkg: string, modelID: string): NonNullable<Model.Info["v
return []
}
function budgetVariant(pkg: string, modelID: string, budget: number): Overlay | undefined {
if (pkg === OPENROUTER) return { settings: { reasoning: { max_tokens: budget } } }
if (ANTHROPIC_FAMILY.includes(pkg)) return { settings: { thinking: { type: "enabled", budgetTokens: budget } } }
if (GEMINI_FAMILY.includes(pkg))
return { settings: { thinkingConfig: { includeThoughts: true, thinkingBudget: budget } } }
if (pkg === BEDROCK) return { body: { additionalModelRequestFields: bedrockBudget(modelID, budget) } }
if (pkg === LEGACY_BEDROCK) return { settings: { reasoningConfig: { type: "enabled", budgetTokens: budget } } }
if (pkg === GATEWAY) {
function settingsForBudget(npm: string, modelID: string, budget: number): Provider.Settings | undefined {
if (npm === "@openrouter/ai-sdk-provider") return { reasoning: { max_tokens: budget } }
if (npm === "@ai-sdk/anthropic" || npm === "@ai-sdk/google-vertex/anthropic")
return { thinking: { type: "enabled", budgetTokens: budget } }
if (npm === "@ai-sdk/google" || npm === "@ai-sdk/google-vertex")
return { thinkingConfig: { includeThoughts: true, thinkingBudget: budget } }
if (npm === "@ai-sdk/amazon-bedrock") return { reasoningConfig: { type: "enabled", budgetTokens: budget } }
if (npm === "@ai-sdk/gateway") {
const upstream = gatewayPackage(modelID)
return upstream ? budgetVariant(upstream, modelID, budget) : { settings: { reasoning: { max_tokens: budget } } }
return upstream ? settingsForBudget(upstream, modelID, budget) : { reasoning: { max_tokens: budget } }
}
if (pkg === COHERE) return { settings: { thinking: { type: "enabled", tokenBudget: budget } } }
if (pkg === ALIBABA) return { settings: { enableThinking: true, thinkingBudget: budget } }
if (pkg === SAP) {
if (npm === "@ai-sdk/cohere") return { thinking: { type: "enabled", tokenBudget: budget } }
if (npm === "@ai-sdk/alibaba") return { enableThinking: true, thinkingBudget: budget }
if (npm === "@jerome-benoit/sap-ai-provider-v2") {
if (modelID.includes("anthropic"))
return {
settings: {
modelParams: { additionalModelRequestFields: { thinking: { type: "enabled", budget_tokens: budget } } },
modelParams: {
additionalModelRequestFields: { thinking: { type: "enabled", budget_tokens: budget } },
},
}
if (modelID.includes("gemini"))
return { settings: { modelParams: { thinkingConfig: { includeThoughts: true, thinkingBudget: budget } } } }
if (modelID.includes("cohere"))
return { settings: { modelParams: { thinking: { type: "enabled", token_budget: budget } } } }
return { modelParams: { thinkingConfig: { includeThoughts: true, thinkingBudget: budget } } }
if (modelID.includes("cohere")) return { modelParams: { thinking: { type: "enabled", token_budget: budget } } }
}
}
function bedrockBudget(modelID: string, budget: number) {
if (modelID.includes("anthropic")) return { thinking: { type: "enabled", budget_tokens: budget } }
return { reasoningConfig: { type: "enabled", budgetTokens: budget } }
}
function gatewayPackage(modelID: string) {
const separator = modelID.indexOf("/")
if (separator <= 0) return
const prefix = modelID.slice(0, separator)
if (prefix === "anthropic") return Provider.aisdk("@ai-sdk/anthropic")
if (prefix === "google") return Provider.aisdk("@ai-sdk/google")
if (prefix === "amazon") return LEGACY_BEDROCK
if (prefix === "alibaba") return ALIBABA
if (prefix === "anthropic") return "@ai-sdk/anthropic"
if (prefix === "google") return "@ai-sdk/google"
if (prefix === "amazon") return "@ai-sdk/amazon-bedrock"
if (prefix === "alibaba") return "@ai-sdk/alibaba"
}
function anthropicManualThinking(modelID: string) {
@@ -494,12 +483,12 @@ function modelInfo(
providerID: Provider.ID,
id: Model.ID,
model: SourceModel,
input: AISDKNative.Overlay & {
input: {
readonly name?: string
readonly package?: string
readonly cost?: Model.Info["cost"]
readonly request?: NonNullable<NonNullable<SourceModel["experimental"]>["modes"]>[string]["provider"]
readonly variants?: NonNullable<Model.Info["variants"]>
},
} = {},
): Model.Info {
return {
id,
@@ -508,8 +497,8 @@ function modelInfo(
name: input.name ?? model.name,
compatibility: Model.compatibility(model.interleaved),
family: model.family ? Model.Family.make(model.family) : undefined,
package: input.package,
settings: input.settings,
package: model.provider?.npm ? Provider.aisdk(model.provider.npm) : undefined,
settings: model.provider?.api ? { baseURL: model.provider.api } : undefined,
capabilities: {
tools: model.tool_call,
input: [...(model.modalities?.input ?? [])],
@@ -525,8 +514,8 @@ function modelInfo(
status: model.status ?? "active",
enabled: true,
limit: { context: model.limit.context, input: model.limit.input, output: model.limit.output },
headers: input.headers,
body: input.body,
headers: input.request?.headers ? { ...input.request.headers } : undefined,
body: input.request?.body ? { ...input.request.body } : undefined,
}
}
-15
View File
@@ -1,15 +0,0 @@
export * as AISDKNativePlugin from "./aisdk-native.js"
import { Effect } from "effect"
import { define } from "@opencode/plugin/effect/plugin"
import { AISDKNative } from "../aisdk-native.js"
/** Legacy `aisdk:` packages written by config or plugins become their native replacements before anything reads them. */
export const Plugin = define({
id: "opencode.aisdk-native",
effect: Effect.fn(function* (ctx) {
yield* ctx.catalog.transform((catalog) => {
for (const record of catalog.provider.list()) AISDKNative.rewrite(record.provider, record.models.values())
})
}),
})
-2
View File
@@ -82,7 +82,6 @@ import { AgentPlugin } from "./agent.js"
import BrowserPlugin from "@opencode/plugin-browser"
import { CommandPlugin } from "./command.js"
import { PlanPlugin } from "./plan.js"
import { AISDKNativePlugin } from "./aisdk-native.js"
import { ModelsDevPlugin } from "./models-dev.js"
import { McpCodeModeExclusionPlugin } from "./mcp-codemode-exclusion.js"
import { ProviderPlugins } from "./provider.js"
@@ -241,7 +240,6 @@ const post = [
ConfigProviderPlugin.Plugin,
ConfigWebSearchPlugin.Plugin,
ConfigWorktreePlugin.Plugin,
AISDKNativePlugin.Plugin,
VariantPlugin.Plugin,
ConfigPolicyPlugin.Plugin,
] as const satisfies readonly InternalPlugin[]
@@ -13,8 +13,10 @@ const CHAIN_ENV = [
"AWS_CONTAINER_CREDENTIALS_FULL_URI",
]
const isBedrock = (item: { readonly package: string }) =>
Provider.packageName(item.package).startsWith("@opencode/ai/providers/amazon-bedrock")
const isBedrock = (item: { readonly package: string }) => {
const name = Provider.packageName(item.package)
return name.startsWith("@ai-sdk/amazon-bedrock") || name.startsWith("@opencode/ai/providers/amazon-bedrock")
}
export const AmazonBedrockPlugin = define({
id: "opencode.provider.amazon.bedrock",
+3 -6
View File
@@ -134,10 +134,7 @@ export const AzurePlugin = define({
yield* load()
yield* ctx.catalog.transform((evt) => {
for (const item of evt.provider.list()) {
if (
item.provider.id !== Provider.ID.azure &&
!Provider.packageName(item.provider.package)?.startsWith("@opencode/ai/providers/azure")
)
if (item.provider.id !== Provider.ID.azure && Provider.packageName(item.provider.package) !== "@ai-sdk/azure")
continue
const resourceName = resolveResourceName(item.provider.settings, loaded.resource)
if (resourceName)
@@ -212,9 +209,9 @@ function expandResourceName(baseURL: string, resourceName: string) {
}
function responsesWebSocketCapable(provider: Provider.Info, model: Model.Info) {
if (Provider.packageName(model.package ?? provider.package) !== "@opencode/ai/providers/azure/responses") return false
if (Provider.packageName(model.package ?? provider.package) !== "@ai-sdk/azure") return false
const settings = Provider.mergeOverlay(provider.settings, model.settings)
if (settings?.useDeploymentBasedUrls === true) return false
if (settings?.useCompletionUrls === true || settings?.useDeploymentBasedUrls === true) return false
if (settings?.apiVersion !== undefined && settings.apiVersion !== "v1") return false
if (typeof settings?.baseURL !== "string") return true
return /^https:\/\/[^/]+\.openai\.azure\.com(?:\/|$)/i.test(settings.baseURL)
@@ -7,7 +7,8 @@ export const CerebrasPlugin = define({
effect: Effect.fn(function* (ctx) {
yield* ctx.catalog.transform((evt) => {
for (const item of evt.provider.list()) {
if (Provider.packageName(item.provider.package) !== "@opencode/ai/providers/cerebras") continue
const name = Provider.packageName(item.provider.package)
if (name !== "@ai-sdk/cerebras" && name !== "@opencode/ai/providers/cerebras") continue
evt.provider.update(item.provider.id, (provider) => {
provider.headers = { ...provider.headers, "X-Cerebras-3rd-Party-Integration": "opencode" }
})
@@ -59,10 +59,13 @@ export const GoogleVertexPlugin = define({
effect: Effect.fn(function* (ctx) {
yield* ctx.catalog.transform((evt) => {
for (const item of evt.provider.list()) {
const packageName = Provider.packageName(item.provider.package)
if (!Provider.isAISDK(item.provider.package)) continue
if (
packageName !== "@opencode/ai/providers/google-vertex" &&
!(item.provider.id === Provider.ID.googleVertex && packageName === "@opencode/ai/providers/openai-compatible")
Provider.packageName(item.provider.package) !== "@ai-sdk/google-vertex" &&
!(
item.provider.id === Provider.ID.googleVertex &&
Provider.packageName(item.provider.package)?.includes("@ai-sdk/openai-compatible")
)
)
continue
const project = resolveProject(item.provider.settings ?? {})
+2 -1
View File
@@ -7,7 +7,8 @@ export const KiloPlugin = define({
effect: Effect.fn(function* (ctx) {
yield* ctx.catalog.transform((evt) => {
for (const item of evt.provider.list()) {
if (Provider.packageName(item.provider.package) !== "@opencode/ai/providers/openai-compatible") continue
if (!Provider.isAISDK(item.provider.package)) continue
if (Provider.packageName(item.provider.package) !== "@ai-sdk/openai-compatible") continue
if (item.provider.settings?.baseURL !== "https://api.kilo.ai/api/gateway") continue
evt.provider.update(item.provider.id, (provider) => {
provider.headers = { ...provider.headers, "HTTP-Referer": "https://opencode.ai/", "X-Title": "opencode" }
@@ -11,7 +11,8 @@ export const LLMGatewayPlugin = define({
yield* ctx.catalog.transform((evt) => {
for (const item of evt.provider.list()) {
if (item.provider.activation === "disabled") continue
if (Provider.packageName(item.provider.package) !== "@opencode/ai/providers/openai-compatible") continue
if (!Provider.isAISDK(item.provider.package)) continue
if (Provider.packageName(item.provider.package) !== "@ai-sdk/openai-compatible") continue
if (item.provider.settings?.baseURL !== "https://api.llmgateway.io/v1") continue
if (!configured.has(Integration.ID.make(item.provider.id))) continue
evt.provider.update(item.provider.id, (provider) => {
+2 -1
View File
@@ -7,7 +7,8 @@ export const NvidiaPlugin = define({
effect: Effect.fn(function* (ctx) {
yield* ctx.catalog.transform((evt) => {
for (const item of evt.provider.list()) {
if (Provider.packageName(item.provider.package) !== "@opencode/ai/providers/openai-compatible") continue
if (!Provider.isAISDK(item.provider.package)) continue
if (Provider.packageName(item.provider.package) !== "@ai-sdk/openai-compatible") continue
if (item.provider.settings?.baseURL !== "https://integrate.api.nvidia.com/v1") continue
evt.provider.update(item.provider.id, (provider) => {
provider.headers = {
@@ -8,7 +8,8 @@ export const OpenRouterPlugin = define({
effect: Effect.fn(function* (ctx) {
yield* ctx.catalog.transform((evt) => {
for (const item of evt.provider.list()) {
if (Provider.packageName(item.provider.package) !== "@opencode/ai/providers/openrouter") continue
if (!Provider.isAISDK(item.provider.package)) continue
if (Provider.packageName(item.provider.package) !== "@openrouter/ai-sdk-provider") continue
evt.provider.update(item.provider.id, (provider) => {
provider.headers = { ...provider.headers, "HTTP-Referer": "https://opencode.ai/", "X-Title": "opencode" }
})
+2 -1
View File
@@ -7,7 +7,8 @@ export const ZenmuxPlugin = define({
effect: Effect.fn(function* (ctx) {
yield* ctx.catalog.transform((evt) => {
for (const item of evt.provider.list()) {
if (Provider.packageName(item.provider.package) !== "@opencode/ai/providers/openai-compatible") continue
if (!Provider.isAISDK(item.provider.package)) continue
if (Provider.packageName(item.provider.package) !== "@ai-sdk/openai-compatible") continue
if (item.provider.settings?.baseURL !== "https://zenmux.ai/api/v1") continue
evt.provider.update(item.provider.id, (provider) => {
provider.headers = {
+2 -1
View File
@@ -33,7 +33,8 @@ export function generate(
model: { readonly id: string; readonly modelID?: string; readonly package?: string },
provider?: { readonly package: string },
): NonNullable<Model.Info["variants"]> {
if (Provider.packageName(model.package ?? provider?.package) !== "@opencode/ai/providers/openai-compatible") return []
const packageName = model.package ?? provider?.package
if (!Provider.isAISDK(packageName) || Provider.packageName(packageName) !== "@ai-sdk/openai-compatible") return []
const ids = `${model.id} ${model.modelID ?? ""}`.toLowerCase()
if (!["glm-5.2", "glm-5-2", "glm-5p2"].some((name) => ids.includes(name))) return []
return ["high", "max"].map((id) => ({
+3 -14
View File
@@ -9,7 +9,6 @@ import { ConfigCommandV1 } from "./command.js"
import { ConfigMCPV1 } from "./mcp.js"
import { ConfigPermissionV1 } from "./permission.js"
import { ConfigProviderV1 } from "./provider.js"
import { AISDKNative } from "../../aisdk-native.js"
import { ConfigProviderOptionsV1 } from "./provider-options.js"
import { Provider } from "../../provider.js"
import { Model } from "../../model.js"
@@ -240,19 +239,9 @@ function providers(info?: Readonly<Record<string, ConfigProviderV1.Info>>) {
}
export function migrateProvider(sourceID: string, info: ConfigProviderV1.Info) {
const migrated =
sourceID === "azure-cognitive-services"
? migrateAzureCognitiveServicesProvider(info)
: sourceID === "google-vertex-anthropic"
? migrateGoogleVertexAnthropicProvider(info)
: migrateStandardProvider(info)
// V1 only knew AI SDK packages; the migrated document names the native replacements directly.
const models = Object.entries(migrated.models ?? {}).map(([id, model]) => ({ id, ...model }))
AISDKNative.rewrite(migrated, models)
return {
...migrated,
models: migrated.models && Object.fromEntries(models.map(({ id, ...model }) => [id, model])),
}
if (sourceID === "azure-cognitive-services") return migrateAzureCognitiveServicesProvider(info)
if (sourceID === "google-vertex-anthropic") return migrateGoogleVertexAnthropicProvider(info)
return migrateStandardProvider(info)
}
function migrateStandardProvider(info: ConfigProviderV1.Info) {
+12 -70
View File
@@ -1,76 +1,10 @@
import { describe, expect, test } from "bun:test"
import { AISDKNative } from "@opencode/core/aisdk-native"
import { Provider } from "@opencode/core/provider"
// A model that names a legacy package, rewritten the way the catalog does it.
const map = (packageName: string, settings: Readonly<Record<string, unknown>>, modelID = "test-model") => {
const model: AISDKNative.ModelTarget = { id: modelID, package: Provider.aisdk(packageName), settings }
AISDKNative.rewrite({}, [model])
const { id: _, ...rest } = model
return rest as Partial<AISDKNative.Target>
}
const map = (packageName: string, settings: Readonly<Record<string, unknown>>, modelID = "test-model") =>
AISDKNative.map({ packageName, settings, modelID, providerID: "test-provider" })
describe("AISDKNative", () => {
test("rewrites a provider and its models in place", () => {
const provider: AISDKNative.Target = {
package: Provider.aisdk("@ai-sdk/amazon-bedrock/mantle"),
settings: { region: "us-east-1", bearerToken: "token" },
}
const chat: AISDKNative.ModelTarget = {
id: "openai.gpt-oss-120b",
variants: [{ id: "high", settings: { reasoningEffort: "high" } }],
}
const responses: AISDKNative.ModelTarget = { id: "openai.gpt-5.5" }
const explicit: AISDKNative.ModelTarget = {
id: "claude",
package: Provider.aisdk("@ai-sdk/anthropic"),
settings: { effort: "high" },
}
const opaque: AISDKNative.ModelTarget = {
id: "sonar",
package: Provider.aisdk("@ai-sdk/perplexity"),
settings: { extraBody: {} },
}
AISDKNative.rewrite(provider, [chat, responses, explicit, opaque])
expect(provider).toEqual({
package: "@opencode/ai/providers/amazon-bedrock/mantle/responses",
settings: { region: "us-east-1", apiKey: "token" },
})
// Only models whose native package differs from the provider's name one.
expect(chat).toEqual({
id: "openai.gpt-oss-120b",
package: "@opencode/ai/providers/amazon-bedrock/mantle/chat",
variants: [{ id: "high", settings: { reasoningEffort: "high" } }],
})
expect(responses).toEqual({ id: "openai.gpt-5.5" })
expect(explicit).toEqual({
id: "claude",
package: "@opencode/ai/providers/anthropic",
settings: { effort: "high" },
})
expect(opaque).toEqual({ id: "sonar", package: Provider.aisdk("@ai-sdk/perplexity"), settings: { extraBody: {} } })
})
test("moves provider-level Converse request settings onto each model", () => {
const provider: AISDKNative.Target = {
package: Provider.aisdk("@ai-sdk/amazon-bedrock"),
settings: { region: "us-east-1", reasoningConfig: { maxReasoningEffort: "high" } },
}
const claude: AISDKNative.ModelTarget = { id: "anthropic.claude-sonnet-4-6" }
const nova: AISDKNative.ModelTarget = {
id: "amazon.nova-2-lite-v1:0",
variants: [{ id: "none", settings: { additionalModelRequestFields: { reasoningConfig: { type: "disabled" } } } }],
}
AISDKNative.rewrite(provider, [claude, nova])
expect(provider).toEqual({ package: "@opencode/ai/providers/amazon-bedrock", settings: { region: "us-east-1" } })
expect(claude.body).toEqual({ additionalModelRequestFields: { output_config: { effort: "high" } } })
expect(nova).toEqual({
id: "amazon.nova-2-lite-v1:0",
body: { additionalModelRequestFields: { reasoningConfig: { maxReasoningEffort: "high" } } },
variants: [{ id: "none", body: { additionalModelRequestFields: { reasoningConfig: { type: "disabled" } } } }],
})
})
test("maps OpenAI-family packages and request options to native providers", () => {
expect(
map("@ai-sdk/openai", {
@@ -96,7 +30,11 @@ describe("AISDKNative", () => {
})
expect(map("@ai-sdk/openai-compatible", { baseURL: "https://example.com/v1", reasoningEffort: "high" })).toEqual({
package: "@opencode/ai/providers/openai-compatible",
settings: { baseURL: "https://example.com/v1", reasoningEffort: "high" },
settings: {
baseURL: "https://example.com/v1",
provider: "test-provider",
reasoningEffort: "high",
},
})
})
@@ -139,7 +77,10 @@ describe("AISDKNative", () => {
},
headers: { "x-provider": name },
})
expect(map(`@ai-sdk/${name}`, {})).toEqual({ package: `@opencode/ai/providers/${name}` })
expect(map(`@ai-sdk/${name}`, {})).toEqual({
package: `@opencode/ai/providers/${name}`,
settings: {},
})
}
})
@@ -382,6 +323,7 @@ describe("AISDKNative", () => {
})
expect(map("@ai-sdk/amazon-bedrock", { auth: "bogus" }, "anthropic.claude")).toEqual({
package: "@opencode/ai/providers/amazon-bedrock",
settings: {},
})
})
+8 -8
View File
@@ -712,8 +712,8 @@ describe("Config", () => {
})
expect(migrated.providers?.bedrock).toMatchObject({
package: "@opencode/ai/providers/amazon-bedrock",
models: { claude: { package: "@opencode/ai/providers/anthropic" } },
package: Provider.aisdk("@ai-sdk/amazon-bedrock"),
models: { claude: { package: Provider.aisdk("@ai-sdk/anthropic") } },
settings: { region: "us-east-1", profile: "dev" },
headers: { "x-test": "1" },
body: { trace: true },
@@ -755,14 +755,14 @@ describe("Config", () => {
])
expect(migrated.providers?.azure).toMatchObject({
env: ["AZURE_COGNITIVE_SERVICES_API_KEY"],
package: "@opencode/ai/providers/azure/responses",
package: Provider.aisdk("@ai-sdk/azure"),
models: { deployment: {} },
})
expect(migrated.providers?.["azure-cognitive-services"]).toBeUndefined()
expect(migrated.providers?.["google-vertex"]).toMatchObject({
settings: { project: "test-project", location: "us-central1" },
models: {
"claude-sonnet": { package: "@opencode/ai/providers/google-vertex/messages" },
"claude-sonnet": { package: Provider.aisdk("@ai-sdk/google-vertex/anthropic") },
},
})
expect(migrated.providers?.["google-vertex"]).not.toHaveProperty("package")
@@ -781,7 +781,7 @@ describe("Config", () => {
expect(migrated.providers?.azure).toMatchObject({
env: ["AZURE_COGNITIVE_SERVICES_API_KEY"],
package: "@opencode/ai/providers/openai-compatible",
package: Provider.aisdk("@ai-sdk/openai-compatible"),
settings: {
baseURL: "https://${AZURE_COGNITIVE_SERVICES_RESOURCE_NAME}.cognitiveservices.azure.com/openai",
},
@@ -813,7 +813,7 @@ describe("Config", () => {
expect(migrated.providers?.["google-vertex"]?.package).toBeUndefined()
expect(migrated.providers?.["google-vertex"]?.models?.claude?.package).toBe(
"@opencode/ai/providers/google-vertex/messages",
Provider.aisdk("@ai-sdk/google-vertex/anthropic"),
)
})
@@ -1445,7 +1445,7 @@ describe("Config", () => {
},
})
expect(documents[0]?.info.providers?.openai).toMatchObject({
package: "@opencode/ai/providers/openai",
package: Provider.aisdk("@ai-sdk/openai"),
settings: { apiKey: "secret", organization: "org" },
models: {
model: {
@@ -1455,7 +1455,7 @@ describe("Config", () => {
},
})
expect(documents[0]?.info.providers?.anthropic).toMatchObject({
package: "@opencode/ai/providers/anthropic",
package: Provider.aisdk("@ai-sdk/anthropic"),
models: {
model: {
settings: {
+10 -10
View File
@@ -116,7 +116,7 @@ describe("ConfigProviderPlugin.Plugin", () => {
info: decode({
providers: {
litellm: {
package: "@opencode/ai/providers/openai-compatible",
package: "aisdk:@ai-sdk/openai-compatible",
models: { chat: {} },
},
},
@@ -145,7 +145,7 @@ describe("ConfigProviderPlugin.Plugin", () => {
info: decode({
providers: {
custom: {
package: "@opencode/ai/providers/openai-compatible",
package: "aisdk:@ai-sdk/openai-compatible",
models: { chat: {} },
},
},
@@ -181,7 +181,7 @@ describe("ConfigProviderPlugin.Plugin", () => {
info: decode({
providers: {
custom: {
package: "@opencode/ai/providers/openai-compatible",
package: "aisdk:@ai-sdk/openai-compatible",
models: {
inherited: { name: "Inherited" },
overridden: {
@@ -273,7 +273,7 @@ describe("ConfigProviderPlugin.Plugin", () => {
models: { chat: {} },
},
native: {
package: "@opencode/ai/providers/openai",
package: "aisdk:@ai-sdk/openai",
settings: { baseURL: "https://proxy.example/v1" },
headers: { "x-provider": "default" },
body: { store: false },
@@ -298,13 +298,13 @@ describe("ConfigProviderPlugin.Plugin", () => {
},
},
native: {
package: "@opencode/ai/providers/openai",
package: "aisdk:@ai-sdk/openai",
settings: { baseURL: "https://proxy.example/v1" },
models: {
chat: {
modelID: "vendor/chat",
name: "Custom chat",
package: "@opencode/ai/providers/anthropic",
package: "aisdk:@ai-sdk/anthropic",
settings: { baseURL: "https://model.example/v1", temperature: 0.2 },
limit: { context: 100000, input: 80000, output: 16000 },
cost: { input: 1, output: 2, cache: { read: 0.1, write: 0.2 } },
@@ -350,7 +350,7 @@ describe("ConfigProviderPlugin.Plugin", () => {
const modelID = Model.ID.make("chat")
yield* catalog.transform((editor) => {
editor.model.update(providerID, modelID, (model) => {
model.package = "@opencode/ai/providers/anthropic"
model.package = "aisdk:@ai-sdk/anthropic"
model.settings = { baseURL: "https://catalog.example/v1" }
model.capabilities = { tools: false, input: ["audio"], output: ["audio"] }
model.limit = { context: 100000, input: 80000, output: 16000 }
@@ -384,7 +384,7 @@ describe("ConfigProviderPlugin.Plugin", () => {
info: decode({
providers: {
opencode: {
package: "@opencode/ai/providers/openai",
package: "aisdk:@ai-sdk/openai",
settings: { baseURL: "https://opencode.test/v1" },
models: {
"alpha-gpt-next": {
@@ -433,7 +433,7 @@ describe("ConfigProviderPlugin.Plugin", () => {
info: decode({
providers: {
opencode: {
package: "@opencode/ai/providers/openai",
package: "aisdk:@ai-sdk/openai",
settings: { baseURL: "https://opencode.test/v1" },
},
},
@@ -557,7 +557,7 @@ describe("ConfigProviderPlugin.Plugin", () => {
yield* catalog.transform((editor) => {
editor.provider.update(Provider.ID.anthropic, (provider) => {
provider.package = "@opencode/ai/providers/anthropic"
provider.package = "aisdk:@ai-sdk/anthropic"
})
editor.model.update(Provider.ID.anthropic, modelID, (model) => {
model.variants = [{ id: Model.VariantID.make("fast"), settings: { effort: "high" } }]
@@ -105,7 +105,7 @@ test("defensively syncs advertised Copilot models", async () => {
Model.VariantID.make("high"),
])
expect(model?.capabilities.input).toEqual(["text", "image", "pdf"])
expect(models.get(Model.ID.make("claude-sonnet"))?.package).toBe("@opencode/ai/providers/anthropic")
expect(models.get(Model.ID.make("claude-sonnet"))?.package).toBe(Provider.aisdk("@ai-sdk/anthropic"))
expect(models.get(Model.ID.make("claude-sonnet"))?.settings).toMatchObject({
baseURL: `${server.url.origin}/v1`,
endpoint: "messages",
@@ -148,16 +148,8 @@ test("prices cache reads from either token price spelling", async () => {
try {
const models = await CopilotModels.get(server.url.origin, {}, [])
expect(models.get(Model.ID.make("renamed"))?.cost[0]).toMatchObject({
input: 2.5,
output: 15,
cache: { read: 0.25 },
})
expect(models.get(Model.ID.make("legacy"))?.cost[0]).toMatchObject({
input: 2.5,
output: 15,
cache: { read: 0.25 },
})
expect(models.get(Model.ID.make("renamed"))?.cost[0]).toMatchObject({ input: 2.5, output: 15, cache: { read: 0.25 } })
expect(models.get(Model.ID.make("legacy"))?.cost[0]).toMatchObject({ input: 2.5, output: 15, cache: { read: 0.25 } })
expect(models.get(Model.ID.make("unpriced"))?.cost[0]).toMatchObject({ cache: { read: 0 } })
} finally {
await server.stop(true)
+1 -1
View File
@@ -657,7 +657,7 @@ describe("LocationServiceMap", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((editor) => {
editor.provider.update(Provider.ID.make("aliased"), (provider) => {
provider.package = "@opencode/ai/providers/openai"
provider.package = Provider.aisdk("@ai-sdk/openai")
})
editor.model.update(Provider.ID.make("aliased"), Model.ID.make("fast"), (model) => {
// Catalog id and package model id intentionally differ, like gpt-5.5-fast -> gpt-5.5.
+1 -1
View File
@@ -65,7 +65,7 @@ test("maps live Modal models onto catalog templates", async () => {
expect(model?.family).toBe(Model.Family.make("catalog-family"))
expect(model?.providerID).toBe(providerID)
expect(model?.modelID).toBe(Model.ID.make("live-model"))
expect(model?.package).toBe("@opencode/ai/providers/openai-compatible")
expect(model?.package).toBe(Provider.aisdk("@ai-sdk/openai-compatible"))
expect(model?.settings).toMatchObject({ baseURL: `${server.url.origin}/v1` })
expect(model?.compatibility).toMatchObject({ reasoningField: "reasoning_content" })
expect(model?.capabilities).toMatchObject({ tools: true, input: ["text", "image"], output: ["text"] })
+6 -12
View File
@@ -11,7 +11,6 @@ import { Provider } from "@opencode/core/provider"
import { ModelResolver } from "@opencode/core/model-resolver"
import { Catalog } from "@opencode/core/catalog"
import { AISDK } from "@opencode/core/aisdk"
import { AISDKNative } from "@opencode/core/aisdk-native"
import { Npm } from "@opencode/util/npm"
import { it } from "./lib/effect"
@@ -27,9 +26,8 @@ interface ModelOptions {
readonly limit?: Info["limit"]
}
// A catalog model as the resolver receives it: legacy packages are already rewritten to native ones.
const model = (packageName: string | undefined, options: ModelOptions = {}) => {
const info = Info.make({
const model = (packageName: string | undefined, options: ModelOptions = {}) =>
Info.make({
id: ID.make("test-model"),
modelID: ID.make(options.modelID ?? "api-test-model"),
providerID: options.providerID ?? Provider.ID.make("test-provider"),
@@ -48,10 +46,6 @@ const model = (packageName: string | undefined, options: ModelOptions = {}) => {
enabled: true,
limit: options.limit ?? { context: 100, output: 20 },
})
const draft = structuredClone(info)
AISDKNative.rewrite({}, [draft])
return draft
}
function withEnv<A, E, R>(variables: Record<string, string | undefined>, effect: () => Effect.Effect<A, E, R>) {
return Effect.acquireUseRelease(
@@ -221,7 +215,7 @@ describe("ModelResolver", () => {
id: "bedrock-mantle-responses",
endpoint: { baseURL: "https://bedrock-mantle.us-west-2.api.aws/openai/v1" },
})
expect(catalog.settings?.baseURL).toBe("https://bedrock-mantle.us-west-2.api.aws/openai/v1")
expect(catalog.settings?.baseURL).toBe("https://bedrock-mantle.${AWS_REGION}.api.aws/openai/v1")
}),
),
)
@@ -484,7 +478,7 @@ describe("ModelResolver", () => {
expect(prepared.body).toMatchObject({ max_completion_tokens: 10 })
expect(prepared.body).not.toHaveProperty("max_tokens")
expect(resolved.route.endpoint.baseURL).toBe("https://compatible.example/v1")
expect(resolved.route.defaults.http?.body ?? {}).toEqual({})
expect(resolved.route.defaults.http?.body).toEqual({})
}),
)
@@ -717,7 +711,7 @@ describe("ModelResolver", () => {
}),
)
expect(resolved.route.defaults.http?.body ?? {}).toEqual({})
expect(resolved.route.defaults.http?.body).toEqual({})
}),
)
@@ -1415,7 +1409,7 @@ describe("ModelResolver", () => {
_tag: "SessionRunnerModel.ModelConfigurationError",
providerID: "azure",
modelID: "test-model",
package: "@opencode/ai/providers/azure/responses",
package: "aisdk:@ai-sdk/azure",
detail: "Azure requires resourceName or baseURL",
})
expect(failure.message).toBe("Cannot initialize azure/test-model: Azure requires resourceName or baseURL")
+4 -4
View File
@@ -49,7 +49,7 @@ const fixtureSnapshot = [
id: Provider.ID.make("acme"),
name: "Acme",
activation: "auto",
package: "@opencode/ai/providers/openai-compatible",
package: Provider.aisdk("@ai-sdk/openai-compatible"),
},
models: [
{
@@ -112,7 +112,7 @@ const fixture2Snapshot = [
id: Provider.ID.make("beta"),
name: "Beta",
activation: "auto",
package: "@opencode/ai/providers/openai-compatible",
package: Provider.aisdk("@ai-sdk/openai-compatible"),
},
models: [
{
@@ -254,8 +254,8 @@ describe("ModelsDev Service", () => {
cache,
ModelsDev.Service.use((service) => service.get()),
)
expect(result[0]?.info.package).toBe("@opencode/ai/providers/openai-compatible")
expect(result[0]?.models[0]?.package).toBe("@opencode/ai/providers/openai")
expect(result[0]?.info.package).toBe(Provider.aisdk("@ai-sdk/openai-compatible"))
expect(result[0]?.models[0]?.package).toBe(Provider.aisdk("@ai-sdk/openai"))
}),
)
+3 -4
View File
@@ -1081,19 +1081,18 @@ describe("ModelsDevPlugin", () => {
Provider.ID.make("amazon-bedrock"),
Model.ID.make("us.amazon.nova-2-lite-v1:0"),
)
// Native Bedrock takes Converse request fields in the body.
expect(bedrock?.variants).toEqual([
{
id: Model.VariantID.make("none"),
body: { additionalModelRequestFields: { reasoningConfig: { type: "disabled" } } },
settings: { additionalModelRequestFields: { reasoningConfig: { type: "disabled" } } },
},
{
id: Model.VariantID.make("low"),
body: { additionalModelRequestFields: { reasoningConfig: { type: "enabled", maxReasoningEffort: "low" } } },
settings: { reasoningConfig: { type: "enabled", maxReasoningEffort: "low" } },
},
{
id: Model.VariantID.make("high"),
body: { additionalModelRequestFields: { reasoningConfig: { type: "enabled", maxReasoningEffort: "high" } } },
settings: { reasoningConfig: { type: "enabled", maxReasoningEffort: "high" } },
},
])
@@ -57,7 +57,7 @@ const seedBedrock = Effect.fn(function* (settings?: Record<string, unknown>) {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => {
catalog.provider.update(Provider.ID.amazonBedrock, (item) => {
item.package = "@opencode/ai/providers/amazon-bedrock"
item.package = Provider.aisdk("@ai-sdk/amazon-bedrock")
if (settings) item.settings = settings
})
})
@@ -71,7 +71,7 @@ describe("AmazonBedrockPlugin", () => {
const catalog = yield* seedBedrock({ endpoint: "https://bedrock.example" })
yield* addPlugin()
const result = required(yield* catalog.provider.get(Provider.ID.amazonBedrock))
expect(result.package).toBe("@opencode/ai/providers/amazon-bedrock")
expect(result.package).toBe(Provider.aisdk("@ai-sdk/amazon-bedrock"))
expect(result.settings).toEqual({ baseURL: "https://bedrock.example", region: "us-east-1" })
}),
),
@@ -152,7 +152,7 @@ describe("AmazonBedrockPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => {
catalog.provider.update(Provider.ID.amazonBedrock, (item) => {
item.package = "@opencode/ai/providers/amazon-bedrock"
item.package = Provider.aisdk("@ai-sdk/amazon-bedrock")
item.activation = "disabled"
})
})
@@ -206,13 +206,13 @@ describe("AmazonBedrockPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => {
catalog.provider.update(Provider.ID.make("mantle"), (item) => {
item.package = "@opencode/ai/providers/amazon-bedrock/mantle/responses"
item.package = Provider.aisdk("@ai-sdk/amazon-bedrock/mantle")
})
catalog.provider.update(Provider.ID.make("native"), (item) => {
item.package = "@opencode/ai/providers/amazon-bedrock"
})
catalog.provider.update(Provider.ID.make("other"), (item) => {
item.package = "@opencode/ai/providers/anthropic"
item.package = Provider.aisdk("@ai-sdk/anthropic")
})
})
yield* addPlugin()
@@ -244,7 +244,7 @@ describe("AzurePlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((editor) => {
editor.provider.update(Provider.ID.azure, (provider) => {
provider.package = "@opencode/ai/providers/azure/responses"
provider.package = Provider.aisdk("@ai-sdk/azure")
})
editor.model.update(Provider.ID.azure, Model.ID.make("gpt-5-mini"), () => {})
editor.model.update(Provider.ID.azure, Model.ID.make("gpt-5-nano"), () => {})
@@ -311,7 +311,7 @@ describe("AzurePlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => {
catalog.provider.update(Provider.ID.azure, (item) => {
item.package = "@opencode/ai/providers/azure/responses"
item.package = Provider.aisdk("@ai-sdk/azure")
})
})
yield* addPlugin()
@@ -326,7 +326,7 @@ describe("AzurePlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => {
catalog.provider.update(Provider.ID.azure, (item) => {
item.package = "@opencode/ai/providers/azure/responses"
item.package = Provider.aisdk("@ai-sdk/azure")
})
})
yield* addPlugin()
@@ -341,13 +341,13 @@ describe("AzurePlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => {
catalog.provider.update(Provider.ID.azure, (provider) => {
provider.package = "@opencode/ai/providers/openai-compatible"
provider.package = Provider.aisdk("@ai-sdk/openai-compatible")
provider.settings = {
baseURL: "https://${AZURE_COGNITIVE_SERVICES_RESOURCE_NAME}.cognitiveservices.azure.com/openai",
}
})
catalog.model.update(Provider.ID.azure, Model.ID.make("anthropic"), (model) => {
model.package = "@opencode/ai/providers/anthropic"
model.package = Provider.aisdk("@ai-sdk/anthropic")
model.settings = {
resourceName: "model-resource",
baseURL: "https://${AZURE_RESOURCE_NAME}.services.ai.azure.com/anthropic/v1",
@@ -376,7 +376,7 @@ describe("AzurePlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => {
catalog.provider.update(Provider.ID.azure, (item) => {
item.package = "@opencode/ai/providers/azure/responses"
item.package = Provider.aisdk("@ai-sdk/azure")
item.settings = { resourceName: "from-config" }
})
catalog.provider.update(Provider.ID.openai, () => {})
@@ -394,7 +394,7 @@ describe("AzurePlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => {
catalog.provider.update(Provider.ID.azure, (item) => {
item.package = "@opencode/ai/providers/azure/responses"
item.package = Provider.aisdk("@ai-sdk/azure")
item.settings = { resourceName: "" }
})
})
@@ -410,7 +410,7 @@ describe("AzurePlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => {
catalog.provider.update(Provider.ID.azure, (item) => {
item.package = "@opencode/ai/providers/azure/responses"
item.package = Provider.aisdk("@ai-sdk/azure")
item.settings = { resourceName: " " }
})
})
@@ -434,11 +434,11 @@ describe("AzurePlugin", () => {
}
yield* catalog.transform((editor) => {
editor.provider.update(Provider.ID.azure, (provider) => {
provider.package = "@opencode/ai/providers/azure/responses"
provider.package = Provider.aisdk("@ai-sdk/azure")
})
editor.model.update(Provider.ID.azure, models.responses, () => {})
editor.model.update(Provider.ID.azure, models.chat, (model) => {
model.package = "@opencode/ai/providers/azure/chat"
model.settings = { useCompletionUrls: true }
})
editor.model.update(Provider.ID.azure, models.preview, (model) => {
model.settings = { apiVersion: "2025-04-01-preview" }
@@ -450,7 +450,7 @@ describe("AzurePlugin", () => {
model.settings = { baseURL: "https://gateway.example/azure" }
})
editor.model.update(Provider.ID.azure, models.nonAzure, (model) => {
model.package = "@opencode/ai/providers/anthropic"
model.package = Provider.aisdk("@ai-sdk/anthropic")
})
})
@@ -467,4 +467,5 @@ describe("AzurePlugin", () => {
}),
),
)
})
@@ -22,7 +22,7 @@ describe("CerebrasPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => {
catalog.provider.update(Provider.ID.make("cerebras"), (item) => {
item.package = "@opencode/ai/providers/cerebras"
item.package = Provider.aisdk("@ai-sdk/cerebras")
item.headers = { ...item.headers, Existing: "1" }
})
})
@@ -302,7 +302,7 @@ describe("GithubCopilotPlugin", () => {
yield* catalog.transform((catalog) => {
catalog.provider.update(Provider.ID.githubCopilot, () => {})
catalog.model.update(Provider.ID.githubCopilot, Model.ID.make("gpt-5.6-sol"), (model) => {
model.package = "@opencode/ai/providers/openai-compatible"
model.package = Provider.aisdk("@ai-sdk/openai-compatible")
})
})
yield* addPlugin()
@@ -69,7 +69,7 @@ describe("GoogleVertexPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) =>
catalog.provider.update(Provider.ID.opencode, (provider) => {
provider.package = "@opencode/ai/providers/openai-compatible"
provider.package = Provider.aisdk("@ai-sdk/openai-compatible")
provider.settings = { ...provider.settings, baseURL: "https://opencode.ai/zen/v1" }
}),
)
@@ -95,7 +95,7 @@ describe("GoogleVertexPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) =>
catalog.provider.update(Provider.ID.make("google-vertex"), (provider) => {
provider.package = "@opencode/ai/providers/openai-compatible"
provider.package = Provider.aisdk("@ai-sdk/openai-compatible")
provider.settings = {
...provider.settings,
baseURL:
@@ -108,7 +108,7 @@ describe("GoogleVertexPlugin", () => {
expect(provider.settings?.project).toBe("google-cloud-project")
expect(provider.settings?.location).toBe("google-vertex-location")
expect(provider).toMatchObject({
package: "@opencode/ai/providers/openai-compatible",
package: "aisdk:@ai-sdk/openai-compatible",
settings: {
baseURL:
"https://google-vertex-location-aiplatform.googleapis.com/v1/projects/google-cloud-project/locations/google-vertex-location",
@@ -131,7 +131,7 @@ describe("GoogleVertexPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) =>
catalog.provider.update(Provider.ID.make("google-vertex"), (provider) => {
provider.package = "@opencode/ai/providers/google-vertex"
provider.package = Provider.aisdk("@ai-sdk/google-vertex")
}),
)
yield* addPlugin()
@@ -154,7 +154,7 @@ describe("GoogleVertexPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) =>
catalog.provider.update(Provider.ID.make("google-vertex"), (provider) => {
provider.package = "@opencode/ai/providers/google-vertex"
provider.package = Provider.aisdk("@ai-sdk/google-vertex")
}),
)
yield* addPlugin()
@@ -180,7 +180,7 @@ describe("GoogleVertexPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) =>
catalog.provider.update(Provider.ID.make("google-vertex"), (provider) => {
provider.package = "@opencode/ai/providers/openai-compatible"
provider.package = Provider.aisdk("@ai-sdk/openai-compatible")
provider.settings = {
...provider.settings,
baseURL:
@@ -193,7 +193,7 @@ describe("GoogleVertexPlugin", () => {
expect(provider.settings?.project).toBe("vertex-project")
expect(provider).toMatchObject({
package: "@opencode/ai/providers/openai-compatible",
package: "aisdk:@ai-sdk/openai-compatible",
settings: {
baseURL:
"https://europe-west4-aiplatform.googleapis.com/v1/projects/vertex-project/locations/europe-west4",
@@ -218,7 +218,7 @@ describe("GoogleVertexPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => {
catalog.provider.update(Provider.ID.make("google-vertex"), (provider) => {
provider.package = "@opencode/ai/providers/openai-compatible"
provider.package = Provider.aisdk("@ai-sdk/openai-compatible")
provider.settings = {
...provider.settings,
baseURL:
@@ -234,7 +234,7 @@ describe("GoogleVertexPlugin", () => {
expect(provider.settings?.project).toBe("config-project")
expect(provider.settings?.location).toBe("global")
expect(provider).toMatchObject({
package: "@opencode/ai/providers/openai-compatible",
package: "aisdk:@ai-sdk/openai-compatible",
settings: { baseURL: "https://aiplatform.googleapis.com/v1/projects/config-project/locations/global" },
})
expect(model.settings).toEqual(provider.settings)
@@ -247,7 +247,7 @@ describe("GoogleVertexPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) =>
catalog.provider.update(Provider.ID.make("google-vertex"), (provider) => {
provider.package = "@opencode/ai/providers/openai-compatible"
provider.package = Provider.aisdk("@ai-sdk/openai-compatible")
provider.settings = {
...provider.settings,
baseURL:
@@ -259,7 +259,7 @@ describe("GoogleVertexPlugin", () => {
yield* addPlugin()
const provider = required(yield* catalog.provider.get(Provider.ID.make("google-vertex")))
expect(provider).toMatchObject({
package: "@opencode/ai/providers/openai-compatible",
package: "aisdk:@ai-sdk/openai-compatible",
settings: { baseURL: "https://eu-aiplatform.googleapis.com/v1/projects/config-project/locations/eu" },
})
}),
@@ -280,7 +280,7 @@ describe("GoogleVertexPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) =>
catalog.provider.update(Provider.ID.make("google-vertex"), (provider) => {
provider.package = "@opencode/ai/providers/google-vertex"
provider.package = Provider.aisdk("@ai-sdk/google-vertex")
provider.settings = { ...provider.settings, project: "config-project" }
}),
)
@@ -313,7 +313,7 @@ describe("GoogleVertexPlugin", () => {
model: Model.Info.make({
...Model.Info.default(Provider.ID.make("google-vertex"), Model.ID.make("gemini")),
modelID: Model.ID.make("gemini"),
package: "@opencode/ai/providers/openai-compatible",
package: "aisdk:@ai-sdk/openai-compatible",
}),
package: "@ai-sdk/openai-compatible",
options: {
@@ -27,7 +27,7 @@ describe("KiloPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => {
catalog.provider.update(Provider.ID.make("kilo"), (provider) => {
provider.package = "@opencode/ai/providers/openai-compatible"
provider.package = Provider.aisdk("@ai-sdk/openai-compatible")
provider.settings = { baseURL: "https://api.kilo.ai/api/gateway" }
provider.headers = { Existing: "value" }
})
@@ -48,7 +48,7 @@ describe("KiloPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => {
catalog.provider.update(Provider.ID.make("kilo"), (provider) => {
provider.package = "@opencode/ai/providers/openai-compatible"
provider.package = Provider.aisdk("@ai-sdk/openai-compatible")
provider.settings = { baseURL: "https://api.kilo.ai/api/gateway" }
})
})
@@ -72,7 +72,7 @@ describe("KiloPlugin", () => {
provider.package = Provider.aisdk("kilo")
})
catalog.provider.update(Provider.ID.make("custom-kilo"), (provider) => {
provider.package = "@opencode/ai/providers/openai-compatible"
provider.package = Provider.aisdk("@ai-sdk/openai-compatible")
provider.settings = { baseURL: "https://api.kilo.ai/api/gateway" }
})
})
@@ -33,7 +33,7 @@ describe("LLMGatewayPlugin", () => {
})
yield* catalog.transform((catalog) => {
catalog.provider.update(Provider.ID.make("llmgateway"), (provider) => {
provider.package = "@opencode/ai/providers/openai-compatible"
provider.package = Provider.aisdk("@ai-sdk/openai-compatible")
provider.settings = { baseURL: "https://api.llmgateway.io/v1" }
provider.headers = { Existing: "value" }
})
@@ -60,7 +60,7 @@ describe("LLMGatewayPlugin", () => {
yield* catalog.transform((catalog) => {
catalog.provider.update(Provider.ID.make("llmgateway"), (provider) => {
provider.activation = "disabled"
provider.package = "@opencode/ai/providers/openai-compatible"
provider.package = Provider.aisdk("@ai-sdk/openai-compatible")
provider.settings = { baseURL: "https://api.llmgateway.io/v1" }
})
})
@@ -27,7 +27,7 @@ describe("NvidiaPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => {
catalog.provider.update(Provider.ID.make("nvidia"), (provider) => {
provider.package = "@opencode/ai/providers/openai-compatible"
provider.package = Provider.aisdk("@ai-sdk/openai-compatible")
provider.settings = { baseURL: "https://integrate.api.nvidia.com/v1" }
provider.headers = { Existing: "value" }
})
@@ -49,7 +49,7 @@ describe("NvidiaPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => {
catalog.provider.update(Provider.ID.make("nvidia"), (provider) => {
provider.package = "@opencode/ai/providers/openai-compatible"
provider.package = Provider.aisdk("@ai-sdk/openai-compatible")
provider.settings = { baseURL: "https://integrate.api.nvidia.com/v1" }
})
})
@@ -68,7 +68,7 @@ describe("NvidiaPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => {
catalog.provider.update(Provider.ID.make("nvidia"), (provider) => {
provider.package = "@opencode/ai/providers/openai-compatible"
provider.package = Provider.aisdk("@ai-sdk/openai-compatible")
provider.settings = { baseURL: "https://integrate.api.nvidia.com/v1" }
provider.headers = { "X-BILLING-INVOKE-ORIGIN": "CustomOrigin" }
})
@@ -71,7 +71,7 @@ describe("OllamaPlugin", () => {
}
: body.model === "unknown-context"
? show({ family: "unknown", capabilities: ["completion"], context: 0 })
: show({ family: "nomic-bert", capabilities: ["embedding"], context: 8192 }),
: show({ family: "nomic-bert", capabilities: ["embedding"], context: 8192 }),
)
},
}),
@@ -370,7 +370,7 @@ describe("OpencodePlugin", () => {
remote: {
canonical: "openai",
name: "Remote",
package: "@opencode/ai/providers/openai-compatible",
package: "aisdk:@ai-sdk/openai-compatible",
env: ["REMOTE_API_KEY"],
settings: {
baseURL: `${origin}/v1`,
@@ -409,7 +409,7 @@ describe("OpencodePlugin", () => {
},
override: {
name: "Override",
package: "@opencode/ai/providers/anthropic",
package: "aisdk:@ai-sdk/anthropic",
settings: { baseURL: `${origin}/anthropic` },
},
disabled: { name: "Disabled", disabled: true },
@@ -428,11 +428,11 @@ describe("OpencodePlugin", () => {
const integrations = yield* Integration.Service
yield* catalog.transform((editor) => {
editor.provider.update(Provider.ID.openai, (provider) => {
provider.package = "@opencode/ai/providers/openai"
provider.package = Provider.aisdk("@ai-sdk/openai")
provider.integrationID = Integration.ID.make("openai")
})
editor.model.update(Provider.ID.openai, Model.ID.make("api-model"), (model) => {
model.package = "@opencode/ai/providers/openai"
model.package = Provider.aisdk("@ai-sdk/openai")
model.settings = { baseURL: "https://upstream.example/v1" }
model.variants = [
{
@@ -464,7 +464,7 @@ describe("OpencodePlugin", () => {
canonical: "openai",
name: "Remote",
integrationID: "opencode",
package: "@opencode/ai/providers/openai-compatible",
package: Provider.aisdk("@ai-sdk/openai-compatible"),
settings: { baseURL: `${server.url.origin}/v1`, custom: "value" },
headers: { "x-org-id": "org" },
})
@@ -481,13 +481,13 @@ describe("OpencodePlugin", () => {
capabilities: { tools: true, input: ["text", "image"], output: ["text"] },
cost: [{ input: 1, output: 2, cache: { read: 0.1, write: 0 } }],
limit: { context: 1000, output: 100 },
package: "@opencode/ai/providers/openai-compatible",
package: Provider.aisdk("@ai-sdk/openai-compatible"),
settings: { baseURL: `${server.url.origin}/v1`, custom: "value", temperature: 0.5 },
headers: { "x-org-id": "org" },
})
expect(model.settings).toEqual({ baseURL: `${server.url.origin}/v1`, custom: "value", temperature: 0.5 })
const override = required(yield* catalog.model.get(Provider.ID.make("remote"), Model.ID.make("override")))
expect(override.package).toBe("@opencode/ai/providers/anthropic")
expect(override.package).toBe(Provider.aisdk("@ai-sdk/anthropic"))
expect(override.settings?.baseURL).toBe(`${server.url.origin}/anthropic`)
expect(model.variants).toEqual([
{
@@ -990,16 +990,16 @@ describe("OpencodePlugin", () => {
providers: {
remote: {
canonical: "openai",
package: "@opencode/ai/providers/openai",
package: "aisdk:@ai-sdk/openai",
settings: { baseURL: new URL(request.url).origin },
models: {
...models,
override: { ...models.astra, package: "@opencode/ai/providers/openai-compatible" },
override: { ...models.astra, package: "aisdk:@ai-sdk/openai-compatible" },
},
},
compatible: {
canonical: "openai",
package: "@opencode/ai/providers/openai-compatible",
package: "aisdk:@ai-sdk/openai-compatible",
models,
},
},
@@ -28,7 +28,7 @@ describe("OpenRouterPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => {
catalog.provider.update(Provider.ID.openrouter, (provider) => {
provider.package = "@opencode/ai/providers/openrouter"
provider.package = Provider.aisdk("@openrouter/ai-sdk-provider")
provider.headers = { Existing: "value" }
})
catalog.provider.update(Provider.ID.make("nvidia"), () => {})
@@ -49,7 +49,7 @@ describe("OpenRouterPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => {
catalog.provider.update(Provider.ID.openrouter, (provider) => {
provider.package = "@opencode/ai/providers/openrouter"
provider.package = Provider.aisdk("@openrouter/ai-sdk-provider")
})
catalog.provider.update(Provider.ID.openai, () => {})
catalog.model.update(Provider.ID.openrouter, Model.ID.make("openai/gpt-5-chat"), () => {})
@@ -43,7 +43,7 @@ const fixture = Effect.gen(function* () {
})
yield* catalog.transform((editor) => {
editor.provider.update(providerID, (provider) => {
provider.package = "@opencode/ai/providers/openai-compatible"
provider.package = Provider.aisdk("@ai-sdk/openai-compatible")
provider.settings = { baseURL: "https://api.poe.com/v1" }
})
editor.model.update(providerID, modelID, () => {})
@@ -56,7 +56,7 @@ const fixture = Effect.fn(function* () {
const host = yield* PluginHost.make(plugin)
yield* catalog.transform((editor) => {
editor.provider.update(providerID, (provider) => {
provider.package = "@opencode/ai/providers/openai-compatible"
provider.package = Provider.aisdk("@ai-sdk/openai-compatible")
provider.settings = { baseURL: "https://${SNOWFLAKE_ACCOUNT}.snowflakecomputing.com/api/v2/cortex/v1" }
})
editor.model.update(providerID, modelID, () => {})
@@ -225,7 +225,9 @@ it.live("uses environment tokens rather than the account identifier and bypasses
type: "env",
name: "SNOWFLAKE_CORTEX_TOKEN",
})
expect((yield* test.catalog.provider.get(providerID))?.package).toBe("@opencode/ai/providers/openai-compatible")
expect((yield* test.catalog.provider.get(providerID))?.package).toBe(
Provider.aisdk("@ai-sdk/openai-compatible"),
)
expect(yield* test.hooks.has("aisdk", "sdk", providerID)).toBe(false)
yield* test.stop
@@ -32,7 +32,7 @@ describe("ZenmuxPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => {
catalog.provider.update(Provider.ID.make("zenmux"), (provider) => {
provider.package = "@opencode/ai/providers/openai-compatible"
provider.package = Provider.aisdk("@ai-sdk/openai-compatible")
provider.settings = { ...provider.settings, baseURL: "https://zenmux.ai/api/v1" }
})
})
@@ -48,7 +48,7 @@ describe("ZenmuxPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => {
catalog.provider.update(Provider.ID.make("zenmux"), (provider) => {
provider.package = "@opencode/ai/providers/openai-compatible"
provider.package = Provider.aisdk("@ai-sdk/openai-compatible")
provider.settings = { ...provider.settings, baseURL: "https://zenmux.ai/api/v1" }
provider.headers = { ...provider.headers, Existing: "value" }
})
@@ -68,7 +68,7 @@ describe("ZenmuxPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => {
catalog.provider.update(Provider.ID.make("zenmux"), (provider) => {
provider.package = "@opencode/ai/providers/openai-compatible"
provider.package = Provider.aisdk("@ai-sdk/openai-compatible")
provider.settings = { ...provider.settings, baseURL: "https://zenmux.ai/api/v1" }
provider.headers = { "HTTP-Referer": "https://example.com/", "X-Title": "custom-title" }
})
+6 -1
View File
@@ -71,7 +71,12 @@ describe("SkillPlugin.Plugin", () => {
expect(report?.content).toContain("- Active plugins: -disabled, local.ts, package-plugin, package-plugin")
}).pipe(
Effect.provide(
config(["package-plugin", "-disabled", "local.ts", { package: "package-plugin", options: { enabled: true } }]),
config([
"package-plugin",
"-disabled",
"local.ts",
{ package: "package-plugin", options: { enabled: true } },
]),
),
),
)
+3 -3
View File
@@ -23,11 +23,11 @@ describe("VariantPlugin", () => {
const service = yield* Catalog.Service
yield* service.transform((catalog) => {
catalog.provider.update(Provider.ID.opencode, (provider) => {
provider.package = "@opencode/ai/providers/openai-compatible"
provider.package = Provider.aisdk("@ai-sdk/openai-compatible")
})
catalog.model.update(Provider.ID.opencode, Model.ID.make("glm-5.2"), (model) => {
model.modelID = Model.ID.make("glm-5.2")
model.package = "@opencode/ai/providers/openai-compatible"
model.package = Provider.aisdk("@ai-sdk/openai-compatible")
})
})
yield* VariantPlugin.Plugin.effect(host({ catalog: catalogHost(service) }))
@@ -45,7 +45,7 @@ describe("VariantPlugin", () => {
yield* service.transform((catalog) => {
catalog.model.update(Provider.ID.opencode, Model.ID.make("glm-5.2"), (model) => {
model.modelID = Model.ID.make("glm-5.2")
model.package = "@opencode/ai/providers/openai-compatible"
model.package = Provider.aisdk("@ai-sdk/openai-compatible")
model.variants = [{ id: Model.VariantID.make("high"), settings: {}, headers: { custom: "true" }, body: {} }]
})
})
@@ -6,7 +6,7 @@ title: "Effect"
and decodes responses into OpenCode schema values.
```sh
bun add @opencode/client@beta effect
bun add @opencode/client effect
```
## Create a client
@@ -10,7 +10,7 @@ network. Its native types and methods are generated from the same contract as th
## Install
```sh
bun add @opencode/client@beta
bun add @opencode/client
```
## Create a client
@@ -7,7 +7,7 @@ Effects or Streams, callbacks return Effects, and plugin lifetime is represented
plugin package.
```sh
bun add @opencode/plugin@beta effect
bun add @opencode/plugin effect
```
Export an Effect plugin from `.opencode/plugins/` to load it automatically.
@@ -6,7 +6,7 @@ Use `@opencode/sdk/workerd` inside a Cloudflare Durable Object. This profile use
persists durable events for eviction recovery, and replaces unavailable local filesystem and process services.
```sh
bun add @opencode/sdk@beta
bun add @opencode/sdk
```
Hold one host for the lifetime of the Durable Object instance instead of creating one for every request.
@@ -6,7 +6,7 @@ title: "Effect"
the owning Scope releases the router, Location services, fibers, and plugin registrations.
```sh
bun add @opencode/sdk@beta effect
bun add @opencode/sdk effect
```
## Create a host
@@ -12,7 +12,7 @@ For Cloudflare Durable Objects, see the [Cloudflare guide](/build/sdk/cloudflare
Install the SDK:
```sh
bun add @opencode/sdk@beta
bun add @opencode/sdk
```
## Create a host
@@ -26,7 +26,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm install --global @opencode/cli@beta
- run: npm install --global @opencode/cli
- run: opencode run --standalone --model anthropic/claude-sonnet-4-5 "Review this repository for correctness and summarize any issues."
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+6 -8
View File
@@ -10,11 +10,10 @@ OpenCode installs and runs as `opencode`.
<div data-install-code>
<CodeBlock
code={`$ npm install -g @opencode/cli@beta
$ bun install -g --trust @opencode/cli@beta
$ pnpm add -g --allow-build=@opencode/cli @opencode/cli@beta
$ yarn global add @opencode/cli@beta
$ paru -S opencode-beta
code={`$ npm install -g @opencode/cli
$ bun install -g --trust @opencode/cli
$ pnpm add -g --allow-build=@opencode/cli @opencode/cli
$ yarn global add @opencode/cli
$ curl -fsSL https://opencode.ai/v2/install | bash`}
/>
</div>
@@ -22,10 +21,9 @@ $ curl -fsSL https://opencode.ai/v2/install | bash`}
The npm package uses a trusted postinstall script to select the native `opencode` binary for your platform. The Bun and pnpm
commands above explicitly allow that script to run.
On Arch Linux, install [`opencode-beta`](https://aur.archlinux.org/packages/opencode-beta) from the AUR with `paru`.
It provides the `opencode` command; manage updates through your AUR helper.
Docker images use versioned tags, for example `ghcr.io/anomalyco/opencode:2.0.0`.
Homebrew, Windows package managers, Docker, and standalone binaries are not supported.
Homebrew, AUR, Windows package managers, and standalone binaries are not supported.
---
+3 -5
View File
@@ -336,7 +336,7 @@ Rename the singular `provider` map to `providers`. V2 separates the runtime pack
{
"providers": {
"acme": {
"package": "@opencode/ai/providers/openai-compatible",
"package": "aisdk:@ai-sdk/openai-compatible",
"settings": {
"baseURL": "https://llm.example.com/v1",
"apiKey": "{env:ACME_API_KEY}"
@@ -346,10 +346,8 @@ Rename the singular `provider` map to `providers`. V2 separates the runtime pack
}
```
V1 `npm` becomes `package`. AI SDK packages with a native OpenCode implementation become that native package, and their
options are rewritten to the native spelling; other AI SDK packages keep running on the AI SDK with an `aisdk:` prefix.
`api` becomes `settings.baseURL`. Provider `options` are separated into `settings`, `headers`, and `body` according to
their request role. See [Providers](/providers).
V1 `npm` becomes `package`, and AI SDK packages receive the `aisdk:` prefix. `api` becomes `settings.baseURL`. Provider
`options` are separated into `settings`, `headers`, and `body` according to their request role. See [Providers](/providers).
V2 consolidated two legacy provider namespaces: