mirror of
https://github.com/anomalyco/opencode.git
synced 2026-09-12 03:46:22 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f0c26f6023 |
@@ -191,7 +191,7 @@ jobs:
|
||||
|
||||
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: opencode-preview-cli-macos
|
||||
name: opencode-preview-cli
|
||||
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' && !(github.ref_name == 'v2' && (inputs.bump || inputs.version))
|
||||
if: github.repository == 'anomalyco/opencode'
|
||||
steps:
|
||||
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
|
||||
|
||||
@@ -221,7 +221,7 @@ jobs:
|
||||
needs:
|
||||
- version
|
||||
- build-node-app-archive
|
||||
if: github.repository == 'anomalyco/opencode' && !(github.ref_name == 'v2' && (inputs.bump || inputs.version))
|
||||
if: github.repository == 'anomalyco/opencode'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -276,9 +276,10 @@ jobs:
|
||||
|
||||
sign-cli-windows:
|
||||
needs:
|
||||
- sign-cli-macos
|
||||
- build-cli
|
||||
- version
|
||||
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 }}
|
||||
@@ -291,8 +292,15 @@ jobs:
|
||||
|
||||
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
with:
|
||||
name: opencode-preview-cli-macos
|
||||
path: packages/cli/dist
|
||||
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: Azure login
|
||||
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0
|
||||
@@ -307,9 +315,9 @@ jobs:
|
||||
signing-account-name: ${{ env.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
|
||||
certificate-profile-name: ${{ env.AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE }}
|
||||
files: |
|
||||
${{ 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
|
||||
${{ 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
|
||||
exclude-environment-credential: true
|
||||
exclude-workload-identity-credential: true
|
||||
exclude-managed-identity-credential: true
|
||||
@@ -325,9 +333,9 @@ jobs:
|
||||
shell: pwsh
|
||||
run: |
|
||||
$files = @(
|
||||
"${{ 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"
|
||||
"${{ 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"
|
||||
)
|
||||
|
||||
foreach ($file in $files) {
|
||||
@@ -337,16 +345,39 @@ 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-preview-cli
|
||||
path: packages/cli/dist/cli-*
|
||||
if-no-files-found: error
|
||||
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
|
||||
|
||||
build-electron:
|
||||
needs:
|
||||
- version
|
||||
- sign-cli-windows
|
||||
- sign-cli-macos
|
||||
if: github.repository == 'anomalyco/opencode' && (github.ref_name != 'v2' || needs.version.outputs.release != '')
|
||||
continue-on-error: false
|
||||
env:
|
||||
|
||||
@@ -366,14 +366,15 @@ install_legacy_shim() {
|
||||
if [[ "$shim_os" == "windows" ]]; then
|
||||
cat > "$INSTALL_DIR/opencode2.cmd" <<'EOF'
|
||||
@echo off
|
||||
"%~dp0opencode.exe" %*
|
||||
exit /b %errorlevel%
|
||||
echo opencode2 is now just opencode. run opencode
|
||||
exit /b 1
|
||||
EOF
|
||||
return
|
||||
fi
|
||||
cat > "$INSTALL_DIR/opencode2" <<'EOF'
|
||||
#!/bin/sh
|
||||
exec "$(dirname "$0")/opencode" "$@"
|
||||
echo 'opencode2 is now just opencode. run opencode'
|
||||
exit 1
|
||||
EOF
|
||||
chmod 755 "$INSTALL_DIR/opencode2"
|
||||
}
|
||||
|
||||
@@ -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/cli-linux-x64-baseline-musl/bin/opencode /usr/local/bin/opencode
|
||||
COPY dist/opencode-linux-x64-baseline-musl/bin/opencode /usr/local/bin/opencode
|
||||
|
||||
FROM base AS build-arm64
|
||||
COPY dist/cli-linux-arm64-musl/bin/opencode /usr/local/bin/opencode
|
||||
COPY dist/opencode-linux-arm64-musl/bin/opencode /usr/local/bin/opencode
|
||||
|
||||
ARG TARGETARCH
|
||||
FROM build-${TARGETARCH}
|
||||
RUN ln -s opencode /usr/local/bin/opencode2 && opencode --version && opencode2 --version
|
||||
RUN opencode --version
|
||||
ENTRYPOINT ["opencode"]
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
require("./opencode.cjs")
|
||||
console.log("opencode2 is now just opencode. run opencode")
|
||||
process.exit(1)
|
||||
|
||||
@@ -17,9 +17,7 @@ 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
|
||||
exec "$(dirname "$0")/opencode" "$@"
|
||||
`
|
||||
const shim = "#!/bin/sh\necho 'opencode2 is now just opencode. run opencode'\nexit 1\n"
|
||||
|
||||
await rm(outdir, { recursive: true, force: true })
|
||||
await mkdir(path.dirname(outdir), { recursive: true })
|
||||
|
||||
@@ -60,13 +60,17 @@ 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.command}.exe` } : {}),
|
||||
...(input.legacyCommand ? { [input.legacyCommand]: `./bin/${input.legacyCommand}.cjs` } : {}),
|
||||
},
|
||||
...(input.command !== input.binary ? { opencodeSourceBinary: input.binary } : {}),
|
||||
scripts: { postinstall: "node ./postinstall.mjs" },
|
||||
@@ -132,7 +136,7 @@ await publishDistribution({
|
||||
packagePrefix: "@opencode/cli-",
|
||||
artifact: "cli",
|
||||
})
|
||||
if (Script.channel !== "latest" && existsSync(path.join(root, "node"))) {
|
||||
if (existsSync(path.join(root, "node"))) {
|
||||
await publishDistribution({
|
||||
root: path.join(root, "node"),
|
||||
name: "@opencode/cli-node",
|
||||
|
||||
@@ -3,18 +3,87 @@ export * as AISDKNative from "./aisdk-native.js"
|
||||
import { Effect, Option, Schema, Struct } from "effect"
|
||||
import { Provider } from "./provider.js"
|
||||
|
||||
export interface Mapping {
|
||||
readonly package: string
|
||||
readonly settings: Provider.Settings
|
||||
/** A provider, model, or variant overlay written in legacy AI SDK vocabulary. */
|
||||
export interface Overlay {
|
||||
readonly settings?: Provider.Settings
|
||||
readonly headers?: Readonly<Record<string, string>>
|
||||
readonly body?: Readonly<Record<string, unknown>>
|
||||
}
|
||||
|
||||
export interface MapInput {
|
||||
readonly packageName: string | undefined
|
||||
readonly settings: Provider.Settings
|
||||
readonly modelID: string
|
||||
readonly providerID: string
|
||||
/** 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"
|
||||
}
|
||||
}
|
||||
|
||||
// A wrongly typed legacy value is dropped rather than failing the whole decode.
|
||||
@@ -75,51 +144,36 @@ const Legacy = Schema.StructWithRest(
|
||||
type Legacy = typeof Legacy.Type
|
||||
const decode = Schema.decodeUnknownSync(Legacy)
|
||||
|
||||
/** 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) : {}),
|
||||
}
|
||||
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,
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
||||
// 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),
|
||||
)
|
||||
}
|
||||
|
||||
// AI SDK spellings the native Bedrock packages do not read.
|
||||
@@ -132,7 +186,7 @@ const BEDROCK_KEYS = [
|
||||
"secretAccessKey",
|
||||
"sessionToken",
|
||||
]
|
||||
// Request settings Converse takes in the body; translated by `bedrockRequest`.
|
||||
// Request settings Converse takes in the body; translated by `bedrockBody`.
|
||||
const CONVERSE_KEYS = ["additionalModelRequestFields", "reasoningConfig", "anthropicBeta", "serviceTier"]
|
||||
|
||||
function bedrockSettings(settings: Legacy, converse: boolean) {
|
||||
@@ -158,7 +212,7 @@ function bedrockSettings(settings: Legacy, converse: boolean) {
|
||||
}
|
||||
}
|
||||
|
||||
function bedrockRequest(modelID: string, settings: Legacy): Pick<Mapping, "body"> {
|
||||
function bedrockBody(modelID: string, settings: Legacy) {
|
||||
const additional = settings.additionalModelRequestFields ?? {}
|
||||
const reasoning = settings.reasoningConfig
|
||||
const anthropic = modelID.includes("anthropic")
|
||||
@@ -197,24 +251,22 @@ function bedrockRequest(modelID: string, settings: Legacy): Pick<Mapping, "body"
|
||||
...(fields && Object.keys(fields).length > 0 ? { additionalModelRequestFields: fields } : {}),
|
||||
...(settings.serviceTier === undefined ? {} : { serviceTier: { type: settings.serviceTier } }),
|
||||
}
|
||||
return Object.keys(body).length === 0 ? {} : { body }
|
||||
return Object.keys(body).length === 0 ? undefined : 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 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 }
|
||||
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,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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: Provider.aisdk(messages ? "@ai-sdk/anthropic" : "@ai-sdk/github-copilot"),
|
||||
package: messages ? "@opencode/ai/providers/anthropic" : Provider.aisdk("@ai-sdk/github-copilot"),
|
||||
settings: Provider.mergeOverlay(previous?.settings, {
|
||||
baseURL: messages ? `${baseURL}/v1` : baseURL,
|
||||
...(endpoint ? { endpoint } : {}),
|
||||
|
||||
@@ -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: Provider.aisdk("@ai-sdk/openai-compatible"),
|
||||
package: "@opencode/ai/providers/openai-compatible",
|
||||
settings: Provider.mergeOverlay(previous?.settings, { baseURL }),
|
||||
headers: previous?.headers,
|
||||
body: previous?.body,
|
||||
|
||||
@@ -5,7 +5,6 @@ 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"
|
||||
@@ -195,25 +194,15 @@ 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 }
|
||||
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) {
|
||||
if (Provider.isAISDK(resolved.package)) {
|
||||
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,
|
||||
}) ?? {},
|
||||
@@ -222,19 +211,22 @@ const resolveCatalogModel = Effect.fn("ModelResolver.resolveCatalogModel")(funct
|
||||
Effect.mapError((error) => initialization(resolved, "init", error.cause)),
|
||||
)
|
||||
}
|
||||
if (!native) return yield* unsupported(resolved)
|
||||
const specifier = Provider.packageName(resolved.package)
|
||||
if (!specifier) return yield* unsupported(resolved)
|
||||
|
||||
const specifier = native
|
||||
const mapped = yield* prepareProviderSettings(resolved, Provider.nativeSettings(mapping?.settings ?? configured))
|
||||
const mapped = yield* prepareProviderSettings(resolved, Provider.nativeSettings(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),
|
||||
...(resolved.canonical === undefined ? {} : { provider: resolved.canonical }),
|
||||
...(provider === undefined ? {} : { provider }),
|
||||
...nativeCredentialSettings(specifier, credential),
|
||||
headers: Provider.mergeHeaders(mapping?.headers, resolved.headers),
|
||||
body: Provider.mergeOverlay(mapping?.body, resolved.body),
|
||||
headers: resolved.headers,
|
||||
body: resolved.body,
|
||||
}
|
||||
return yield* Effect.try({
|
||||
try: () => {
|
||||
@@ -424,18 +416,6 @@ 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" ||
|
||||
|
||||
+193
-182
@@ -2,6 +2,7 @@ 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"
|
||||
@@ -87,23 +88,30 @@ function normalize(input: Record<string, SourceProvider>): readonly Snapshot[] {
|
||||
id: providerID,
|
||||
name: item.name,
|
||||
activation: "auto",
|
||||
package: Provider.aisdk(item.npm),
|
||||
...(item.api ? { settings: { baseURL: item.api } } : {}),
|
||||
...packageInfo(item.npm, undefined, item.api),
|
||||
} satisfies Provider.Info
|
||||
const models: Model.Info[] = []
|
||||
for (const model of Object.values(item.models)) {
|
||||
const baseCost = cost(model.cost)
|
||||
const variants = reasoningVariants(item, model)
|
||||
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 id = Model.ID.make(model.id)
|
||||
models.push(modelInfo(providerID, id, model, { cost: baseCost, variants }))
|
||||
models.push(modelInfo(providerID, id, model, { ...request, cost: baseCost }))
|
||||
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),
|
||||
request: options.provider,
|
||||
variants,
|
||||
headers: Provider.mergeHeaders(own.headers, options.provider?.headers),
|
||||
body: Provider.mergeOverlay(own.body, options.provider?.body),
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -113,6 +121,18 @@ 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
|
||||
@@ -187,14 +207,45 @@ function mergeCost(base: Model.Info["cost"], override: SourceModel["cost"] | und
|
||||
const OPENAI_INCLUDE_ENCRYPTED_REASONING = ["reasoning.encrypted_content"]
|
||||
const OUTPUT_TOKEN_MAX = 32_000
|
||||
|
||||
function reasoningVariants(provider: SourceProvider, model: SourceModel): NonNullable<Model.Info["variants"]> {
|
||||
const npm = model.provider?.npm ?? provider.npm
|
||||
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"]> {
|
||||
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(npm, model.id).filter((variant) => variant.id === "none") : []
|
||||
const off = toggle ? toggleVariants(pkg, model.id).filter((variant) => variant.id === "none") : []
|
||||
const variants = [
|
||||
...off,
|
||||
...effort.values.flatMap((value) => {
|
||||
@@ -202,8 +253,8 @@ function reasoningVariants(provider: SourceProvider, model: SourceModel): NonNul
|
||||
const id = typeof raw === "string" && raw !== "null" ? raw : undefined
|
||||
if (id === undefined) return []
|
||||
if (id === "none" && off.length > 0) return []
|
||||
const settings = settingsForEffort(npm, model.id, id)
|
||||
return settings ? [{ id: Model.VariantID.make(id), settings }] : []
|
||||
const overlay = effortVariant(pkg, model.id, id)
|
||||
return overlay ? [{ id: Model.VariantID.make(id), ...overlay }] : []
|
||||
}),
|
||||
]
|
||||
return [...new Map(variants.map((variant) => [variant.id, variant])).values()]
|
||||
@@ -211,80 +262,78 @@ function reasoningVariants(provider: SourceProvider, model: SourceModel): NonNul
|
||||
const budget = options.find((option) => option.type === "budget_tokens")
|
||||
if (budget?.type === "budget_tokens")
|
||||
return [
|
||||
...(toggle ? toggleVariants(npm, model.id).filter((variant) => variant.id === "none") : []),
|
||||
...budgetVariants(npm, model, budget),
|
||||
...(toggle ? toggleVariants(pkg, model.id).filter((variant) => variant.id === "none") : []),
|
||||
...budgetVariants(pkg, model, budget),
|
||||
]
|
||||
if (toggle) return toggleVariants(npm, model.id)
|
||||
if (toggle) return toggleVariants(pkg, model.id)
|
||||
return []
|
||||
}
|
||||
|
||||
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 }
|
||||
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)
|
||||
return {
|
||||
thinking: { type: "adaptive", display: "summarized" },
|
||||
effort,
|
||||
settings: {
|
||||
reasoningConfig: modelID.includes("anthropic")
|
||||
? {
|
||||
...(anthropicManualThinking(modelID) ? {} : { type: "adaptive", display: "summarized" }),
|
||||
maxReasoningEffort: effort,
|
||||
}
|
||||
: { type: "enabled", maxReasoningEffort: effort },
|
||||
},
|
||||
}
|
||||
}
|
||||
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 {
|
||||
reasoningConfig: {
|
||||
...(anthropicManualThinking(modelID) ? {} : { type: "adaptive", display: "summarized" }),
|
||||
maxReasoningEffort: effort,
|
||||
},
|
||||
}
|
||||
return { reasoningConfig: { type: "enabled", maxReasoningEffort: effort } }
|
||||
}
|
||||
if (npm === "@ai-sdk/gateway") {
|
||||
if (pkg === GATEWAY) {
|
||||
const upstream = gatewayPackage(modelID)
|
||||
if (upstream) return settingsForEffort(upstream, modelID, effort)
|
||||
return { reasoningEffort: effort }
|
||||
if (upstream) return effortVariant(upstream, modelID, effort)
|
||||
return { settings: { 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 (OPENAI_FAMILY.includes(pkg))
|
||||
return {
|
||||
settings: { reasoningEffort: effort, reasoningSummary: "auto", include: OPENAI_INCLUDE_ENCRYPTED_REASONING },
|
||||
}
|
||||
if (pkg === SAP) {
|
||||
if (modelID.includes("anthropic"))
|
||||
return {
|
||||
modelParams: {
|
||||
additionalModelRequestFields: {
|
||||
...(anthropicManualThinking(modelID) ? {} : { thinking: { type: "adaptive", display: "summarized" } }),
|
||||
output_config: { effort },
|
||||
settings: {
|
||||
modelParams: {
|
||||
additionalModelRequestFields: {
|
||||
...(anthropicManualThinking(modelID) ? {} : { thinking: { type: "adaptive", display: "summarized" } }),
|
||||
output_config: { effort },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
if (modelID.includes("gemini"))
|
||||
return { modelParams: { thinkingConfig: { includeThoughts: true, thinkingLevel: effort } } }
|
||||
return { settings: { modelParams: { thinkingConfig: { includeThoughts: true, thinkingLevel: effort } } } }
|
||||
if (modelID.includes("amazon--nova"))
|
||||
return { modelParams: { additionalModelRequestFields: { output_config: { effort } } } }
|
||||
return { modelParams: { reasoning_effort: effort } }
|
||||
return { settings: { modelParams: { additionalModelRequestFields: { output_config: { effort } } } } }
|
||||
return { settings: { modelParams: { reasoning_effort: 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 }
|
||||
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 } }
|
||||
}
|
||||
|
||||
function budgetVariants(
|
||||
npm: string,
|
||||
pkg: string,
|
||||
model: SourceModel,
|
||||
option: Extract<NonNullable<SourceModel["reasoning_options"]>[number], { type: "budget_tokens" }>,
|
||||
): NonNullable<Model.Info["variants"]> {
|
||||
@@ -295,133 +344,89 @@ function budgetVariants(
|
||||
{ id: "high", budget: high },
|
||||
{ id: "max", budget: maximum },
|
||||
].flatMap((item) => {
|
||||
const settings = settingsForBudget(npm, model.id, item.budget)
|
||||
return settings ? [{ id: Model.VariantID.make(item.id), settings }] : []
|
||||
const overlay = budgetVariant(pkg, model.id, item.budget)
|
||||
return overlay ? [{ id: Model.VariantID.make(item.id), ...overlay }] : []
|
||||
})
|
||||
}
|
||||
|
||||
function toggleVariants(npm: string, modelID: string): NonNullable<Model.Info["variants"]> {
|
||||
if (npm === "@ai-sdk/gateway") {
|
||||
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) {
|
||||
const upstream = gatewayPackage(modelID)
|
||||
if (upstream) return toggleVariants(upstream, modelID)
|
||||
return [
|
||||
{
|
||||
id: Model.VariantID.make("none"),
|
||||
settings: { reasoning: { enabled: false } },
|
||||
},
|
||||
{
|
||||
id: Model.VariantID.make("thinking"),
|
||||
settings: { reasoning: { enabled: true } },
|
||||
},
|
||||
{ id: none, settings: { reasoning: { enabled: false } } },
|
||||
{ id: thinking, settings: { reasoning: { enabled: true } } },
|
||||
]
|
||||
}
|
||||
if (npm === "@openrouter/ai-sdk-provider")
|
||||
if (pkg === OPENROUTER)
|
||||
return [
|
||||
{ id: Model.VariantID.make("none"), settings: { reasoning: { enabled: false } } },
|
||||
{ id: Model.VariantID.make("thinking"), settings: { reasoning: { enabled: true } } },
|
||||
{ id: none, settings: { reasoning: { enabled: false } } },
|
||||
{ id: thinking, settings: { reasoning: { enabled: true } } },
|
||||
]
|
||||
if (npm === "@ai-sdk/anthropic" || npm === "@ai-sdk/google-vertex/anthropic")
|
||||
if (ANTHROPIC_FAMILY.includes(pkg))
|
||||
return [
|
||||
{ id: Model.VariantID.make("none"), settings: { thinking: { type: "disabled" } } },
|
||||
{
|
||||
id: Model.VariantID.make("thinking"),
|
||||
settings: {
|
||||
thinking: { type: "adaptive", display: "summarized" },
|
||||
},
|
||||
},
|
||||
{ id: none, settings: { thinking: { type: "disabled" } } },
|
||||
{ id: thinking, settings: { thinking: { type: "adaptive", display: "summarized" } } },
|
||||
]
|
||||
if (npm === "@ai-sdk/google" || npm === "@ai-sdk/google-vertex")
|
||||
if (GEMINI_FAMILY.includes(pkg))
|
||||
return [
|
||||
{
|
||||
id: Model.VariantID.make("none"),
|
||||
settings: { thinkingConfig: { includeThoughts: false, thinkingBudget: 0 } },
|
||||
},
|
||||
{
|
||||
id: Model.VariantID.make("thinking"),
|
||||
settings: { thinkingConfig: { includeThoughts: true, thinkingBudget: -1 } },
|
||||
},
|
||||
{ id: none, settings: { thinkingConfig: { includeThoughts: false, thinkingBudget: 0 } } },
|
||||
{ id: thinking, settings: { thinkingConfig: { includeThoughts: true, thinkingBudget: -1 } } },
|
||||
]
|
||||
if (npm === "@ai-sdk/amazon-bedrock") {
|
||||
if (pkg === BEDROCK || pkg === LEGACY_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: 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" } },
|
||||
},
|
||||
},
|
||||
{ id: none, body: { additionalModelRequestFields: off } },
|
||||
{ id: thinking, body: { additionalModelRequestFields: on } },
|
||||
]
|
||||
}
|
||||
if (npm === "@ai-sdk/alibaba")
|
||||
if (pkg === ALIBABA)
|
||||
return [
|
||||
{ id: Model.VariantID.make("none"), settings: { enableThinking: false } },
|
||||
{ id: Model.VariantID.make("thinking"), settings: { enableThinking: true } },
|
||||
{ id: none, settings: { enableThinking: false } },
|
||||
{ id: thinking, settings: { enableThinking: true } },
|
||||
]
|
||||
if (npm === "@ai-sdk/cohere")
|
||||
if (pkg === COHERE)
|
||||
return [
|
||||
{ id: Model.VariantID.make("none"), settings: { thinking: { type: "disabled" } } },
|
||||
{ id: Model.VariantID.make("thinking"), settings: { thinking: { type: "enabled" } } },
|
||||
{ id: none, settings: { thinking: { type: "disabled" } } },
|
||||
{ id: thinking, settings: { thinking: { type: "enabled" } } },
|
||||
]
|
||||
if (npm === "@jerome-benoit/sap-ai-provider-v2") {
|
||||
if (pkg === SAP) {
|
||||
if (modelID.includes("gemini"))
|
||||
return [
|
||||
{
|
||||
id: Model.VariantID.make("none"),
|
||||
settings: { modelParams: { thinkingConfig: { includeThoughts: false, thinkingBudget: 0 } } },
|
||||
},
|
||||
{
|
||||
id: Model.VariantID.make("thinking"),
|
||||
settings: { modelParams: { thinkingConfig: { includeThoughts: true, thinkingBudget: -1 } } },
|
||||
},
|
||||
{ id: none, settings: { modelParams: { thinkingConfig: { includeThoughts: false, thinkingBudget: 0 } } } },
|
||||
{ id: thinking, settings: { modelParams: { thinkingConfig: { includeThoughts: true, thinkingBudget: -1 } } } },
|
||||
]
|
||||
if (modelID.includes("cohere"))
|
||||
return [
|
||||
{
|
||||
id: Model.VariantID.make("none"),
|
||||
settings: { modelParams: { thinking: { type: "disabled" } } },
|
||||
},
|
||||
{
|
||||
id: Model.VariantID.make("thinking"),
|
||||
settings: { modelParams: { thinking: { type: "enabled" } } },
|
||||
},
|
||||
{ id: none, settings: { modelParams: { thinking: { type: "disabled" } } } },
|
||||
{ id: thinking, settings: { modelParams: { thinking: { type: "enabled" } } } },
|
||||
]
|
||||
if (modelID.includes("amazon--nova"))
|
||||
return [
|
||||
{ id: none, settings: { modelParams: { additionalModelRequestFields: { thinking: { type: "disabled" } } } } },
|
||||
{
|
||||
id: Model.VariantID.make("none"),
|
||||
settings: { modelParams: { additionalModelRequestFields: { thinking: { type: "disabled" } } } },
|
||||
},
|
||||
{
|
||||
id: Model.VariantID.make("thinking"),
|
||||
id: thinking,
|
||||
settings: { modelParams: { additionalModelRequestFields: { thinking: { type: "enabled" } } } },
|
||||
},
|
||||
]
|
||||
if (modelID.includes("anthropic"))
|
||||
return [
|
||||
{ id: none, settings: { modelParams: { additionalModelRequestFields: { thinking: { type: "disabled" } } } } },
|
||||
{
|
||||
id: Model.VariantID.make("none"),
|
||||
id: thinking,
|
||||
settings: {
|
||||
modelParams: { additionalModelRequestFields: { thinking: { type: "disabled" } } },
|
||||
},
|
||||
},
|
||||
{
|
||||
id: Model.VariantID.make("thinking"),
|
||||
settings: {
|
||||
modelParams: {
|
||||
additionalModelRequestFields: {
|
||||
thinking: { type: "adaptive", display: "summarized" },
|
||||
},
|
||||
},
|
||||
modelParams: { additionalModelRequestFields: { thinking: { type: "adaptive", display: "summarized" } } },
|
||||
},
|
||||
},
|
||||
]
|
||||
@@ -429,40 +434,46 @@ function toggleVariants(npm: string, modelID: string): NonNullable<Model.Info["v
|
||||
return []
|
||||
}
|
||||
|
||||
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") {
|
||||
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) {
|
||||
const upstream = gatewayPackage(modelID)
|
||||
return upstream ? settingsForBudget(upstream, modelID, budget) : { reasoning: { max_tokens: budget } }
|
||||
return upstream ? budgetVariant(upstream, modelID, budget) : { settings: { reasoning: { max_tokens: budget } } }
|
||||
}
|
||||
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 (pkg === COHERE) return { settings: { thinking: { type: "enabled", tokenBudget: budget } } }
|
||||
if (pkg === ALIBABA) return { settings: { enableThinking: true, thinkingBudget: budget } }
|
||||
if (pkg === SAP) {
|
||||
if (modelID.includes("anthropic"))
|
||||
return {
|
||||
modelParams: {
|
||||
additionalModelRequestFields: { thinking: { type: "enabled", budget_tokens: budget } },
|
||||
settings: {
|
||||
modelParams: { additionalModelRequestFields: { thinking: { type: "enabled", budget_tokens: budget } } },
|
||||
},
|
||||
}
|
||||
if (modelID.includes("gemini"))
|
||||
return { modelParams: { thinkingConfig: { includeThoughts: true, thinkingBudget: budget } } }
|
||||
if (modelID.includes("cohere")) return { modelParams: { thinking: { type: "enabled", token_budget: budget } } }
|
||||
return { settings: { modelParams: { thinkingConfig: { includeThoughts: true, thinkingBudget: budget } } } }
|
||||
if (modelID.includes("cohere"))
|
||||
return { settings: { 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 "@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"
|
||||
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
|
||||
}
|
||||
|
||||
function anthropicManualThinking(modelID: string) {
|
||||
@@ -483,12 +494,12 @@ function modelInfo(
|
||||
providerID: Provider.ID,
|
||||
id: Model.ID,
|
||||
model: SourceModel,
|
||||
input: {
|
||||
input: AISDKNative.Overlay & {
|
||||
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,
|
||||
@@ -497,8 +508,8 @@ function modelInfo(
|
||||
name: input.name ?? model.name,
|
||||
compatibility: Model.compatibility(model.interleaved),
|
||||
family: model.family ? Model.Family.make(model.family) : undefined,
|
||||
package: model.provider?.npm ? Provider.aisdk(model.provider.npm) : undefined,
|
||||
settings: model.provider?.api ? { baseURL: model.provider.api } : undefined,
|
||||
package: input.package,
|
||||
settings: input.settings,
|
||||
capabilities: {
|
||||
tools: model.tool_call,
|
||||
input: [...(model.modalities?.input ?? [])],
|
||||
@@ -514,8 +525,8 @@ function modelInfo(
|
||||
status: model.status ?? "active",
|
||||
enabled: true,
|
||||
limit: { context: model.limit.context, input: model.limit.input, output: model.limit.output },
|
||||
headers: input.request?.headers ? { ...input.request.headers } : undefined,
|
||||
body: input.request?.body ? { ...input.request.body } : undefined,
|
||||
headers: input.headers,
|
||||
body: input.body,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
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())
|
||||
})
|
||||
}),
|
||||
})
|
||||
@@ -82,6 +82,7 @@ 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"
|
||||
@@ -240,6 +241,7 @@ const post = [
|
||||
ConfigProviderPlugin.Plugin,
|
||||
ConfigWebSearchPlugin.Plugin,
|
||||
ConfigWorktreePlugin.Plugin,
|
||||
AISDKNativePlugin.Plugin,
|
||||
VariantPlugin.Plugin,
|
||||
ConfigPolicyPlugin.Plugin,
|
||||
] as const satisfies readonly InternalPlugin[]
|
||||
|
||||
@@ -13,10 +13,8 @@ const CHAIN_ENV = [
|
||||
"AWS_CONTAINER_CREDENTIALS_FULL_URI",
|
||||
]
|
||||
|
||||
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")
|
||||
}
|
||||
const isBedrock = (item: { readonly package: string }) =>
|
||||
Provider.packageName(item.package).startsWith("@opencode/ai/providers/amazon-bedrock")
|
||||
|
||||
export const AmazonBedrockPlugin = define({
|
||||
id: "opencode.provider.amazon.bedrock",
|
||||
|
||||
@@ -134,7 +134,10 @@ 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) !== "@ai-sdk/azure")
|
||||
if (
|
||||
item.provider.id !== Provider.ID.azure &&
|
||||
!Provider.packageName(item.provider.package)?.startsWith("@opencode/ai/providers/azure")
|
||||
)
|
||||
continue
|
||||
const resourceName = resolveResourceName(item.provider.settings, loaded.resource)
|
||||
if (resourceName)
|
||||
@@ -209,9 +212,9 @@ function expandResourceName(baseURL: string, resourceName: string) {
|
||||
}
|
||||
|
||||
function responsesWebSocketCapable(provider: Provider.Info, model: Model.Info) {
|
||||
if (Provider.packageName(model.package ?? provider.package) !== "@ai-sdk/azure") return false
|
||||
if (Provider.packageName(model.package ?? provider.package) !== "@opencode/ai/providers/azure/responses") return false
|
||||
const settings = Provider.mergeOverlay(provider.settings, model.settings)
|
||||
if (settings?.useCompletionUrls === true || settings?.useDeploymentBasedUrls === true) return false
|
||||
if (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,8 +7,7 @@ export const CerebrasPlugin = define({
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
yield* ctx.catalog.transform((evt) => {
|
||||
for (const item of evt.provider.list()) {
|
||||
const name = Provider.packageName(item.provider.package)
|
||||
if (name !== "@ai-sdk/cerebras" && name !== "@opencode/ai/providers/cerebras") continue
|
||||
if (Provider.packageName(item.provider.package) !== "@opencode/ai/providers/cerebras") continue
|
||||
evt.provider.update(item.provider.id, (provider) => {
|
||||
provider.headers = { ...provider.headers, "X-Cerebras-3rd-Party-Integration": "opencode" }
|
||||
})
|
||||
|
||||
@@ -59,13 +59,10 @@ export const GoogleVertexPlugin = define({
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
yield* ctx.catalog.transform((evt) => {
|
||||
for (const item of evt.provider.list()) {
|
||||
if (!Provider.isAISDK(item.provider.package)) continue
|
||||
const packageName = Provider.packageName(item.provider.package)
|
||||
if (
|
||||
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")
|
||||
)
|
||||
packageName !== "@opencode/ai/providers/google-vertex" &&
|
||||
!(item.provider.id === Provider.ID.googleVertex && packageName === "@opencode/ai/providers/openai-compatible")
|
||||
)
|
||||
continue
|
||||
const project = resolveProject(item.provider.settings ?? {})
|
||||
|
||||
@@ -7,8 +7,7 @@ export const KiloPlugin = define({
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
yield* ctx.catalog.transform((evt) => {
|
||||
for (const item of evt.provider.list()) {
|
||||
if (!Provider.isAISDK(item.provider.package)) continue
|
||||
if (Provider.packageName(item.provider.package) !== "@ai-sdk/openai-compatible") continue
|
||||
if (Provider.packageName(item.provider.package) !== "@opencode/ai/providers/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,8 +11,7 @@ export const LLMGatewayPlugin = define({
|
||||
yield* ctx.catalog.transform((evt) => {
|
||||
for (const item of evt.provider.list()) {
|
||||
if (item.provider.activation === "disabled") continue
|
||||
if (!Provider.isAISDK(item.provider.package)) continue
|
||||
if (Provider.packageName(item.provider.package) !== "@ai-sdk/openai-compatible") continue
|
||||
if (Provider.packageName(item.provider.package) !== "@opencode/ai/providers/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) => {
|
||||
|
||||
@@ -7,8 +7,7 @@ export const NvidiaPlugin = define({
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
yield* ctx.catalog.transform((evt) => {
|
||||
for (const item of evt.provider.list()) {
|
||||
if (!Provider.isAISDK(item.provider.package)) continue
|
||||
if (Provider.packageName(item.provider.package) !== "@ai-sdk/openai-compatible") continue
|
||||
if (Provider.packageName(item.provider.package) !== "@opencode/ai/providers/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,8 +8,7 @@ export const OpenRouterPlugin = define({
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
yield* ctx.catalog.transform((evt) => {
|
||||
for (const item of evt.provider.list()) {
|
||||
if (!Provider.isAISDK(item.provider.package)) continue
|
||||
if (Provider.packageName(item.provider.package) !== "@openrouter/ai-sdk-provider") continue
|
||||
if (Provider.packageName(item.provider.package) !== "@opencode/ai/providers/openrouter") continue
|
||||
evt.provider.update(item.provider.id, (provider) => {
|
||||
provider.headers = { ...provider.headers, "HTTP-Referer": "https://opencode.ai/", "X-Title": "opencode" }
|
||||
})
|
||||
|
||||
@@ -7,8 +7,7 @@ export const ZenmuxPlugin = define({
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
yield* ctx.catalog.transform((evt) => {
|
||||
for (const item of evt.provider.list()) {
|
||||
if (!Provider.isAISDK(item.provider.package)) continue
|
||||
if (Provider.packageName(item.provider.package) !== "@ai-sdk/openai-compatible") continue
|
||||
if (Provider.packageName(item.provider.package) !== "@opencode/ai/providers/openai-compatible") continue
|
||||
if (item.provider.settings?.baseURL !== "https://zenmux.ai/api/v1") continue
|
||||
evt.provider.update(item.provider.id, (provider) => {
|
||||
provider.headers = {
|
||||
|
||||
@@ -33,8 +33,7 @@ export function generate(
|
||||
model: { readonly id: string; readonly modelID?: string; readonly package?: string },
|
||||
provider?: { readonly package: string },
|
||||
): NonNullable<Model.Info["variants"]> {
|
||||
const packageName = model.package ?? provider?.package
|
||||
if (!Provider.isAISDK(packageName) || Provider.packageName(packageName) !== "@ai-sdk/openai-compatible") return []
|
||||
if (Provider.packageName(model.package ?? provider?.package) !== "@opencode/ai/providers/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) => ({
|
||||
|
||||
@@ -9,6 +9,7 @@ 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"
|
||||
@@ -239,9 +240,19 @@ function providers(info?: Readonly<Record<string, ConfigProviderV1.Info>>) {
|
||||
}
|
||||
|
||||
export function migrateProvider(sourceID: string, info: ConfigProviderV1.Info) {
|
||||
if (sourceID === "azure-cognitive-services") return migrateAzureCognitiveServicesProvider(info)
|
||||
if (sourceID === "google-vertex-anthropic") return migrateGoogleVertexAnthropicProvider(info)
|
||||
return migrateStandardProvider(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])),
|
||||
}
|
||||
}
|
||||
|
||||
function migrateStandardProvider(info: ConfigProviderV1.Info) {
|
||||
|
||||
@@ -1,10 +1,76 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { AISDKNative } from "@opencode/core/aisdk-native"
|
||||
import { Provider } from "@opencode/core/provider"
|
||||
|
||||
const map = (packageName: string, settings: Readonly<Record<string, unknown>>, modelID = "test-model") =>
|
||||
AISDKNative.map({ packageName, settings, modelID, providerID: "test-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>
|
||||
}
|
||||
|
||||
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", {
|
||||
@@ -30,11 +96,7 @@ 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",
|
||||
provider: "test-provider",
|
||||
reasoningEffort: "high",
|
||||
},
|
||||
settings: { baseURL: "https://example.com/v1", reasoningEffort: "high" },
|
||||
})
|
||||
})
|
||||
|
||||
@@ -77,10 +139,7 @@ describe("AISDKNative", () => {
|
||||
},
|
||||
headers: { "x-provider": name },
|
||||
})
|
||||
expect(map(`@ai-sdk/${name}`, {})).toEqual({
|
||||
package: `@opencode/ai/providers/${name}`,
|
||||
settings: {},
|
||||
})
|
||||
expect(map(`@ai-sdk/${name}`, {})).toEqual({ package: `@opencode/ai/providers/${name}` })
|
||||
}
|
||||
})
|
||||
|
||||
@@ -323,7 +382,6 @@ describe("AISDKNative", () => {
|
||||
})
|
||||
expect(map("@ai-sdk/amazon-bedrock", { auth: "bogus" }, "anthropic.claude")).toEqual({
|
||||
package: "@opencode/ai/providers/amazon-bedrock",
|
||||
settings: {},
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -712,8 +712,8 @@ describe("Config", () => {
|
||||
})
|
||||
|
||||
expect(migrated.providers?.bedrock).toMatchObject({
|
||||
package: Provider.aisdk("@ai-sdk/amazon-bedrock"),
|
||||
models: { claude: { package: Provider.aisdk("@ai-sdk/anthropic") } },
|
||||
package: "@opencode/ai/providers/amazon-bedrock",
|
||||
models: { claude: { package: "@opencode/ai/providers/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: Provider.aisdk("@ai-sdk/azure"),
|
||||
package: "@opencode/ai/providers/azure/responses",
|
||||
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: Provider.aisdk("@ai-sdk/google-vertex/anthropic") },
|
||||
"claude-sonnet": { package: "@opencode/ai/providers/google-vertex/messages" },
|
||||
},
|
||||
})
|
||||
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: Provider.aisdk("@ai-sdk/openai-compatible"),
|
||||
package: "@opencode/ai/providers/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(
|
||||
Provider.aisdk("@ai-sdk/google-vertex/anthropic"),
|
||||
"@opencode/ai/providers/google-vertex/messages",
|
||||
)
|
||||
})
|
||||
|
||||
@@ -1445,7 +1445,7 @@ describe("Config", () => {
|
||||
},
|
||||
})
|
||||
expect(documents[0]?.info.providers?.openai).toMatchObject({
|
||||
package: Provider.aisdk("@ai-sdk/openai"),
|
||||
package: "@opencode/ai/providers/openai",
|
||||
settings: { apiKey: "secret", organization: "org" },
|
||||
models: {
|
||||
model: {
|
||||
@@ -1455,7 +1455,7 @@ describe("Config", () => {
|
||||
},
|
||||
})
|
||||
expect(documents[0]?.info.providers?.anthropic).toMatchObject({
|
||||
package: Provider.aisdk("@ai-sdk/anthropic"),
|
||||
package: "@opencode/ai/providers/anthropic",
|
||||
models: {
|
||||
model: {
|
||||
settings: {
|
||||
|
||||
@@ -116,7 +116,7 @@ describe("ConfigProviderPlugin.Plugin", () => {
|
||||
info: decode({
|
||||
providers: {
|
||||
litellm: {
|
||||
package: "aisdk:@ai-sdk/openai-compatible",
|
||||
package: "@opencode/ai/providers/openai-compatible",
|
||||
models: { chat: {} },
|
||||
},
|
||||
},
|
||||
@@ -145,7 +145,7 @@ describe("ConfigProviderPlugin.Plugin", () => {
|
||||
info: decode({
|
||||
providers: {
|
||||
custom: {
|
||||
package: "aisdk:@ai-sdk/openai-compatible",
|
||||
package: "@opencode/ai/providers/openai-compatible",
|
||||
models: { chat: {} },
|
||||
},
|
||||
},
|
||||
@@ -181,7 +181,7 @@ describe("ConfigProviderPlugin.Plugin", () => {
|
||||
info: decode({
|
||||
providers: {
|
||||
custom: {
|
||||
package: "aisdk:@ai-sdk/openai-compatible",
|
||||
package: "@opencode/ai/providers/openai-compatible",
|
||||
models: {
|
||||
inherited: { name: "Inherited" },
|
||||
overridden: {
|
||||
@@ -273,7 +273,7 @@ describe("ConfigProviderPlugin.Plugin", () => {
|
||||
models: { chat: {} },
|
||||
},
|
||||
native: {
|
||||
package: "aisdk:@ai-sdk/openai",
|
||||
package: "@opencode/ai/providers/openai",
|
||||
settings: { baseURL: "https://proxy.example/v1" },
|
||||
headers: { "x-provider": "default" },
|
||||
body: { store: false },
|
||||
@@ -298,13 +298,13 @@ describe("ConfigProviderPlugin.Plugin", () => {
|
||||
},
|
||||
},
|
||||
native: {
|
||||
package: "aisdk:@ai-sdk/openai",
|
||||
package: "@opencode/ai/providers/openai",
|
||||
settings: { baseURL: "https://proxy.example/v1" },
|
||||
models: {
|
||||
chat: {
|
||||
modelID: "vendor/chat",
|
||||
name: "Custom chat",
|
||||
package: "aisdk:@ai-sdk/anthropic",
|
||||
package: "@opencode/ai/providers/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 = "aisdk:@ai-sdk/anthropic"
|
||||
model.package = "@opencode/ai/providers/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: "aisdk:@ai-sdk/openai",
|
||||
package: "@opencode/ai/providers/openai",
|
||||
settings: { baseURL: "https://opencode.test/v1" },
|
||||
models: {
|
||||
"alpha-gpt-next": {
|
||||
@@ -433,7 +433,7 @@ describe("ConfigProviderPlugin.Plugin", () => {
|
||||
info: decode({
|
||||
providers: {
|
||||
opencode: {
|
||||
package: "aisdk:@ai-sdk/openai",
|
||||
package: "@opencode/ai/providers/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 = "aisdk:@ai-sdk/anthropic"
|
||||
provider.package = "@opencode/ai/providers/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(Provider.aisdk("@ai-sdk/anthropic"))
|
||||
expect(models.get(Model.ID.make("claude-sonnet"))?.package).toBe("@opencode/ai/providers/anthropic")
|
||||
expect(models.get(Model.ID.make("claude-sonnet"))?.settings).toMatchObject({
|
||||
baseURL: `${server.url.origin}/v1`,
|
||||
endpoint: "messages",
|
||||
@@ -148,8 +148,16 @@ 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)
|
||||
|
||||
@@ -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 = Provider.aisdk("@ai-sdk/openai")
|
||||
provider.package = "@opencode/ai/providers/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.
|
||||
|
||||
@@ -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(Provider.aisdk("@ai-sdk/openai-compatible"))
|
||||
expect(model?.package).toBe("@opencode/ai/providers/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"] })
|
||||
|
||||
@@ -11,6 +11,7 @@ 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"
|
||||
|
||||
@@ -26,8 +27,9 @@ interface ModelOptions {
|
||||
readonly limit?: Info["limit"]
|
||||
}
|
||||
|
||||
const model = (packageName: string | undefined, options: ModelOptions = {}) =>
|
||||
Info.make({
|
||||
// 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({
|
||||
id: ID.make("test-model"),
|
||||
modelID: ID.make(options.modelID ?? "api-test-model"),
|
||||
providerID: options.providerID ?? Provider.ID.make("test-provider"),
|
||||
@@ -46,6 +48,10 @@ 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(
|
||||
@@ -215,7 +221,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.${AWS_REGION}.api.aws/openai/v1")
|
||||
expect(catalog.settings?.baseURL).toBe("https://bedrock-mantle.us-west-2.api.aws/openai/v1")
|
||||
}),
|
||||
),
|
||||
)
|
||||
@@ -478,7 +484,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({})
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -711,7 +717,7 @@ describe("ModelResolver", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
expect(resolved.route.defaults.http?.body).toEqual({})
|
||||
expect(resolved.route.defaults.http?.body ?? {}).toEqual({})
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -1409,7 +1415,7 @@ describe("ModelResolver", () => {
|
||||
_tag: "SessionRunnerModel.ModelConfigurationError",
|
||||
providerID: "azure",
|
||||
modelID: "test-model",
|
||||
package: "aisdk:@ai-sdk/azure",
|
||||
package: "@opencode/ai/providers/azure/responses",
|
||||
detail: "Azure requires resourceName or baseURL",
|
||||
})
|
||||
expect(failure.message).toBe("Cannot initialize azure/test-model: Azure requires resourceName or baseURL")
|
||||
|
||||
@@ -49,7 +49,7 @@ const fixtureSnapshot = [
|
||||
id: Provider.ID.make("acme"),
|
||||
name: "Acme",
|
||||
activation: "auto",
|
||||
package: Provider.aisdk("@ai-sdk/openai-compatible"),
|
||||
package: "@opencode/ai/providers/openai-compatible",
|
||||
},
|
||||
models: [
|
||||
{
|
||||
@@ -112,7 +112,7 @@ const fixture2Snapshot = [
|
||||
id: Provider.ID.make("beta"),
|
||||
name: "Beta",
|
||||
activation: "auto",
|
||||
package: Provider.aisdk("@ai-sdk/openai-compatible"),
|
||||
package: "@opencode/ai/providers/openai-compatible",
|
||||
},
|
||||
models: [
|
||||
{
|
||||
@@ -254,8 +254,8 @@ describe("ModelsDev Service", () => {
|
||||
cache,
|
||||
ModelsDev.Service.use((service) => service.get()),
|
||||
)
|
||||
expect(result[0]?.info.package).toBe(Provider.aisdk("@ai-sdk/openai-compatible"))
|
||||
expect(result[0]?.models[0]?.package).toBe(Provider.aisdk("@ai-sdk/openai"))
|
||||
expect(result[0]?.info.package).toBe("@opencode/ai/providers/openai-compatible")
|
||||
expect(result[0]?.models[0]?.package).toBe("@opencode/ai/providers/openai")
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
@@ -1081,18 +1081,19 @@ 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"),
|
||||
settings: { additionalModelRequestFields: { reasoningConfig: { type: "disabled" } } },
|
||||
body: { additionalModelRequestFields: { reasoningConfig: { type: "disabled" } } },
|
||||
},
|
||||
{
|
||||
id: Model.VariantID.make("low"),
|
||||
settings: { reasoningConfig: { type: "enabled", maxReasoningEffort: "low" } },
|
||||
body: { additionalModelRequestFields: { reasoningConfig: { type: "enabled", maxReasoningEffort: "low" } } },
|
||||
},
|
||||
{
|
||||
id: Model.VariantID.make("high"),
|
||||
settings: { reasoningConfig: { type: "enabled", maxReasoningEffort: "high" } },
|
||||
body: { additionalModelRequestFields: { 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 = Provider.aisdk("@ai-sdk/amazon-bedrock")
|
||||
item.package = "@opencode/ai/providers/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(Provider.aisdk("@ai-sdk/amazon-bedrock"))
|
||||
expect(result.package).toBe("@opencode/ai/providers/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 = Provider.aisdk("@ai-sdk/amazon-bedrock")
|
||||
item.package = "@opencode/ai/providers/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 = Provider.aisdk("@ai-sdk/amazon-bedrock/mantle")
|
||||
item.package = "@opencode/ai/providers/amazon-bedrock/mantle/responses"
|
||||
})
|
||||
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 = Provider.aisdk("@ai-sdk/anthropic")
|
||||
item.package = "@opencode/ai/providers/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 = Provider.aisdk("@ai-sdk/azure")
|
||||
provider.package = "@opencode/ai/providers/azure/responses"
|
||||
})
|
||||
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 = Provider.aisdk("@ai-sdk/azure")
|
||||
item.package = "@opencode/ai/providers/azure/responses"
|
||||
})
|
||||
})
|
||||
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 = Provider.aisdk("@ai-sdk/azure")
|
||||
item.package = "@opencode/ai/providers/azure/responses"
|
||||
})
|
||||
})
|
||||
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 = Provider.aisdk("@ai-sdk/openai-compatible")
|
||||
provider.package = "@opencode/ai/providers/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 = Provider.aisdk("@ai-sdk/anthropic")
|
||||
model.package = "@opencode/ai/providers/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 = Provider.aisdk("@ai-sdk/azure")
|
||||
item.package = "@opencode/ai/providers/azure/responses"
|
||||
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 = Provider.aisdk("@ai-sdk/azure")
|
||||
item.package = "@opencode/ai/providers/azure/responses"
|
||||
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 = Provider.aisdk("@ai-sdk/azure")
|
||||
item.package = "@opencode/ai/providers/azure/responses"
|
||||
item.settings = { resourceName: " " }
|
||||
})
|
||||
})
|
||||
@@ -434,11 +434,11 @@ describe("AzurePlugin", () => {
|
||||
}
|
||||
yield* catalog.transform((editor) => {
|
||||
editor.provider.update(Provider.ID.azure, (provider) => {
|
||||
provider.package = Provider.aisdk("@ai-sdk/azure")
|
||||
provider.package = "@opencode/ai/providers/azure/responses"
|
||||
})
|
||||
editor.model.update(Provider.ID.azure, models.responses, () => {})
|
||||
editor.model.update(Provider.ID.azure, models.chat, (model) => {
|
||||
model.settings = { useCompletionUrls: true }
|
||||
model.package = "@opencode/ai/providers/azure/chat"
|
||||
})
|
||||
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 = Provider.aisdk("@ai-sdk/anthropic")
|
||||
model.package = "@opencode/ai/providers/anthropic"
|
||||
})
|
||||
})
|
||||
|
||||
@@ -467,5 +467,4 @@ 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 = Provider.aisdk("@ai-sdk/cerebras")
|
||||
item.package = "@opencode/ai/providers/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 = Provider.aisdk("@ai-sdk/openai-compatible")
|
||||
model.package = "@opencode/ai/providers/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 = Provider.aisdk("@ai-sdk/openai-compatible")
|
||||
provider.package = "@opencode/ai/providers/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 = Provider.aisdk("@ai-sdk/openai-compatible")
|
||||
provider.package = "@opencode/ai/providers/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: "aisdk:@ai-sdk/openai-compatible",
|
||||
package: "@opencode/ai/providers/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 = Provider.aisdk("@ai-sdk/google-vertex")
|
||||
provider.package = "@opencode/ai/providers/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 = Provider.aisdk("@ai-sdk/google-vertex")
|
||||
provider.package = "@opencode/ai/providers/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 = Provider.aisdk("@ai-sdk/openai-compatible")
|
||||
provider.package = "@opencode/ai/providers/openai-compatible"
|
||||
provider.settings = {
|
||||
...provider.settings,
|
||||
baseURL:
|
||||
@@ -193,7 +193,7 @@ describe("GoogleVertexPlugin", () => {
|
||||
|
||||
expect(provider.settings?.project).toBe("vertex-project")
|
||||
expect(provider).toMatchObject({
|
||||
package: "aisdk:@ai-sdk/openai-compatible",
|
||||
package: "@opencode/ai/providers/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 = Provider.aisdk("@ai-sdk/openai-compatible")
|
||||
provider.package = "@opencode/ai/providers/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: "aisdk:@ai-sdk/openai-compatible",
|
||||
package: "@opencode/ai/providers/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 = Provider.aisdk("@ai-sdk/openai-compatible")
|
||||
provider.package = "@opencode/ai/providers/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: "aisdk:@ai-sdk/openai-compatible",
|
||||
package: "@opencode/ai/providers/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 = Provider.aisdk("@ai-sdk/google-vertex")
|
||||
provider.package = "@opencode/ai/providers/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: "aisdk:@ai-sdk/openai-compatible",
|
||||
package: "@opencode/ai/providers/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 = Provider.aisdk("@ai-sdk/openai-compatible")
|
||||
provider.package = "@opencode/ai/providers/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 = Provider.aisdk("@ai-sdk/openai-compatible")
|
||||
provider.package = "@opencode/ai/providers/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 = Provider.aisdk("@ai-sdk/openai-compatible")
|
||||
provider.package = "@opencode/ai/providers/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 = Provider.aisdk("@ai-sdk/openai-compatible")
|
||||
provider.package = "@opencode/ai/providers/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 = Provider.aisdk("@ai-sdk/openai-compatible")
|
||||
provider.package = "@opencode/ai/providers/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 = Provider.aisdk("@ai-sdk/openai-compatible")
|
||||
provider.package = "@opencode/ai/providers/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 = Provider.aisdk("@ai-sdk/openai-compatible")
|
||||
provider.package = "@opencode/ai/providers/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 = Provider.aisdk("@ai-sdk/openai-compatible")
|
||||
provider.package = "@opencode/ai/providers/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: "aisdk:@ai-sdk/openai-compatible",
|
||||
package: "@opencode/ai/providers/openai-compatible",
|
||||
env: ["REMOTE_API_KEY"],
|
||||
settings: {
|
||||
baseURL: `${origin}/v1`,
|
||||
@@ -409,7 +409,7 @@ describe("OpencodePlugin", () => {
|
||||
},
|
||||
override: {
|
||||
name: "Override",
|
||||
package: "aisdk:@ai-sdk/anthropic",
|
||||
package: "@opencode/ai/providers/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 = Provider.aisdk("@ai-sdk/openai")
|
||||
provider.package = "@opencode/ai/providers/openai"
|
||||
provider.integrationID = Integration.ID.make("openai")
|
||||
})
|
||||
editor.model.update(Provider.ID.openai, Model.ID.make("api-model"), (model) => {
|
||||
model.package = Provider.aisdk("@ai-sdk/openai")
|
||||
model.package = "@opencode/ai/providers/openai"
|
||||
model.settings = { baseURL: "https://upstream.example/v1" }
|
||||
model.variants = [
|
||||
{
|
||||
@@ -464,7 +464,7 @@ describe("OpencodePlugin", () => {
|
||||
canonical: "openai",
|
||||
name: "Remote",
|
||||
integrationID: "opencode",
|
||||
package: Provider.aisdk("@ai-sdk/openai-compatible"),
|
||||
package: "@opencode/ai/providers/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: Provider.aisdk("@ai-sdk/openai-compatible"),
|
||||
package: "@opencode/ai/providers/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(Provider.aisdk("@ai-sdk/anthropic"))
|
||||
expect(override.package).toBe("@opencode/ai/providers/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: "aisdk:@ai-sdk/openai",
|
||||
package: "@opencode/ai/providers/openai",
|
||||
settings: { baseURL: new URL(request.url).origin },
|
||||
models: {
|
||||
...models,
|
||||
override: { ...models.astra, package: "aisdk:@ai-sdk/openai-compatible" },
|
||||
override: { ...models.astra, package: "@opencode/ai/providers/openai-compatible" },
|
||||
},
|
||||
},
|
||||
compatible: {
|
||||
canonical: "openai",
|
||||
package: "aisdk:@ai-sdk/openai-compatible",
|
||||
package: "@opencode/ai/providers/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 = Provider.aisdk("@openrouter/ai-sdk-provider")
|
||||
provider.package = "@opencode/ai/providers/openrouter"
|
||||
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 = Provider.aisdk("@openrouter/ai-sdk-provider")
|
||||
provider.package = "@opencode/ai/providers/openrouter"
|
||||
})
|
||||
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 = Provider.aisdk("@ai-sdk/openai-compatible")
|
||||
provider.package = "@opencode/ai/providers/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 = Provider.aisdk("@ai-sdk/openai-compatible")
|
||||
provider.package = "@opencode/ai/providers/openai-compatible"
|
||||
provider.settings = { baseURL: "https://${SNOWFLAKE_ACCOUNT}.snowflakecomputing.com/api/v2/cortex/v1" }
|
||||
})
|
||||
editor.model.update(providerID, modelID, () => {})
|
||||
@@ -225,9 +225,7 @@ 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(
|
||||
Provider.aisdk("@ai-sdk/openai-compatible"),
|
||||
)
|
||||
expect((yield* test.catalog.provider.get(providerID))?.package).toBe("@opencode/ai/providers/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 = Provider.aisdk("@ai-sdk/openai-compatible")
|
||||
provider.package = "@opencode/ai/providers/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 = Provider.aisdk("@ai-sdk/openai-compatible")
|
||||
provider.package = "@opencode/ai/providers/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 = Provider.aisdk("@ai-sdk/openai-compatible")
|
||||
provider.package = "@opencode/ai/providers/openai-compatible"
|
||||
provider.settings = { ...provider.settings, baseURL: "https://zenmux.ai/api/v1" }
|
||||
provider.headers = { "HTTP-Referer": "https://example.com/", "X-Title": "custom-title" }
|
||||
})
|
||||
|
||||
@@ -71,12 +71,7 @@ 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 } }]),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -23,11 +23,11 @@ describe("VariantPlugin", () => {
|
||||
const service = yield* Catalog.Service
|
||||
yield* service.transform((catalog) => {
|
||||
catalog.provider.update(Provider.ID.opencode, (provider) => {
|
||||
provider.package = Provider.aisdk("@ai-sdk/openai-compatible")
|
||||
provider.package = "@opencode/ai/providers/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 = Provider.aisdk("@ai-sdk/openai-compatible")
|
||||
model.package = "@opencode/ai/providers/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 = Provider.aisdk("@ai-sdk/openai-compatible")
|
||||
model.package = "@opencode/ai/providers/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 effect
|
||||
bun add @opencode/client@beta 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
|
||||
bun add @opencode/client@beta
|
||||
```
|
||||
|
||||
## 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 effect
|
||||
bun add @opencode/plugin@beta 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
|
||||
bun add @opencode/sdk@beta
|
||||
```
|
||||
|
||||
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 effect
|
||||
bun add @opencode/sdk@beta 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
|
||||
bun add @opencode/sdk@beta
|
||||
```
|
||||
|
||||
## Create a host
|
||||
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: npm install --global @opencode/cli
|
||||
- run: npm install --global @opencode/cli@beta
|
||||
- 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 }}
|
||||
|
||||
@@ -10,10 +10,11 @@ OpenCode installs and runs as `opencode`.
|
||||
|
||||
<div data-install-code>
|
||||
<CodeBlock
|
||||
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
|
||||
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
|
||||
$ curl -fsSL https://opencode.ai/v2/install | bash`}
|
||||
/>
|
||||
</div>
|
||||
@@ -21,9 +22,10 @@ $ 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.
|
||||
|
||||
Docker images use versioned tags, for example `ghcr.io/anomalyco/opencode:2.0.0`.
|
||||
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.
|
||||
|
||||
Homebrew, AUR, Windows package managers, and standalone binaries are not supported.
|
||||
Homebrew, Windows package managers, Docker, and standalone binaries are not supported.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -336,7 +336,7 @@ Rename the singular `provider` map to `providers`. V2 separates the runtime pack
|
||||
{
|
||||
"providers": {
|
||||
"acme": {
|
||||
"package": "aisdk:@ai-sdk/openai-compatible",
|
||||
"package": "@opencode/ai/providers/openai-compatible",
|
||||
"settings": {
|
||||
"baseURL": "https://llm.example.com/v1",
|
||||
"apiKey": "{env:ACME_API_KEY}"
|
||||
@@ -346,8 +346,10 @@ Rename the singular `provider` map to `providers`. V2 separates the runtime pack
|
||||
}
|
||||
```
|
||||
|
||||
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).
|
||||
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).
|
||||
|
||||
V2 consolidated two legacy provider namespaces:
|
||||
|
||||
|
||||
Reference in New Issue
Block a user