Compare commits

..
Author SHA1 Message Date
OpenCode Agent 37b2c91937 fix(core): attribute one-shot generation requests 2026-09-10 16:33:50 +00:00
309 changed files with 4954 additions and 9771 deletions
+52 -21
View File
@@ -25,7 +25,7 @@ on:
required: false
type: string
concurrency: ${{ github.workflow }}-${{ github.ref }}-${{ (github.ref_name == 'v2' && (inputs.version || inputs.bump) && 'release') || inputs.version || inputs.bump }}
concurrency: ${{ github.workflow }}-${{ github.ref }}-${{ inputs.version || inputs.bump }}
permissions:
id-token: write
@@ -33,7 +33,7 @@ permissions:
packages: write
env:
OPENCODE_CHANNEL: ${{ (github.ref_name == 'v2' && !inputs.bump && !inputs.version && 'dev') || '' }}
OPENCODE_CHANNEL: ${{ (github.ref_name == 'v2' && 'dev') || '' }}
jobs:
version:
@@ -168,7 +168,7 @@ jobs:
fi
found=0
for file in packages/cli/dist/cli-darwin-*/bin/opencode; do
for file in packages/cli/dist/cli-darwin-*/bin/opencode2; do
if [ ! -f "$file" ]; then
continue
fi
@@ -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,17 +345,40 @@ 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
if: github.repository == 'anomalyco/opencode' && (github.ref_name != 'v2' || needs.version.outputs.release != '')
- sign-cli-macos
if: github.repository == 'anomalyco/opencode' && github.ref_name != 'v2'
continue-on-error: false
env:
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
+2 -2
View File
@@ -8,9 +8,9 @@
## Live V2 TUI Testing
- Run `bun run dev:live` from a development worktree to test its TUI against the currently elected `opencode` background server and live sessions.
- Run `bun run dev:live` from a development worktree to test its TUI against the currently elected `opencode2` background server and live sessions.
- Pass a directory after the script when needed, for example `bun run dev:live /path/to/project`.
- The script discovers the server with `opencode service status`, injects its private local credential from `opencode service get password`, and uses the `dev` TUI storage channel so tabs and other client-local state match the installed client.
- The script discovers the server with `opencode2 service status`, injects its private local credential from `opencode2 service get password`, and uses the `dev` TUI storage channel so tabs and other client-local state match the installed client.
- Prefer `dev:live` over plain `bun run dev` for this workflow. An implicit managed-service connection may replace the live server when the worktree client version differs; explicit `--server` warns and continues without replacing it.
## V2 TUI Stories
+35 -35
View File
@@ -31,7 +31,7 @@
},
"packages/ai": {
"name": "@opencode/ai",
"version": "2.0.0",
"version": "1.17.20",
"dependencies": {
"@aws-sdk/credential-providers": "3.1057.0",
"@opencode/schema": "workspace:*",
@@ -53,7 +53,7 @@
},
"packages/app": {
"name": "@opencode/app",
"version": "2.0.0",
"version": "1.18.15",
"dependencies": {
"@corvu/drawer": "catalog:",
"@dnd-kit/abstract": "0.5.0",
@@ -111,7 +111,7 @@
},
"packages/cli": {
"name": "@opencode/cli",
"version": "2.0.0",
"version": "1.18.4",
"bin": {
"opencode2": "./bin/opencode2.cjs",
},
@@ -175,7 +175,7 @@
},
"packages/client": {
"name": "@opencode/client",
"version": "2.0.0",
"version": "1.17.13",
"dependencies": {
"@opencode/protocol": "workspace:*",
"@opencode/schema": "workspace:*",
@@ -201,7 +201,7 @@
},
"packages/codemode": {
"name": "@opencode/codemode",
"version": "2.0.0",
"version": "1.18.4",
"dependencies": {
"acorn": "8.15.0",
"effect": "catalog:",
@@ -215,7 +215,7 @@
},
"packages/console/app": {
"name": "@opencode/console-app",
"version": "2.0.0",
"version": "1.18.15",
"dependencies": {
"@cloudflare/vite-plugin": "1.15.2",
"@ibm/plex": "6.4.1",
@@ -251,7 +251,7 @@
},
"packages/console/core": {
"name": "@opencode/console-core",
"version": "2.0.0",
"version": "1.18.15",
"dependencies": {
"@aws-sdk/client-sts": "3.782.0",
"@jsx-email/render": "1.1.1",
@@ -278,7 +278,7 @@
},
"packages/console/function": {
"name": "@opencode/console-function",
"version": "2.0.0",
"version": "1.18.15",
"dependencies": {
"@openauthjs/openauth": "0.0.0-20250322224806",
"@opencode/console-core": "workspace:*",
@@ -295,7 +295,7 @@
},
"packages/console/mail": {
"name": "@opencode/console-mail",
"version": "2.0.0",
"version": "1.18.15",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
@@ -319,7 +319,7 @@
},
"packages/console/support": {
"name": "@opencode/console-support",
"version": "2.0.0",
"version": "1.18.15",
"dependencies": {
"@cloudflare/vite-plugin": "1.15.2",
"@opencode/console-core": "workspace:*",
@@ -339,7 +339,7 @@
},
"packages/core": {
"name": "@opencode/core",
"version": "2.0.0",
"version": "1.18.4",
"dependencies": {
"@ai-sdk/alibaba": "1.0.17",
"@ai-sdk/anthropic": "3.0.82",
@@ -411,7 +411,7 @@
},
"packages/desktop": {
"name": "@opencode/desktop",
"version": "2.0.0",
"version": "1.18.15",
"dependencies": {
"@zip.js/zip.js": "2.7.62",
"electron-context-menu": "4.1.2",
@@ -463,7 +463,7 @@
},
"packages/enterprise": {
"name": "@opencode/enterprise",
"version": "2.0.0",
"version": "1.18.15",
"dependencies": {
"@hono/standard-validator": "catalog:",
"@opencode-ai/sdk": "1.18.21",
@@ -500,7 +500,7 @@
},
"packages/function": {
"name": "@opencode/function",
"version": "2.0.0",
"version": "1.18.15",
"dependencies": {
"@octokit/auth-app": "8.0.1",
"@octokit/rest": "catalog:",
@@ -516,7 +516,7 @@
},
"packages/http-recorder": {
"name": "@opencode/http-recorder",
"version": "2.0.0",
"version": "1.18.15",
"dependencies": {
"@effect/platform-node-shared": "4.0.0-rc.112",
},
@@ -535,7 +535,7 @@
},
"packages/httpapi-codegen": {
"name": "@opencode/httpapi-codegen",
"version": "2.0.0",
"version": "0.0.0",
"dependencies": {
"effect": "catalog:",
"prettier": "3.6.2",
@@ -548,7 +548,7 @@
},
"packages/latex": {
"name": "@opencode/latex",
"version": "2.0.0",
"version": "0.0.0",
"dependencies": {
"@opencode/plugin": "workspace:*",
"@opentui/core": "catalog:",
@@ -562,7 +562,7 @@
},
"packages/merman": {
"name": "@opencode/merman",
"version": "2.0.0",
"version": "0.0.0",
"dependencies": {
"@opencode/plugin": "workspace:*",
"@opentui/core": "catalog:",
@@ -577,7 +577,7 @@
},
"packages/plugin": {
"name": "@opencode/plugin",
"version": "2.0.0",
"version": "1.18.15",
"dependencies": {
"@ai-sdk/provider": "3.0.8",
"@opencode/ai": "workspace:*",
@@ -616,7 +616,7 @@
},
"packages/plugin-browser": {
"name": "@opencode/plugin-browser",
"version": "2.0.0",
"version": "0.0.0",
"dependencies": {
"@opencode/plugin": "workspace:*",
"@opencode/schema": "workspace:*",
@@ -646,7 +646,7 @@
},
"packages/protocol": {
"name": "@opencode/protocol",
"version": "2.0.0",
"version": "1.17.11",
"dependencies": {
"@opencode/schema": "workspace:*",
"effect": "catalog:",
@@ -661,7 +661,7 @@
},
"packages/schema": {
"name": "@opencode/schema",
"version": "2.0.0",
"version": "1.17.11",
"dependencies": {
"@standard-schema/spec": "catalog:",
"effect": "catalog:",
@@ -685,7 +685,7 @@
},
"packages/sdk": {
"name": "@opencode/sdk",
"version": "2.0.0",
"version": "1.18.4",
"dependencies": {
"@opencode/client": "workspace:*",
"@opencode/core": "workspace:*",
@@ -706,7 +706,7 @@
},
"packages/server": {
"name": "@opencode/server",
"version": "2.0.0",
"version": "1.18.4",
"dependencies": {
"@effect/platform-node": "catalog:",
"@effect/platform-node-shared": "catalog:",
@@ -728,7 +728,7 @@
},
"packages/session-ui": {
"name": "@opencode/session-ui",
"version": "2.0.0",
"version": "1.18.15",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode/client": "workspace:*",
@@ -763,7 +763,7 @@
},
"packages/simulation": {
"name": "@opencode/simulation",
"version": "2.0.0",
"version": "1.17.13",
"dependencies": {
"@opencode/ai": "workspace:*",
"@opencode/core": "workspace:*",
@@ -783,7 +783,7 @@
},
"packages/stats/app": {
"name": "@opencode/stats-app",
"version": "2.0.0",
"version": "1.18.15",
"dependencies": {
"@ibm/plex": "6.4.1",
"@kobalte/core": "catalog:",
@@ -817,7 +817,7 @@
},
"packages/stats/core": {
"name": "@opencode/stats-core",
"version": "2.0.0",
"version": "1.18.15",
"dependencies": {
"@aws-sdk/client-athena": "3.933.0",
"@planetscale/database": "1.19.0",
@@ -836,7 +836,7 @@
},
"packages/stats/server": {
"name": "@opencode/stats-server",
"version": "2.0.0",
"version": "1.18.15",
"dependencies": {
"@aws-sdk/client-firehose": "3.933.0",
"@effect/platform-node": "catalog:",
@@ -882,7 +882,7 @@
},
"packages/theme": {
"name": "@opencode/theme",
"version": "2.0.0",
"version": "0.0.0",
"dependencies": {
"@opentui/core": "catalog:",
"effect": "catalog:",
@@ -896,7 +896,7 @@
},
"packages/tui": {
"name": "@opencode/tui",
"version": "2.0.0",
"version": "1.18.4",
"dependencies": {
"@opencode/client": "workspace:*",
"@opencode/core": "workspace:*",
@@ -931,7 +931,7 @@
},
"packages/ui": {
"name": "@opencode/ui",
"version": "2.0.0",
"version": "1.18.15",
"dependencies": {
"@kobalte/core": "catalog:",
"@pierre/diffs": "catalog:",
@@ -966,7 +966,7 @@
},
"packages/util": {
"name": "@opencode/util",
"version": "2.0.0",
"version": "1.18.3",
"dependencies": {
"@effect/opentelemetry": "catalog:",
"@effect/platform-node": "catalog:",
@@ -999,7 +999,7 @@
},
"packages/web": {
"name": "@opencode/web",
"version": "2.0.0",
"version": "1.18.15",
"dependencies": {
"@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1",
@@ -1040,7 +1040,7 @@
},
"services/update": {
"name": "@opencode/update",
"version": "2.0.0",
"version": "1.18.4",
"dependencies": {
"jose": "6.0.11",
"semver": "catalog:",
+15 -43
View File
@@ -1,7 +1,6 @@
#!/usr/bin/env bash
set -euo pipefail
APP=opencode
SOURCE_APP=opencode
APP=opencode2
MUTED='\033[0;2m'
RED='\033[0;31m'
@@ -23,7 +22,7 @@ Options:
Examples:
curl -fsSL https://opencode.ai/v2/install | bash
curl -fsSL https://opencode.ai/v2/install | bash -s -- --version 0.0.0-beta-17236
./install --binary /path/to/opencode
./install --binary /path/to/opencode2
EOF
}
@@ -202,9 +201,9 @@ else
filename="cli-$target-$specific_version.tgz"
url="https://registry.npmjs.org/$package_name/-/$filename"
binary_name="$SOURCE_APP"
binary_name="$APP"
if [ "$os" = "windows" ]; then
binary_name="$SOURCE_APP.exe"
binary_name="$APP.exe"
fi
fi
@@ -231,7 +230,12 @@ check_version() {
installed_version="${installed_version##* }"
installed_version="${installed_version#v}"
print_message info "${MUTED}Installed version: ${NC}$installed_version."
if [[ "$installed_version" != "$specific_version" ]]; then
print_message info "${MUTED}Installed version: ${NC}$installed_version."
else
print_message info "${MUTED}Version ${NC}$specific_version${MUTED} already installed"
exit 0
fi
fi
}
@@ -336,46 +340,15 @@ download_and_install() {
fi
tar -xzf "$tmp_dir/$filename" -C "$tmp_dir"
local installed_binary="$APP"
if [ "$os" = "windows" ]; then
installed_binary="$APP.exe"
fi
mv "$tmp_dir/package/bin/$binary_name" "$INSTALL_DIR/$installed_binary"
chmod 755 "$INSTALL_DIR/$installed_binary"
mv "$tmp_dir/package/bin/$binary_name" "$INSTALL_DIR"
chmod 755 "${INSTALL_DIR}/$binary_name"
rm -rf "$tmp_dir"
}
install_from_binary() {
print_message info "\n${MUTED}Installing ${NC}$APP ${MUTED}from: ${NC}$binary_path"
local installed_binary="$APP"
case "$(uname -s)" in
MINGW*|MSYS*|CYGWIN*) installed_binary="$APP.exe" ;;
esac
cp "$binary_path" "$INSTALL_DIR/$installed_binary"
chmod 755 "$INSTALL_DIR/$installed_binary"
}
install_legacy_shim() {
local shim_os="${os:-}"
if [[ -z "$shim_os" ]]; then
case "$(uname -s)" in
MINGW*|MSYS*|CYGWIN*) shim_os="windows" ;;
esac
fi
rm -f "$INSTALL_DIR/opencode2" "$INSTALL_DIR/opencode2.exe" "$INSTALL_DIR/opencode2.cmd"
if [[ "$shim_os" == "windows" ]]; then
cat > "$INSTALL_DIR/opencode2.cmd" <<'EOF'
@echo off
"%~dp0opencode.exe" %*
exit /b %errorlevel%
EOF
return
fi
cat > "$INSTALL_DIR/opencode2" <<'EOF'
#!/bin/sh
exec "$(dirname "$0")/opencode" "$@"
EOF
chmod 755 "$INSTALL_DIR/opencode2"
cp "$binary_path" "${INSTALL_DIR}/$APP"
chmod 755 "${INSTALL_DIR}/$APP"
}
if [ -n "$binary_path" ]; then
@@ -384,7 +357,6 @@ else
check_version
download_and_install
fi
install_legacy_shim
add_to_path() {
@@ -481,7 +453,7 @@ echo -e ""
echo -e "${MUTED}OpenCode includes free models, to start:${NC}"
echo -e ""
echo -e "cd <project> ${MUTED}# Open directory${NC}"
echo -e "opencode ${MUTED}# Run command${NC}"
echo -e "opencode2 ${MUTED}# Run command${NC}"
echo -e ""
echo -e "${MUTED}For more information visit ${NC}https://opencode.ai/v2/docs"
echo -e ""
+3 -3
View File
@@ -2,15 +2,15 @@
"$schema": "https://json.schemastore.org/package.json",
"name": "opencode",
"description": "AI-powered development tool",
"version": "2.0.0",
"version": "0.0.0",
"private": true,
"type": "module",
"packageManager": "bun@1.4.2",
"scripts": {
"dev": "bun run --cwd packages/cli src/index.ts",
"dev:live": "sh -c 'OPENCODE_TUI_CHANNEL=dev OPENCODE_PASSWORD=\"$(opencode service get password)\" exec bun run dev \"$@\" --server \"$(opencode service status)\"' --",
"dev:live": "sh -c 'OPENCODE_TUI_CHANNEL=dev OPENCODE_PASSWORD=\"$(opencode2 service get password)\" exec bun run dev \"$@\" --server \"$(opencode2 service status)\"' --",
"dev:vite": "bun run --cwd packages/cli --conditions=browser dev/vite.ts",
"dev:vite:live": "sh -c 'OPENCODE_TUI_CHANNEL=dev OPENCODE_PASSWORD=\"$(opencode service get password)\" exec bun run dev:vite \"$@\" --server \"$(opencode service status)\"' --",
"dev:vite:live": "sh -c 'OPENCODE_TUI_CHANNEL=dev OPENCODE_PASSWORD=\"$(opencode2 service get password)\" exec bun run dev:vite \"$@\" --server \"$(opencode2 service status)\"' --",
"dev:desktop": "bun --cwd packages/desktop dev",
"dev:web": "bun --cwd packages/app dev",
"dev:console": "ulimit -n 10240 2>/dev/null; bun run --cwd packages/console/app dev",
+1 -2
View File
@@ -122,14 +122,13 @@ Keep provider facades small and explicit:
### Provider Package Entrypoints
Catalog-selected native providers use package-like export paths from `@opencode/ai`. They are internal entrypoints in one npm package, not separately published provider packages. Every entrypoint implements `ProviderPackage.Definition` and exposes `model(modelID, settings)`, where settings are one flat serializable object: the connection keys the entrypoint declares (`apiKey`, `baseURL`, `region`, …), the common `headers` and `body` overlays, and the protocol's request options (`reasoningEffort`, `thinking`, …) side by side. Each entrypoint destructures its own connection keys and passes the rest to the route as `providerOptions`; there is no nested `providerOptions` at the entrypoint.
Catalog-selected native providers use package-like export paths from `@opencode/ai`. They are internal entrypoints in one npm package, not separately published provider packages. Every entrypoint implements `ProviderPackage.Definition` and exposes `model(modelID, settings)`, where settings are serializable provider configuration plus common `headers`, `body`, and `limits` overlays.
```ts
import { model } from "@opencode/ai/providers/openai/responses"
const selected = model("gpt-5", {
apiKey,
reasoningEffort: "high",
})
```
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "2.0.0",
"version": "1.17.20",
"name": "@opencode/ai",
"type": "module",
"license": "MIT",
@@ -1708,10 +1708,7 @@ export const transport = <
}
function requiredBetaHeaders(body: Pick<AnthropicMessagesBody, "messages" | "context_management" | "thinking">) {
// Always request interleaved thinking. The API accepts the header on any
// model and ignores it where unsupported, while manual-thinking models need
// it for thinking between tool calls.
const betas: string[] = ["interleaved-thinking-2025-05-14"]
const betas: string[] = []
const requestsCompaction = (body.context_management?.edits.length ?? 0) > 0
const replaysCompaction = body.messages.some((message) =>
message.content.some((block) => block.type === "compaction"),
+4 -14
View File
@@ -1,4 +1,4 @@
import { Effect, Option, Schema, SchemaGetter } from "effect"
import { Effect, Option, Schema } from "effect"
import type { Content } from "@opencode/schema/tool"
import { HttpTransport } from "../route/transport/index.js"
import { Protocol } from "../route/protocol.js"
@@ -325,8 +325,9 @@ export const StreamItem = Schema.StructWithRest(
export type StreamItem = Schema.Schema.Type<typeof StreamItem>
export type OutputItem = StreamItem & { readonly id: string }
// Responses-compatible providers put streaming error details at the top level or
// under `error`, and response failures under `response.error`. Accept all three shapes.
// The Responses schema puts streaming error details at the top level and
// response failures under `response.error`. WebSocket failures use an
// event-level `error` envelope, so accept all three shapes here.
// https://www.openresponses.org/specification
const OpenResponsesErrorPayload = Schema.Struct({
type: optionalNull(Schema.String),
@@ -400,17 +401,6 @@ export const Event = Schema.StructWithRest(
headers: Schema.optional(Schema.Unknown),
}),
[Schema.Record(Schema.String, Schema.Unknown)],
).pipe(
Schema.decode({
decode: SchemaGetter.transform((event) => {
if (event.type !== "error" || event.error != null) return event
const { code, message, param, ...rest } = event
if (code === undefined && message === undefined && param === undefined) return event
// Flat errors (for example, Meta's) can also arrive through generic Responses endpoints.
return { ...rest, error: { code, message, param } }
}),
encode: SchemaGetter.passthrough(),
}),
)
export type Event = Schema.Schema.Type<typeof Event>
export type NormalizedEvent = Event & { readonly item?: OutputItem | null }
-4
View File
@@ -1,10 +1,6 @@
import type { LanguageModel, ProviderOptions } from "./schema/index.js"
import type { CompactionOperations } from "./route/client.js"
/**
* Flat, serializable settings for `model(modelID, settings)`. Each entrypoint declares the connection keys it
* reads; every other key is a request option for the route's protocol.
*/
export interface Settings extends Readonly<Record<string, unknown>> {
readonly baseURL?: string
readonly headers?: Readonly<Record<string, string>>
+6 -16
View File
@@ -1,4 +1,3 @@
import { Struct } from "effect"
import type { ProviderPackage } from "../provider-package.js"
import { AlibabaChat } from "../protocols/alibaba-chat.js"
import { AlibabaMessages } from "../protocols/alibaba-messages.js"
@@ -7,7 +6,7 @@ import { AuthOptions, type AtLeastOne, type ProviderAuthOption } from "../route/
import { Route, type RouteDefaultsInput } from "../route/client.js"
import { Endpoint } from "../route/endpoint.js"
import { Framing } from "../route/framing.js"
import { ProviderConfigurationError, ProviderID, ToolDefinition, type ModelID } from "../schema/index.js"
import { ProviderID, ToolDefinition, type ModelID } from "../schema/index.js"
export const id = ProviderID.make("alibaba")
@@ -35,9 +34,9 @@ export type Config = Location &
readonly providerOptions?: ChatOptionsInput | MessagesOptionsInput | ResponsesOptionsInput
}
export type Settings<Options = ChatOptionsInput> = Location &
ProviderPackage.Settings &
Options & {
ProviderPackage.Settings & {
readonly apiKey?: string
readonly providerOptions?: Options
}
const hosts = new Map<string, string>([
@@ -83,13 +82,8 @@ export const configure = (input: Config) => {
? hosts.get(region)
: `${workspaceID}.${region}.maas.aliyuncs.com`
if (baseURL === undefined) {
if (region === undefined)
throw new ProviderConfigurationError({ provider: id, message: "Alibaba requires region or baseURL" })
if (host === undefined)
throw new ProviderConfigurationError({
provider: id,
message: `Alibaba region ${region} requires workspaceID or baseURL`,
})
if (region === undefined) throw new Error("Alibaba requires region or baseURL")
if (host === undefined) throw new Error(`Alibaba region ${region} requires workspaceID or baseURL`)
}
const opts = { ...rest, auth: AuthOptions.bearer(input, ["DASHSCOPE_API_KEY", "ALIBABA_API_KEY"]) }
const common = { ...opts, endpoint: { baseURL: baseURL ?? `https://${host}/compatible-mode/v1` } }
@@ -121,11 +115,7 @@ export const responsesModel: ProviderPackage.Definition<
function fromSettings(input: Settings<Config["providerOptions"]>) {
const { body, ...rest } = input
return configure({
...rest,
http: body === undefined ? undefined : { body },
providerOptions: Struct.omit(rest, ["apiKey", "baseURL", "headers", "region", "workspaceID"]),
})
return configure({ ...rest, http: body === undefined ? undefined : { body } })
}
export const webSearch = () => hostedTool("web_search", "Search the web with Alibaba's hosted search tool.")
@@ -4,7 +4,7 @@ import type { ProviderPackage } from "../provider-package.js"
import { OpenAIChat } from "../protocols/openai-chat.js"
import { OpenResponses } from "../protocols/open-responses.js"
import { BedrockAuth, type Credentials } from "../protocols/utils/bedrock-auth.js"
import { ProviderConfigurationError, ProviderID, type ModelID } from "../schema/index.js"
import { ProviderID, type ModelID } from "../schema/index.js"
import { withOpenAIOptions, type OpenAIProviderOptionsInput } from "./openai-options.js"
export const id = ProviderID.make("amazon-bedrock")
@@ -23,16 +23,16 @@ export type Config = RouteDefaultsInput & {
readonly providerOptions?: OpenAIProviderOptionsInput
}
export type Settings = ProviderPackage.Settings &
OpenAIProviderOptionsInput & {
readonly apiKey?: string
readonly auth?: "bearer" | "sigv4"
readonly baseURL?: string
readonly credentials?: Credentials
readonly profile?: string
readonly region?: string
readonly topP?: number
}
export interface Settings extends ProviderPackage.Settings {
readonly apiKey?: string
readonly auth?: "bearer" | "sigv4"
readonly baseURL?: string
readonly credentials?: Credentials
readonly profile?: string
readonly region?: string
readonly topP?: number
readonly providerOptions?: OpenAIProviderOptionsInput
}
const responsesRoute = Route.make({
id: "bedrock-mantle-responses",
@@ -79,12 +79,9 @@ const defaults = (input: Config) => {
export const configure = (input: Config = {}) => {
if (input.auth === "bearer" && input.apiKey === undefined && process.env.AWS_BEARER_TOKEN_BEDROCK === undefined)
throw new ProviderConfigurationError({ provider: id, message: "Amazon Bedrock Mantle bearer auth requires apiKey" })
throw new Error("Amazon Bedrock Mantle bearer auth requires apiKey")
if (input.auth === "sigv4" && input.apiKey !== undefined)
throw new ProviderConfigurationError({
provider: id,
message: "Amazon Bedrock Mantle SigV4 auth does not accept apiKey",
})
throw new Error("Amazon Bedrock Mantle SigV4 auth does not accept apiKey")
const configuredResponsesRoute = configuredRoute(responsesRoute, input)
const configuredChatRoute = configuredRoute(chatRoute, input)
const modelDefaults = defaults(input)
@@ -108,29 +105,18 @@ export const configure = (input: Config = {}) => {
export const provider = configure()
const fromSettings = ({
apiKey,
auth,
baseURL,
body,
credentials,
headers,
profile,
region,
topP,
...providerOptions
}: Settings) =>
const fromSettings = (settings: Settings) =>
configure({
apiKey,
auth,
baseURL,
credentials,
generation: topP === undefined ? undefined : { topP },
headers: headers === undefined ? undefined : { ...headers },
http: body === undefined ? undefined : { body: { ...body } },
profile,
providerOptions,
region,
apiKey: settings.apiKey,
auth: settings.auth,
baseURL: settings.baseURL,
credentials: settings.credentials,
generation: settings.topP === undefined ? undefined : { topP: settings.topP },
headers: settings.headers === undefined ? undefined : { ...settings.headers },
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
profile: settings.profile,
providerOptions: settings.providerOptions,
region: settings.region,
})
export const chatModel: ProviderPackage.Definition<Settings, OpenAIProviderOptionsInput>["model"] = (
+3 -4
View File
@@ -1,6 +1,6 @@
import type { RouteDefaultsInput } from "../route/client.js"
import type { ProviderPackage } from "../provider-package.js"
import { ProviderConfigurationError, ProviderID, type ModelID } from "../schema/index.js"
import { ProviderID, type ModelID } from "../schema/index.js"
import * as BedrockConverse from "../protocols/bedrock-converse.js"
import type { BedrockCredentials } from "../protocols/bedrock-converse.js"
import { BedrockAuth } from "../protocols/utils/bedrock-auth.js"
@@ -39,9 +39,8 @@ const bedrockBaseURL = (region: string) => `https://bedrock-runtime.${region}.am
const configuredRoute = (input: Config) => {
const { apiKey, auth, credentials, profile, region, baseURL, ...rest } = input
if (auth === "bearer" && apiKey === undefined && process.env.AWS_BEARER_TOKEN_BEDROCK === undefined)
throw new ProviderConfigurationError({ provider: id, message: "Amazon Bedrock bearer auth requires apiKey" })
if (auth === "sigv4" && apiKey !== undefined)
throw new ProviderConfigurationError({ provider: id, message: "Amazon Bedrock SigV4 auth does not accept apiKey" })
throw new Error("Amazon Bedrock bearer auth requires apiKey")
if (auth === "sigv4" && apiKey !== undefined) throw new Error("Amazon Bedrock SigV4 auth does not accept apiKey")
const resolvedRegion = BedrockAuth.resolveRegion(input)
return BedrockConverse.route.with({
...rest,
@@ -3,7 +3,7 @@ import { AnthropicMessages } from "../protocols/anthropic-messages.js"
import { Auth } from "../route/auth.js"
import type { ProviderAuthOption } from "../route/auth-options.js"
import type { RouteDefaultsInput } from "../route/client.js"
import { ProviderConfigurationError, ProviderID, type ModelID } from "../schema/index.js"
import { ProviderID, type ModelID } from "../schema/index.js"
export type AnthropicOptionsInput = AnthropicMessages.OptionsInput
export type AnthropicProviderOptionsInput = AnthropicMessages.ProviderOptionsInput
@@ -19,13 +19,13 @@ export type Config = RouteDefaultsInput &
}
export type Settings = ProviderPackage.Settings &
AnthropicMessages.ProviderOptionsInput &
(
| { readonly apiKey?: string; readonly authToken?: never }
| { readonly apiKey?: never; readonly authToken?: string }
) & {
readonly baseURL: string
readonly provider?: string
readonly providerOptions?: AnthropicMessages.ProviderOptionsInput
}
export const routes = [AnthropicMessages.route]
@@ -36,12 +36,8 @@ const auth = (input: ProviderAuthOption<"optional">) => {
}
export const configure = (input: Config) => {
if (!input.baseURL) throw new Error("Anthropic-compatible providers require a baseURL")
const provider = input.provider ?? "anthropic-compatible"
if (!input.baseURL)
throw new ProviderConfigurationError({
provider: ProviderID.make(provider),
message: "Anthropic-compatible providers require a baseURL",
})
const { provider: _, baseURL, apiKey: _apiKey, auth: _auth, ...rest } = input
const route = AnthropicMessages.route.with({
...rest,
@@ -63,20 +59,17 @@ export const provider = {
export const model: ProviderPackage.Definition<Settings, AnthropicMessages.ProviderOptionsInput>["model"] = (
modelID,
{ apiKey, authToken, baseURL, body, headers, provider, ...providerOptions },
settings,
) => {
if (apiKey !== undefined && authToken !== undefined)
throw new ProviderConfigurationError({
provider: ProviderID.make(provider ?? id),
message: "Anthropic-compatible apiKey cannot be combined with authToken",
})
if (settings.apiKey !== undefined && settings.authToken !== undefined)
throw new Error("Anthropic-compatible apiKey cannot be combined with authToken")
return configure({
...(authToken === undefined ? { apiKey: apiKey } : { auth: Auth.bearer(authToken) }),
baseURL,
headers: headers === undefined ? undefined : { ...headers },
http: body === undefined ? undefined : { body: { ...body } },
provider,
providerOptions,
...(settings.authToken === undefined ? { apiKey: settings.apiKey } : { auth: Auth.bearer(settings.authToken) }),
baseURL: settings.baseURL,
headers: settings.headers === undefined ? undefined : { ...settings.headers },
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
provider: settings.provider,
providerOptions: settings.providerOptions,
}).model(modelID)
}
+10 -13
View File
@@ -2,7 +2,7 @@ import type { RouteDefaultsInput } from "../route/client.js"
import { Auth } from "../route/auth.js"
import type { ProviderAuthOption } from "../route/auth-options.js"
import type { ProviderPackage } from "../provider-package.js"
import { ProviderConfigurationError, ProviderID, type ModelID } from "../schema/index.js"
import { ProviderID, type ModelID } from "../schema/index.js"
import { AnthropicMessages } from "../protocols/anthropic-messages.js"
import { AnthropicCompatible } from "./anthropic-compatible.js"
@@ -21,12 +21,12 @@ export type Config = RouteDefaultsInput &
}
export type Settings = ProviderPackage.Settings &
AnthropicMessages.ProviderOptionsInput &
(
| { readonly apiKey?: string; readonly authToken?: never }
| { readonly apiKey?: never; readonly authToken?: string }
) & {
readonly baseURL?: string
readonly providerOptions?: AnthropicMessages.ProviderOptionsInput
}
const auth = (options: ProviderAuthOption<"optional">) => {
@@ -54,18 +54,15 @@ export const configure = (input: Config = {}) => {
export const provider = configure()
export const model: ProviderPackage.Definition<Settings, AnthropicMessages.ProviderOptionsInput>["model"] = (
modelID,
{ apiKey, authToken, baseURL, body, headers, ...providerOptions },
settings,
) => {
if (apiKey !== undefined && authToken !== undefined)
throw new ProviderConfigurationError({
provider: id,
message: "Anthropic apiKey cannot be combined with authToken",
})
if (settings.apiKey !== undefined && settings.authToken !== undefined)
throw new Error("Anthropic apiKey cannot be combined with authToken")
return configure({
...(authToken === undefined ? { apiKey: apiKey } : { auth: Auth.bearer(authToken) }),
baseURL,
headers: headers === undefined ? undefined : { ...headers },
http: body === undefined ? undefined : { body: { ...body } },
providerOptions,
...(settings.authToken === undefined ? { apiKey: settings.apiKey } : { auth: Auth.bearer(settings.authToken) }),
baseURL: settings.baseURL,
headers: settings.headers === undefined ? undefined : { ...settings.headers },
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
providerOptions: settings.providerOptions,
}).model(modelID)
}
+13 -23
View File
@@ -3,7 +3,7 @@ import { Auth } from "../route/auth.js"
import { type AtLeastOne, type ProviderAuthOption } from "../route/auth-options.js"
import type { Route, RouteDefaultsInput, CompactionOperations } from "../route/client.js"
import type { ProviderPackage } from "../provider-package.js"
import { ProviderConfigurationError, ProviderID, type ModelID } from "../schema/index.js"
import { ProviderID, type ModelID } from "../schema/index.js"
import * as OpenAIChat from "../protocols/openai-chat.js"
import * as OpenAIResponses from "../protocols/openai-responses.js"
import { ProviderShared } from "../protocols/shared.js"
@@ -28,12 +28,12 @@ export type LanguageModelOptions = AzureURL &
export type Config = LanguageModelOptions
export type Settings = ProviderPackage.Settings &
OpenAIProviderOptionsInput &
AzureURL & {
readonly apiKey?: string
readonly apiVersion?: string
readonly queryParams?: Readonly<Record<string, string>>
readonly useDeploymentBasedUrls?: boolean
readonly providerOptions?: OpenAIProviderOptionsInput
}
const resourceBaseURL = (resourceName: string) => `https://${resourceName.trim()}.openai.azure.com/openai`
@@ -151,29 +151,19 @@ export const provider = {
configure,
}
const config = ({
apiKey,
apiVersion,
baseURL,
body,
headers,
queryParams,
resourceName,
useDeploymentBasedUrls,
...providerOptions
}: Settings): Config => {
const config = (settings: Settings): Config => {
const common = {
apiKey,
apiVersion,
headers: headers === undefined ? undefined : { ...headers },
http: body === undefined ? undefined : { body: { ...body } },
providerOptions,
queryParams: queryParams === undefined ? undefined : { ...queryParams },
useDeploymentBasedUrls,
apiKey: settings.apiKey,
apiVersion: settings.apiVersion,
headers: settings.headers === undefined ? undefined : { ...settings.headers },
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
providerOptions: settings.providerOptions,
queryParams: settings.queryParams === undefined ? undefined : { ...settings.queryParams },
useDeploymentBasedUrls: settings.useDeploymentBasedUrls,
}
if (baseURL !== undefined) return { ...common, baseURL }
if (resourceName !== undefined) return { ...common, resourceName }
throw new ProviderConfigurationError({ provider: id, message: "Azure requires resourceName or baseURL" })
if (settings.baseURL !== undefined) return { ...common, baseURL: settings.baseURL }
if (settings.resourceName !== undefined) return { ...common, resourceName: settings.resourceName }
throw new Error("Azure requires resourceName or baseURL")
}
export const responsesModel: ProviderPackage.Definition<
+11 -14
View File
@@ -15,11 +15,11 @@ export type LanguageModelOptions = Omit<RouteDefaultsInput, "providerOptions"> &
readonly providerOptions?: OpenAIProviderOptionsInput
}
export type Settings = ProviderPackage.Settings &
OpenAIProviderOptionsInput & {
readonly apiKey?: string
readonly baseURL?: string
}
export interface Settings extends ProviderPackage.Settings {
readonly apiKey?: string
readonly baseURL?: string
readonly providerOptions?: OpenAIProviderOptionsInput
}
export const route = Route.make({
id: "baseten-chat",
@@ -48,16 +48,13 @@ export const configure = (input: LanguageModelOptions = {}) => {
export const provider = configure()
export const model: ProviderPackage.Definition<Settings, OpenAIProviderOptionsInput>["model"] = (
modelID,
{ apiKey, baseURL, body, headers, ...providerOptions },
) =>
export const model: ProviderPackage.Definition<Settings, OpenAIProviderOptionsInput>["model"] = (modelID, settings) =>
configure({
apiKey,
baseURL,
headers: headers === undefined ? undefined : { ...headers },
http: body === undefined ? undefined : { body: { ...body } },
providerOptions,
apiKey: settings.apiKey,
baseURL: settings.baseURL,
headers: settings.headers === undefined ? undefined : { ...settings.headers },
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
providerOptions: settings.providerOptions,
}).model(modelID)
export * as Baseten from "./baseten.js"
+11 -14
View File
@@ -15,11 +15,11 @@ export type LanguageModelOptions = Omit<RouteDefaultsInput, "providerOptions"> &
readonly providerOptions?: OpenAIProviderOptionsInput
}
export type Settings = ProviderPackage.Settings &
OpenAIProviderOptionsInput & {
readonly apiKey?: string
readonly baseURL?: string
}
export interface Settings extends ProviderPackage.Settings {
readonly apiKey?: string
readonly baseURL?: string
readonly providerOptions?: OpenAIProviderOptionsInput
}
export const route = Route.make({
id: "cerebras-chat",
@@ -52,14 +52,11 @@ export const configure = (input: LanguageModelOptions = {}) => {
export const provider = configure()
export const model: ProviderPackage.Definition<Settings, OpenAIProviderOptionsInput>["model"] = (
modelID,
{ apiKey, baseURL, body, headers, ...providerOptions },
) =>
export const model: ProviderPackage.Definition<Settings, OpenAIProviderOptionsInput>["model"] = (modelID, settings) =>
configure({
apiKey,
baseURL,
headers: headers === undefined ? undefined : { ...headers },
http: body === undefined ? undefined : { body: { ...body } },
providerOptions,
apiKey: settings.apiKey,
baseURL: settings.baseURL,
headers: settings.headers === undefined ? undefined : { ...settings.headers },
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
providerOptions: settings.providerOptions,
}).model(modelID)
@@ -5,7 +5,7 @@ import { Auth } from "../route/auth.js"
import type { AtLeastOne, ProviderAuthOption } from "../route/auth-options.js"
import { Route, type RouteDefaultsInput } from "../route/client.js"
import { Endpoint } from "../route/endpoint.js"
import { ProviderConfigurationError, ProviderID, type ModelID } from "../schema/index.js"
import { ProviderID, type ModelID } from "../schema/index.js"
import type { OpenAIProviderOptionsInput } from "./openai-options.js"
export const id = ProviderID.make("cloudflare-ai-gateway")
@@ -27,19 +27,15 @@ export type LanguageModelOptions = GatewayURL &
}
export type Settings = ProviderPackage.Settings &
OpenAIProviderOptionsInput &
GatewayURL & {
readonly apiKey?: string
readonly gatewayApiKey?: string
readonly providerOptions?: OpenAIProviderOptionsInput
}
export const baseURL = (input: GatewayURL) => {
if (input.baseURL) return input.baseURL
if (!input.accountId)
throw new ProviderConfigurationError({
provider: id,
message: "CloudflareAIGateway.configure requires accountId unless baseURL is supplied",
})
if (!input.accountId) throw new Error("CloudflareAIGateway.configure requires accountId unless baseURL is supplied")
return `https://gateway.ai.cloudflare.com/v1/${encodeURIComponent(input.accountId)}/${encodeURIComponent(input.gatewayId?.trim() || "default")}/compat`
}
@@ -89,25 +85,14 @@ export const configure = (input: LanguageModelOptions) => {
export const provider = { id, configure }
export const model: ProviderPackage.Definition<Settings, OpenAIProviderOptionsInput>["model"] = (modelID, settings) => {
const {
accountId: _,
apiKey,
baseURL: _url,
body,
gatewayApiKey,
gatewayId: _id,
headers,
...providerOptions
} = settings
return configure({
apiKey,
gatewayApiKey,
export const model: ProviderPackage.Definition<Settings, OpenAIProviderOptionsInput>["model"] = (modelID, settings) =>
configure({
apiKey: settings.apiKey,
gatewayApiKey: settings.gatewayApiKey,
baseURL: baseURL(settings),
headers: headers === undefined ? undefined : { ...headers },
http: body === undefined ? undefined : { body: { ...body } },
providerOptions,
headers: settings.headers === undefined ? undefined : { ...settings.headers },
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
providerOptions: settings.providerOptions,
}).model(modelID)
}
export * as CloudflareAIGateway from "./cloudflare-ai-gateway.js"
@@ -3,7 +3,7 @@ import { OpenAIChat } from "../protocols/openai-chat.js"
import { AuthOptions, type AtLeastOne, type ProviderAuthOption } from "../route/auth-options.js"
import { Route, type RouteDefaultsInput } from "../route/client.js"
import { Endpoint } from "../route/endpoint.js"
import { ProviderConfigurationError, ProviderID, type ModelID } from "../schema/index.js"
import { ProviderID, type ModelID } from "../schema/index.js"
import type { OpenAIProviderOptionsInput } from "./openai-options.js"
export const id = ProviderID.make("cloudflare-workers-ai")
@@ -21,18 +21,14 @@ export type LanguageModelOptions = WorkersAIURL &
}
export type Settings = ProviderPackage.Settings &
OpenAIProviderOptionsInput &
WorkersAIURL & {
readonly apiKey?: string
readonly providerOptions?: OpenAIProviderOptionsInput
}
export const baseURL = (input: WorkersAIURL) => {
if (input.baseURL) return input.baseURL
if (!input.accountId)
throw new ProviderConfigurationError({
provider: id,
message: "CloudflareWorkersAI.configure requires accountId unless baseURL is supplied",
})
if (!input.accountId) throw new Error("CloudflareWorkersAI.configure requires accountId unless baseURL is supplied")
return `https://api.cloudflare.com/client/v4/accounts/${encodeURIComponent(input.accountId)}/ai/v1`
}
@@ -63,15 +59,13 @@ export const configure = (input: LanguageModelOptions) => {
export const provider = { id, configure }
export const model: ProviderPackage.Definition<Settings, OpenAIProviderOptionsInput>["model"] = (modelID, settings) => {
const { accountId: _, apiKey, baseURL: _url, body, headers, ...providerOptions } = settings
return configure({
apiKey,
export const model: ProviderPackage.Definition<Settings, OpenAIProviderOptionsInput>["model"] = (modelID, settings) =>
configure({
apiKey: settings.apiKey,
baseURL: baseURL(settings),
headers: headers === undefined ? undefined : { ...headers },
http: body === undefined ? undefined : { body: { ...body } },
providerOptions,
headers: settings.headers === undefined ? undefined : { ...settings.headers },
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
providerOptions: settings.providerOptions,
}).model(modelID)
}
export * as CloudflareWorkersAI from "./cloudflare-workers-ai.js"
+11 -14
View File
@@ -15,11 +15,11 @@ export type LanguageModelOptions = Omit<RouteDefaultsInput, "providerOptions"> &
readonly providerOptions?: OpenAIProviderOptionsInput
}
export type Settings = ProviderPackage.Settings &
OpenAIProviderOptionsInput & {
readonly apiKey?: string
readonly baseURL?: string
}
export interface Settings extends ProviderPackage.Settings {
readonly apiKey?: string
readonly baseURL?: string
readonly providerOptions?: OpenAIProviderOptionsInput
}
export const route = Route.make({
id: "deepinfra-chat",
@@ -55,14 +55,11 @@ export const configure = (input: LanguageModelOptions = {}) => {
export const provider = configure()
export const model: ProviderPackage.Definition<Settings, OpenAIProviderOptionsInput>["model"] = (
modelID,
{ apiKey, baseURL, body, headers, ...providerOptions },
) =>
export const model: ProviderPackage.Definition<Settings, OpenAIProviderOptionsInput>["model"] = (modelID, settings) =>
configure({
apiKey,
baseURL,
headers: headers === undefined ? undefined : { ...headers },
http: body === undefined ? undefined : { body: { ...body } },
providerOptions,
apiKey: settings.apiKey,
baseURL: settings.baseURL,
headers: settings.headers === undefined ? undefined : { ...settings.headers },
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
providerOptions: settings.providerOptions,
}).model(modelID)
+11 -14
View File
@@ -15,11 +15,11 @@ export type LanguageModelOptions = Omit<RouteDefaultsInput, "providerOptions"> &
readonly providerOptions?: OpenAIProviderOptionsInput
}
export type Settings = ProviderPackage.Settings &
OpenAIProviderOptionsInput & {
readonly apiKey?: string
readonly baseURL?: string
}
export interface Settings extends ProviderPackage.Settings {
readonly apiKey?: string
readonly baseURL?: string
readonly providerOptions?: OpenAIProviderOptionsInput
}
export const route = Route.make({
id: "deepseek-chat",
@@ -52,16 +52,13 @@ export const configure = (input: LanguageModelOptions = {}) => {
export const provider = configure()
export const model: ProviderPackage.Definition<Settings, OpenAIProviderOptionsInput>["model"] = (
modelID,
{ apiKey, baseURL, body, headers, ...providerOptions },
) =>
export const model: ProviderPackage.Definition<Settings, OpenAIProviderOptionsInput>["model"] = (modelID, settings) =>
configure({
apiKey,
baseURL,
headers: headers === undefined ? undefined : { ...headers },
http: body === undefined ? undefined : { body: { ...body } },
providerOptions,
apiKey: settings.apiKey,
baseURL: settings.baseURL,
headers: settings.headers === undefined ? undefined : { ...settings.headers },
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
providerOptions: settings.providerOptions,
}).model(modelID)
export * as DeepSeek from "./deepseek.js"
+11 -14
View File
@@ -15,11 +15,11 @@ export type LanguageModelOptions = Omit<RouteDefaultsInput, "providerOptions"> &
readonly providerOptions?: OpenAIProviderOptionsInput
}
export type Settings = ProviderPackage.Settings &
OpenAIProviderOptionsInput & {
readonly apiKey?: string
readonly baseURL?: string
}
export interface Settings extends ProviderPackage.Settings {
readonly apiKey?: string
readonly baseURL?: string
readonly providerOptions?: OpenAIProviderOptionsInput
}
export const route = Route.make({
id: "fireworks-chat",
@@ -48,16 +48,13 @@ export const configure = (input: LanguageModelOptions = {}) => {
export const provider = configure()
export const model: ProviderPackage.Definition<Settings, OpenAIProviderOptionsInput>["model"] = (
modelID,
{ apiKey, baseURL, body, headers, ...providerOptions },
) =>
export const model: ProviderPackage.Definition<Settings, OpenAIProviderOptionsInput>["model"] = (modelID, settings) =>
configure({
apiKey,
baseURL,
headers: headers === undefined ? undefined : { ...headers },
http: body === undefined ? undefined : { body: { ...body } },
providerOptions,
apiKey: settings.apiKey,
baseURL: settings.baseURL,
headers: settings.headers === undefined ? undefined : { ...settings.headers },
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
providerOptions: settings.providerOptions,
}).model(modelID)
export * as Fireworks from "./fireworks.js"
+19 -24
View File
@@ -2,7 +2,7 @@ import type { ProviderPackage } from "../provider-package.js"
import { OpenAIChat } from "../protocols/openai-chat.js"
import { Route, type RouteDefaultsInput } from "../route/client.js"
import { Endpoint } from "../route/endpoint.js"
import { ProviderConfigurationError, ProviderID, type ModelID } from "../schema/index.js"
import { ProviderID, type ModelID } from "../schema/index.js"
import { GoogleVertexShared } from "./google-vertex-shared.js"
import type { OpenAIProviderOptionsInput } from "./openai-options.js"
@@ -16,14 +16,14 @@ export type Config = RouteDefaultsInput &
readonly providerOptions?: OpenAIProviderOptionsInput
}
export type Settings = ProviderPackage.Settings &
OpenAIProviderOptionsInput & {
readonly accessToken?: string
readonly apiKey?: never
readonly baseURL?: string
readonly location?: string
readonly project?: string
}
export interface Settings extends ProviderPackage.Settings {
readonly accessToken?: string
readonly apiKey?: never
readonly baseURL?: string
readonly location?: string
readonly project?: string
readonly providerOptions?: OpenAIProviderOptionsInput
}
const route = Route.make({
id: "google-vertex-chat",
@@ -37,8 +37,7 @@ const route = Route.make({
export const routes = [route]
const configuredRoute = (input: Config) => {
if ("apiKey" in input && input.apiKey !== undefined)
throw new ProviderConfigurationError({ provider: id, message: "Google Vertex Chat does not support API keys" })
if ("apiKey" in input && input.apiKey !== undefined) throw new Error("Google Vertex Chat does not support API keys")
const {
accessToken: _accessToken,
auth: _auth,
@@ -74,19 +73,15 @@ export const provider = {
configure,
}
export const model: ProviderPackage.Definition<Settings, OpenAIProviderOptionsInput>["model"] = (
modelID,
{ accessToken, apiKey, baseURL, body, headers, location, project, ...providerOptions },
) => {
if (apiKey !== undefined)
throw new ProviderConfigurationError({ provider: id, message: "Google Vertex Chat does not support API keys" })
export const model: ProviderPackage.Definition<Settings, OpenAIProviderOptionsInput>["model"] = (modelID, settings) => {
if (settings.apiKey !== undefined) throw new Error("Google Vertex Chat does not support API keys")
return configure({
accessToken,
baseURL,
headers: headers === undefined ? undefined : { ...headers },
http: body === undefined ? undefined : { body: { ...body } },
location,
project,
providerOptions,
accessToken: settings.accessToken,
baseURL: settings.baseURL,
headers: settings.headers === undefined ? undefined : { ...settings.headers },
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
location: settings.location,
project: settings.project,
providerOptions: settings.providerOptions,
}).model(modelID)
}
@@ -5,7 +5,7 @@ import { Auth } from "../route/auth.js"
import { Route, type RouteDefaultsInput } from "../route/client.js"
import { Endpoint } from "../route/endpoint.js"
import { Protocol } from "../route/protocol.js"
import { ProviderConfigurationError, ProviderID, type ModelID } from "../schema/index.js"
import { ProviderID, type ModelID } from "../schema/index.js"
import { GoogleVertexShared } from "./google-vertex-shared.js"
export type AnthropicOptionsInput = AnthropicMessages.OptionsInput
@@ -25,14 +25,14 @@ export type Config = RouteDefaultsInput &
readonly providerOptions?: AnthropicMessages.ProviderOptionsInput
}
export type Settings = ProviderPackage.Settings &
AnthropicMessages.ProviderOptionsInput & {
readonly accessToken?: string
readonly apiKey?: never
readonly baseURL?: string
readonly location?: string
readonly project?: string
}
export interface Settings extends ProviderPackage.Settings {
readonly accessToken?: string
readonly apiKey?: never
readonly baseURL?: string
readonly location?: string
readonly project?: string
readonly providerOptions?: AnthropicMessages.ProviderOptionsInput
}
const route = Route.make({
id: "google-vertex-messages",
@@ -67,7 +67,7 @@ export const routes = [route]
const configuredRoute = (input: Config) => {
if ("apiKey" in input && input.apiKey !== undefined)
throw new ProviderConfigurationError({ provider: id, message: "Google Vertex Messages does not support API keys" })
throw new Error("Google Vertex Messages does not support API keys")
const {
accessToken: _accessToken,
auth: _auth,
@@ -105,17 +105,16 @@ export const provider = {
export const model: ProviderPackage.Definition<Settings, AnthropicMessages.ProviderOptionsInput>["model"] = (
modelID,
{ accessToken, apiKey, baseURL, body, headers, location, project, ...providerOptions },
settings,
) => {
if (apiKey !== undefined)
throw new ProviderConfigurationError({ provider: id, message: "Google Vertex Messages does not support API keys" })
if (settings.apiKey !== undefined) throw new Error("Google Vertex Messages does not support API keys")
return configure({
accessToken,
baseURL,
headers: headers === undefined ? undefined : { ...headers },
http: body === undefined ? undefined : { body: { ...body } },
location,
project,
providerOptions,
accessToken: settings.accessToken,
baseURL: settings.baseURL,
headers: settings.headers === undefined ? undefined : { ...settings.headers },
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
location: settings.location,
project: settings.project,
providerOptions: settings.providerOptions,
}).model(modelID)
}
@@ -2,7 +2,7 @@ import type { ProviderPackage } from "../provider-package.js"
import { OpenResponses } from "../protocols/open-responses.js"
import { Route, type RouteDefaultsInput } from "../route/client.js"
import { Endpoint } from "../route/endpoint.js"
import { ProviderConfigurationError, ProviderID, type ModelID } from "../schema/index.js"
import { ProviderID, type ModelID } from "../schema/index.js"
import { GoogleVertexShared } from "./google-vertex-shared.js"
import type { OpenResponsesProviderOptionsInput } from "./open-responses-options.js"
@@ -16,14 +16,14 @@ export type Config = RouteDefaultsInput &
readonly providerOptions?: OpenResponsesProviderOptionsInput
}
export type Settings = ProviderPackage.Settings &
OpenResponsesProviderOptionsInput & {
readonly accessToken?: string
readonly apiKey?: never
readonly baseURL?: string
readonly location?: string
readonly project?: string
}
export interface Settings extends ProviderPackage.Settings {
readonly accessToken?: string
readonly apiKey?: never
readonly baseURL?: string
readonly location?: string
readonly project?: string
readonly providerOptions?: OpenResponsesProviderOptionsInput
}
const route = Route.make({
id: "google-vertex-responses",
@@ -39,7 +39,7 @@ export const routes = [route]
const configuredRoute = (input: Config) => {
if ("apiKey" in input && input.apiKey !== undefined)
throw new ProviderConfigurationError({ provider: id, message: "Google Vertex Responses does not support API keys" })
throw new Error("Google Vertex Responses does not support API keys")
const {
accessToken: _accessToken,
auth: _auth,
@@ -77,17 +77,16 @@ export const provider = {
export const model: ProviderPackage.Definition<Settings, OpenResponsesProviderOptionsInput>["model"] = (
modelID,
{ accessToken, apiKey, baseURL, body, headers, location, project, ...providerOptions },
settings,
) => {
if (apiKey !== undefined)
throw new ProviderConfigurationError({ provider: id, message: "Google Vertex Responses does not support API keys" })
if (settings.apiKey !== undefined) throw new Error("Google Vertex Responses does not support API keys")
return configure({
accessToken,
baseURL,
headers: headers === undefined ? undefined : { ...headers },
http: body === undefined ? undefined : { body: { ...body } },
location,
project,
providerOptions,
accessToken: settings.accessToken,
baseURL: settings.baseURL,
headers: settings.headers === undefined ? undefined : { ...settings.headers },
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
location: settings.location,
project: settings.project,
providerOptions: settings.providerOptions,
}).model(modelID)
}
@@ -1,10 +1,8 @@
import type { AnyAuthClient } from "google-auth-library"
import { Effect, Redacted } from "effect"
import { Auth, MissingCredentialError } from "../route/auth.js"
import { ProviderConfigurationError, ProviderID } from "../schema/index.js"
const SCOPE = "https://www.googleapis.com/auth/cloud-platform"
const id = ProviderID.make("google-vertex")
export type OAuthOptions =
| { readonly accessToken?: string; readonly auth?: never }
@@ -37,18 +35,12 @@ export const host = (location: string) => {
export const requireProject = (value: string | undefined) => {
if (value) return value
throw new ProviderConfigurationError({
provider: id,
message: "Google Vertex requires a project when baseURL is not configured",
})
throw new Error("Google Vertex requires a project when baseURL is not configured")
}
export const apiKey = (input: ApiKeyOptions) => {
if (input.apiKey !== undefined && (input.accessToken !== undefined || input.auth !== undefined))
throw new ProviderConfigurationError({
provider: id,
message: "Google Vertex apiKey cannot be combined with accessToken or auth",
})
throw new Error("Google Vertex apiKey cannot be combined with accessToken or auth")
if (input.accessToken !== undefined || input.auth !== undefined) return undefined
return input.apiKey ?? process.env.GOOGLE_VERTEX_API_KEY
}
@@ -76,10 +68,7 @@ const adc = (project?: string) => {
export const oauth = (input: OAuthOptions, project?: string) => {
if (input.accessToken !== undefined && input.auth !== undefined)
throw new ProviderConfigurationError({
provider: id,
message: "Google Vertex accessToken cannot be combined with auth",
})
throw new Error("Google Vertex accessToken cannot be combined with auth")
if (input.auth) return input.auth
if (input.accessToken !== undefined) return Auth.bearer(input.accessToken)
return adc(project)
+13 -22
View File
@@ -6,7 +6,7 @@ import { Auth } from "../route/auth.js"
import { Route, type RouteDefaultsInput } from "../route/client.js"
import { Endpoint } from "../route/endpoint.js"
import { Framing } from "../route/framing.js"
import { ProviderConfigurationError, ProviderID, type LLMRequest, type ModelID } from "../schema/index.js"
import { ProviderID, type LLMRequest, type ModelID } from "../schema/index.js"
import { GoogleVertexShared } from "./google-vertex-shared.js"
export interface GeminiOptionsInput extends Gemini.OptionsInput {
@@ -26,7 +26,6 @@ export type Config = RouteDefaultsInput &
}
export type Settings = ProviderPackage.Settings &
GeminiProviderOptionsInput &
(
| { readonly accessToken?: string; readonly apiKey?: never }
| { readonly accessToken?: never; readonly apiKey?: string }
@@ -34,6 +33,7 @@ export type Settings = ProviderPackage.Settings &
readonly baseURL?: string
readonly location?: string
readonly project?: string
readonly providerOptions?: GeminiProviderOptionsInput
}
const fromRequest = Effect.fn("GoogleVertex.fromRequest")(function* (request: LLMRequest) {
@@ -93,10 +93,7 @@ const configuredRoute = (input: Config, modelID: string | ModelID) => {
const apiKey = GoogleVertexShared.apiKey(input)
const endpointModel = String(modelID).startsWith("endpoints/")
if (apiKey !== undefined && endpointModel)
throw new ProviderConfigurationError({
provider: id,
message: "Google Vertex tuned models do not support Express Mode API keys",
})
throw new Error("Google Vertex tuned models do not support Express Mode API keys")
const location = GoogleVertexShared.location(inputLocation, "us-central1")
const project = GoogleVertexShared.project(inputProject)
const endpoint =
@@ -124,22 +121,16 @@ export const provider = {
id,
configure,
}
export const model: ProviderPackage.Definition<Settings, GeminiProviderOptionsInput>["model"] = (
modelID,
{ accessToken, apiKey, baseURL, body, headers, location, project, ...providerOptions },
) => {
if (apiKey !== undefined && accessToken !== undefined)
throw new ProviderConfigurationError({
provider: id,
message: "Google Vertex apiKey cannot be combined with accessToken or auth",
})
export const model: ProviderPackage.Definition<Settings, GeminiProviderOptionsInput>["model"] = (modelID, settings) => {
if (settings.apiKey !== undefined && settings.accessToken !== undefined)
throw new Error("Google Vertex apiKey cannot be combined with accessToken or auth")
return configure({
...(apiKey === undefined ? { accessToken: accessToken } : { apiKey: apiKey }),
baseURL,
headers: headers === undefined ? undefined : { ...headers },
http: body === undefined ? undefined : { body: { ...body } },
location,
project,
providerOptions,
...(settings.apiKey === undefined ? { accessToken: settings.accessToken } : { apiKey: settings.apiKey }),
baseURL: settings.baseURL,
headers: settings.headers === undefined ? undefined : { ...settings.headers },
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
location: settings.location,
project: settings.project,
providerOptions: settings.providerOptions,
}).model(modelID)
}
+11 -14
View File
@@ -20,11 +20,11 @@ export type Config = RouteDefaultsInput &
readonly providerOptions?: Gemini.ProviderOptionsInput
}
export type Settings = ProviderPackage.Settings &
Gemini.ProviderOptionsInput & {
readonly apiKey?: string
readonly baseURL?: string
}
export interface Settings extends ProviderPackage.Settings {
readonly apiKey?: string
readonly baseURL?: string
readonly providerOptions?: Gemini.ProviderOptionsInput
}
const auth = (options: ProviderAuthOption<"optional">) => {
if ("auth" in options && options.auth) return options.auth
@@ -57,16 +57,13 @@ export const configure = (input: Config = {}) => {
}
export const provider = configure()
export const model: ProviderPackage.Definition<Settings, Gemini.ProviderOptionsInput>["model"] = (
modelID,
{ apiKey, baseURL, body, headers, ...providerOptions },
) =>
export const model: ProviderPackage.Definition<Settings, Gemini.ProviderOptionsInput>["model"] = (modelID, settings) =>
configure({
apiKey,
baseURL,
headers: headers === undefined ? undefined : { ...headers },
http: body === undefined ? undefined : { body: { ...body } },
providerOptions,
apiKey: settings.apiKey,
baseURL: settings.baseURL,
headers: settings.headers === undefined ? undefined : { ...settings.headers },
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
providerOptions: settings.providerOptions,
}).model(modelID)
export const image = provider.image
+11 -14
View File
@@ -26,11 +26,11 @@ export type LanguageModelOptions = Omit<RouteDefaultsInput, "providerOptions"> &
readonly providerOptions?: ProviderOptions
}
export type Settings = ProviderPackage.Settings &
ProviderOptions & {
readonly apiKey?: string
readonly baseURL?: string
}
export interface Settings extends ProviderPackage.Settings {
readonly apiKey?: string
readonly baseURL?: string
readonly providerOptions?: ProviderOptions
}
const Options = Schema.Struct({
includeReasoning: Schema.optional(Schema.Boolean),
@@ -103,16 +103,13 @@ export const configure = (input: LanguageModelOptions = {}) => {
export const provider = configure()
export const model: ProviderPackage.Definition<Settings, ProviderOptions>["model"] = (
modelID,
{ apiKey, baseURL, body, headers, ...providerOptions },
) =>
export const model: ProviderPackage.Definition<Settings, ProviderOptions>["model"] = (modelID, settings) =>
configure({
apiKey,
baseURL,
headers: headers === undefined ? undefined : { ...headers },
http: body === undefined ? undefined : { body: { ...body } },
providerOptions,
apiKey: settings.apiKey,
baseURL: settings.baseURL,
headers: settings.headers === undefined ? undefined : { ...settings.headers },
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
providerOptions: settings.providerOptions,
}).model(modelID)
export * as Groq from "./groq.js"
+11 -11
View File
@@ -79,11 +79,11 @@ export type LanguageModelOptions = Omit<RouteDefaultsInput, "providerOptions"> &
readonly providerOptions?: ProviderOptionsInput
}
export type Settings = ProviderPackage.Settings &
ProviderOptionsInput & {
readonly apiKey?: string
readonly baseURL?: string
}
export interface Settings extends ProviderPackage.Settings {
readonly apiKey?: string
readonly baseURL?: string
readonly providerOptions?: ProviderOptionsInput
}
const responsesRoute = Route.make({
id: "meta-responses",
@@ -169,13 +169,13 @@ export const chatModel: ProviderPackage.Definition<Settings, OpenResponsesProvid
export const messagesModel: ProviderPackage.Definition<Settings, MessagesOptionsInput>["model"] = (modelID, settings) =>
fromSettings(settings).messages(modelID)
function fromSettings({ apiKey, baseURL, body, headers, ...providerOptions }: Settings) {
function fromSettings(settings: Settings) {
return configure({
apiKey,
baseURL,
headers,
http: body === undefined ? undefined : { body: { ...body } },
providerOptions,
apiKey: settings.apiKey,
baseURL: settings.baseURL,
headers: settings.headers,
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
providerOptions: settings.providerOptions,
})
}
+11 -11
View File
@@ -40,11 +40,11 @@ export type Config = Omit<RouteDefaultsInput, "providerOptions"> &
readonly providerOptions?: ProviderOptionsInput
}
export type Settings<Options = MessagesOptionsInput> = ProviderPackage.Settings &
Options & {
readonly apiKey?: string
readonly baseURL?: string
}
export interface Settings<Options = MessagesOptionsInput> extends ProviderPackage.Settings {
readonly apiKey?: string
readonly baseURL?: string
readonly providerOptions?: Options
}
const ChatOptions = Schema.Struct({
thinking: Schema.optional(Schema.Struct({ type: Schema.String })),
@@ -127,14 +127,14 @@ export const provider = configure()
export const model: ProviderPackage.Definition<Settings<MessagesOptionsInput>, MessagesOptionsInput>["model"] = (
modelID,
{ apiKey, baseURL, body, headers, ...providerOptions },
settings,
) =>
configure({
apiKey,
baseURL,
headers,
http: body === undefined ? undefined : { body: { ...body } },
providerOptions,
apiKey: settings.apiKey,
baseURL: settings.baseURL,
headers: settings.headers,
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
providerOptions: settings.providerOptions,
}).model(modelID)
export const messages = provider.messages
+6 -9
View File
@@ -3,14 +3,11 @@ import { MiniMax } from "../minimax.js"
export type Settings = MiniMax.Settings<MiniMax.ChatOptionsInput>
export const model: ProviderPackage.Definition<Settings, MiniMax.ChatOptionsInput>["model"] = (
modelID,
{ apiKey, baseURL, body, headers, ...providerOptions },
) =>
export const model: ProviderPackage.Definition<Settings, MiniMax.ChatOptionsInput>["model"] = (modelID, settings) =>
MiniMax.configure({
apiKey,
baseURL,
headers,
http: body === undefined ? undefined : { body: { ...body } },
providerOptions,
apiKey: settings.apiKey,
baseURL: settings.baseURL,
headers: settings.headers,
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
providerOptions: settings.providerOptions,
}).chat(modelID)
@@ -5,12 +5,12 @@ export type Settings = MiniMax.Settings<MiniMax.ResponsesOptionsInput>
export const model: ProviderPackage.Definition<Settings, MiniMax.ResponsesOptionsInput>["model"] = (
modelID,
{ apiKey, baseURL, body, headers, ...providerOptions },
settings,
) =>
MiniMax.configure({
apiKey,
baseURL,
headers,
http: body === undefined ? undefined : { body: { ...body } },
providerOptions,
apiKey: settings.apiKey,
baseURL: settings.baseURL,
headers: settings.headers,
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
providerOptions: settings.providerOptions,
}).responses(modelID)
+11 -14
View File
@@ -14,11 +14,11 @@ export type LanguageModelOptions = Omit<RouteDefaultsInput, "providerOptions"> &
readonly providerOptions?: ProviderOptions
}
export type Settings = ProviderPackage.Settings &
ProviderOptions & {
readonly apiKey?: string
readonly baseURL?: string
}
export interface Settings extends ProviderPackage.Settings {
readonly apiKey?: string
readonly baseURL?: string
readonly providerOptions?: ProviderOptions
}
export const route = MistralChat.route
export const routes = [route]
@@ -39,16 +39,13 @@ export const configure = (input: LanguageModelOptions = {}) => {
export const provider = configure()
export const model: ProviderPackage.Definition<Settings, ProviderOptions>["model"] = (
modelID,
{ apiKey, baseURL, body, headers, ...providerOptions },
) =>
export const model: ProviderPackage.Definition<Settings, ProviderOptions>["model"] = (modelID, settings) =>
configure({
apiKey,
baseURL,
headers: headers === undefined ? undefined : { ...headers },
http: body === undefined ? undefined : { body: { ...body } },
providerOptions,
apiKey: settings.apiKey,
baseURL: settings.baseURL,
headers: settings.headers === undefined ? undefined : { ...settings.headers },
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
providerOptions: settings.providerOptions,
}).model(modelID)
export * as Mistral from "./mistral.js"
+11 -14
View File
@@ -42,11 +42,11 @@ export type Config = Omit<RouteDefaultsInput, "providerOptions"> &
readonly providerOptions?: ChatOptionsInput | MessagesOptionsInput | ResponsesOptionsInput
}
export type Settings<Options = ChatOptionsInput> = ProviderPackage.Settings &
Options & {
readonly apiKey?: string
readonly baseURL?: string
}
export interface Settings<Options = ChatOptionsInput> extends ProviderPackage.Settings {
readonly apiKey?: string
readonly baseURL?: string
readonly providerOptions?: Options
}
const ChatOptions = Schema.Struct({
reasoningEffort: Schema.optional(Schema.String),
@@ -133,16 +133,13 @@ export const chat = provider.chat
export const messages = provider.messages
export const responses = provider.responses
export const model: ProviderPackage.Definition<Settings, ChatOptionsInput>["model"] = (
modelID,
{ apiKey, baseURL, body, headers, ...providerOptions },
) =>
export const model: ProviderPackage.Definition<Settings, ChatOptionsInput>["model"] = (modelID, settings) =>
configure({
apiKey,
baseURL,
headers,
http: body === undefined ? undefined : { body: { ...body } },
providerOptions,
apiKey: settings.apiKey,
baseURL: settings.baseURL,
headers: settings.headers,
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
providerOptions: settings.providerOptions,
}).model(modelID)
export * as Moonshot from "./moonshot.js"
@@ -5,12 +5,12 @@ export type Settings = Moonshot.Settings<Moonshot.MessagesOptionsInput>
export const model: ProviderPackage.Definition<Settings, Moonshot.MessagesOptionsInput>["model"] = (
modelID,
{ apiKey, baseURL, body, headers, ...providerOptions },
settings,
) =>
Moonshot.configure({
apiKey,
baseURL,
headers,
http: body === undefined ? undefined : { body: { ...body } },
providerOptions,
apiKey: settings.apiKey,
baseURL: settings.baseURL,
headers: settings.headers,
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
providerOptions: settings.providerOptions,
}).messages(modelID)
@@ -5,12 +5,12 @@ export type Settings = Moonshot.Settings<Moonshot.ResponsesOptionsInput>
export const model: ProviderPackage.Definition<Settings, Moonshot.ResponsesOptionsInput>["model"] = (
modelID,
{ apiKey, baseURL, body, headers, ...providerOptions },
settings,
) =>
Moonshot.configure({
apiKey,
baseURL,
headers,
http: body === undefined ? undefined : { body: { ...body } },
providerOptions,
apiKey: settings.apiKey,
baseURL: settings.baseURL,
headers: settings.headers,
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
providerOptions: settings.providerOptions,
}).responses(modelID)
@@ -16,12 +16,12 @@ export type Config = RouteDefaultsInput &
readonly providerOptions?: OpenResponsesProviderOptionsInput
}
export type Settings = ProviderPackage.Settings &
OpenResponsesProviderOptionsInput & {
readonly apiKey?: string
readonly baseURL: string
readonly provider?: string
}
export interface Settings extends ProviderPackage.Settings {
readonly apiKey?: string
readonly baseURL: string
readonly provider?: string
readonly providerOptions?: OpenResponsesProviderOptionsInput
}
export const routes = [OpenAICompatibleResponses.route]
@@ -48,13 +48,13 @@ export const provider = {
export const model: ProviderPackage.Definition<Settings, OpenResponsesProviderOptionsInput>["model"] = (
modelID,
{ apiKey, baseURL, body, headers, provider, ...providerOptions },
settings,
) =>
configure({
apiKey,
baseURL,
headers: headers === undefined ? undefined : { ...headers },
http: body === undefined ? undefined : { body: { ...body } },
provider,
providerOptions,
apiKey: settings.apiKey,
baseURL: settings.baseURL,
headers: settings.headers === undefined ? undefined : { ...settings.headers },
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
provider: settings.provider,
providerOptions: settings.providerOptions,
}).model(modelID)
+13 -16
View File
@@ -14,12 +14,12 @@ type GenericModelOptions = Omit<RouteDefaultsInput, "providerOptions"> &
readonly providerOptions?: OpenAIProviderOptionsInput
}
export type Settings = ProviderPackage.Settings &
OpenAIProviderOptionsInput & {
readonly apiKey?: string
readonly baseURL: string
readonly provider?: string
}
export interface Settings extends ProviderPackage.Settings {
readonly apiKey?: string
readonly baseURL: string
readonly provider?: string
readonly providerOptions?: OpenAIProviderOptionsInput
}
export const routes = [OpenAICompatibleChat.route]
@@ -45,17 +45,14 @@ export const provider = {
configure,
}
export const model: ProviderPackage.Definition<Settings, OpenAIProviderOptionsInput>["model"] = (
modelID,
{ apiKey, baseURL, body, headers, provider, ...providerOptions },
) =>
export const model: ProviderPackage.Definition<Settings, OpenAIProviderOptionsInput>["model"] = (modelID, settings) =>
configure({
apiKey,
baseURL,
headers: headers === undefined ? undefined : { ...headers },
http: body === undefined ? undefined : { body: { ...body } },
provider,
providerOptions,
apiKey: settings.apiKey,
baseURL: settings.baseURL,
headers: settings.headers === undefined ? undefined : { ...settings.headers },
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
provider: settings.provider,
providerOptions: settings.providerOptions,
}).model(modelID)
export * as OpenAICompatible from "./openai-compatible.js"
+17 -26
View File
@@ -57,14 +57,14 @@ export const imageGeneration = (options: ImageGenerationOptions = {}) =>
},
})
export type Settings = ProviderPackage.Settings &
OpenAIProviderOptionsInput & {
readonly apiKey?: string
readonly baseURL?: string
readonly organization?: string
readonly project?: string
readonly queryParams?: Readonly<Record<string, string>>
}
export interface Settings extends ProviderPackage.Settings {
readonly apiKey?: string
readonly baseURL?: string
readonly organization?: string
readonly project?: string
readonly queryParams?: Readonly<Record<string, string>>
readonly providerOptions?: OpenAIProviderOptionsInput
}
const auth = (options: ProviderAuthOption<"optional">) => AuthOptions.bearer(options, "OPENAI_API_KEY")
@@ -116,28 +116,19 @@ export const configure = (input: Config = {}) => {
export const provider = configure()
const config = ({
apiKey,
baseURL,
body,
headers: given,
organization,
project,
queryParams,
...providerOptions
}: Settings): Config => {
const config = (settings: Settings): Config => {
const headers = {
...(organization === undefined ? {} : { "OpenAI-Organization": organization }),
...(project === undefined ? {} : { "OpenAI-Project": project }),
...given,
...(settings.organization === undefined ? {} : { "OpenAI-Organization": settings.organization }),
...(settings.project === undefined ? {} : { "OpenAI-Project": settings.project }),
...settings.headers,
}
return {
apiKey,
baseURL,
apiKey: settings.apiKey,
baseURL: settings.baseURL,
headers: Object.keys(headers).length === 0 ? undefined : headers,
http: body === undefined ? undefined : { body: { ...body } },
providerOptions,
queryParams: queryParams === undefined ? undefined : { ...queryParams },
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
providerOptions: settings.providerOptions,
queryParams: settings.queryParams === undefined ? undefined : { ...settings.queryParams },
}
}
+11 -11
View File
@@ -77,11 +77,11 @@ export type LanguageModelOptions = Omit<RouteDefaultsInput, "providerOptions"> &
readonly providerOptions?: OpenRouterProviderOptionsInput
}
export type Settings = ProviderPackage.Settings &
OpenRouterProviderOptionsInput & {
readonly apiKey?: string
readonly baseURL?: string
}
export interface Settings extends ProviderPackage.Settings {
readonly apiKey?: string
readonly baseURL?: string
readonly providerOptions?: OpenRouterProviderOptionsInput
}
const OpenRouterBody = Schema.StructWithRest(Schema.Struct(OpenAIChat.bodyFields), [
Schema.Record(Schema.String, Schema.Any),
@@ -191,12 +191,12 @@ export const configure = (input: LanguageModelOptions = {}) => {
export const provider = configure()
export const model: ProviderPackage.Definition<Settings, OpenRouterProviderOptionsInput>["model"] = (
modelID,
{ apiKey, baseURL, body, headers, ...providerOptions },
settings,
) =>
configure({
apiKey,
baseURL,
headers,
http: body === undefined ? undefined : { body: { ...body } },
providerOptions,
apiKey: settings.apiKey,
baseURL: settings.baseURL,
headers: settings.headers,
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
providerOptions: settings.providerOptions,
}).model(modelID)
+11 -14
View File
@@ -15,11 +15,11 @@ export type LanguageModelOptions = Omit<RouteDefaultsInput, "providerOptions"> &
readonly providerOptions?: OpenAIProviderOptionsInput
}
export type Settings = ProviderPackage.Settings &
OpenAIProviderOptionsInput & {
readonly apiKey?: string
readonly baseURL?: string
}
export interface Settings extends ProviderPackage.Settings {
readonly apiKey?: string
readonly baseURL?: string
readonly providerOptions?: OpenAIProviderOptionsInput
}
export const route = Route.make({
id: "togetherai-chat",
@@ -52,14 +52,11 @@ export const configure = (input: LanguageModelOptions = {}) => {
export const provider = configure()
export const model: ProviderPackage.Definition<Settings, OpenAIProviderOptionsInput>["model"] = (
modelID,
{ apiKey, baseURL, body, headers, ...providerOptions },
) =>
export const model: ProviderPackage.Definition<Settings, OpenAIProviderOptionsInput>["model"] = (modelID, settings) =>
configure({
apiKey,
baseURL,
headers: headers === undefined ? undefined : { ...headers },
http: body === undefined ? undefined : { body: { ...body } },
providerOptions,
apiKey: settings.apiKey,
baseURL: settings.baseURL,
headers: settings.headers === undefined ? undefined : { ...settings.headers },
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
providerOptions: settings.providerOptions,
}).model(modelID)
+11 -11
View File
@@ -20,11 +20,11 @@ export type LanguageModelOptions = Omit<RouteDefaultsInput, "providerOptions"> &
readonly providerOptions?: XAIProviderOptionsInput
}
export type Settings = ProviderPackage.Settings &
XAIProviderOptionsInput & {
readonly apiKey?: string
readonly baseURL?: string
}
export interface Settings extends ProviderPackage.Settings {
readonly apiKey?: string
readonly baseURL?: string
readonly providerOptions?: XAIProviderOptionsInput
}
export type { XAIImageOptions } from "../protocols/xai-images.js"
@@ -110,13 +110,13 @@ export const model: ProviderPackage.Definition<
Settings,
XAIProviderOptionsInput,
typeof responsesRoute.compact
>["model"] = (modelID, { apiKey, baseURL, body, headers, ...providerOptions }) =>
>["model"] = (modelID, settings) =>
configure({
apiKey,
baseURL,
headers,
http: body === undefined ? undefined : { body: { ...body } },
providerOptions,
apiKey: settings.apiKey,
baseURL: settings.baseURL,
headers: settings.headers,
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
providerOptions: settings.providerOptions,
}).model(modelID)
export const responses = provider.responses
export const chat = provider.chat
+11 -14
View File
@@ -23,11 +23,11 @@ export type Config = Omit<RouteDefaultsInput, "providerOptions"> &
readonly providerOptions?: ChatOptionsInput | MessagesOptionsInput | ResponsesOptionsInput
}
export type Settings<Options = ChatOptionsInput> = ProviderPackage.Settings &
Options & {
readonly apiKey?: string
readonly baseURL?: string
}
export interface Settings<Options = ChatOptionsInput> extends ProviderPackage.Settings {
readonly apiKey?: string
readonly baseURL?: string
readonly providerOptions?: Options
}
const chatRoute = Route.make({
id: "zai-coding-chat",
@@ -80,16 +80,13 @@ export const chat = provider.chat
export const messages = provider.messages
export const responses = provider.responses
export const model: ProviderPackage.Definition<Settings, ChatOptionsInput>["model"] = (
modelID,
{ apiKey, baseURL, body, headers, ...providerOptions },
) =>
export const model: ProviderPackage.Definition<Settings, ChatOptionsInput>["model"] = (modelID, settings) =>
configure({
apiKey,
baseURL,
headers,
http: body === undefined ? undefined : { body: { ...body } },
providerOptions,
apiKey: settings.apiKey,
baseURL: settings.baseURL,
headers: settings.headers,
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
providerOptions: settings.providerOptions,
}).model(modelID)
export * as ZAICodingPlan from "./zai-coding-plan.js"
@@ -5,12 +5,12 @@ export type Settings = ZAICodingPlan.Settings<ZAICodingPlan.MessagesOptionsInput
export const model: ProviderPackage.Definition<Settings, ZAICodingPlan.MessagesOptionsInput>["model"] = (
modelID,
{ apiKey, baseURL, body, headers, ...providerOptions },
settings,
) =>
ZAICodingPlan.configure({
apiKey,
baseURL,
headers,
http: body === undefined ? undefined : { body: { ...body } },
providerOptions,
apiKey: settings.apiKey,
baseURL: settings.baseURL,
headers: settings.headers,
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
providerOptions: settings.providerOptions,
}).messages(modelID)
@@ -5,12 +5,12 @@ export type Settings = ZAICodingPlan.Settings<ZAICodingPlan.ResponsesOptionsInpu
export const model: ProviderPackage.Definition<Settings, ZAICodingPlan.ResponsesOptionsInput>["model"] = (
modelID,
{ apiKey, baseURL, body, headers, ...providerOptions },
settings,
) =>
ZAICodingPlan.configure({
apiKey,
baseURL,
headers,
http: body === undefined ? undefined : { body: { ...body } },
providerOptions,
apiKey: settings.apiKey,
baseURL: settings.baseURL,
headers: settings.headers,
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
providerOptions: settings.providerOptions,
}).responses(modelID)
+11 -14
View File
@@ -17,11 +17,11 @@ export type Config = Omit<RouteDefaultsInput, "providerOptions"> &
readonly providerOptions?: ChatOptionsInput
}
export type Settings = ProviderPackage.Settings &
ChatOptionsInput & {
readonly apiKey?: string
readonly baseURL?: string
}
export interface Settings extends ProviderPackage.Settings {
readonly apiKey?: string
readonly baseURL?: string
readonly providerOptions?: ChatOptionsInput
}
export type { ZAIImageOptions } from "../protocols/zai-images.js"
@@ -70,16 +70,13 @@ export const provider = configure()
export const image = provider.image
export const chat = provider.chat
export const model: ProviderPackage.Definition<Settings, ChatOptionsInput>["model"] = (
modelID,
{ apiKey, baseURL, body, headers, ...providerOptions },
) =>
export const model: ProviderPackage.Definition<Settings, ChatOptionsInput>["model"] = (modelID, settings) =>
configure({
apiKey,
baseURL,
headers,
http: body === undefined ? undefined : { body: { ...body } },
providerOptions,
apiKey: settings.apiKey,
baseURL: settings.baseURL,
headers: settings.headers,
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
providerOptions: settings.providerOptions,
}).model(modelID)
export * as ZAI from "./zai.js"
+1 -5
View File
@@ -23,7 +23,6 @@ import {
LanguageModel,
LLMEvent,
InvalidProviderOutputError,
ProviderConfigurationError,
ProviderID,
mergeGenerationOptions,
mergeHttpOptions,
@@ -129,10 +128,7 @@ const makeRouteLanguageModel = <Options extends ProviderOptions, Compact extends
const provider = route.provider ?? ("provider" in mapped ? mapped.provider : undefined)
if (!provider) throw new Error(`Route.model(${route.id}) requires a provider`)
if (!endpointBaseURL(route.endpoint))
throw new ProviderConfigurationError({
provider: ProviderID.make(provider),
message: `Route.model(${route.id}) requires an endpoint baseURL — configure it on the route first`,
})
throw new Error(`Route.model(${route.id}) requires an endpoint baseURL — configure it on the route first`)
return LanguageModel.make<Options, Compact>({
...mapped,
provider,
-13
View File
@@ -50,19 +50,6 @@ export class UnsupportedOperationError extends Schema.TaggedError<UnsupportedOpe
route: Schema.optional(RouteID),
}) {}
/**
* Provider settings that are missing, conflicting, or unsupported, such as
* Azure without `resourceName` or `baseURL`. Thrown synchronously while a
* provider facade or package entrypoint configures a model, before any
* request exists, so it is not an `AIError` reason.
*/
export class ProviderConfigurationError extends Schema.TaggedError<ProviderConfigurationError>(
"AI.Error.ProviderConfiguration",
)("ProviderConfiguration", {
provider: ProviderID,
message: Schema.String,
}) {}
export class NoRouteError extends Schema.TaggedError<NoRouteError>("AI.Error.NoRoute")("NoRoute", {
...ReasonFields,
route: RouteID,
+20 -26
View File
@@ -3,9 +3,6 @@ import { model } from "@opencode/ai/providers/openai"
import { LLM } from "../src/index.js"
import { Endpoint } from "../src/route/endpoint.js"
const configuration = (provider: string, message: string) =>
expect.objectContaining({ _tag: "ProviderConfiguration", provider, message })
describe("provider package entrypoints", () => {
test("semantic API aliases expose the same contract", async () => {
const modules = await Promise.all([
@@ -188,13 +185,13 @@ describe("provider package entrypoints", () => {
baseURL: "https://provider.example.test/v1/",
headers: { "x-application": "opencode" },
body: { service_tier: "priority" },
reasoningEffort: "high" as const,
providerOptions: { reasoningEffort: "high" as const },
}
const deepinfra = DeepInfra.model("google/gemma-3-27b-it", settings)
expect(deepinfra.route.id).toBe("deepinfra-chat")
expect(deepinfra.route.endpoint.baseURL).toBe("https://provider.example.test/v1/openai")
expect(deepinfra.route.defaults.providerOptions).toEqual({ reasoningEffort: "high" })
expect(deepinfra.route.defaults.providerOptions).toEqual(settings.providerOptions)
expect(deepinfra.route.defaults.headers).toEqual(settings.headers)
expect(deepinfra.route.defaults.http?.body).toEqual(settings.body)
})
@@ -210,7 +207,7 @@ describe("provider package entrypoints", () => {
apiKey: "fixture",
headers: { "x-application": "opencode" },
body: { custom: true },
reasoningEffort: "high",
providerOptions: { reasoningEffort: "high" },
})
expect(selected.provider).toBe(provider.id)
expect(selected.route.endpoint.baseURL).toBe(provider.baseURL({ accountId: "account" }))
@@ -231,11 +228,11 @@ describe("provider package entrypoints", () => {
}
const openrouter = OpenRouter.model("anthropic/claude-sonnet-4", {
...settings,
usage: true,
providerOptions: { usage: true },
})
const xai = XAI.model("grok-4", {
...settings,
reasoningEffort: "high",
providerOptions: { reasoningEffort: "high" },
})
for (const selected of [openrouter, xai]) {
@@ -269,8 +266,7 @@ describe("provider package entrypoints", () => {
provider: "example",
headers: { "x-application": "opencode" },
body: { service_tier: "priority" },
reasoningEffort: "low",
store: true,
providerOptions: { reasoningEffort: "low", store: true },
})
expect(String(selected.provider)).toBe("example")
@@ -296,7 +292,7 @@ describe("provider package entrypoints", () => {
provider: "example",
headers: { "x-application": "opencode" },
body: { metadata: { user_id: "user_1" } },
effort: "low",
providerOptions: { effort: "low" },
})
expect(String(selected.provider)).toBe("example")
@@ -316,7 +312,7 @@ describe("provider package entrypoints", () => {
const Anthropic = await import("@opencode/ai/providers/anthropic")
const selected = Anthropic.model("claude-sonnet-4-6", {
apiKey: "fixture",
thinking: { type: "adaptive" },
providerOptions: { thinking: { type: "adaptive" } },
})
expect(selected.route.defaults.providerOptions).toEqual({ thinking: { type: "adaptive" } })
@@ -326,7 +322,7 @@ describe("provider package entrypoints", () => {
const AnthropicCompatible = await import("@opencode/ai/providers/anthropic-compatible")
expect(() =>
Reflect.apply(AnthropicCompatible.model, undefined, ["compatible-model", { apiKey: "fixture" }]),
).toThrow(configuration("anthropic-compatible", "Anthropic-compatible providers require a baseURL"))
).toThrow("Anthropic-compatible providers require a baseURL")
})
test("rejects conflicting Anthropic-compatible auth settings at runtime", async () => {
@@ -341,10 +337,10 @@ describe("provider package entrypoints", () => {
baseURL: "https://messages.example.test/v1",
},
]),
).toThrow(configuration("anthropic-compatible", "Anthropic-compatible apiKey cannot be combined with authToken"))
).toThrow("Anthropic-compatible apiKey cannot be combined with authToken")
expect(() =>
Reflect.apply(Anthropic.model, undefined, ["claude-sonnet-4-6", { apiKey: "fixture", authToken: "token" }]),
).toThrow(configuration("anthropic", "Anthropic apiKey cannot be combined with authToken"))
).toThrow("Anthropic apiKey cannot be combined with authToken")
})
test("maps legacy OpenAI organization and project settings to headers", () => {
@@ -410,7 +406,7 @@ describe("provider package entrypoints", () => {
baseURL: "https://generativelanguage.test/v1beta",
headers: { "x-application": "opencode" },
body: { safetySettings: [] },
thinkingConfig: { thinkingBudget: 1_024 },
providerOptions: { thinkingConfig: { thinkingBudget: 1_024 } },
})
expect(selected.route.id).toBe("gemini")
@@ -494,45 +490,43 @@ describe("provider package entrypoints", () => {
"gemini-3.5-flash",
{ accessToken: "token", apiKey: "fixture", project: "vertex-project" },
]),
).toThrow(configuration("google-vertex", "Google Vertex apiKey cannot be combined with accessToken or auth"))
).toThrow("Google Vertex apiKey cannot be combined with accessToken or auth")
const configured = Reflect.apply(GoogleVertex.configure, undefined, [
{ accessToken: "token", auth: {}, project: "vertex-project" },
])
expect(() => configured.model("gemini-3.5-flash")).toThrow(
configuration("google-vertex", "Google Vertex accessToken cannot be combined with auth"),
)
expect(() => configured.model("gemini-3.5-flash")).toThrow("Google Vertex accessToken cannot be combined with auth")
expect(() =>
Reflect.apply(GoogleVertexMessages.model, undefined, [
"claude-sonnet-4-6",
{ apiKey: "fixture", project: "vertex-project" },
]),
).toThrow(configuration("google-vertex", "Google Vertex Messages does not support API keys"))
).toThrow("Google Vertex Messages does not support API keys")
expect(() =>
Reflect.apply(Providers.GoogleVertexMessages.configure, undefined, [
{ apiKey: "fixture", project: "vertex-project" },
]),
).toThrow(configuration("google-vertex", "Google Vertex Messages does not support API keys"))
).toThrow("Google Vertex Messages does not support API keys")
expect(() =>
Reflect.apply(GoogleVertexChat.model, undefined, [
"deepseek-ai/deepseek-v3.2-maas",
{ apiKey: "fixture", project: "vertex-project" },
]),
).toThrow(configuration("google-vertex", "Google Vertex Chat does not support API keys"))
).toThrow("Google Vertex Chat does not support API keys")
expect(() =>
Reflect.apply(Providers.GoogleVertexChat.configure, undefined, [
{ apiKey: "fixture", project: "vertex-project" },
]),
).toThrow(configuration("google-vertex", "Google Vertex Chat does not support API keys"))
).toThrow("Google Vertex Chat does not support API keys")
expect(() =>
Reflect.apply(GoogleVertexResponses.model, undefined, [
"xai/grok-4.20-reasoning",
{ apiKey: "fixture", project: "vertex-project" },
]),
).toThrow(configuration("google-vertex", "Google Vertex Responses does not support API keys"))
).toThrow("Google Vertex Responses does not support API keys")
expect(() =>
Reflect.apply(Providers.GoogleVertexResponses.configure, undefined, [
{ apiKey: "fixture", project: "vertex-project" },
]),
).toThrow(configuration("google-vertex", "Google Vertex Responses does not support API keys"))
).toThrow("Google Vertex Responses does not support API keys")
})
})
+1 -7
View File
@@ -76,13 +76,7 @@ it.effect("Alibaba owns regional shared and workspace-specific endpoints", () =>
test("Alibaba requires explicit placement and supports complete base URL overrides", () => {
for (const region of ["eu-central-1", "ap-northeast-1", "future-region"])
expect(() => Alibaba.configure({ region })).toThrow(
expect.objectContaining({
_tag: "ProviderConfiguration",
provider: "alibaba",
message: `Alibaba region ${region} requires workspaceID or baseURL`,
}),
)
expect(() => Alibaba.configure({ region })).toThrow("requires workspaceID or baseURL")
for (const config of [
{ baseURL: "https://gateway.example/prefix" },
{ region: "future-region", workspaceID: "ignored", baseURL: "https://gateway.example/prefix" },
@@ -69,9 +69,7 @@ for (const model of [
dynamicResponse(({ request, text, respond }) =>
Effect.sync(() => {
const body = JSON.parse(text)
expect(request.headers["anthropic-beta"]).toBe(
"existing-beta,interleaved-thinking-2025-05-14,compact-2026-01-12",
)
expect(request.headers["anthropic-beta"]).toBe("existing-beta,compact-2026-01-12")
if (body.messages.length === 1) {
expect(body.context_management.edits).toEqual([
{
@@ -32,9 +32,7 @@ for (const [id, enabled] of [
enabled ? { type: "adaptive", block_binding: { prefix_mismatch_behavior: "drop_block" } } : undefined,
)
expect(prepared.request.headers["anthropic-beta"]).toBe(
enabled
? "existing-beta,interleaved-thinking-2025-05-14,thinking-binding-controls-2026-08-01"
: "existing-beta,interleaved-thinking-2025-05-14",
enabled ? "existing-beta,thinking-binding-controls-2026-08-01" : "existing-beta",
)
}),
)
@@ -55,9 +53,7 @@ it.effect("preserves explicit thinking settings and combines required beta heade
const prepared = yield* AnthropicMessages.route.prepareTransport(compiled.body, request)
expect(compiled.body.thinking).toEqual(thinking)
expect(prepared.request.headers["anthropic-beta"]).toBe(
thinking.type === "disabled"
? "interleaved-thinking-2025-05-14,compact-2026-01-12"
: "interleaved-thinking-2025-05-14,compact-2026-01-12,thinking-binding-controls-2026-08-01",
thinking.type === "disabled" ? "compact-2026-01-12" : "compact-2026-01-12,thinking-binding-controls-2026-08-01",
)
}
}),
@@ -1458,13 +1458,7 @@ describe("Bedrock Converse route", () => {
expect(headers.get("authorization")).toContain("Credential=AKIACHAINEXAMPLE/")
expect(headers.get("authorization")).toContain("/ap-southeast-2/bedrock/aws4_request")
}
expect(() => AmazonBedrock.configure({ auth: "sigv4", apiKey: "k" })).toThrow(
expect.objectContaining({
_tag: "ProviderConfiguration",
provider: "amazon-bedrock",
message: "Amazon Bedrock SigV4 auth does not accept apiKey",
}),
)
expect(() => AmazonBedrock.configure({ auth: "sigv4", apiKey: "k" })).toThrow("does not accept apiKey")
}).pipe(
withProcessEnv({
...noAmbientAWS,
@@ -378,11 +378,7 @@ describe("Google Vertex providers", () => {
test("rejects tuned Gemini models in express mode", () => {
expect(() => GoogleVertex.configure({ apiKey: "fixture" }).model("endpoints/1234567890")).toThrow(
expect.objectContaining({
_tag: "ProviderConfiguration",
provider: "google-vertex",
message: "Google Vertex tuned models do not support Express Mode API keys",
}),
"Google Vertex tuned models do not support Express Mode API keys",
)
})
})
+6 -4
View File
@@ -34,10 +34,12 @@ it.effect("Groq lowers its own options for custom catalog identities and endpoin
baseURL: "https://gateway.example/v1",
headers: { "x-client": "test" },
body: { custom: "value" },
reasoningEffort: "default",
parallelToolCalls: true,
serviceTier: "flex",
user: "test-user",
providerOptions: {
reasoningEffort: "default",
parallelToolCalls: true,
serviceTier: "flex",
user: "test-user",
},
}),
{ provider: "custom-groq" },
)
+1 -1
View File
@@ -87,7 +87,7 @@ it.effect("Meta package selectors preserve overrides and Chat token policy on cu
baseURL: "https://gateway.example/v1",
headers: { "x-client": "test" },
body: { custom: "value" },
reasoningEffort: "future-effort",
providerOptions: { reasoningEffort: "future-effort" },
})
expect(model.route.endpoint.baseURL).toBe("https://gateway.example/v1")
expect(model.route.defaults.headers).toEqual({ "x-client": "test" })
@@ -99,7 +99,7 @@ describe("native OpenAI-compatible providers", () => {
const settings = {
apiKey: "fixture",
baseURL: "https://gateway.example/v1",
reasoningEffort: "high",
providerOptions: { reasoningEffort: "high" },
}
const selected = provider.configure(settings).model("test-model")
expect(selected.provider).toBe(provider.id)
@@ -143,7 +143,7 @@ describe("native OpenAI-compatible providers", () => {
tools: [
ToolDefinition.make({ name: "lookup", description: "Look up data", inputSchema: { type: "object" } }),
],
store: true,
providerOptions: { store: true },
}),
)
@@ -168,7 +168,7 @@ describe("native OpenAI-compatible providers", () => {
]),
Message.user("Continue."),
],
store: true,
providerOptions: { store: true },
}),
)
@@ -206,7 +206,7 @@ describe("native OpenAI-compatible providers", () => {
baseURL: "https://gateway.example/v1",
headers: { "x-application": "opencode" },
body: { service_tier: "priority" },
reasoningEffort: "high",
providerOptions: { reasoningEffort: "high" },
})
expect(selected.route.endpoint.baseURL).toBe("https://gateway.example/v1")
@@ -1,78 +0,0 @@
import { expect } from "bun:test"
import { Effect, Schema } from "effect"
import { LLM, LLMClient } from "../../src/index.js"
import { OpenResponses } from "../../src/protocols/open-responses.js"
import { Meta } from "../../src/providers/index.js"
import { configure } from "../../src/providers/openai-compatible-responses.js"
import { it } from "../lib/effect.js"
import { fixedResponse } from "../lib/http.js"
import { sseEvents } from "../lib/sse.js"
const decodeEvent = Schema.decodeUnknownEffect(OpenResponses.protocol.stream.event)
it.effect("normalizes flat errors in shared SSE and WebSocket decoding", () =>
Effect.gen(function* () {
const frame = {
type: "error",
sequence_number: 4,
code: "server_shutting_down",
message: "Server is shutting down. Please retry your request.",
param: null,
}
for (const decode of [decodeEvent, OpenResponses.decodeChannelEvent]) {
const event = yield* decode(JSON.stringify(frame))
expect(event).toEqual({
type: "error",
sequence_number: 4,
error: { code: frame.code, message: frame.message, param: null },
})
for (const unchanged of [
event,
{ type: "error" },
{
type: "response.failed",
response: { id: "resp_failed", error: { code: "server_error", message: "Internal server error" } },
},
{ type: "response.output_text.delta", item_id: "msg_text", delta: "Hello" },
]) {
expect(yield* decode(JSON.stringify(unchanged))).toEqual(unchanged)
}
}
}),
)
it.effect("continues to normalize untyped xAI WebSocket errors", () =>
Effect.gen(function* () {
const frame = { error: { type: "api_error", message: "gRPC error: Response with id=resp_missing not found" } }
expect(yield* OpenResponses.decodeChannelEvent(JSON.stringify(frame))).toEqual({ ...frame, type: "error" })
}),
)
it.effect("retains classification and original error bodies through Meta and generic Responses routes", () =>
Effect.gen(function* () {
const raw = `{
"type": "error",
"sequence_number": 4,
"code": "server_shutting_down",
"message": "Server is shutting down. Please retry your request.",
"param": null,
"diagnostic": "retain-original-frame"
}`
for (const model of [
Meta.configure({ apiKey: "fixture" }).responses("muse-spark-1.3"),
configure({ apiKey: "fixture", provider: "gateway", baseURL: "https://responses.example.test/v1" }).model(
"example-model",
),
]) {
const error = yield* LLMClient.generate(LLM.request({ model, prompt: "Hello" })).pipe(
Effect.provide(fixedResponse(sseEvents(raw.replaceAll("\n", "\ndata: ")))),
Effect.flip,
)
expect(error.reason._tag).toBe("ProviderInternal")
expect(error.message).toBe("server_shutting_down: Server is shutting down. Please retry your request.")
expect(error.reason.body).toBe(raw)
expect(error.reason.http?.status).toBe(200)
}
}),
)
@@ -19,13 +19,11 @@ story("cancelling a version mismatch permits reconnecting again", async ({ mount
const component = await mount("app-dialog-ssh--incompatible-session")
await component.getByRole("button", { name: "Reconnect", exact: true }).click()
const dialog = page.getByRole("dialog")
await expect(dialog.getByRole("status")).toContainText("Server update required")
await expect(dialog.getByRole("textbox")).toHaveCount(0)
await expect(dialog.getByRole("alert")).toBeVisible()
await dialog.getByRole("button", { name: "Cancel", exact: true }).click()
await expect(dialog).toHaveCount(0)
await component.getByRole("button", { name: "Reconnect", exact: true }).click()
await expect(dialog.getByRole("status")).toContainText("Server update required")
await expect(dialog.getByRole("textbox")).toHaveCount(0)
await expect(dialog.getByRole("alert")).toBeVisible()
await dialog.getByRole("button", { name: "Cancel", exact: true }).click()
await expect(dialog).toHaveCount(0)
})
@@ -45,17 +43,6 @@ story("adding a server keeps all SSH challenges in the original connection dialo
await expect(dialog).toHaveCount(0)
})
story("adding an incompatible server advances to a dedicated update step", async ({ mount, page }) => {
await mount("app-dialog-ssh--incompatible-host")
const dialog = page.getByRole("dialog")
await dialog.getByRole("textbox", { name: "Host or SSH command" }).fill("ssh devbox")
await dialog.getByRole("button", { name: "Add server", exact: true }).click()
await expect(dialog.getByRole("status")).toContainText("Server update required")
await expect(dialog.getByRole("textbox")).toHaveCount(0)
await expect(dialog.getByRole("alert")).toHaveCount(0)
await expect(dialog.getByRole("button", { name: "Update and reconnect", exact: true })).toBeVisible()
})
story("updating an incompatible connection continues authentication in the same dialog", async ({ mount, page }) => {
const component = await mount("app-dialog-ssh--incompatible-session")
await component.getByRole("button", { name: "Reconnect", exact: true }).click()
@@ -115,7 +115,7 @@ test("combines follow-up patches into one three-file stack inside Used", async (
})
const group = page.locator('[data-component="collapsed-tool-group"]')
await group.getByRole("button", { name: "Used 2 Shell, Patch", exact: true }).click()
await expect(group.locator('[data-slot="apply-patch-filename"]')).toHaveText(["a.ts", "b.ts"])
await expect(group.getByText("2 files", { exact: true })).toBeVisible()
await timeline.send(
partUpdated(
toolPart(
@@ -134,6 +134,7 @@ test("combines follow-up patches into one three-file stack inside Used", async (
"true",
)
await expect(group.locator('[data-component="apply-patch-tool"]')).toHaveCount(1)
await expect(group.getByText("3 files", { exact: true })).toBeVisible()
await expect(group.locator('[data-slot="apply-patch-filename"]')).toHaveText(["a.ts", "b.ts", "c.ts"])
})
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode/app",
"version": "2.0.0",
"version": "1.18.15",
"description": "",
"type": "module",
"exports": {
@@ -285,7 +285,7 @@ export const DESKTOP_NATIVE_ENGLISH = {
"desktop.updater.dialog.later": "Later",
"desktop.cli.installed.title": "CLI Installed",
"desktop.cli.installed.message": "CLI installed to {{path}}\n\nRestart your terminal to use the 'opencode' command.",
"desktop.cli.installed.message": "CLI installed to {{path}}\n\nRestart your terminal to use the 'opencode2' command.",
"desktop.cli.failed.title": "Installation Failed",
"desktop.cli.failed.message": "Failed to install CLI: {{error}}",
+2 -6
View File
@@ -54,12 +54,8 @@ export function createWebPlatform(version: string) {
function getCurrentServerUrl() {
if (import.meta.env.VITE_OPENCODE_SERVER_MODE === "none") return undefined
if (import.meta.env.DEV) {
const loopback =
location.hostname === "localhost" || location.hostname === "[::1]" || location.hostname.startsWith("127.")
const host = import.meta.env.VITE_OPENCODE_SERVER_HOST ?? (loopback ? location.hostname : "localhost")
return `http://${host}:${import.meta.env.VITE_OPENCODE_SERVER_PORT ?? "4096"}`
}
if (import.meta.env.DEV)
return `http://${import.meta.env.VITE_OPENCODE_SERVER_HOST ?? "localhost"}:${import.meta.env.VITE_OPENCODE_SERVER_PORT ?? "4096"}`
return location.origin
}
+2 -5
View File
@@ -10,7 +10,6 @@ import { createData } from "@opencode/client/solid"
import type { ServerScope } from "@/runtime/server/scope"
import { createPermissionAutoApprover } from "@/session/requests/auto-approve"
import { createServerNotificationState } from "@/shell/notifications/notification"
import { createNotificationCoordinator } from "@/shell/notifications/coordinator"
import { Persist, persisted } from "@/runtime/persistence/storage"
import { createDesktopData } from "./data"
import { ModelState } from "./persistence"
@@ -34,7 +33,6 @@ export const { use: useGlobal, provider: GlobalProvider } = createSimpleContext(
},
})
const models = createGlobalModels()
const notificationCoordinator = createNotificationCoordinator()
const settingsServer = createMemo(() => {
const list = server.list
@@ -59,7 +57,7 @@ export const { use: useGlobal, provider: GlobalProvider } = createSimpleContext(
if (existing) return existing
const serverCtx = createRoot((dispose) => {
serverCtxDisposers.set(key, dispose)
return createServerController(conn, server.scope(key), server.projects.forServer(key), notificationCoordinator)
return createServerController(conn, server.scope(key), server.projects.forServer(key))
}, owner)
serverCtxs.set(key, serverCtx)
return serverCtx
@@ -133,7 +131,6 @@ function createServerController(
conn: ServerConnection.Any,
scope: ServerScope,
projects: ReturnType<typeof createServerProjects>,
notificationCoordinator: ReturnType<typeof createNotificationCoordinator>,
) {
const language = useLanguage()
const settings = useSettings()
@@ -162,7 +159,7 @@ function createServerController(
})
const sync = createServerSyncContext(sdk, data)
createPermissionAutoApprover({ sdk, data })
const notification = createServerNotificationState({ sdk, data, key: connKey, coordinator: notificationCoordinator })
const notification = createServerNotificationState({ sdk, data, key: connKey })
function enrich(project: { worktree: string; expanded: boolean }) {
const [childStore] = sync.child(project.worktree, { bootstrap: false })
@@ -278,7 +278,6 @@ function Open(props: { initial?: string }) {
export default { title: "App/Dialogs/SSH", id: "app-dialog-ssh" }
export const AuthenticationRequired = { render: () => <Fixture initial="required" /> }
export const SettingsReconnect = { render: () => <Fixture initial="required" settings connectionDelay={200} /> }
export const IncompatibleHost = { render: () => <Fixture incompatible /> }
export const IncompatibleSession = { render: () => <Fixture initial="required" session incompatible /> }
export const InactiveSession = { render: () => <Fixture initial="required" session connectionDelay={3000} /> }
export const KeyReconnect = { render: () => <Fixture initial="required" session keyOnly connectionDelay={3000} /> }
+2 -14
View File
@@ -120,13 +120,7 @@ export function DialogSsh(props: {
<Divider />
<DialogBody class="flex w-full min-w-0 flex-1 flex-col px-4 pt-4 pb-2">
<div class="flex w-full min-w-0 flex-col gap-6">
<Show
when={
!props.promptOnly &&
item()?.stage !== "incompatible" &&
(!state.prompted || (!!error() && !prompt()))
}
>
<Show when={!props.promptOnly && (!state.prompted || (!!error() && !prompt()))}>
<div class="flex w-full min-w-0 flex-col gap-2">
<label class="settings-server-dialog-label" for="ssh-target">
{language.t("ssh.target")}
@@ -166,12 +160,6 @@ export function DialogSsh(props: {
/>
</div>
</Show>
<Show when={item()?.stage === "incompatible"}>
<div class="flex w-full min-w-0 flex-col gap-2" role="status" aria-live="polite">
<span class="text-14-medium text-v2-text-text-base">{language.t("ssh.stage.incompatible")}</span>
<span class="text-13-regular text-v2-text-text-muted">{language.t("ssh.error.version")}</span>
</div>
</Show>
<Show when={prompt()} keyed>
{(prompt) => (
<div class="flex w-full min-w-0 flex-col gap-2">
@@ -207,7 +195,7 @@ export function DialogSsh(props: {
</div>
)}
</Show>
<Show when={item()?.stage !== "incompatible" && error()}>
<Show when={error()}>
{(error) => (
<span class="settings-server-dialog-error !leading-[var(--line-height-compact)]" role="alert">
{error()}
@@ -1,6 +1,6 @@
import { createMemo, createUniqueId, Show } from "solid-js"
import { createStore } from "solid-js/store"
import { createQuery, keepPreviousData } from "@tanstack/solid-query"
import { createQuery } from "@tanstack/solid-query"
import { Icon } from "@opencode/ui/icon"
import { SessionFilePanelV2, SessionFilePanelV2Empty } from "@opencode/session-ui/v2/session-file-panel-v2"
import { SessionReviewV2Sidebar } from "@opencode/session-ui/v2/session-review-v2"
@@ -56,7 +56,6 @@ export function SessionFileBrowserTab(props: {
queryKey: [serverSDK.scope, "session-open-file", workspaceKey(), value] as const,
enabled: serverSDK.connection.status() === "connected" && value.length > 0,
queryFn: ({ signal }) => file.searchFiles(value, { limit: 200, signal }),
placeholderData: keepPreviousData,
}
})
const files = createMemo(() => {
@@ -1,93 +0,0 @@
import { onCleanup } from "solid-js"
const FOCUS_LOCK = "opencode:notification-focus"
const MAX_CLAIMED = 500
export function createNotificationCoordinator() {
const locks = typeof navigator === "undefined" ? undefined : navigator.locks
const claimed = new Set<string>()
const focus = { pending: false, release: undefined as (() => void) | undefined }
const updateFocus = () => {
if (typeof document === "undefined" || !document.hasFocus()) {
focus.release?.()
return
}
if (!locks || focus.pending || focus.release) return
focus.pending = true
void locks
.request(FOCUS_LOCK, { mode: "shared" }, async () => {
focus.pending = false
if (!document.hasFocus()) return
await new Promise<void>((resolve) => {
focus.release = resolve
})
focus.release = undefined
})
.catch(() => {
focus.pending = false
})
}
if (typeof window !== "undefined") {
window.addEventListener("focus", updateFocus)
window.addEventListener("blur", updateFocus)
document.addEventListener("visibilitychange", updateFocus)
updateFocus()
onCleanup(() => {
window.removeEventListener("focus", updateFocus)
window.removeEventListener("blur", updateFocus)
document.removeEventListener("visibilitychange", updateFocus)
focus.release?.()
})
}
const once = async (kind: "sound" | "system", eventID: string, run: () => Promise<unknown> | void) => {
const key = `${kind}:${eventID}`
const execute = async () => {
if (!claim(kind, key, claimed)) return
await run()
}
if (!locks) return execute()
await locks.request(`opencode:notification:${key}`, execute)
}
return {
sound(eventID: string, run: () => Promise<unknown> | void) {
return once("sound", eventID, run)
},
system(eventID: string, run: () => Promise<unknown> | void) {
return once("system", eventID, async () => {
if (typeof document !== "undefined" && document.hasFocus()) return
if (!locks) return run()
await locks.request(FOCUS_LOCK, { mode: "exclusive", ifAvailable: true }, async (lock) => {
if (!lock) return
await run()
})
})
},
}
}
function claim(kind: "sound" | "system", eventID: string, claimed: Set<string>) {
if (claimed.has(eventID)) return false
if (typeof localStorage !== "undefined") {
try {
const storageKey = `opencode:notification-${kind}`
const value: unknown = JSON.parse(localStorage.getItem(storageKey) ?? "[]")
const events = Array.isArray(value) ? value.filter((item): item is string => typeof item === "string") : []
if (events.includes(eventID)) {
claimed.add(eventID)
return false
}
localStorage.setItem(storageKey, JSON.stringify([...events, eventID].slice(-MAX_CLAIMED)))
} catch {
// The in-memory claim still prevents duplicates in this renderer when storage is unavailable.
}
}
claimed.add(eventID)
return true
}
@@ -11,8 +11,7 @@ import { useSettings } from "@/settings/model"
import { decode64 } from "@/runtime/persistence/base64"
import { Persist, persisted } from "@/runtime/persistence/storage"
import { Persistence } from "@/runtime/persistence/schema"
import { playSoundById } from "@/shell/notifications/sound"
import type { createNotificationCoordinator } from "@/shell/notifications/coordinator"
import { playSoundByIdOnce } from "@/shell/notifications/sound"
import { useGlobal } from "@/runtime/server/runtime"
import { ServerConnection, useServers } from "@/runtime/server/registry"
import { sessionIDHasOpenTab, useTabs } from "@/shell/tabs/tabs"
@@ -115,12 +114,7 @@ function buildNotificationIndex(list: Notification[]) {
return index
}
export function createServerNotificationState(input: {
sdk: ServerSDK
data: Data
key: ServerConnection.Key
coordinator: ReturnType<typeof createNotificationCoordinator>
}) {
export function createServerNotificationState(input: { sdk: ServerSDK; data: Data; key: ServerConnection.Key }) {
const platform = usePlatform()
const settings = useSettings()
const language = useLanguage()
@@ -229,7 +223,7 @@ export function createServerNotificationState(input: {
if (session.parentID) return
if (sessionIDHasOpenTab(tabs.store, input.key, sessionID) && settings.sounds.agentEnabled()) {
void input.coordinator.sound(`${input.key}\0${eventID}`, () => playSoundById(settings.sounds.agent()))
void playSoundByIdOnce(settings.sounds.agent(), `${input.key}\0${eventID}`)
}
append({
@@ -241,10 +235,8 @@ export function createServerNotificationState(input: {
})
if (settings.notifications.agent()) {
void input.coordinator.system(`${input.key}\0${eventID}`, () =>
platform.notify(language.t("notification.session.responseReady.title"), session.title ?? sessionID, () =>
openNotificationSession(tabs, input.key, sessionID),
),
void platform.notify(language.t("notification.session.responseReady.title"), session.title ?? sessionID, () =>
openNotificationSession(tabs, input.key, sessionID),
)
}
})
@@ -256,7 +248,7 @@ export function createServerNotificationState(input: {
if (session?.parentID) return
if (sessionIDHasOpenTab(tabs.store, input.key, sessionID) && settings.sounds.errorsEnabled()) {
void input.coordinator.sound(`${input.key}\0${eventID}`, () => playSoundById(settings.sounds.errors()))
void playSoundByIdOnce(settings.sounds.errors(), `${input.key}\0${eventID}`)
}
append({
@@ -271,10 +263,8 @@ export function createServerNotificationState(input: {
session?.title ??
(typeof error === "string" ? error : language.t("notification.session.error.fallbackDescription"))
if (settings.notifications.errors()) {
void input.coordinator.system(`${input.key}\0${eventID}`, () =>
platform.notify(language.t("notification.session.error.title"), description, () =>
openNotificationSession(tabs, input.key, sessionID),
),
void platform.notify(language.t("notification.session.error.title"), description, () =>
openNotificationSession(tabs, input.key, sessionID),
)
}
})
@@ -74,6 +74,9 @@ function getLoads() {
}
const cache = new Map<SoundID, Promise<string | undefined>>()
const claimed = new Set<string>()
const CLAIMED_STORAGE_KEY = "opencode:notification-sounds"
const MAX_CLAIMED = 500
export function soundSrc(id: string | undefined) {
const loads = getLoads()
@@ -100,3 +103,34 @@ export function playSound(src: string | undefined) {
export function playSoundById(id: string | undefined) {
return soundSrc(id).then((src) => playSound(src))
}
export async function playSoundByIdOnce(id: string | undefined, eventID: string) {
const play = async () => {
if (!claim(eventID)) return
await playSoundById(id)
}
if (typeof navigator === "undefined" || !navigator.locks) return play()
await navigator.locks.request(`${CLAIMED_STORAGE_KEY}:${eventID}`, play)
}
function claim(eventID: string) {
if (claimed.has(eventID)) return false
if (typeof localStorage !== "undefined") {
try {
const value: unknown = JSON.parse(localStorage.getItem(CLAIMED_STORAGE_KEY) ?? "[]")
const events = Array.isArray(value) ? value.filter((item): item is string => typeof item === "string") : []
if (events.includes(eventID)) {
claimed.add(eventID)
return false
}
localStorage.setItem(CLAIMED_STORAGE_KEY, JSON.stringify([...events, eventID].slice(-MAX_CLAIMED)))
} catch {
// The in-memory claim still prevents duplicates in this renderer when storage is unavailable.
}
}
claimed.add(eventID)
return true
}
-16
View File
@@ -1,16 +0,0 @@
FROM alpine AS base
ARG BUN_RUNTIME_TRANSPILER_CACHE_PATH=0
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
FROM base AS build-arm64
COPY dist/cli-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
ENTRYPOINT ["opencode"]
-135
View File
@@ -1,135 +0,0 @@
#!/usr/bin/env node
const childProcess = require("child_process")
const fs = require("fs")
const path = require("path")
const os = require("os")
const forwardedSignals =
process.platform === "win32" ? ["SIGINT", "SIGTERM", "SIGHUP"] : ["SIGINT", "SIGTERM", "SIGHUP", "SIGUSR1"]
function run(target) {
const child = childProcess.spawn(target, process.argv.slice(2), { stdio: "inherit" })
child.on("error", (error) => {
console.error(error.message)
process.exit(1)
})
const forwarders = {}
for (const signal of forwardedSignals) {
forwarders[signal] = () => {
try {
child.kill(signal)
} catch {}
}
process.on(signal, forwarders[signal])
}
child.on("exit", (code, signal) => {
for (const forwardedSignal of forwardedSignals) process.removeListener(forwardedSignal, forwarders[forwardedSignal])
if (signal) return process.kill(process.pid, signal)
process.exit(typeof code === "number" ? code : 0)
})
}
const envPath = process.env.OPENCODE_BIN_PATH
const scriptDir = path.dirname(fs.realpathSync(__filename))
const command = path.basename(__filename).replace(/\.cjs$/, "")
const nodeBuild = command === "opencode-node"
const sourceCommand = nodeBuild ? "opencode2-node" : "opencode"
const cached = path.join(scriptDir, `.${command}`)
const platform = { darwin: "darwin", linux: "linux", win32: "windows" }[os.platform()] || os.platform()
const arch = { x64: "x64", arm64: "arm64", arm: "arm" }[os.arch()] || os.arch()
const base = `@opencode/cli${nodeBuild ? "-node" : ""}-` + platform + "-" + arch
const binary = platform === "windows" ? `${sourceCommand}.exe` : sourceCommand
function supportsAvx2() {
if (arch !== "x64") return false
if (platform === "linux") {
try {
return /(^|\s)avx2(\s|$)/i.test(fs.readFileSync("/proc/cpuinfo", "utf8"))
} catch {
return false
}
}
if (platform === "darwin") {
try {
const result = childProcess.spawnSync("sysctl", ["-n", "hw.optional.avx2_0"], { encoding: "utf8", timeout: 1500 })
return result.status === 0 && (result.stdout || "").trim() === "1"
} catch {
return false
}
}
if (platform === "windows") {
const command =
'(Add-Type -MemberDefinition "[DllImport(""kernel32.dll"")] public static extern bool IsProcessorFeaturePresent(int ProcessorFeature);" -Name Kernel32 -Namespace Win32 -PassThru)::IsProcessorFeaturePresent(40)'
for (const executable of ["powershell.exe", "pwsh.exe", "pwsh", "powershell"]) {
try {
const result = childProcess.spawnSync(executable, ["-NoProfile", "-NonInteractive", "-Command", command], {
encoding: "utf8",
timeout: 3000,
windowsHide: true,
})
if (result.status !== 0) continue
const output = (result.stdout || "").trim().toLowerCase()
if (output === "true" || output === "1") return true
if (output === "false" || output === "0") return false
} catch {
continue
}
}
}
return false
}
const names = (() => {
if (nodeBuild) return [base]
const baseline = arch === "x64" && !supportsAvx2()
if (platform === "linux") {
const musl = (() => {
try {
if (fs.existsSync("/etc/alpine-release")) return true
const result = childProcess.spawnSync("ldd", ["--version"], { encoding: "utf8" })
return ((result.stdout || "") + (result.stderr || "")).toLowerCase().includes("musl")
} catch {
return false
}
})()
if (musl)
return arch === "x64"
? baseline
? [`${base}-baseline-musl`, `${base}-musl`, `${base}-baseline`, base]
: [`${base}-musl`, `${base}-baseline-musl`, base, `${base}-baseline`]
: [`${base}-musl`, base]
return arch === "x64"
? baseline
? [`${base}-baseline`, base, `${base}-baseline-musl`, `${base}-musl`]
: [base, `${base}-baseline`, `${base}-musl`, `${base}-baseline-musl`]
: [base, `${base}-musl`]
}
return arch === "x64" ? (baseline ? [`${base}-baseline`, base] : [base, `${base}-baseline`]) : [base]
})()
function findBinary(startDir) {
let current = startDir
for (;;) {
const modules = path.join(current, "node_modules")
if (fs.existsSync(modules))
for (const name of names) {
const candidate = path.join(modules, name, "bin", binary)
if (fs.existsSync(candidate)) return candidate
}
const parent = path.dirname(current)
if (parent === current) return
current = parent
}
}
const resolved = envPath || (fs.existsSync(cached) ? cached : findBinary(scriptDir))
if (!resolved) {
console.error(
`It seems that your package manager failed to install the right ${command} CLI package. Try manually installing ` +
names.map((name) => `"${name}"`).join(" or ") +
" package",
)
process.exit(1)
}
run(resolved)
+132 -1
View File
@@ -1,3 +1,134 @@
#!/usr/bin/env node
require("./opencode.cjs")
const childProcess = require("child_process")
const fs = require("fs")
const path = require("path")
const os = require("os")
const forwardedSignals =
process.platform === "win32" ? ["SIGINT", "SIGTERM", "SIGHUP"] : ["SIGINT", "SIGTERM", "SIGHUP", "SIGUSR1"]
function run(target) {
const child = childProcess.spawn(target, process.argv.slice(2), { stdio: "inherit" })
child.on("error", (error) => {
console.error(error.message)
process.exit(1)
})
const forwarders = {}
for (const signal of forwardedSignals) {
forwarders[signal] = () => {
try {
child.kill(signal)
} catch {}
}
process.on(signal, forwarders[signal])
}
child.on("exit", (code, signal) => {
for (const forwardedSignal of forwardedSignals) process.removeListener(forwardedSignal, forwarders[forwardedSignal])
if (signal) return process.kill(process.pid, signal)
process.exit(typeof code === "number" ? code : 0)
})
}
const envPath = process.env.OPENCODE_BIN_PATH
const scriptDir = path.dirname(fs.realpathSync(__filename))
const command = path.basename(__filename).replace(/\.cjs$/, "")
const nodeBuild = command === "opencode2-node"
const cached = path.join(scriptDir, `.${command}`)
const platform = { darwin: "darwin", linux: "linux", win32: "windows" }[os.platform()] || os.platform()
const arch = { x64: "x64", arm64: "arm64", arm: "arm" }[os.arch()] || os.arch()
const base = `@opencode/cli${nodeBuild ? "-node" : ""}-` + platform + "-" + arch
const binary = platform === "windows" ? `${command}.exe` : command
function supportsAvx2() {
if (arch !== "x64") return false
if (platform === "linux") {
try {
return /(^|\s)avx2(\s|$)/i.test(fs.readFileSync("/proc/cpuinfo", "utf8"))
} catch {
return false
}
}
if (platform === "darwin") {
try {
const result = childProcess.spawnSync("sysctl", ["-n", "hw.optional.avx2_0"], { encoding: "utf8", timeout: 1500 })
return result.status === 0 && (result.stdout || "").trim() === "1"
} catch {
return false
}
}
if (platform === "windows") {
const command =
'(Add-Type -MemberDefinition "[DllImport(""kernel32.dll"")] public static extern bool IsProcessorFeaturePresent(int ProcessorFeature);" -Name Kernel32 -Namespace Win32 -PassThru)::IsProcessorFeaturePresent(40)'
for (const executable of ["powershell.exe", "pwsh.exe", "pwsh", "powershell"]) {
try {
const result = childProcess.spawnSync(executable, ["-NoProfile", "-NonInteractive", "-Command", command], {
encoding: "utf8",
timeout: 3000,
windowsHide: true,
})
if (result.status !== 0) continue
const output = (result.stdout || "").trim().toLowerCase()
if (output === "true" || output === "1") return true
if (output === "false" || output === "0") return false
} catch {
continue
}
}
}
return false
}
const names = (() => {
if (nodeBuild) return [base]
const baseline = arch === "x64" && !supportsAvx2()
if (platform === "linux") {
const musl = (() => {
try {
if (fs.existsSync("/etc/alpine-release")) return true
const result = childProcess.spawnSync("ldd", ["--version"], { encoding: "utf8" })
return ((result.stdout || "") + (result.stderr || "")).toLowerCase().includes("musl")
} catch {
return false
}
})()
if (musl)
return arch === "x64"
? baseline
? [`${base}-baseline-musl`, `${base}-musl`, `${base}-baseline`, base]
: [`${base}-musl`, `${base}-baseline-musl`, base, `${base}-baseline`]
: [`${base}-musl`, base]
return arch === "x64"
? baseline
? [`${base}-baseline`, base, `${base}-baseline-musl`, `${base}-musl`]
: [base, `${base}-baseline`, `${base}-musl`, `${base}-baseline-musl`]
: [base, `${base}-musl`]
}
return arch === "x64" ? (baseline ? [`${base}-baseline`, base] : [base, `${base}-baseline`]) : [base]
})()
function findBinary(startDir) {
let current = startDir
for (;;) {
const modules = path.join(current, "node_modules")
if (fs.existsSync(modules))
for (const name of names) {
const candidate = path.join(modules, name, "bin", binary)
if (fs.existsSync(candidate)) return candidate
}
const parent = path.dirname(current)
if (parent === current) return
current = parent
}
}
const resolved = envPath || (fs.existsSync(cached) ? cached : findBinary(scriptDir))
if (!resolved) {
console.error(
`It seems that your package manager failed to install the right ${command} CLI package. Try manually installing ` +
names.map((name) => `"${name}"`).join(" or ") +
" package",
)
process.exit(1)
}
run(resolved)
+1 -2
View File
@@ -1,11 +1,10 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/cli",
"version": "2.0.0",
"version": "1.18.4",
"type": "module",
"license": "MIT",
"bin": {
"opencode": "./bin/opencode.cjs",
"opencode2": "./bin/opencode2.cjs"
},
"files": [
+2 -2
View File
@@ -12,7 +12,7 @@ import { verifyArtifact, verifySimulationGraph } from "./verify-artifact"
import { resolveOpencodePty } from "./opencode-pty"
const dir = path.resolve(import.meta.dirname, "..")
const binary = "opencode"
const binary = "opencode2"
const outdir = path.resolve(
dir,
process.argv.find((arg) => arg.startsWith("--outdir="))?.slice("--outdir=".length) ?? "dist",
@@ -149,7 +149,7 @@ export default { path: file, version: ${JSON.stringify(opencodePty.version)}, sh
},
define: {
OPENCODE_VERSION: `'${Script.version}'`,
OPENCODE_CLI_NAME: "'opencode'",
OPENCODE_CLI_NAME: `'${binary}'`,
OPENCODE_CHANNEL: `'${Script.channel}'`,
OPENCODE_ARTIFACT: `'cli'`,
OPENCODE_LIBC: item.os === "linux" ? `'${item.abi ?? "glibc"}'` : "undefined",
+1 -2
View File
@@ -12,11 +12,10 @@ const require = createRequire(import.meta.url)
const packageJson = JSON.parse(fs.readFileSync(path.join(directory, "package.json"), "utf8"))
const command = Object.keys(packageJson.bin ?? {})[0]
if (!command) throw new Error("OpenCode package does not declare a binary")
const sourceCommand = packageJson.opencodeSourceBinary ?? command
const platform = { darwin: "darwin", linux: "linux", win32: "windows" }[os.platform()] ?? os.platform()
const arch = { x64: "x64", arm64: "arm64", arm: "arm" }[os.arch()] ?? os.arch()
const sourceBinary = platform === "windows" ? `${sourceCommand}.exe` : sourceCommand
const sourceBinary = platform === "windows" ? `${command}.exe` : command
const targetBinary = path.resolve(directory, packageJson.bin[command])
const dependencies = packageJson.optionalDependencies ?? {}
const base = Object.keys(dependencies).find((name) => name.endsWith(`-${platform}-${arch}`))
+16 -16
View File
@@ -6,10 +6,15 @@ import path from "node:path"
import { fileURLToPath } from "node:url"
import { UpdateArtifact } from "../../../script/update-artifact"
if (Script.channel !== "beta") throw new Error("AUR publishing requires the beta channel")
const name = "opencode-beta"
const command = "opencode"
if (!/^\d+\.\d+\.\d+-beta[.-]\d+(?:\.\d+)?$/.test(Script.version)) throw new Error("Expected a beta release version")
if (Script.channel !== "beta" && Script.channel !== "latest") {
throw new Error("AUR publishing requires the beta or latest channel")
}
const beta = Script.channel === "beta"
const name = beta ? "opencode-beta" : "opencode-bin"
const command = beta ? "opencode2" : "opencode"
if (!(beta ? /^\d+\.\d+\.\d+-beta[.-]\d+(?:\.\d+)?$/ : /^\d+\.\d+\.\d+$/).test(Script.version)) {
throw new Error(`Expected a ${Script.channel} release version`)
}
const dir = fileURLToPath(new URL("..", import.meta.url))
const root = path.resolve(process.env.OPENCODE_CLI_DIST ?? path.join(dir, "dist"))
@@ -17,9 +22,6 @@ 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" "$@"
`
await rm(outdir, { recursive: true, force: true })
await mkdir(path.dirname(outdir), { recursive: true })
@@ -46,7 +48,6 @@ const sources = await Promise.all(
)
await Bun.write(path.join(outdir, "LICENSE"), license)
await Bun.write(path.join(outdir, "opencode2"), shim)
await Bun.write(
path.join(outdir, "PKGBUILD"),
[
@@ -54,22 +55,21 @@ await Bun.write(
`pkgname=${name}`,
`pkgver=${pkgver}`,
"pkgrel=1",
"pkgdesc='OpenCode beta - the AI coding agent for the terminal'",
`pkgdesc='OpenCode${beta ? " V2 beta" : ""} - the AI coding agent for the terminal'`,
"url='https://github.com/anomalyco/opencode'",
"arch=('x86_64' 'aarch64')",
"license=('MIT')",
"depends=('glibc' 'gcc-libs' 'ripgrep')",
`provides=('${command}' 'opencode2')`,
`conflicts=('${command}' 'opencode2')`,
`provides=('${command}')`,
`conflicts=('${command}')`,
// Stripping a compiled Bun executable can damage its embedded application.
"options=('!strip' '!debug')",
"source=('LICENSE' 'opencode2')",
`sha256sums=('${new Bun.CryptoHasher("sha256").update(await license.arrayBuffer()).digest("hex")}' '${new Bun.CryptoHasher("sha256").update(shim).digest("hex")}')`,
"source=('LICENSE')",
`sha256sums=('${new Bun.CryptoHasher("sha256").update(await license.arrayBuffer()).digest("hex")}')`,
...sources,
"",
"package() {",
` install -Dm755 "$srcdir/package/bin/opencode" "$pkgdir/usr/bin/${command}"`,
' install -Dm755 "$srcdir/opencode2" "$pkgdir/usr/bin/opencode2"',
` install -Dm755 "$srcdir/package/bin/opencode2" "$pkgdir/usr/bin/${command}"`,
' install -Dm644 "$srcdir/LICENSE" "$pkgdir/usr/share/licenses/$pkgname/LICENSE"',
"}",
"",
@@ -79,7 +79,7 @@ await Bun.write(path.join(outdir, ".SRCINFO"), await $`makepkg --printsrcinfo`.c
console.log(`Prepared ${name} ${pkgver} in ${outdir}`)
if (dryRun) process.exit(0)
await $`git add PKGBUILD .SRCINFO LICENSE opencode2`.cwd(outdir)
await $`git add PKGBUILD .SRCINFO LICENSE`.cwd(outdir)
if ((await $`git diff --cached --quiet`.cwd(outdir).nothrow()).exitCode !== 0) {
await $`git commit -m ${`chore: update ${name} to ${pkgver}`}`.cwd(outdir)
await $`git push origin master`.cwd(outdir)
+5 -18
View File
@@ -29,8 +29,6 @@ async function publish(dir: string, name: string, version: string) {
async function publishDistribution(input: {
root: string
name: string
command: string
legacyCommand?: string
binary: string
packagePrefix: string
artifact: string
@@ -50,7 +48,7 @@ async function publishDistribution(input: {
await $`mkdir -p ${input.root}/${input.name}/bin`
await $`cp ./script/postinstall.mjs ${input.root}/${input.name}/postinstall.mjs`
await Bun.file(`${input.root}/${input.name}/bin/${input.command}.exe`).write(
await Bun.file(`${input.root}/${input.name}/bin/${input.binary}.exe`).write(
[
`echo "Error: ${input.name}'s postinstall script was not run." >&2`,
'echo "" >&2',
@@ -64,11 +62,7 @@ async function publishDistribution(input: {
JSON.stringify(
{
name: input.name,
bin: {
[input.command]: `./bin/${input.command}.exe`,
...(input.legacyCommand ? { [input.legacyCommand]: `./bin/${input.command}.exe` } : {}),
},
...(input.command !== input.binary ? { opencodeSourceBinary: input.binary } : {}),
bin: { [input.binary]: `./bin/${input.binary}.exe` },
scripts: { postinstall: "node ./postinstall.mjs" },
version,
license: pkg.license,
@@ -126,28 +120,21 @@ async function publishDistribution(input: {
await publishDistribution({
root,
name: pkg.name,
command: "opencode",
legacyCommand: "opencode2",
binary: "opencode",
binary: "opencode2",
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",
command: "opencode2-node",
binary: "opencode2-node",
packagePrefix: "@opencode/cli-node-",
artifact: "cli-node",
})
}
if (Script.channel === "latest" && Script.release && !dryRun) {
await $`docker buildx build --platform linux/amd64,linux/arm64 --tag ghcr.io/anomalyco/opencode:${Script.version} --push .`
}
if (Script.channel === "beta" && Script.release) {
if ((Script.channel === "beta" || Script.channel === "latest") && Script.release) {
await $`bun ./script/publish-aur.ts ${dryRun ? ["--dry-run"] : []}`.env({ ...process.env, OPENCODE_CLI_DIST: root })
}
+1 -4
View File
@@ -11,10 +11,7 @@ import path from "node:path"
const nodeBuild = process.argv.includes("--node")
const target = `cli${nodeBuild ? "-node" : ""}-${process.platform === "win32" ? "windows" : process.platform}-${process.arch}`
const directory = path.join(import.meta.dir, "..", "dist", ...(nodeBuild ? ["node"] : []), target, "bin")
const binary = path.join(
directory,
`${nodeBuild ? "opencode2-node" : "opencode"}${process.platform === "win32" ? ".exe" : ""}`,
)
const binary = path.join(directory, `opencode2${nodeBuild ? "-node" : ""}${process.platform === "win32" ? ".exe" : ""}`)
if (!(await Bun.file(binary).exists())) throw new Error(`Missing compiled CLI in ${directory}`)
const root = await fs.realpath(await fs.mkdtemp(path.join(os.tmpdir(), "opencode-service-smoke-")))
+1 -5
View File
@@ -76,10 +76,7 @@ export function buildEffortSelectOption(input: {
category: "thought_level",
type: "select",
currentValue: selectVariant(input.currentVariant, input.variants),
options: [...new Set([...input.variants, DEFAULT_VARIANT_VALUE])].map((variant) => ({
value: variant,
name: formatVariantName(variant),
})),
options: input.variants.map((variant) => ({ value: variant, name: formatVariantName(variant) })),
}
}
@@ -128,7 +125,6 @@ export function formatVariantName(variant: string) {
}
function selectVariant(variant: string | undefined, variants: readonly string[]) {
if (!variant || variant === DEFAULT_VARIANT_VALUE) return DEFAULT_VARIANT_VALUE
if (variant && variants.includes(variant)) return variant
if (variants.includes(DEFAULT_VARIANT_VALUE)) return DEFAULT_VARIANT_VALUE
return variants[0] ?? DEFAULT_VARIANT_VALUE
+2 -4
View File
@@ -201,7 +201,7 @@ export async function streamTurn(input: {
if (!child) assistantMessageID = event.data.assistantMessageID
await send({
sessionUpdate: "agent_thought_chunk",
messageId: `${event.data.assistantMessageID}:reasoning:${event.data.ordinal}`,
messageId: event.data.assistantMessageID,
content: { type: "text", text: event.data.delta },
})
continue
@@ -455,8 +455,6 @@ async function replayMessage(
return
}
if (message.type !== "assistant") return
// Live reasoning ordinals count only reasoning parts, not the mixed content array.
let reasoningOrdinal = 0
for (const part of message.content) {
if (part.type === "text") {
await connection.sessionUpdate({
@@ -474,7 +472,7 @@ async function replayMessage(
sessionId: sessionID,
update: {
sessionUpdate: "agent_thought_chunk",
messageId: `${message.id}:reasoning:${reasoningOrdinal++}`,
messageId: message.id,
content: { type: "text", text: part.text },
},
})
+5 -20
View File
@@ -41,12 +41,7 @@ import type {
} from "@agentclientprotocol/sdk"
import { OPENCODE_VERSION } from "../version"
import { SessionMessage } from "@opencode/schema/session-message"
import {
buildConfigOptions,
DEFAULT_VARIANT_VALUE,
parseModelSelection,
type ConfigOptionProvider,
} from "./config-option"
import { buildConfigOptions, parseModelSelection, type ConfigOptionProvider } from "./config-option"
import { promptContentToParts } from "./content"
import {
ChildSessionUpdateMethod,
@@ -280,7 +275,7 @@ export function make(input: { readonly client: OpenCodeClient; readonly connecti
if (typeof params.value !== "string") throw new ACPError.InvalidConfigOptionError({ configId: params.configId })
switch (params.configId) {
case "model": {
const selected = requireModel(state.catalog, params.value, state.model)
const selected = requireModel(state.catalog, params.value)
state.model = selected
await input.client.session.switchModel({ sessionID: state.id, model: selected })
break
@@ -289,10 +284,7 @@ export function make(input: { readonly client: OpenCodeClient; readonly connecti
const model = state.catalog.models.find(
(item) => item.providerID === state.model.providerID && item.id === state.model.id,
)
if (
!model ||
(params.value !== DEFAULT_VARIANT_VALUE && !model.variants.some((variant) => variant.id === params.value))
)
if (!model?.variants.some((variant) => variant.id === params.value))
throw new ACPError.InvalidEffortError({ effort: params.value })
state.model = { ...state.model, variant: params.value }
await input.client.session.switchModel({ sessionID: state.id, model: state.model })
@@ -461,7 +453,7 @@ function providers(models: readonly ModelInfo[]): ConfigOptionProvider[] {
}))
}
function requireModel(catalog: Catalog, modelID: string, current: ModelRef): ModelRef {
function requireModel(catalog: Catalog, modelID: string): ModelRef {
const selected = parseModelSelection(modelID, catalog.providers)
const model = catalog.models.find(
(item) => item.providerID === selected.model.providerID && item.id === selected.model.modelID,
@@ -469,14 +461,7 @@ function requireModel(catalog: Catalog, modelID: string, current: ModelRef): Mod
if (!model) throw new ACPError.InvalidModelError({ providerId: selected.model.providerID, modelId: modelID })
if (selected.variant && !model.variants.some((variant) => variant.id === selected.variant))
throw new ACPError.InvalidEffortError({ effort: selected.variant })
const variant =
selected.variant ??
(current.providerID === model.providerID &&
current.id === model.id &&
(current.variant === DEFAULT_VARIANT_VALUE || model.variants.some((variant) => variant.id === current.variant))
? current.variant
: undefined)
return { providerID: model.providerID, id: model.id, variant }
return { providerID: model.providerID, id: model.id, variant: selected.variant }
}
async function selectMode(client: OpenCodeClient, state: Attached, modeID: string) {
+1 -1
View File
@@ -36,7 +36,7 @@ const PermissionParams = {
}
const Root = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME : "opencode", {
description: "OpenCode command line interface",
description: "OpenCode 2.0 preview command line interface",
params: {
...ServerParams,
...PermissionParams,
@@ -47,7 +47,7 @@ const handler = Effect.fn("cli.session.list")(function* (
null,
2,
)
: formatList(page.data)) + EOL
: formatTable(page.data)) + EOL
const write = Effect.tryPromise(
() =>
new Promise<void>((resolve, reject) => {
@@ -96,14 +96,18 @@ export default Runtime.handler(Commands.commands.session.commands.list, (input)
),
)
function formatList(sessions: ReadonlyArray<SessionInfo>) {
return sessions
.map((session) =>
[
session.id,
(session.title ?? "Untitled session").replace(/[\r\n\t]/g, " "),
new Date(session.time.updated).toLocaleString(),
].join("\t"),
)
.join(EOL)
function formatTable(sessions: ReadonlyArray<SessionInfo>) {
const rows = sessions.map((session) => ({
id: session.id,
title: (session.title ?? "Untitled session").replace(/[\r\n\t]/g, " "),
updated: new Date(session.time.updated).toLocaleString(),
}))
const idWidth = Math.max(20, ...rows.map((row) => row.id.length))
const titleWidth = Math.max(25, ...rows.map((row) => row.title.length))
const header = `${"Session ID".padEnd(idWidth)} ${"Title".padEnd(titleWidth)} Updated`
return [
header,
"─".repeat(header.length),
...rows.map((row) => `${row.id.padEnd(idWidth)} ${row.title.padEnd(titleWidth)} ${row.updated}`),
].join(EOL)
}
+2 -5
View File
@@ -105,7 +105,7 @@ const make = Effect.gen(function* () {
global.home,
".opencode",
"bin",
process.platform === "win32" ? "opencode.exe" : "opencode",
process.platform === "win32" ? "opencode2.exe" : "opencode2",
)
if (path.resolve(process.execPath) === path.resolve(binary)) return "curl"
if (!installedPackage) return
@@ -186,10 +186,7 @@ const make = Effect.gen(function* () {
"npm",
"install",
"--global",
...((OPENCODE_ARTIFACT === "cli" && !installedPackage?.endsWith("/cli-node")) ||
(installedPackage && packageName !== installedPackage)
? ["--force"]
: []),
...(installedPackage && packageName !== installedPackage ? ["--force"] : []),
target,
],
pnpm: ["pnpm", "add", "--global", `--allow-build=${packageName}`, target],
@@ -50,11 +50,11 @@ describe("acp config option subprocess", () => {
const effort = requireSelectOption((await newSession(acp, fixture.home)).configOptions, "effort")
expect(effort.category).toBe("thought_level")
expect(effort.currentValue).toBe("default")
expect(flattenSelectOptions(effort).map((option) => option.value)).toEqual(["low", "high", "default"])
expect(effort.currentValue).toBe("low")
expect(flattenSelectOptions(effort).map((option) => option.value)).toEqual(["low", "high"])
}, 60_000)
test("effort survives model synchronization and can be reset to default", async () => {
test("effort switch updates currentValue", async () => {
await using fixture = await createAcpFixture()
const acp = fixture.spawn()
await initialize(acp)
@@ -70,23 +70,5 @@ describe("acp config option subprocess", () => {
)
expect(selectConfigOption(updated.configOptions, "effort")?.currentValue).toBe(nextEffort)
const synchronized = expectOk(
await acp.request<SetSessionConfigOptionResponse>("session/set_config_option", {
sessionId: session.sessionId,
configId: "model",
value: requireSelectOption(session.configOptions, "model").currentValue,
}),
)
expect(selectConfigOption(synchronized.configOptions, "effort")?.currentValue).toBe(nextEffort)
const reset = expectOk(
await acp.request<SetSessionConfigOptionResponse>("session/set_config_option", {
sessionId: session.sessionId,
configId: "effort",
value: "default",
}),
)
expect(selectConfigOption(reset.configOptions, "effort")?.currentValue).toBe("default")
}, 60_000)
})
+6 -44
View File
@@ -91,7 +91,7 @@ describe("acp event behavior", () => {
}
})
test("preserves reasoning boundaries and update order during streaming and replay", async () => {
test("preserves text and reasoning order before returning the terminal response", async () => {
const firstUpdate = Promise.withResolvers<void>()
const releaseUpdate = Promise.withResolvers<void>()
const allUpdates = Promise.withResolvers<void>()
@@ -108,14 +108,6 @@ describe("acp event behavior", () => {
delta: "think-1",
}),
)
send(
ephemeralEvent("session.reasoning.delta", {
sessionID: "ses_order",
assistantMessageID: "msg_order",
ordinal: 0,
delta: " continued",
}),
)
send(
ephemeralEvent("session.text.delta", {
sessionID: "ses_order",
@@ -128,7 +120,7 @@ describe("acp event behavior", () => {
ephemeralEvent("session.reasoning.delta", {
sessionID: "ses_order",
assistantMessageID: "msg_order",
ordinal: 1,
ordinal: 2,
delta: "think-2",
}),
)
@@ -152,7 +144,7 @@ describe("acp event behavior", () => {
firstUpdate.resolve()
await releaseUpdate.promise
}
if (updates.length === 4) allUpdates.resolve()
if (updates.length === 3) allUpdates.resolve()
},
requestPermission: async () => ({ outcome: { outcome: "cancelled" } }),
} satisfies Connection
@@ -179,48 +171,18 @@ describe("acp event behavior", () => {
) {
return [
item.update.sessionUpdate,
item.update.messageId,
item.update.content.type === "text" ? item.update.content.text : undefined,
]
}
return [item.update.sessionUpdate, undefined]
}),
).toEqual([
["agent_thought_chunk", "msg_order:reasoning:0", "think-1"],
["agent_thought_chunk", "msg_order:reasoning:0", " continued"],
["agent_message_chunk", "msg_order", "answer"],
["agent_thought_chunk", "msg_order:reasoning:1", "think-2"],
["agent_thought_chunk", "think-1"],
["agent_message_chunk", "answer"],
["agent_thought_chunk", "think-2"],
])
expect(fixture.requests.at(-1)?.path).toBe("/api/session/ses_order/message/msg_order")
expect(response).toMatchObject({ stopReason: "end_turn", usage: { totalTokens: 2 } })
const replayed: SessionUpdateParams[] = []
await replayMessages(recordingConnection(replayed), "ses_order", "/workspace", [
{
id: "msg_order",
type: "assistant",
agent: "build",
model: { providerID: "test", id: "test-model" },
time: { created: 1 },
content: [
{ type: "reasoning", text: "think-1 continued" },
{ type: "text", text: "answer" },
{ type: "reasoning", text: "think-2" },
],
},
])
expect(replayed).toEqual([
{
sessionId: "ses_order",
update: {
sessionUpdate: "agent_thought_chunk",
messageId: "msg_order:reasoning:0",
content: { type: "text", text: "think-1 continued" },
},
},
updates[2],
updates[3],
])
} finally {
releaseUpdate.resolve()
releaseSubmit.resolve()
@@ -222,7 +222,7 @@ describe("acp service directory behavior", () => {
await fixture.service.setSessionMode({ sessionId: session.sessionId, modeId: "build" })
expect(currentValue(selectedModel, "model")).toBe("test/second-model")
expect(currentValue(selectedModel, "effort")).toBe("default")
expect(currentValue(selectedModel, "effort")).toBe("low")
expect(currentValue(selectedEffort, "effort")).toBe("medium")
expect(currentValue(selectedMode, "mode")).toBe("plan")
expect(
@@ -32,7 +32,7 @@ describe("acp service lifecycle", () => {
model: { providerID: "test", id: "second-model" },
},
})
expect(currentValue(created, "effort")).toBe("default")
expect(currentValue(created, "effort")).toBe("none")
})
test("loads and forks with paginated replay while resume does not replay", async () => {
+5 -5
View File
@@ -423,24 +423,24 @@ test("updates effective duplicate canonical keybinds", async () => {
const file = path.join(directory.path, "cli.json")
await Bun.write(
file,
`{"keybinds":{"session.delete":"first","session.delete":"last","opencode.settings":"off","opencode.settings":"on"}}`,
`{"keybinds":{"session.delete":"first","session.delete":"last","permission.mode":"off","permission.mode":"on"}}`,
)
const config = await run(
directory.path,
Effect.gen(function* () {
const service = yield* Config.Service
expect((yield* service.get()).keybinds).toEqual({ "session.delete": "last", "opencode.settings": "on" })
expect((yield* service.get()).keybinds).toEqual({ "session.delete": "last", "permission.mode": "on" })
return yield* service.update((draft) => {
draft.keybinds = { ...draft.keybinds, "session.delete": "changed", "opencode.settings": "changed" }
draft.keybinds = { ...draft.keybinds, "session.delete": "changed", "permission.mode": "changed" }
})
}),
)
expect(config.keybinds).toEqual({ "session.delete": "changed", "opencode.settings": "changed" })
expect(config.keybinds).toEqual({ "session.delete": "changed", "permission.mode": "changed" })
expect(parse(await Bun.file(file).text()).keybinds).toEqual({
"session.delete": "changed",
"opencode.settings": "changed",
"permission.mode": "changed",
})
})
+1 -1
View File
@@ -100,7 +100,7 @@ function fixture(
}
const installs = [
{ method: "npm", command: ["npm", "install", "--global", "--force", "@opencode/cli@2.3.4-beta.1"] },
{ method: "npm", command: ["npm", "install", "--global", "@opencode/cli@2.3.4-beta.1"] },
{
method: "pnpm",
command: ["pnpm", "add", "--global", "--allow-build=@opencode/cli", "@opencode/cli@2.3.4-beta.1"],
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/client",
"version": "2.0.0",
"version": "1.17.13",
"type": "module",
"license": "MIT",
"repository": {
+1 -19
View File
@@ -11,7 +11,6 @@ import type { RelativePath } from "@opencode/schema/schema"
import type { Brand } from "effect"
import type { Model } from "@opencode/schema/model"
import type { DateTime } from "effect"
import type { Permission } from "@opencode/schema/permission"
import type { SessionMessage } from "@opencode/schema/session-message"
import type { SessionInbox } from "@opencode/schema/session-inbox"
import type { PromptInput } from "@opencode/schema/prompt-input"
@@ -27,6 +26,7 @@ import type { Integration } from "@opencode/schema/integration"
import type { Form } from "@opencode/schema/form"
import type { Mcp } from "@opencode/schema/mcp"
import type { Credential } from "@opencode/schema/credential"
import type { Permission } from "@opencode/schema/permission"
import type { PermissionSaved } from "@opencode/schema/permission-saved"
import type { FileSystem } from "@opencode/schema/filesystem"
import type { Command } from "@opencode/schema/command"
@@ -209,7 +209,6 @@ export type SessionCreateInput = {
readonly model?: Model.Ref | undefined
readonly location?: Location.Ref | undefined
readonly metadata?: Session.Metadata | undefined
readonly permissions?: Permission.Ruleset | undefined
}
export type SessionCreateOutput = Session.Info
export type SessionCreateOperation<E = never> = (input?: SessionCreateInput) => Effect.Effect<SessionCreateOutput, E>
@@ -438,7 +437,6 @@ export type SessionLogOutput =
readonly agent?: Agent.ID | undefined
readonly model?: Model.Ref | undefined
readonly metadata?: Session.Metadata | undefined
readonly permissions?: Permission.Ruleset | undefined
readonly version: string
}
}
@@ -491,15 +489,6 @@ export type SessionLogOutput =
readonly location?: Location.Ref | undefined
readonly data: { readonly sessionID: Session.ID; readonly title: string }
}
| {
readonly id: Event.ID
readonly created: number
readonly metadata?: { readonly [x: string]: unknown } | undefined
readonly type: "session.permissions.updated"
readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version }
readonly location?: Location.Ref | undefined
readonly data: { readonly sessionID: Session.ID; readonly permissions: Permission.Ruleset }
}
| {
readonly id: Event.ID
readonly created: number
@@ -1596,12 +1585,6 @@ export type PermissionReplyOperation<E = never> = (
input: PermissionReplyInput,
) => Effect.Effect<PermissionReplyOutput, E>
export type PermissionRulesInput = { readonly sessionID: Session.ID; readonly permissions: Permission.Ruleset }
export type PermissionRulesOutput = void
export type PermissionRulesOperation<E = never> = (
input: PermissionRulesInput,
) => Effect.Effect<PermissionRulesOutput, E>
export interface PermissionApi<E = never> {
readonly request: { readonly list: PermissionRequestListOperation<E> }
readonly saved: { readonly list: PermissionSavedListOperation<E>; readonly remove: PermissionSavedRemoveOperation<E> }
@@ -1609,7 +1592,6 @@ export interface PermissionApi<E = never> {
readonly list: PermissionListOperation<E>
readonly get: PermissionGetOperation<E>
readonly reply: PermissionReplyOperation<E>
readonly rules: PermissionRulesOperation<E>
}
export type FileListInput = {
@@ -181,8 +181,6 @@ import type {
PermissionGetOutput,
PermissionReplyInput,
PermissionReplyOutput,
PermissionRulesInput,
PermissionRulesOutput,
FileListInput,
FileListOutput,
FileFindInput,
@@ -397,7 +395,6 @@ const EndpointSessionCreate = (raw: RawClient["server.session"]) => (input?: Ses
model: input?.["model"],
location: input?.["location"],
metadata: input?.["metadata"],
permissions: input?.["permissions"],
},
}).pipe(
Effect.mapError(mapClientError),
@@ -1148,14 +1145,6 @@ const EndpointPermissionReply = (raw: RawClient["server.permission"]) => (input:
}).pipe(Effect.mapError(mapClientError)),
)
const EndpointPermissionRules = (raw: RawClient["server.permission"]) => (input: PermissionRulesInput) =>
preserveEffect<PermissionRulesOutput>()(
raw["session.permission.rules"]({
params: { sessionID: input["sessionID"] },
payload: { permissions: input["permissions"] },
}).pipe(Effect.mapError(mapClientError)),
)
const adaptGroupPermission = (raw: RawClient["server.permission"]) => ({
request: { list: EndpointPermissionRequestList(raw) },
saved: { list: EndpointPermissionSavedList(raw), remove: EndpointPermissionSavedRemove(raw) },
@@ -1163,7 +1152,6 @@ const adaptGroupPermission = (raw: RawClient["server.permission"]) => ({
list: EndpointPermissionList(raw),
get: EndpointPermissionGet(raw),
reply: EndpointPermissionReply(raw),
rules: EndpointPermissionRules(raw),
})
const EndpointFileList = (raw: RawClient["server.fs"]) => (input?: FileListInput) =>
@@ -175,8 +175,6 @@ import type {
PermissionGetOutput,
PermissionReplyInput,
PermissionReplyOutput,
PermissionRulesInput,
PermissionRulesOutput,
FileReadInput,
FileReadOutput,
FileListInput,
@@ -567,7 +565,6 @@ export function make(options: ClientOptions) {
model: input?.["model"],
location: input?.["location"],
metadata: input?.["metadata"],
permissions: input?.["permissions"],
},
successStatus: 200,
declaredStatuses: [400, 401],
@@ -1569,18 +1566,6 @@ export function make(options: ClientOptions) {
},
requestOptions,
),
rules: (input: PermissionRulesInput, requestOptions?: RequestOptions) =>
request<PermissionRulesOutput>(
{
method: "PUT",
path: `/api/session/${encodeURIComponent(input.sessionID)}/permission/rules`,
body: { permissions: input["permissions"] },
successStatus: 204,
declaredStatuses: [400, 401, 404],
empty: true,
},
requestOptions,
),
},
file: {
read: (input: FileReadInput, requestOptions?: RequestOptions) =>
+42 -127
View File
@@ -551,6 +551,28 @@ export type InstructionEntryInfo = { key: InstructionEntryKey; value: JsonValue
export type InstructionEntrySnapshot = Array<{ key: InstructionEntryKey; value: JsonValue; removed: boolean }>
export type SessionCreated = {
id: string
created: number
metadata?: { [x: string]: any }
type: "session.created"
durable: { aggregateID: string; seq: number; version: 1 }
location?: LocationRef
data: {
sessionID: string
projectID: string
location: LocationRef
subpath?: string
parentID?: string
slug: string
title?: string
agent?: string
model?: ModelRef
metadata?: SessionMetadata
version: string
}
}
export type SessionAgentSelected = {
id: string
created: number
@@ -1629,6 +1651,24 @@ export type SessionInboxMove = {
delivery: SessionInboxDelivery
}
export type SessionInfo = {
id: string
parentID?: string
fork?: { sessionID: string; boundary: SessionForkBoundary }
projectID: string
agent?: string
model?: ModelRef
cost: MoneyUSD
tokens: TokenUsageInfo
outcome?: "succeeded" | "failed" | "interrupted"
time: { created: number; updated: number; idle?: number; viewed?: number; archived?: number }
title?: string
location: LocationRef
subpath?: string
metadata?: SessionMetadata
revert?: SessionRevert
}
export type SessionRevertStaged = {
id: string
created: number
@@ -1872,58 +1912,6 @@ export type AgentInfo = {
permissions: PermissionRuleset
}
export type SessionPermissionsUpdated = {
id: string
created: number
metadata?: { [x: string]: any }
type: "session.permissions.updated"
durable: { aggregateID: string; seq: number; version: 1 }
location?: LocationRef
data: { sessionID: string; permissions: PermissionRuleset }
}
export type SessionInfo = {
id: string
parentID?: string
fork?: { sessionID: string; boundary: SessionForkBoundary }
projectID: string
agent?: string
model?: ModelRef
cost: MoneyUSD
tokens: TokenUsageInfo
outcome?: "succeeded" | "failed" | "interrupted"
time: { created: number; updated: number; idle?: number; viewed?: number; archived?: number }
title?: string
location: LocationRef
subpath?: string
metadata?: SessionMetadata
permissions?: PermissionRuleset
revert?: SessionRevert
}
export type SessionCreated = {
id: string
created: number
metadata?: { [x: string]: any }
type: "session.created"
durable: { aggregateID: string; seq: number; version: 1 }
location?: LocationRef
data: {
sessionID: string
projectID: string
location: LocationRef
subpath?: string
parentID?: string
slug: string
title?: string
agent?: string
model?: ModelRef
metadata?: SessionMetadata
permissions?: PermissionRuleset
version: string
}
}
export type ConfigEntry =
| {
type: "document"
@@ -2096,6 +2084,8 @@ export type ConfigEntry =
| { type: "agents"; path: string }
| { type: "claude"; path: string }
export type SessionsResponse = { data: Array<SessionInfo>; cursor: { previous?: string | null; next?: string | null } }
export type SessionInboxUser = {
id: string
sessionID: string
@@ -2150,8 +2140,6 @@ export type FormFields = [FormField, ...Array<FormField>]
export type FormFields2 = [FormField1, ...Array<FormField1>]
export type SessionsResponse = { data: Array<SessionInfo>; cursor: { previous?: string | null; next?: string | null } }
export type SessionInboxInfo = SessionInboxUser | SessionInboxSynthetic | SessionInboxCompaction | SessionInboxMove
export type SessionInboxEnqueued = {
@@ -2245,7 +2233,6 @@ export type SessionEventDurable =
| SessionModelSelected
| SessionMoved
| SessionRenamed
| SessionPermissionsUpdated
| SessionViewed
| SessionDeleted
| SessionForked
@@ -2305,7 +2292,6 @@ export type V2Event =
| SessionModelSelected
| SessionMoved
| SessionRenamed
| SessionPermissionsUpdated
| SessionViewed
| SessionUsageUpdated
| SessionDeleted
@@ -2818,11 +2804,6 @@ export type SessionCreateInput = {
readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null
readonly location?: { readonly directory: string; readonly workspaceID?: string } | null
readonly metadata?: { readonly [x: string]: JsonValue } | null
readonly permissions?: ReadonlyArray<{
readonly action: string
readonly resource: string
readonly effect: "allow" | "deny" | "ask"
}> | null
}["id"]
readonly title?: {
readonly id?: string | null
@@ -2831,11 +2812,6 @@ export type SessionCreateInput = {
readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null
readonly location?: { readonly directory: string; readonly workspaceID?: string } | null
readonly metadata?: { readonly [x: string]: JsonValue } | null
readonly permissions?: ReadonlyArray<{
readonly action: string
readonly resource: string
readonly effect: "allow" | "deny" | "ask"
}> | null
}["title"]
readonly agent?: {
readonly id?: string | null
@@ -2844,11 +2820,6 @@ export type SessionCreateInput = {
readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null
readonly location?: { readonly directory: string; readonly workspaceID?: string } | null
readonly metadata?: { readonly [x: string]: JsonValue } | null
readonly permissions?: ReadonlyArray<{
readonly action: string
readonly resource: string
readonly effect: "allow" | "deny" | "ask"
}> | null
}["agent"]
readonly model?: {
readonly id?: string | null
@@ -2857,11 +2828,6 @@ export type SessionCreateInput = {
readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null
readonly location?: { readonly directory: string; readonly workspaceID?: string } | null
readonly metadata?: { readonly [x: string]: JsonValue } | null
readonly permissions?: ReadonlyArray<{
readonly action: string
readonly resource: string
readonly effect: "allow" | "deny" | "ask"
}> | null
}["model"]
readonly location?: {
readonly id?: string | null
@@ -2870,11 +2836,6 @@ export type SessionCreateInput = {
readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null
readonly location?: { readonly directory: string; readonly workspaceID?: string } | null
readonly metadata?: { readonly [x: string]: JsonValue } | null
readonly permissions?: ReadonlyArray<{
readonly action: string
readonly resource: string
readonly effect: "allow" | "deny" | "ask"
}> | null
}["location"]
readonly metadata?: {
readonly id?: string | null
@@ -2883,25 +2844,7 @@ export type SessionCreateInput = {
readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null
readonly location?: { readonly directory: string; readonly workspaceID?: string } | null
readonly metadata?: { readonly [x: string]: JsonValue } | null
readonly permissions?: ReadonlyArray<{
readonly action: string
readonly resource: string
readonly effect: "allow" | "deny" | "ask"
}> | null
}["metadata"]
readonly permissions?: {
readonly id?: string | null
readonly title?: string | null
readonly agent?: string | null
readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null
readonly location?: { readonly directory: string; readonly workspaceID?: string } | null
readonly metadata?: { readonly [x: string]: JsonValue } | null
readonly permissions?: ReadonlyArray<{
readonly action: string
readonly resource: string
readonly effect: "allow" | "deny" | "ask"
}> | null
}["permissions"]
}
export type SessionCreateOutput = { data: SessionInfo }["data"]
@@ -2939,11 +2882,6 @@ export type SessionImportInput = {
readonly location: { readonly directory: string; readonly workspaceID?: string }
readonly subpath?: string
readonly metadata?: { readonly [x: string]: JsonValue }
readonly permissions?: ReadonlyArray<{
readonly action: string
readonly resource: string
readonly effect: "allow" | "deny" | "ask"
}>
readonly revert?: {
readonly messageID: string
readonly partID?: string
@@ -3249,11 +3187,6 @@ export type SessionImportInput = {
readonly location: { readonly directory: string; readonly workspaceID?: string }
readonly subpath?: string
readonly metadata?: { readonly [x: string]: JsonValue }
readonly permissions?: ReadonlyArray<{
readonly action: string
readonly resource: string
readonly effect: "allow" | "deny" | "ask"
}>
readonly revert?: {
readonly messageID: string
readonly partID?: string
@@ -3559,11 +3492,6 @@ export type SessionImportInput = {
readonly location: { readonly directory: string; readonly workspaceID?: string }
readonly subpath?: string
readonly metadata?: { readonly [x: string]: JsonValue }
readonly permissions?: ReadonlyArray<{
readonly action: string
readonly resource: string
readonly effect: "allow" | "deny" | "ask"
}>
readonly revert?: {
readonly messageID: string
readonly partID?: string
@@ -5825,19 +5753,6 @@ export type PermissionReplyInput = {
export type PermissionReplyOutput = void
export type PermissionRulesInput = {
readonly sessionID: { readonly sessionID: string }["sessionID"]
readonly permissions: {
readonly permissions: ReadonlyArray<{
readonly action: string
readonly resource: string
readonly effect: "allow" | "deny" | "ask"
}>
}["permissions"]
}
export type PermissionRulesOutput = void
export type FileReadInput = {
readonly location?: {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
-4
View File
@@ -695,10 +695,6 @@ export function createData(config: CreateDataInput) {
})
return
}
case "session.permissions.updated":
if (store.session.info[event.data.sessionID])
setStore("session", "info", event.data.sessionID, "permissions", event.data.permissions)
return
case "session.moved": {
const current = store.session.info[event.data.sessionID]
if (current) {
+5 -37
View File
@@ -9,8 +9,7 @@ standard-library surface that programs can use today, plus concrete gaps that ma
- Intentional boundaries are not listed as compatibility work.
When behavior changes, update this file and the tests in the same change. The implementation and tests remain the
ultimate source of truth. Upstream test262 files run verbatim from `test/test262`; a failing file is listed in
`test/test262/skipped.txt` and its gap is an unchecked item here (see `test/test262/README.md`).
ultimate source of truth.
## Source and execution model
@@ -26,10 +25,6 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
- [x] The global `search(...)` built-in: synchronous tool discovery that counts as an admitted tool call and is
shadowable by program declarations like other globals.
- [x] Cooperative timeout, an optional total tool-call limit, output bounding, and unrestricted tool-call concurrency.
- [ ] Strict-mode early errors: duplicate parameter names, `yield` as an identifier, and a trailing comma after a
rest parameter are accepted unless the program itself begins with `"use strict"`.
- [ ] Valid JavaScript rejected by TypeScript transpilation before interpretation, such as `in` inside a destructuring
default in a `for...of` head and Unicode-escaped keywords.
## Values and literals
@@ -69,11 +64,6 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
- [x] Array binding and assignment destructuring from strings, Maps, Sets, URLSearchParams, custom synchronous
iterators, and synchronous generators, including stepwise elisions/rest and `IteratorClose` on early completion
or binding/default failure.
- [ ] Object destructuring from primitives follows ToObject (`const { length } = "abc"`, `const {} = 1`); non-object
sources are rejected.
- [ ] Destructuring a key that member access resolves through the owning built-in, such as
`const { constructor } = error`, reads `undefined`.
- [ ] Member expressions as `for...in` targets (`for (x.y in obj)`).
## Statements and control flow
@@ -121,15 +111,6 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
- [ ] User-defined constructor calls.
- [ ] `Function.prototype.call`, `apply`, and `bind` for CodeMode functions.
- [ ] Classes and private fields.
- [x] Functions are objects: they hold own properties (`fn.count = 1`), enumerate them, and expose read-only `name`
and `length`. Names follow JavaScript's NamedEvaluation: declarations, named expressions, bindings,
assignments, object literal keys, and destructuring or parameter defaults.
- [ ] `name` and `length` of built-in functions such as `Math.max` or `"a".includes`.
- [ ] A named function expression's name is not bound inside its own body.
- [ ] Redeclaring a function in the same scope is rejected; in JavaScript the last declaration wins.
- [ ] A line terminator between `async function` and the function name.
- [ ] Async generator functions evaluate parameter defaults and destructuring at the first `next()` rather than at the
call, so their errors are not thrown synchronously.
- [x] Synchronous and async generator declarations/expressions, `yield`, and `yield*`, including lazy bodies,
`next(value)`, `return(value)`, `throw(value)`, exhaustion, promise adoption, async request ordering,
`try`/`catch`/`finally`, and sync/async iterator symbols. Async `yield*` awaits values while adapting a sync
@@ -173,8 +154,6 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
- [x] Plain, arithmetic, bitwise, and logical assignment operators.
- [x] Property deletion on plain data objects and arrays, including computed and optional forms; deleting an array index
creates a hole without changing its length.
- [ ] Operators, `switch` discriminants, and coercion helpers such as `String` and `isNaN` applied to functions and
namespaces; JavaScript coerces them, the interpreter rejects non-data operands.
## Promises and tools
@@ -246,8 +225,7 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
- [x] Circular references are rejected when created (`o.self = o`, `array.push(array)`), not at serialization as in JS.
- [x] `Object.is` for supported data values.
- [x] `Object.groupBy` over finite collections and custom synchronous iterators/generators, with string-key coercion
and plain-object results.
- [ ] `Object.prototype` methods on values: `toString`, `toLocaleString`, `valueOf`, and `hasOwnProperty`.
and null-prototype results.
## Arrays
@@ -267,18 +245,10 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
- [x] `length`, numeric indexing, index assignment, spread, and `for...of`.
- [x] The `thisArg` argument of `Array.from` is accepted and ignored, like JS arrows.
- [x] `Array.prototype.toSpliced`.
- [x] Canonical array/string index parsing: keys such as `"01"` are ordinary properties rather than aliases of index
`1`.
- [x] Canonical array/string index parsing: keys such as `"01"` remain non-index properties rather than aliasing index
`1`; arbitrary array-property assignment remains unsupported.
- [x] `Array.prototype.sort` preserves trailing holes, while `toSorted` densifies holes into `undefined` elements,
like JavaScript.
- [x] Assigning `length` to truncate or extend an array; invalid lengths throw `RangeError`.
- [x] Non-index own properties on arrays (`arr.foo = 1`, `arr.constructor = null`). They are excluded from the JSON
form, like `JSON.stringify`.
- [ ] Argument coercion for `indexOf`, `lastIndexOf`, `includes`, `fill`, `flat`, `copyWithin`, and the `join`
separator: JavaScript applies ToIntegerOrInfinity/ToString (including `valueOf`, strings, and `undefined`), the
interpreter requires numbers and strings; `includes()`/`indexOf()` with no argument should search for
`undefined`.
- [ ] Iterator objects from `keys`, `values`, and `entries` with a live `next()`.
## Strings
@@ -362,7 +332,7 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
- [x] `test`, `exec`, and `toString`.
- [x] Readable `source`, `flags`, `lastIndex`, `hasIndices`, `global`, `ignoreCase`, `multiline`, `sticky`, `unicode`,
`unicodeSets`, and `dotAll`.
- [x] Captures, named groups, match `.index` and `.input`, and stateful global matching.
- [x] Captures, named groups, match `.index`, and stateful global matching.
- [x] Integration with supported String methods, including function replacers.
- [x] Writable `lastIndex`.
- [x] Match `indices` metadata for the `d` flag, including named groups on `exec`, `match`, and `matchAll` results.
@@ -421,5 +391,3 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
- [x] Caught errors do not distinguish user throws, interpreter failures, and tool failures; a program sees one
Error-shaped value with `name` and `message` in `catch`, rejection handlers, and `Promise.allSettled` reasons.
This is deliberate: the program should handle a failure the same way regardless of where it originated.
- [ ] Failures raised by the interpreter itself carry the generic `Error` name where JavaScript throws a `TypeError`,
`RangeError`, or `ReferenceError`, so `e instanceof TypeError` and `e.constructor === TypeError` are false.

Some files were not shown because too many files have changed in this diff Show More