mirror of
https://github.com/anomalyco/opencode.git
synced 2026-09-09 18:36:22 +00:00
Compare commits
133
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e648913e3 | ||
|
|
e28fcf5edb | ||
|
|
4ea368e09e | ||
|
|
c6977a836f | ||
|
|
1dcc6551d9 | ||
|
|
9c8fb89979 | ||
|
|
51c926c3ac | ||
|
|
65152b7936 | ||
|
|
3c4c7b41be | ||
|
|
d461154a8d | ||
|
|
cebd25022f | ||
|
|
9128e847bd | ||
|
|
8b92833624 | ||
|
|
f4dd76913f | ||
|
|
ef88566d61 | ||
|
|
b3f765c17d | ||
|
|
ab2366de2e | ||
|
|
c0aa963c13 | ||
|
|
d55d941f3c | ||
|
|
2dea1f3d0e | ||
|
|
fcafe82cdc | ||
|
|
1fe06bb4ed | ||
|
|
c1f4beaf40 | ||
|
|
e628143448 | ||
|
|
148042ab81 | ||
|
|
b7aea8b0ef | ||
|
|
1623ac3ba9 | ||
|
|
f02c5f8648 | ||
|
|
0da0772bc0 | ||
|
|
08e28fb915 | ||
|
|
74b0fa9d1f | ||
|
|
dbd9b18f3d | ||
|
|
b43e1c682b | ||
|
|
d11f5916ee | ||
|
|
f1ce69d2ce | ||
|
|
d39290fbb9 | ||
|
|
6ee2ed7510 | ||
|
|
594635b5ae | ||
|
|
18b05e86fb | ||
|
|
cc501650c6 | ||
|
|
37b6fbc5bf | ||
|
|
d24f8b0810 | ||
|
|
79a6a90862 | ||
|
|
8c5eca5bb2 | ||
|
|
f3128fa241 | ||
|
|
883d16d2ad | ||
|
|
5c30292daa | ||
|
|
11aee1b49f | ||
|
|
a322d7f98f | ||
|
|
546b9fb3c1 | ||
|
|
83288921f6 | ||
|
|
510b771946 | ||
|
|
78915a0f17 | ||
|
|
1ead17547b | ||
|
|
0c1bf08ca6 | ||
|
|
8b09f6415a | ||
|
|
e791afdfa3 | ||
|
|
e655fed6c3 | ||
|
|
ccbc018072 | ||
|
|
4432956490 | ||
|
|
375bf4908f | ||
|
|
cc6bff39a0 | ||
|
|
8a5709324f | ||
|
|
be58ca806c | ||
|
|
9e42e5cc4c | ||
|
|
c2a1649dd4 | ||
|
|
ded9c7e505 | ||
|
|
f9bc2233dd | ||
|
|
7487999e06 | ||
|
|
2eea36e731 | ||
|
|
4fef8edbe8 | ||
|
|
50c552f763 | ||
|
|
a3d5923aca | ||
|
|
ea2c0184ce | ||
|
|
09c318094c | ||
|
|
22a534a0bb | ||
|
|
c3f1bdaf97 | ||
|
|
2bf9bec897 | ||
|
|
90dd682e66 | ||
|
|
64684b118f | ||
|
|
cab8e39ad5 | ||
|
|
5165d6008c | ||
|
|
4d74854e8c | ||
|
|
50e17b7f95 | ||
|
|
e8177238f6 | ||
|
|
b3f36c0967 | ||
|
|
1f77408ff2 | ||
|
|
a912a6ee4f | ||
|
|
2ac698d65a | ||
|
|
c1c6ab593d | ||
|
|
d1d1c6f890 | ||
|
|
0ab661a9cc | ||
|
|
a55dc8c84a | ||
|
|
be41bc4e7d | ||
|
|
567f8b9743 | ||
|
|
6263a35b3f | ||
|
|
74ca560c75 | ||
|
|
a68d6f904d | ||
|
|
cc8c2f8810 | ||
|
|
ad31bff969 | ||
|
|
a5312e169b | ||
|
|
16aca14bc7 | ||
|
|
4aba093c98 | ||
|
|
a3bbcd5c73 | ||
|
|
c05d07cd73 | ||
|
|
ef34ada9fb | ||
|
|
e15fb426ec | ||
|
|
72433f2ed8 | ||
|
|
b32d8c3e58 | ||
|
|
6af8515f69 | ||
|
|
5c50edb9bb | ||
|
|
fcddc84225 | ||
|
|
582a2108ce | ||
|
|
9c65a69937 | ||
|
|
1d391908f4 | ||
|
|
596dca4dee | ||
|
|
1827832775 | ||
|
|
898692af26 | ||
|
|
5c3f2ddf8c | ||
|
|
1382cebe10 | ||
|
|
0732cdd8e1 | ||
|
|
2bf6a9813a | ||
|
|
f094a10645 | ||
|
|
89f1943d2d | ||
|
|
fc4f319676 | ||
|
|
b51fdb331c | ||
|
|
8a1a6b8fb4 | ||
|
|
1a2b985777 | ||
|
|
00067d23a0 | ||
|
|
3ebc4b01b9 | ||
|
|
17f362e0fe | ||
|
|
5b83c15fb5 | ||
|
|
d86c3041c9 |
@@ -1,5 +1,5 @@
|
||||
---
|
||||
"@opencode-ai/core": patch
|
||||
"@opencode/core": patch
|
||||
---
|
||||
|
||||
Correct directory page headings when the read offset is zero.
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
name: deploy-files
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
- v2
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: deploy-files-${{ github.ref_name }}
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
if: github.repository == 'anomalyco/opencode' && (github.ref_name == 'dev' || github.ref_name == 'v2')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
|
||||
|
||||
- uses: ./.github/actions/setup-bun
|
||||
|
||||
- name: Typecheck
|
||||
working-directory: services/files
|
||||
run: bun typecheck
|
||||
|
||||
- name: Deploy
|
||||
working-directory: services/files
|
||||
run: bun run deploy --env ${{ github.ref_name == 'v2' && 'production' || 'dev' }}
|
||||
env:
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
@@ -24,13 +24,13 @@ jobs:
|
||||
- uses: ./.github/actions/setup-bun
|
||||
|
||||
- name: Build
|
||||
working-directory: packages/www
|
||||
working-directory: services/www
|
||||
run: bun run build
|
||||
env:
|
||||
CLOUDFLARE_ENV: ${{ github.ref_name == 'v2' && 'production' || 'dev' }}
|
||||
|
||||
- name: Deploy
|
||||
working-directory: packages/www
|
||||
working-directory: services/www
|
||||
run: bun run deploy
|
||||
env:
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
|
||||
@@ -11,6 +11,7 @@ on:
|
||||
- "bun.lock"
|
||||
- "package.json"
|
||||
- "packages/*/package.json"
|
||||
- "services/*/package.json"
|
||||
- "flake.lock"
|
||||
- "nix/node_modules.nix"
|
||||
- "nix/scripts/**"
|
||||
|
||||
@@ -48,7 +48,7 @@ jobs:
|
||||
|
||||
- name: Deploy update service
|
||||
if: github.ref_name == 'v2'
|
||||
working-directory: packages/updates
|
||||
working-directory: services/update
|
||||
run: bun run deploy
|
||||
env:
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
@@ -670,19 +670,6 @@ jobs:
|
||||
git config --global user.name "opencode"
|
||||
ssh-keyscan -H aur.archlinux.org >> ~/.ssh/known_hosts || true
|
||||
|
||||
- name: Upload desktop release assets
|
||||
if: needs.version.outputs.release
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.committer.outputs.token }}
|
||||
run: |
|
||||
shopt -s nullglob
|
||||
files=(/tmp/desktop/*.{exe,blockmap,dmg,zip,AppImage,deb,rpm} /tmp/desktop/*.app.tar.gz)
|
||||
if (( ${#files[@]} == 0 )); then
|
||||
echo "No desktop release assets found"
|
||||
exit 1
|
||||
fi
|
||||
gh release upload "v${{ needs.version.outputs.version }}" "${files[@]}" --clobber --repo "${{ needs.version.outputs.repo }}"
|
||||
|
||||
- run: ./script/publish.ts
|
||||
env:
|
||||
OPENCODE_VERSION: ${{ needs.version.outputs.version }}
|
||||
@@ -694,3 +681,6 @@ jobs:
|
||||
LATEST_YML_DIR: /tmp/latest-yml
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
||||
OPENCODE_DESKTOP_DIST: /tmp/desktop
|
||||
CLOUDFLARE_ACCOUNT_ID: 15d29c8639fd3733b1b5486a2acfd968
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
|
||||
@@ -49,7 +49,7 @@ jobs:
|
||||
echo "app=true" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
bun x turbo@2.10.2 ls --affected --filter=@opencode-ai/app --output=json > affected.json
|
||||
bun x turbo@2.10.2 ls --affected --filter=@opencode/app --output=json > affected.json
|
||||
bun -e 'const result = await Bun.file("affected.json").json(); console.log(`app=${result.packages.count > 0}`)' >> "$GITHUB_OUTPUT"
|
||||
|
||||
unit:
|
||||
@@ -132,10 +132,10 @@ jobs:
|
||||
timeout-minutes: 15
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
bun turbo verify:package --filter=@opencode-ai/sdk
|
||||
bun turbo verify:package --filter=@opencode/sdk
|
||||
exit 0
|
||||
fi
|
||||
bun turbo verify:package --affected --filter=@opencode-ai/sdk
|
||||
bun turbo verify:package --affected --filter=@opencode/sdk
|
||||
env:
|
||||
TURBO_SCM_BASE: ${{ github.event_name == 'pull_request' && format('{0}^1', github.sha) || github.event.before }}
|
||||
TURBO_SCM_HEAD: ${{ github.sha }}
|
||||
@@ -173,7 +173,7 @@ jobs:
|
||||
|
||||
- name: Check generated documentation
|
||||
if: runner.os == 'Linux'
|
||||
working-directory: packages/www
|
||||
working-directory: services/www
|
||||
run: bun run check:generated
|
||||
|
||||
e2e:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/// <reference path="../env.d.ts" />
|
||||
import { tool } from "@opencode-ai/plugin"
|
||||
import { tool } from "@opencode/plugin"
|
||||
async function githubFetch(endpoint: string, options: RequestInit = {}) {
|
||||
const response = await fetch(`https://api.github.com${endpoint}`, {
|
||||
...options,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/// <reference path="../env.d.ts" />
|
||||
import { tool } from "@opencode-ai/plugin"
|
||||
import { tool } from "@opencode/plugin"
|
||||
|
||||
const TEAM = {
|
||||
tui: ["kommander", "simonklee"],
|
||||
|
||||
@@ -84,9 +84,9 @@ const { a, b } = obj
|
||||
### Imports
|
||||
|
||||
- Never alias imports. Do not use `import { foo as bar } from "..."` or renamed imports like `resolve as pathResolve`.
|
||||
- Never use type-position `import("...")` references such as `Schema.declare<import("@opencode-ai/plugin/effect/plugin").Plugin["effect"]>`. Only when two imports genuinely collide on a name and no other option exists, an aliased type import (`import type { Plugin as PluginDefinition } from "..."`) is permitted as a last resort — still strongly preferred not to.
|
||||
- Never use type-position `import("...")` references such as `Schema.declare<import("@opencode/plugin/effect/plugin").Plugin["effect"]>`. Only when two imports genuinely collide on a name and no other option exists, an aliased type import (`import type { Plugin as PluginDefinition } from "..."`) is permitted as a last resort — still strongly preferred not to.
|
||||
- Never use star imports. Do not use `import * as Foo from "..."` or `import type * as Foo from "..."`.
|
||||
- If a namespace-style value is needed, import the module's own exported namespace by name, for example `import { Project } from "@opencode-ai/core/project"`, then reference `Project.ID`.
|
||||
- If a namespace-style value is needed, import the module's own exported namespace by name, for example `import { Project } from "@opencode/core/project"`, then reference `Project.ID`.
|
||||
- Prefer dynamic imports for heavy modules that are only needed in selected code paths, especially in startup-sensitive entrypoints. Destructure dynamic import bindings near the top of the narrowest scope that needs them so they read like normal imports. Avoid inline chains such as `await import("./module").then((mod) => mod.value())` or `(await import("./module")).value()`. Keep branch-specific imports inside the branch that needs them to preserve lazy loading.
|
||||
|
||||
### Variables
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
# Slash-command follow-up behavior
|
||||
|
||||
Both screenshots submit `/review current changes` with the follow-up preference set to **Queue** while a session is running.
|
||||
|
||||
- `before.png`: production build of `v2` at `1dcc6551d9`; the command is sent as Steer.
|
||||
- `after.png`: production build with this fix; the command appears in the queue above the composer.
|
||||
|
||||
Captured with the fixture-backed `slash commands respect queue preference and alternate submit` Playwright scenario in `packages/app/e2e/regression/session-queue.spec.ts`. The production UI is rendered against an isolated command/session fixture.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
+1
-1
@@ -2,7 +2,7 @@
|
||||
exact = true
|
||||
# Only install newly resolved package versions published at least 3 days ago.
|
||||
minimumReleaseAge = 259200
|
||||
minimumReleaseAgeExcludes = ["@ai-sdk/amazon-bedrock", "@ai-sdk/anthropic", "@brendonovich/vite-plugin-opencode", "@opencode-ai/sdk", "@opencode-ai/pty", "@opencode-ai/pty-darwin-arm64", "@opencode-ai/pty-darwin-x64", "@opencode-ai/pty-linux-arm64-gnu", "@opencode-ai/pty-linux-arm64-musl", "@opencode-ai/pty-linux-x64-gnu", "@opencode-ai/pty-linux-x64-musl", "@opentui/core", "@opentui/core-darwin-arm64", "@opentui/core-darwin-x64", "@opentui/core-linux-arm64", "@opentui/core-linux-arm64-musl", "@opentui/core-linux-x64", "@opentui/core-linux-x64-musl", "@opentui/core-win32-arm64", "@opentui/core-win32-x64", "@opentui/keymap", "@opentui/solid", "opentui-spinner", "gitlab-ai-provider", "opencode-gitlab-auth", "@ff-labs/fff-node", "@ff-labs/fff-bun", "@ff-labs/fff-bin-darwin-arm64", "@ff-labs/fff-bin-darwin-x64", "@ff-labs/fff-bin-linux-arm64-gnu", "@ff-labs/fff-bin-linux-arm64-musl", "@ff-labs/fff-bin-linux-x64-gnu", "@ff-labs/fff-bin-linux-x64-musl", "@ff-labs/fff-bin-win32-arm64", "@ff-labs/fff-bin-win32-x64", "@pierre/diffs", "@pierre/theming", "app-builder-lib", "dmg-builder", "electron", "electron-builder", "electron-publish", "blume", "mermaid"]
|
||||
minimumReleaseAgeExcludes = ["@ai-sdk/amazon-bedrock", "@ai-sdk/anthropic", "@brendonovich/vite-plugin-opencode", "@opencode/sdk", "@opencode-ai/pty", "@opencode-ai/pty-darwin-arm64", "@opencode-ai/pty-darwin-x64", "@opencode-ai/pty-linux-arm64-gnu", "@opencode-ai/pty-linux-arm64-musl", "@opencode-ai/pty-linux-x64-gnu", "@opencode-ai/pty-linux-x64-musl", "@opentui/core", "@opentui/core-darwin-arm64", "@opentui/core-darwin-x64", "@opentui/core-linux-arm64", "@opentui/core-linux-arm64-musl", "@opentui/core-linux-x64", "@opentui/core-linux-x64-musl", "@opentui/core-win32-arm64", "@opentui/core-win32-x64", "@opentui/keymap", "@opentui/solid", "opentui-spinner", "gitlab-ai-provider", "opencode-gitlab-auth", "@ff-labs/fff-node", "@ff-labs/fff-bun", "@ff-labs/fff-bin-darwin-arm64", "@ff-labs/fff-bin-darwin-x64", "@ff-labs/fff-bin-linux-arm64-gnu", "@ff-labs/fff-bin-linux-arm64-musl", "@ff-labs/fff-bin-linux-x64-gnu", "@ff-labs/fff-bin-linux-x64-musl", "@ff-labs/fff-bin-win32-arm64", "@ff-labs/fff-bin-win32-x64", "@pierre/diffs", "@pierre/theming", "app-builder-lib", "dmg-builder", "electron", "electron-builder", "electron-publish", "blume", "mermaid"]
|
||||
|
||||
[test]
|
||||
root = "./do-not-run-tests-from-root"
|
||||
|
||||
@@ -6,6 +6,7 @@ export function createWebApp(domain: string) {
|
||||
$app.stage === "beta"
|
||||
? {
|
||||
OPENCODE_CHANNEL: "beta",
|
||||
VITE_OPENCODE_SERVER_MODE: "none",
|
||||
VITE_SENTRY_ENVIRONMENT: "beta",
|
||||
}
|
||||
: undefined,
|
||||
|
||||
@@ -165,22 +165,30 @@ else
|
||||
exit 1
|
||||
fi
|
||||
|
||||
package_scope="@opencode"
|
||||
if [ -z "$requested_version" ]; then
|
||||
metadata=$(curl -fsSL https://registry.npmjs.org/@opencode-ai%2fcli/beta || true)
|
||||
metadata=$(curl -fsSL https://opencode.ai/update/api/beta/cli/npm || true)
|
||||
specific_version=$(echo "$metadata" | sed -n 's/.*"version":"\([^"]*\)".*/\1/p')
|
||||
package=$(echo "$metadata" | sed -n 's/.*"package":"\([^"]*\)".*/\1/p')
|
||||
|
||||
if [ -z "$specific_version" ]; then
|
||||
if [ -z "$specific_version" ] || [ -z "$package" ]; then
|
||||
echo -e "${RED}Failed to fetch version information${NC}"
|
||||
exit 1
|
||||
fi
|
||||
package_scope="${package%/cli}"
|
||||
else
|
||||
# Strip leading 'v' if present
|
||||
requested_version="${requested_version#v}"
|
||||
specific_version=$requested_version
|
||||
fi
|
||||
|
||||
package_name="@opencode-ai/cli-$target"
|
||||
http_status=$(curl -s -o /dev/null -w "%{http_code}" "https://registry.npmjs.org/@opencode-ai%2fcli-$target/$specific_version" || true)
|
||||
package_name="$package_scope/cli-$target"
|
||||
http_status=$(curl -s -o /dev/null -w "%{http_code}" "https://registry.npmjs.org/$package_scope%2fcli-$target/$specific_version" || true)
|
||||
# Older clients install the minimum release before they can migrate package names.
|
||||
if [ "$http_status" = "404" ] && [ -n "$requested_version" ]; then
|
||||
package_name="@opencode-ai/cli-$target"
|
||||
http_status=$(curl -s -o /dev/null -w "%{http_code}" "https://registry.npmjs.org/@opencode-ai%2fcli-$target/$specific_version" || true)
|
||||
fi
|
||||
if [ "$http_status" = "404" ]; then
|
||||
echo -e "${RED}Error: Version ${specific_version} is not available for $target${NC}"
|
||||
echo -e "${MUTED}Available versions: https://www.npmjs.com/package/$package_name?activeTab=versions${NC}"
|
||||
|
||||
+1
-1
@@ -88,7 +88,7 @@ stdenv.mkDerivation (finalAttrs: {
|
||||
cd packages/desktop
|
||||
|
||||
export OPENCODE_CLI_DIST="$TMPDIR/desktop-cli"
|
||||
cli_package=$(bun -e 'import { getCurrentCli } from "./scripts/utils.ts"; console.log(getCurrentCli().package.replace("@opencode-ai/", ""))')
|
||||
cli_package=$(bun -e 'import { getCurrentCli } from "./scripts/utils.ts"; console.log(getCurrentCli().package.replace("@opencode/", ""))')
|
||||
mkdir -p "$OPENCODE_CLI_DIST/$cli_package/bin"
|
||||
cp ${lib.getExe opencode} "$OPENCODE_CLI_DIST/$cli_package/bin/opencode2"
|
||||
|
||||
|
||||
+4
-4
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"nodeModules": {
|
||||
"x86_64-linux": "sha256-T1JUv8gUrXavDI1HdVGqNRal4y2Bh5QavpR4DV+bJdA=",
|
||||
"aarch64-linux": "sha256-wlbWdEUVWAafgxaZis0Q+SAas8k6hu3s0C39CnMZ2gw=",
|
||||
"aarch64-darwin": "sha256-74rRue8KQWKzgMMb540beqMzp1DUShlPte6qklYCVTs=",
|
||||
"x86_64-darwin": "sha256-ihr3DoLd/4Lw6WbyXzSQXMP4hrVGk/UiRkO2aoQn8vg="
|
||||
"x86_64-linux": "sha256-yzCk746pospz8EVakHRcDhYJkhGYGSt9dHOPbzO4OYo=",
|
||||
"aarch64-linux": "sha256-MFJVLos4v2r9jazmmr3ldVCJKLrO+Qp/BpGpyM/1lf8=",
|
||||
"aarch64-darwin": "sha256-k8r/HVSgdRSTlJ1lI7EebqbxeA3AElnaw1sDYOPEdQw=",
|
||||
"x86_64-darwin": "sha256-ACJdJfz12xLBQvWIkbuve86znSoGJ2PLDQbgh0cT6/g="
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,11 +27,12 @@ stdenvNoCC.mkDerivation {
|
||||
fileset = lib.fileset.intersection (lib.fileset.fromSource (lib.sources.cleanSource ../.)) (
|
||||
lib.fileset.unions [
|
||||
../packages
|
||||
../services
|
||||
../bun.lock
|
||||
../package.json
|
||||
../patches
|
||||
../install # required by desktop build (cli.rs include_str!)
|
||||
../.github/TEAM_MEMBERS # required by @opencode-ai/script
|
||||
../.github/TEAM_MEMBERS # required by @opencode/script
|
||||
]
|
||||
);
|
||||
};
|
||||
|
||||
+6
-3
@@ -9,11 +9,13 @@
|
||||
"scripts": {
|
||||
"dev": "bun run --cwd packages/cli src/index.ts",
|
||||
"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=\"$(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",
|
||||
"dev:stats": "bun sst shell --stage=production -- bun run --cwd packages/stats/app dev",
|
||||
"dev:www": "bun run --cwd packages/www dev",
|
||||
"dev:www": "bun run --cwd services/www dev",
|
||||
"dev:storybook": "bun --cwd packages/storybook storybook",
|
||||
"bench:devex": "bun run --cwd packages/app test:bench:devex",
|
||||
"lint": "oxlint",
|
||||
@@ -34,6 +36,7 @@
|
||||
"workspaces": {
|
||||
"packages": [
|
||||
"packages/*",
|
||||
"services/*",
|
||||
"packages/console/*",
|
||||
"packages/stats/*"
|
||||
],
|
||||
@@ -128,8 +131,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "3.933.0",
|
||||
"@opencode-ai/plugin": "workspace:*",
|
||||
"@opencode-ai/script": "workspace:*",
|
||||
"@opencode/plugin": "workspace:*",
|
||||
"@opencode/script": "workspace:*",
|
||||
"heap-snapshot-toolkit": "1.1.3",
|
||||
"typescript": "catalog:"
|
||||
},
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
Per-type constructors live on the type, not as top-level re-exports. Use `Message.system(...)`, `Message.user(...)`, `Message.assistant(...)`, `Message.tool(...)`, `LanguageModel.make(...)`, `ToolDefinition.make(...)`, `ToolCallPart.make(...)`, `ToolResultPart.make(...)`, `ToolChoice.make(...)`, `ToolChoice.named(...)`, `SystemPart.make(...)`, and `GenerationOptions.make(...)` directly. The top-level `LLM` namespace is reserved for request-shaped call APIs: `LLM.request`, `LLM.generate`, `LLM.stream`, and `LLM.generateObject`. Use `LLMRequest.update(...)` when deriving canonical request data; do not add a duplicate `LLM.updateRequest(...)` path. Two ways to construct the same thing is one too many.
|
||||
|
||||
- Keep provider-defined string enums forward-compatible. Expose known values for autocomplete while accepting future values with `Known | (string & {})`; use `Schema.String` at runtime unless rejecting unknown values is required for correctness.
|
||||
- Order reasoning-effort values from lowest to highest: `none`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max`. Provider-specific subsets follow the same relative order in types, schemas, option lists, and tests.
|
||||
|
||||
## Tests
|
||||
|
||||
@@ -81,8 +82,6 @@ export const route = Route.make({
|
||||
|
||||
Route defaults are request-shaping defaults such as `headers`, `limits`, `generation`, `providerOptions`, and `http`. Endpoint host/query belongs on the route endpoint. Selected `LanguageModel` values carry only model id, provider id, and the configured route value. Model capability/catalog metadata lives outside this package; protocol support is enforced by request lowering and typed `AIError`s.
|
||||
|
||||
LLM `ProviderMetadata` is indexed by the selected `LanguageModel.route.id`. Use that same route ID when emitting usage, reasoning signatures, item IDs, and hosted-tool state and when reading them for continuation. There is no independently configurable metadata namespace. Reassigning a provider does not rename the route or its metadata; changing the route ID changes both. Provider-specific APIs with different replay formats must have distinct route IDs.
|
||||
|
||||
The four-axis decomposition is the reason DeepSeek, TogetherAI, Cerebras, Baseten, Fireworks, and DeepInfra all reuse `OpenAIChat.protocol` verbatim — each provider owns a small `Route.make(...)` composition instead of a protocol clone. Bug fixes in one protocol propagate to every consumer of that protocol in a single commit.
|
||||
|
||||
When a provider supports multiple physical transports, selection remains execution policy below its semantic route. `OpenResponsesChannel.transport(...)` owns the provider-neutral Responses WebSocket concept: it prepares one final request, executes HTTP by default, strips WebSocket-disallowed fields, and passes a generic channel exchange to a per-call `WebSocketChannelExecutor` when supplied. Provider-specific Responses routes opt in with handshake and connection-age policy. `Route.streamPrepared` owns decoding and acknowledges channel completion only after successful full consumption.
|
||||
@@ -123,10 +122,10 @@ Keep provider facades small and explicit:
|
||||
|
||||
### Provider Package Entrypoints
|
||||
|
||||
Catalog-selected native providers use package-like export paths from `@opencode-ai/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.
|
||||
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/ai/providers/openai/responses"
|
||||
import { model } from "@opencode/ai/providers/openai/responses"
|
||||
|
||||
const selected = model("gpt-5", {
|
||||
apiKey,
|
||||
|
||||
+343
-62
@@ -1,12 +1,12 @@
|
||||
# @opencode-ai/ai
|
||||
# @opencode/ai
|
||||
|
||||
Schema-first language model and image-generation APIs built with Effect.
|
||||
|
||||
```ts
|
||||
import { Effect, Layer } from "effect"
|
||||
import { LLM, LLMClient } from "@opencode-ai/ai"
|
||||
import { RequestExecutor } from "@opencode-ai/ai/route"
|
||||
import { OpenAI } from "@opencode-ai/ai/providers"
|
||||
import { LLM, LLMClient } from "@opencode/ai"
|
||||
import { RequestExecutor } from "@opencode/ai/route"
|
||||
import { OpenAI } from "@opencode/ai/providers"
|
||||
|
||||
const model = OpenAI.configure({ apiKey: process.env.OPENAI_API_KEY }).responses("gpt-4o-mini")
|
||||
|
||||
@@ -29,13 +29,323 @@ await Effect.runPromise(program.pipe(Effect.provide(llmLayer)))
|
||||
|
||||
Run `LLMClient.stream(request)` instead of `generate` when you want incremental `LLMEvent`s. The event stream is provider-neutral — same shape across OpenAI Chat, OpenAI Responses, Anthropic Messages, Gemini, Bedrock Converse, and any OpenAI-compatible deployment.
|
||||
|
||||
## Alibaba Cloud Model Studio
|
||||
|
||||
`Alibaba` provides standard Model Studio inference. Configure a region explicitly, then select
|
||||
Chat Completions (`.model` or `.chat`), Anthropic-compatible Messages (`.messages`), or OpenAI-compatible
|
||||
Responses (`.responses`). These routes use HTTP/SSE.
|
||||
|
||||
```ts
|
||||
import { LLM } from "@opencode/ai"
|
||||
import { Alibaba } from "@opencode/ai/providers"
|
||||
|
||||
const alibaba = Alibaba.configure({
|
||||
region: "ap-southeast-1", // Singapore
|
||||
apiKey: process.env.DASHSCOPE_API_KEY,
|
||||
// workspaceID: "llm-your-workspace", // use a workspace-dedicated endpoint
|
||||
})
|
||||
|
||||
const request = LLM.request({
|
||||
model: alibaba.model("qwen3.8-max"),
|
||||
prompt: "Explain this design.",
|
||||
providerOptions: { reasoningEffort: "medium" },
|
||||
})
|
||||
```
|
||||
|
||||
### Regions and credentials
|
||||
|
||||
| Region | `region` | Shared host when `workspaceID` is omitted |
|
||||
| ------------------- | ---------------- | ----------------------------------------- |
|
||||
| Singapore | `ap-southeast-1` | `dashscope-intl.aliyuncs.com` |
|
||||
| China (Beijing) | `cn-beijing` | `dashscope.aliyuncs.com` |
|
||||
| China (Hong Kong) | `cn-hongkong` | `cn-hongkong.dashscope.aliyuncs.com` |
|
||||
| US (Virginia) | `us-east-1` | `dashscope-us.aliyuncs.com` |
|
||||
| Germany (Frankfurt) | `eu-central-1` | Supply `workspaceID` or `baseURL` |
|
||||
| Japan (Tokyo) | `ap-northeast-1` | Supply `workspaceID` or `baseURL` |
|
||||
|
||||
With `workspaceID`, the host is `{workspaceID}.{region}.maas.aliyuncs.com`. A complete `baseURL`
|
||||
overrides regional setup, including the API prefix: `/compatible-mode/v1` for Chat/Responses,
|
||||
or `/apps/anthropic/v1` for Messages. The selector appends its operation path.
|
||||
|
||||
Keys and model availability are region-specific. Auth resolves from explicit `auth` or `apiKey`,
|
||||
then `DASHSCOPE_API_KEY`, then `ALIBABA_API_KEY`.
|
||||
|
||||
The access region and inference scope differ: Virginia's `-us` model IDs request US-only inference;
|
||||
some regions select scope through their workspace. Model IDs pass through unchanged.
|
||||
Alibaba's [regional guide](https://www.alibabacloud.com/help/en/model-studio/regions) and
|
||||
[base URL table](https://www.alibabacloud.com/help/en/model-studio/base-url) disagree about Virginia's
|
||||
shared host; the entry above follows the base URL table. Dedicated hosts can be copied from the console.
|
||||
|
||||
### Native options
|
||||
|
||||
- **Chat:** `reasoningEffort` → `reasoning_effort`, `enableThinking` → `enable_thinking`,
|
||||
`thinkingBudget` → `thinking_budget`, and `preserveThinking` → `preserve_thinking`.
|
||||
Replay complete `response.message` values to retain `reasoning_content` separately from answer text.
|
||||
Qwen 3.8 defaults to preserving thinking; older models have different defaults.
|
||||
Additional options include `toolStream`, `parallelToolCalls`, `repetitionPenalty`, `responseFormat`,
|
||||
`enableSearch`, and native `searchOptions`. `generation.topK` lowers to `top_k`.
|
||||
`clearThinking` is a hosted GLM control, and `thinking.type` is available for hosted MiniMax models.
|
||||
- **Messages:** `effort` → `output_config.effort`. `thinking.type` accepts enabled/disabled with an
|
||||
optional `budgetTokens` (or native `budget_tokens`). `outputConfig.format` accepts a JSON schema.
|
||||
Model Studio's empty thinking signatures are accepted; supplied signatures are replayed unchanged.
|
||||
- **Responses:** `reasoningEffort` → `reasoning.effort`, plus `enableThinking`, `store`,
|
||||
`previousResponseId`, and `conversation`. Omitted `store` retains the API's default (`true`);
|
||||
set it to `false` for client-managed history. `previousResponseId` requires a stored response.
|
||||
Hosted tools are `Alibaba.webSearch()`, `Alibaba.webExtractor()`, and `Alibaba.codeInterpreter()`.
|
||||
Web extraction is used together with web search. Hosted calls/results carry `providerExecuted: true`.
|
||||
|
||||
Omitted options preserve provider defaults. Effort values pass through unchanged and accept future
|
||||
strings. Qwen 3.8 Chat rejects requests combining a thinking budget with effort.
|
||||
|
||||
Package entrypoints are `@opencode/ai/providers/alibaba`, `alibaba/chat`, `alibaba/messages`,
|
||||
and `alibaba/responses`. Live recordings cover all three APIs in Singapore; regional URL construction
|
||||
is unit-tested for all six regions.
|
||||
|
||||
## Z.AI
|
||||
|
||||
`ZAI` uses the standard API. Chat Completions is the default language-model API;
|
||||
the existing `.image(...)` selector provides image generation.
|
||||
|
||||
```ts
|
||||
import { LLM } from "@opencode/ai"
|
||||
import { ZAI, ZAICodingPlan } from "@opencode/ai/providers"
|
||||
|
||||
const zai = ZAI.configure({ apiKey: process.env.ZAI_API_KEY })
|
||||
const request = LLM.request({
|
||||
model: zai.model("glm-5.3"), // also zai.chat("glm-5.3")
|
||||
prompt: "Explain this design.",
|
||||
providerOptions: {
|
||||
reasoningEffort: "high",
|
||||
thinking: { type: "enabled", clear_thinking: false },
|
||||
},
|
||||
})
|
||||
|
||||
const coding = ZAICodingPlan.configure({ apiKey: process.env.ZAI_API_KEY })
|
||||
const messages = LLM.request({
|
||||
model: coding.messages("glm-5.3"),
|
||||
prompt: "Explain this design.",
|
||||
providerOptions: { effort: "high" },
|
||||
})
|
||||
```
|
||||
|
||||
The products have distinct provider identities and endpoints:
|
||||
|
||||
| Provider | Selector | Default base URL |
|
||||
| ----------------------------------- | --------------------------- | ------------------------------------- |
|
||||
| `ZAI` (`zai`) | `.model`, `.chat`, `.image` | `https://api.z.ai/api/paas/v4` |
|
||||
| `ZAICodingPlan` (`zai-coding-plan`) | `.model`, `.chat` | `https://api.z.ai/api/coding/paas/v4` |
|
||||
| `ZAICodingPlan` | `.messages` | `https://api.z.ai/api/anthropic/v1` |
|
||||
| `ZAICodingPlan` | `.responses` | `https://api.z.ai/api/v1` |
|
||||
|
||||
Both read `ZAI_API_KEY` when `apiKey` is omitted and support an explicit `auth` override.
|
||||
Coding Plan requires an active subscription. `baseURL` overrides the selected API's
|
||||
complete base, including its version prefix. Language-model routes use HTTP/SSE.
|
||||
|
||||
Options retain the selected API's native semantics:
|
||||
|
||||
- Chat `reasoningEffort` lowers to `reasoning_effort`; Responses lowers it to `reasoning.effort`.
|
||||
Messages `effort` lowers to `output_config.effort`. Omission preserves provider defaults.
|
||||
- Chat `thinking` passes `type` and `clear_thinking` through unchanged. Set
|
||||
`clear_thinking: false` and replay complete `response.message` values to preserve reasoning
|
||||
across user messages and tool loops. The standard API defaults to clearing historical thinking;
|
||||
Coding Plan documents preservation by default.
|
||||
- Messages accepts `thinking: { type: "enabled" | "adaptive" | "disabled" }` without requiring
|
||||
an Anthropic token budget. Coding Plan documents a disabled toggle as low-effort thinking
|
||||
for GLM-5.3, with explicit effort taking precedence.
|
||||
- Chat also offers `toolStream`, `doSample`, `responseFormat`, `requestID`, and `userID`.
|
||||
Tool-argument streaming is enabled when tools are present on GLM-4.6/4.7/5.x;
|
||||
`toolStream: false` explicitly disables it. Older model families omit the opt-in.
|
||||
- Effort and thinking values remain forward-compatible strings. Their meaning is model-specific:
|
||||
GLM-5.3 accepts `low`, `high`, and `max` effort and rejects disabled thinking with HTTP 400;
|
||||
the direct GLM-5.2 recordings returned reasoning even with `none` and `minimal` effort,
|
||||
whereas explicit `thinking.type: "disabled"` disabled it on GLM-5.2 and GLM-4.7.
|
||||
|
||||
Standard API recordings cover GLM-5.3 efforts and a full preserved-reasoning tool loop with
|
||||
a subsequent user follow-up, GLM-5.2 efforts, older-model thinking toggles, GLM-4.5 tool calls,
|
||||
GLM-5.3-Flash image input, and JSON output. Coding Plan has unit coverage for routing,
|
||||
request options, and reasoning replay; successful live recordings are pending.
|
||||
|
||||
Package entrypoints are `@opencode/ai/providers/zai`, `zai/chat`, `zai-coding-plan`,
|
||||
`zai-coding-plan/chat`, `zai-coding-plan/messages`, and `zai-coding-plan/responses`.
|
||||
|
||||
## Moonshot
|
||||
|
||||
Moonshot defaults to Chat Completions, with Messages and Responses selectors for Kimi K3:
|
||||
|
||||
```ts
|
||||
import { LLM } from "@opencode/ai"
|
||||
import { Moonshot } from "@opencode/ai/providers"
|
||||
|
||||
const moonshot = Moonshot.configure({ apiKey: process.env.MOONSHOT_API_KEY })
|
||||
|
||||
const request = LLM.request({
|
||||
model: moonshot.model("kimi-k3"), // also moonshot.chat("kimi-k3")
|
||||
prompt: "Explain the tradeoffs in this design.",
|
||||
providerOptions: { reasoningEffort: "high" },
|
||||
})
|
||||
|
||||
const messages = LLM.request({
|
||||
model: moonshot.messages("kimi-k3"),
|
||||
prompt: "Explain the tradeoffs in this design.",
|
||||
providerOptions: { effort: "high" },
|
||||
})
|
||||
|
||||
const responses = LLM.request({
|
||||
model: moonshot.responses("kimi-k3"),
|
||||
prompt: "Explain the tradeoffs in this design.",
|
||||
providerOptions: { reasoningEffort: "high" },
|
||||
})
|
||||
```
|
||||
|
||||
When `apiKey` is omitted, authentication reads `MOONSHOT_API_KEY`, then `MOONSHOTAI_API_KEY`.
|
||||
Chat and Responses use `https://api.moonshot.ai/v1`; Messages uses
|
||||
`https://api.moonshot.ai/anthropic/v1`. `baseURL` overrides the selected API's complete base,
|
||||
including the version prefix, for regional endpoints or gateways. Each endpoint requires its own valid credentials.
|
||||
All three routes use HTTP/SSE.
|
||||
|
||||
Reasoning options stay native to the selected API and model:
|
||||
|
||||
| Model/API | Provider options |
|
||||
| --------------------------- | --------------------------------------------------------------------------------------- |
|
||||
| K3 Chat / Responses | `reasoningEffort: "low" \| "high" \| "max"`; default is `max` |
|
||||
| K3 Messages | `effort: "low" \| "high" \| "max"`; default is `max` |
|
||||
| K2.6 Chat | `thinking: { type: "enabled" \| "disabled", keep?: "all" \| null }`; default is enabled |
|
||||
| K2.7 Code / high-speed Chat | Omit `thinking` to use always-on, preserved reasoning |
|
||||
|
||||
Omitting options preserves the model's defaults. K3 uses effort rather than the K2.x `thinking`
|
||||
parameter. Known effort values have autocomplete while future strings remain accepted.
|
||||
For K2.6, `thinking.keep: "all"` enables preservation of reasoning across user messages.
|
||||
K3 and both K2.7 Code variants always preserve reasoning. Continue with the returned
|
||||
`response.message` and matching tool results so reasoning content and any Messages signatures are retained.
|
||||
Leave sampling options such as `temperature` unset to use these models' fixed defaults.
|
||||
|
||||
The recorded suite covers all three K3 APIs, default and explicit efforts, K2.6 thinking modes,
|
||||
both K2.7 Code variants, generated tool loops with a subsequent user follow-up, required/disabled
|
||||
tool choice, image-byte input, and native structured output through `http.body` overlays.
|
||||
K3 Chat and Messages accept required and disabled tool choice. Responses supports automatic tool
|
||||
choice only; explicit `required` and `none` produce a provider `InvalidRequest` error, also covered by recordings.
|
||||
The provider targets the Moonshot Open Platform; Kimi Code is a separate product and endpoint.
|
||||
|
||||
Package entrypoints are `@opencode/ai/providers/moonshot`, `moonshot/chat`, `moonshot/messages`,
|
||||
and `moonshot/responses`; each exports `model(modelID, settings)`.
|
||||
|
||||
## MiniMax
|
||||
|
||||
MiniMax defaults to its Messages API and reads `MINIMAX_API_KEY` when `apiKey` is omitted:
|
||||
|
||||
```ts
|
||||
import { Effect, Layer } from "effect"
|
||||
import { LLM, LLMClient } from "@opencode/ai"
|
||||
import { MiniMax } from "@opencode/ai/providers"
|
||||
import { RequestExecutor } from "@opencode/ai/route"
|
||||
|
||||
const minimax = MiniMax.configure({ apiKey: process.env.MINIMAX_API_KEY })
|
||||
const request = LLM.request({
|
||||
model: minimax.model("MiniMax-M3"), // also minimax.messages("MiniMax-M3")
|
||||
prompt: "What is 173 multiplied by 219?",
|
||||
providerOptions: { thinking: { type: "adaptive" } },
|
||||
generation: { maxTokens: 1536 },
|
||||
})
|
||||
|
||||
const layer = LLMClient.layer.pipe(Layer.provide(RequestExecutor.fetchLayer))
|
||||
const response = await Effect.runPromise(LLMClient.generate(request).pipe(Effect.provide(layer)))
|
||||
console.log(response.text)
|
||||
```
|
||||
|
||||
Select `minimax.chat("MiniMax-M3")` or `minimax.responses("MiniMax-M3")` for MiniMax's native Chat Completions
|
||||
and Responses APIs. The matching package entrypoints are `@opencode/ai/providers/minimax/messages`,
|
||||
`@opencode/ai/providers/minimax/chat`, and `@opencode/ai/providers/minimax/responses`.
|
||||
|
||||
- **Messages:** M3 thinking defaults off. Set `thinking: { type: "adaptive" }` to enable it or
|
||||
`thinking: { type: "disabled" }` to disable it.
|
||||
- **Chat:** M3 thinking defaults on and uses the same `thinking` control. The provider enables `reasoning_split`
|
||||
by default so reasoning is separate from answer text; `reasoningSplit: false` selects native `<think>`-tagged text.
|
||||
- **Responses:** M3 reasoning defaults off. `reasoningEffort: "none"` disables it; `"minimal"`, `"low"`,
|
||||
`"medium"`, and `"high"` enable reasoning without changing its depth.
|
||||
|
||||
M2.x models always think, even when a disabling option is supplied. For tool continuations, retain the complete
|
||||
`response.message` in history before adding `Message.tool(...)` results; this preserves reasoning and any signatures.
|
||||
|
||||
The default API bases are `https://api.minimax.io/anthropic/v1` for Messages and `https://api.minimax.io/v1` for
|
||||
Chat and Responses. `configure({ baseURL })` replaces the selected API's base, including its version prefix.
|
||||
|
||||
## Meta
|
||||
|
||||
Use Meta's direct [Model API](https://dev.meta.ai/docs/overview) with `META_API_KEY`:
|
||||
|
||||
```ts
|
||||
import { Meta } from "@opencode/ai/providers"
|
||||
|
||||
const meta = Meta.configure() // or Meta.configure({ apiKey })
|
||||
const request = LLM.request({
|
||||
model: meta.responses("muse-spark-1.3"), // meta.model(...) also selects Responses
|
||||
prompt: "What is 173 multiplied by 219? Reply with the integer.",
|
||||
providerOptions: { reasoningEffort: "low" },
|
||||
generation: { maxTokens: 1024 },
|
||||
})
|
||||
```
|
||||
|
||||
`meta.chat("muse-spark-1.3")` selects Chat Completions; `meta.messages("muse-spark-1.3")` selects
|
||||
the Anthropic-compatible Messages API. All use `https://api.meta.ai/v1`. The package entrypoints
|
||||
`@opencode/ai/providers/meta/responses`, `meta/chat`, and `meta/messages` expose `model(modelID, settings)`.
|
||||
|
||||
[Muse Spark](https://dev.meta.ai/docs/models) supports `minimal`, `low`, `medium`, `high`, and
|
||||
`xhigh` reasoning effort; standard-tier 1.3 also supports `max`. Omitting effort uses the model's
|
||||
default. Muse Spark always reasons and rejects `none`. The output-token budget includes private reasoning.
|
||||
|
||||
Responses defaults to `store: false` and `include: ["reasoning.encrypted_content"]`. Preserve
|
||||
`response.message` along with matching `Message.tool(...)` results in subsequent requests to replay
|
||||
reasoning through tool loops. Optional `reasoningSummary: "auto"` requests a readable summary.
|
||||
For server-managed history, override `store: true, include: []` and send the response ID through
|
||||
`http: { body: { previous_response_id: responseID } }` with only the new input.
|
||||
Chat Completions redacts private reasoning and cannot carry it between calls.
|
||||
Responses and Chat support only `toolChoice: "auto"` (the default). Messages also accepts `"none"`;
|
||||
its documented forced `"any"` choice currently returns HTTP 400. Messages defaults to adaptive thinking
|
||||
with `display: "omitted"`, preserving encrypted `redacted_thinking` in `response.message`. Use
|
||||
`providerOptions: { effort: "low" }` for depth or `thinking: { type: "enabled", budgetTokens: 1024 }`
|
||||
for budget compatibility (with `generation.maxTokens > 1024`).
|
||||
|
||||
Add `tools: [Meta.webSearch()]` to a Spark Responses or Messages request for hosted web search.
|
||||
Responses exposes hosted results and URL citations in text-part `providerMetadata.meta.annotations`.
|
||||
To include search result lists, set `include: ["reasoning.encrypted_content", "web_search_call.results"]`.
|
||||
Messages exposes hosted search calls; the recorded Messages API stream does not supply structured
|
||||
citations or separate result blocks. Retain `response.message` for either API's continuation.
|
||||
|
||||
Use `Image.generate` for one-off generation or editing:
|
||||
|
||||
```ts
|
||||
import { Image, ImageInput } from "@opencode/ai"
|
||||
|
||||
const generation = Image.generate({
|
||||
model: meta.image("muse-image-1.0"),
|
||||
prompt: "A flat black square on a white background.",
|
||||
options: { n: 1, reasoningStrength: "low" },
|
||||
})
|
||||
|
||||
const edit = Image.generate({
|
||||
model: meta.image("muse-image-1.0"),
|
||||
prompt: "Make the square purple.",
|
||||
images: [ImageInput.bytes(imageBytes, "image/webp")],
|
||||
options: { outputFormat: "png", reasoningStrength: "low" },
|
||||
})
|
||||
```
|
||||
|
||||
The default image format is WEBP; `outputFormat` also accepts PNG/JPEG and `responseFormat: "url"`
|
||||
returns a signed URL. `size` is an aspect-ratio hint. For conversational images, select
|
||||
`meta.responses("muse-image-1.0")` with `tools: [Meta.imageGeneration({ reasoningStrength: "low" })]`.
|
||||
Generated images are provider-executed tool results with file content. Retain `response.message` to
|
||||
replay the signed image handle on the next request. Muse Image accepts only the `image_generation` tool.
|
||||
|
||||
Meta Responses is explicitly HTTP/SSE-only and does not use WebSockets, even when a caller supplies
|
||||
`StreamOptions.webSocket`. The public `/v1/responses` endpoint rejects WebSocket upgrades with HTTP 405 (`Allow: POST`).
|
||||
|
||||
## Image generation
|
||||
|
||||
Use `Image.generate` with an image model for direct asset generation:
|
||||
|
||||
```ts
|
||||
import { Image, ImageInput } from "@opencode-ai/ai"
|
||||
import { OpenAI } from "@opencode-ai/ai/providers"
|
||||
import { Image, ImageInput } from "@opencode/ai"
|
||||
import { OpenAI } from "@opencode/ai/providers"
|
||||
|
||||
const program = Effect.gen(function* () {
|
||||
const response = yield* Image.generate({
|
||||
@@ -131,7 +441,7 @@ yield *
|
||||
Google's current Gemini image models use the same direct API:
|
||||
|
||||
```ts
|
||||
import { Google } from "@opencode-ai/ai/providers"
|
||||
import { Google } from "@opencode/ai/providers"
|
||||
|
||||
const googleProgram = Effect.gen(function* () {
|
||||
const response = yield* Image.generate({
|
||||
@@ -207,12 +517,12 @@ The hosted result is represented as a provider-executed tool call and tool resul
|
||||
|
||||
## Testing
|
||||
|
||||
Use the deterministic test client from `@opencode-ai/ai/testing` to script provider-neutral responses and inspect
|
||||
Use the deterministic test client from `@opencode/ai/testing` to script provider-neutral responses and inspect
|
||||
the requests sent by code under test:
|
||||
|
||||
```ts
|
||||
import { Effect } from "effect"
|
||||
import { TestLLM } from "@opencode-ai/ai/testing"
|
||||
import { TestLLM } from "@opencode/ai/testing"
|
||||
|
||||
const programWithTestClient = Effect.gen(function* () {
|
||||
const test = yield* TestLLM.Test
|
||||
@@ -323,8 +633,8 @@ This capability describes protocol implementation, **not universal availability
|
||||
Inside an `Effect.gen`, enable OpenAI compaction with typed provider options:
|
||||
|
||||
```ts
|
||||
import { LLM, LLMClient, LLMRequest, Message } from "@opencode-ai/ai"
|
||||
import { OpenAI } from "@opencode-ai/ai/providers"
|
||||
import { LLM, LLMClient, LLMRequest, Message } from "@opencode/ai"
|
||||
import { OpenAI } from "@opencode/ai/providers"
|
||||
|
||||
const request = LLM.request({
|
||||
model: OpenAI.configure({ apiKey }).responses("gpt-5.3-codex"),
|
||||
@@ -344,7 +654,7 @@ const next = LLMRequest.update(request, {
|
||||
A compaction part has `provider` and exactly one representation: `encrypted` for Responses, or `text` for Anthropic. Responses also preserves the optional checkpoint `id`. These fields survive message serialization without becoming visible assistant text. Sending a checkpoint to another provider or an incompatible API fails rather than silently losing context.
|
||||
|
||||
```ts
|
||||
import { CompactionPart, ProviderID } from "@opencode-ai/ai"
|
||||
import { CompactionPart, ProviderID } from "@opencode/ai"
|
||||
|
||||
CompactionPart.make({ provider: ProviderID.make("openai"), id: "cmp_123", encrypted: "..." })
|
||||
CompactionPart.make({ provider: ProviderID.make("anthropic"), text: "Summary of the conversation..." })
|
||||
@@ -450,7 +760,7 @@ Normalized cache usage is read back into `response.usage.cacheReadInputTokens` a
|
||||
Provider facades configure endpoint/auth/deployment details first, then expose model selectors that take only a model or deployment id. The selected model carries the executable route value used at runtime.
|
||||
|
||||
```ts
|
||||
import { OpenAI, CloudflareAIGateway } from "@opencode-ai/ai/providers"
|
||||
import { OpenAI, CloudflareAIGateway } from "@opencode/ai/providers"
|
||||
|
||||
const openai = OpenAI.configure({ apiKey: process.env.OPENAI_API_KEY }).responses("gpt-4o-mini")
|
||||
const gateway = CloudflareAIGateway.configure({
|
||||
@@ -464,7 +774,7 @@ Included LLM providers: OpenAI, Anthropic, Google (Gemini), Google Vertex, Amazo
|
||||
Each named provider owns its module, endpoint, authentication, and route setup. Providers with the same wire format compose the shared protocol directly:
|
||||
|
||||
```ts
|
||||
import { DeepSeek, Fireworks } from "@opencode-ai/ai/providers"
|
||||
import { DeepSeek, Fireworks } from "@opencode/ai/providers"
|
||||
|
||||
const deepseek = DeepSeek.configure({ apiKey }).model("deepseek-chat")
|
||||
const fireworks = Fireworks.configure({ apiKey }).model("accounts/fireworks/models/my-model")
|
||||
@@ -474,10 +784,10 @@ The former `OpenAICompatible.baseten`, `.cerebras`, `.deepinfra`, `.deepseek`, `
|
||||
|
||||
### Provider entrypoints
|
||||
|
||||
Provider modules are available through dedicated exports from `@opencode-ai/ai`. Each LLM entrypoint exports `model(modelID, settings)`, where `settings` contains provider configuration plus common `headers` and `body` overlays.
|
||||
Provider modules are available through dedicated exports from `@opencode/ai`. Each LLM entrypoint exports `model(modelID, settings)`, where `settings` contains provider configuration plus common `headers` and `body` overlays.
|
||||
|
||||
```ts
|
||||
import { model } from "@opencode-ai/ai/providers/openai/responses"
|
||||
import { model } from "@opencode/ai/providers/openai/responses"
|
||||
|
||||
const selected = model("gpt-5", {
|
||||
apiKey: process.env.OPENAI_API_KEY,
|
||||
@@ -487,14 +797,14 @@ const selected = model("gpt-5", {
|
||||
|
||||
APIs have separate entrypoints:
|
||||
|
||||
- `@opencode-ai/ai/providers/openai/chat`
|
||||
- `@opencode-ai/ai/providers/openai/responses`
|
||||
- `@opencode-ai/ai/providers/openai-compatible/responses`
|
||||
- `@opencode-ai/ai/providers/anthropic-compatible`
|
||||
- `@opencode-ai/ai/providers/google-vertex/gemini`
|
||||
- `@opencode-ai/ai/providers/google-vertex/chat`
|
||||
- `@opencode-ai/ai/providers/google-vertex/responses`
|
||||
- `@opencode-ai/ai/providers/google-vertex/messages`
|
||||
- `@opencode/ai/providers/openai/chat`
|
||||
- `@opencode/ai/providers/openai/responses`
|
||||
- `@opencode/ai/providers/openai-compatible/responses`
|
||||
- `@opencode/ai/providers/anthropic-compatible`
|
||||
- `@opencode/ai/providers/google-vertex/gemini`
|
||||
- `@opencode/ai/providers/google-vertex/chat`
|
||||
- `@opencode/ai/providers/google-vertex/responses`
|
||||
- `@opencode/ai/providers/google-vertex/messages`
|
||||
|
||||
OpenAI Responses has one semantic route and uses HTTP by default. Advanced callers may supply a per-call WebSocket channel executor through `StreamOptions`; transport policy does not change provider settings, model identity, or route identity. The provider-neutral Open Responses implementation owns the reusable WebSocket request and event contract, while each provider opts in with its own handshake and connection policy. Azure follows the same Chat/Responses split at `providers/azure/chat` and `providers/azure/responses`. Generic OpenAI-compatible Chat remains at `providers/openai-compatible`; the Responses adapter at `providers/openai-compatible/responses` uses the provider-neutral Open Responses protocol. OpenAI Responses extends that baseline with OpenAI tools, event variants, metadata, and defaults. Generic Anthropic Messages-compatible providers use `providers/anthropic-compatible`, which the named Anthropic provider composes. Google Gemini and Amazon Bedrock expose their single native API through their existing provider paths.
|
||||
|
||||
@@ -503,65 +813,36 @@ Vertex Gemini, Vertex Chat, Vertex Responses, and Vertex Messages are separate A
|
||||
Tuned Vertex Gemini deployments use model ids shaped like `endpoints/1234567890` and require OAuth or ADC; Vertex express-mode API keys support publisher models only.
|
||||
|
||||
```ts
|
||||
import { model } from "@opencode-ai/ai/providers/google-vertex/gemini"
|
||||
import { model } from "@opencode/ai/providers/google-vertex/gemini"
|
||||
|
||||
model("gemini-3.5-flash", { project: "my-project", location: "global" })
|
||||
```
|
||||
|
||||
```ts
|
||||
import { model } from "@opencode-ai/ai/providers/google-vertex/chat"
|
||||
import { model } from "@opencode/ai/providers/google-vertex/chat"
|
||||
|
||||
model("deepseek-ai/deepseek-v3.2-maas", { project: "my-project", location: "global" })
|
||||
```
|
||||
|
||||
```ts
|
||||
import { model } from "@opencode-ai/ai/providers/google-vertex/responses"
|
||||
import { model } from "@opencode/ai/providers/google-vertex/responses"
|
||||
|
||||
model("xai/grok-4.20-reasoning", { project: "my-project", location: "global" })
|
||||
```
|
||||
|
||||
```ts
|
||||
import { model } from "@opencode-ai/ai/providers/google-vertex/messages"
|
||||
import { model } from "@opencode/ai/providers/google-vertex/messages"
|
||||
|
||||
model("claude-sonnet-4-6", { project: "my-project", location: "global" })
|
||||
```
|
||||
|
||||
Additional provider entrypoints include:
|
||||
|
||||
- `@opencode-ai/ai/providers/baseten`
|
||||
- `@opencode-ai/ai/providers/deepseek`
|
||||
- `@opencode-ai/ai/providers/fireworks`
|
||||
- `@opencode-ai/ai/providers/cloudflare-ai-gateway`
|
||||
- `@opencode-ai/ai/providers/cloudflare-workers-ai`
|
||||
|
||||
### Route-keyed metadata
|
||||
|
||||
LLM messages, events, and usage retain provider-specific information under `providerMetadata[model.route.id]`. The provider ID identifies the provider; the route ID identifies its API and owns the metadata needed to replay that API's responses.
|
||||
|
||||
```ts
|
||||
const model = GoogleVertexMessages.configure({ project, accessToken }).model("claude-sonnet-4-5")
|
||||
const response = yield * LLM.generate(LLM.request({ model, prompt: "Think through this problem." }))
|
||||
const reasoning = response.message.content.find((part) => part.type === "reasoning")
|
||||
const signature = reasoning?.providerMetadata?.[model.route.id]?.signature
|
||||
// model.route.id === "google-vertex-messages"
|
||||
```
|
||||
|
||||
The former `providerMetadataKey` route option has been removed. Existing application-owned LLM metadata must use route keys instead of provider names, for example:
|
||||
|
||||
| API | Metadata key |
|
||||
| ------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------ |
|
||||
| OpenAI Chat / Responses | `openai-chat` / `openai-responses` |
|
||||
| Azure Chat / Responses | `azure-openai-chat` / `azure-openai-responses` |
|
||||
| Anthropic Messages | `anthropic-messages` |
|
||||
| Google Gemini | `gemini` |
|
||||
| Vertex Gemini / Chat / Responses / Messages | `google-vertex-gemini` / `google-vertex-chat` / `google-vertex-responses` / `google-vertex-messages` |
|
||||
| Bedrock Converse / Mantle Chat / Mantle Responses | `bedrock-converse` / `bedrock-mantle-chat` / `bedrock-mantle-responses` |
|
||||
| Groq / Mistral | `groq-chat` / `mistral-chat` |
|
||||
| xAI Chat / Responses | `xai-chat` / `xai-responses` |
|
||||
| Baseten / Cerebras / DeepInfra / DeepSeek / Fireworks / TogetherAI | `baseten-chat` / `cerebras-chat` / `deepinfra-chat` / `deepseek-chat` / `fireworks-chat` / `togetherai-chat` |
|
||||
| Generic Chat / Responses | `openai-compatible-chat` / `openai-compatible-responses` |
|
||||
|
||||
OpenRouter and Cloudflare already use matching provider and route IDs. Generic Messages uses `anthropic-messages`. Custom routes use their own `id`.
|
||||
- `@opencode/ai/providers/baseten`
|
||||
- `@opencode/ai/providers/deepseek`
|
||||
- `@opencode/ai/providers/fireworks`
|
||||
- `@opencode/ai/providers/cloudflare-ai-gateway`
|
||||
- `@opencode/ai/providers/cloudflare-workers-ai`
|
||||
|
||||
## Provider options & HTTP overlays
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Config, Effect, Formatter, Layer, Schema, Stream } from "effect"
|
||||
import { LLM, LLMClient, LLMRequest, Message, ProviderID, Tool, ToolRuntime } from "@opencode-ai/ai"
|
||||
import { Route, Auth, Endpoint, Framing, Protocol, RequestExecutor } from "@opencode-ai/ai/route"
|
||||
import { OpenAI } from "@opencode-ai/ai/providers"
|
||||
import { LLM, LLMClient, LLMRequest, Message, ProviderID, Tool, ToolRuntime } from "@opencode/ai"
|
||||
import { Route, Auth, Endpoint, Framing, Protocol, RequestExecutor } from "@opencode/ai/route"
|
||||
import { OpenAI } from "@opencode/ai/providers"
|
||||
|
||||
/**
|
||||
* A runnable walkthrough of the LLM package use-site API.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.17.20",
|
||||
"name": "@opencode-ai/ai",
|
||||
"name": "@opencode/ai",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
@@ -21,7 +21,7 @@
|
||||
"devDependencies": {
|
||||
"@clack/prompts": "1.0.0-alpha.1",
|
||||
"@effect/platform-node": "catalog:",
|
||||
"@opencode-ai/http-recorder": "workspace:*",
|
||||
"@opencode/http-recorder": "workspace:*",
|
||||
"@tsconfig/bun": "catalog:",
|
||||
"@types/bun": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
@@ -31,7 +31,7 @@
|
||||
"@aws-sdk/credential-providers": "3.1057.0",
|
||||
"@smithy/eventstream-codec": "4.2.14",
|
||||
"@smithy/util-utf8": "4.2.2",
|
||||
"@opencode-ai/schema": "workspace:*",
|
||||
"@opencode/schema": "workspace:*",
|
||||
"aws4fetch": "1.0.20",
|
||||
"effect": "catalog:",
|
||||
"google-auth-library": "10.5.0"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env bun
|
||||
import { Script } from "@opencode-ai/script"
|
||||
import { Script } from "@opencode/script"
|
||||
import { $ } from "bun"
|
||||
import { fileURLToPath } from "url"
|
||||
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import { Effect, Schema } from "effect"
|
||||
import { Protocol } from "../route/protocol.js"
|
||||
import type { LanguageModelCompatibility } from "../schema/index.js"
|
||||
import { OpenAIChat } from "./openai-chat.js"
|
||||
import { JsonObject, ProviderShared } from "./shared.js"
|
||||
import { OpenResponsesOptions } from "./utils/open-responses-options.js"
|
||||
|
||||
export type ReasoningEffort = OpenResponsesOptions.ReasoningEffort
|
||||
|
||||
const Options = Schema.Struct({
|
||||
reasoningEffort: OpenResponsesOptions.Options.fields.reasoningEffort,
|
||||
enableThinking: Schema.optional(Schema.Boolean),
|
||||
thinkingBudget: Schema.optional(Schema.Int),
|
||||
preserveThinking: Schema.optional(Schema.Boolean),
|
||||
clearThinking: Schema.optional(Schema.Boolean),
|
||||
thinking: Schema.optional(
|
||||
Schema.Struct({
|
||||
type: Schema.declare<"adaptive" | "disabled" | (string & {})>(Schema.is(Schema.String)),
|
||||
}),
|
||||
),
|
||||
toolStream: Schema.optional(Schema.Boolean),
|
||||
parallelToolCalls: OpenResponsesOptions.Options.fields.parallelToolCalls,
|
||||
repetitionPenalty: Schema.optional(Schema.Number),
|
||||
responseFormat: Schema.optional(
|
||||
Schema.Struct({
|
||||
type: Schema.declare<"text" | "json_object" | "json_schema" | (string & {})>(Schema.is(Schema.String)),
|
||||
json_schema: Schema.optional(JsonObject),
|
||||
}),
|
||||
),
|
||||
enableSearch: Schema.optional(Schema.Boolean),
|
||||
searchOptions: Schema.optional(
|
||||
Schema.Struct({
|
||||
forced_search: Schema.optional(Schema.Boolean),
|
||||
search_strategy: Schema.optional(
|
||||
Schema.declare<"turbo" | "max" | "agent" | "agent_max" | (string & {})>(Schema.is(Schema.String)),
|
||||
),
|
||||
enable_search_extension: Schema.optional(Schema.Boolean),
|
||||
}),
|
||||
),
|
||||
})
|
||||
export type OptionsInput = typeof Options.Type
|
||||
|
||||
export const compatibility = {
|
||||
maxTokensField: "max_completion_tokens",
|
||||
supportsStore: false,
|
||||
supportsStrictMode: false,
|
||||
reasoningField: "reasoning_content",
|
||||
zaiToolStream: false,
|
||||
} satisfies LanguageModelCompatibility
|
||||
|
||||
export const protocol = Protocol.make({
|
||||
id: "alibaba-chat",
|
||||
body: {
|
||||
schema: Schema.Struct({
|
||||
...OpenAIChat.bodyFields,
|
||||
enable_thinking: Options.fields.enableThinking,
|
||||
thinking_budget: Options.fields.thinkingBudget,
|
||||
preserve_thinking: Options.fields.preserveThinking,
|
||||
clear_thinking: Options.fields.clearThinking,
|
||||
thinking: Options.fields.thinking,
|
||||
parallel_tool_calls: Options.fields.parallelToolCalls,
|
||||
repetition_penalty: Options.fields.repetitionPenalty,
|
||||
top_k: Schema.optional(Schema.Int),
|
||||
response_format: Options.fields.responseFormat,
|
||||
enable_search: Options.fields.enableSearch,
|
||||
search_options: Options.fields.searchOptions,
|
||||
}),
|
||||
from: Effect.fn("AlibabaChat.fromRequest")(function* (req) {
|
||||
const opts = yield* ProviderShared.validateWith(Schema.decodeUnknownEffect(Options))(req.providerOptions ?? {})
|
||||
return {
|
||||
...(yield* OpenAIChat.protocol.body.from(req)),
|
||||
enable_thinking: opts.enableThinking,
|
||||
thinking_budget: opts.thinkingBudget,
|
||||
preserve_thinking: opts.preserveThinking,
|
||||
clear_thinking: opts.clearThinking,
|
||||
thinking: opts.thinking,
|
||||
tool_stream: opts.toolStream,
|
||||
parallel_tool_calls:
|
||||
opts.parallelToolCalls ??
|
||||
(req.toolChoice?.disableParallelToolUse === undefined ? undefined : !req.toolChoice.disableParallelToolUse),
|
||||
repetition_penalty: opts.repetitionPenalty,
|
||||
top_k: req.generation?.topK,
|
||||
response_format: opts.responseFormat,
|
||||
enable_search: opts.enableSearch,
|
||||
search_options: opts.searchOptions,
|
||||
}
|
||||
}),
|
||||
},
|
||||
stream: OpenAIChat.protocol.stream,
|
||||
})
|
||||
|
||||
export * as AlibabaChat from "./alibaba-chat.js"
|
||||
@@ -0,0 +1,48 @@
|
||||
import { Effect, Schema } from "effect"
|
||||
import { Protocol } from "../route/protocol.js"
|
||||
import { LLMRequest } from "../schema/index.js"
|
||||
import { AnthropicMessages } from "./anthropic-messages.js"
|
||||
import { ProviderShared } from "./shared.js"
|
||||
import { OpenResponsesOptions } from "./utils/open-responses-options.js"
|
||||
|
||||
const Options = Schema.Struct({
|
||||
effort: Schema.optional(OpenResponsesOptions.ReasoningEffort),
|
||||
thinking: Schema.optional(
|
||||
Schema.Struct({
|
||||
type: Schema.declare<"enabled" | "disabled" | (string & {})>(Schema.is(Schema.String)),
|
||||
budgetTokens: Schema.optional(Schema.Int),
|
||||
budget_tokens: Schema.optional(Schema.Int),
|
||||
}),
|
||||
),
|
||||
})
|
||||
export type OptionsInput = typeof Options.Type & Pick<AnthropicMessages.OptionsInput, "outputConfig">
|
||||
export const protocol = Protocol.make({
|
||||
id: "alibaba-messages",
|
||||
body: {
|
||||
schema: Schema.Struct({
|
||||
...AnthropicMessages.AnthropicMessagesBody.fields,
|
||||
thinking: Schema.optional(Schema.Struct({ type: Schema.String, budget_tokens: Schema.optional(Schema.Int) })),
|
||||
}),
|
||||
from: Effect.fn("AlibabaMessages.fromRequest")(function* (req) {
|
||||
const opts = yield* ProviderShared.validateWith(Schema.decodeUnknownEffect(Options))(req.providerOptions ?? {})
|
||||
// Model Studio accepts enabled thinking without Anthropic's mandatory token budget.
|
||||
return {
|
||||
...(yield* AnthropicMessages.protocol.body.from(
|
||||
LLMRequest.update(req, {
|
||||
providerOptions: { ...req.providerOptions, thinking: undefined },
|
||||
}),
|
||||
)),
|
||||
thinking:
|
||||
opts.thinking === undefined
|
||||
? undefined
|
||||
: {
|
||||
type: opts.thinking.type,
|
||||
budget_tokens: opts.thinking.budgetTokens ?? opts.thinking.budget_tokens,
|
||||
},
|
||||
}
|
||||
}),
|
||||
},
|
||||
stream: AnthropicMessages.protocol.stream,
|
||||
})
|
||||
|
||||
export * as AlibabaMessages from "./alibaba-messages.js"
|
||||
@@ -0,0 +1,98 @@
|
||||
import { Effect, Schema } from "effect"
|
||||
import { Protocol } from "../route/protocol.js"
|
||||
import { OpenResponses } from "./open-responses.js"
|
||||
import { JsonObject, optionalArray, ProviderShared } from "./shared.js"
|
||||
import { OpenResponsesOptions } from "./utils/open-responses-options.js"
|
||||
import { ResponsesHostedTools } from "./utils/responses-hosted-tools.js"
|
||||
|
||||
const Options = Schema.Struct({
|
||||
reasoningEffort: OpenResponsesOptions.Options.fields.reasoningEffort,
|
||||
enableThinking: Schema.optional(Schema.Boolean),
|
||||
store: OpenResponsesOptions.Options.fields.store,
|
||||
previousResponseId: Schema.optional(Schema.String),
|
||||
conversation: Schema.optional(Schema.String),
|
||||
})
|
||||
export type OptionsInput = typeof Options.Type
|
||||
const NativeTool = Schema.Struct({ type: Schema.Literals(["web_search", "web_extractor", "code_interpreter"]) })
|
||||
const WebExtractorItem = Schema.StructWithRest(
|
||||
Schema.Struct({
|
||||
type: Schema.Literal("web_extractor_call"),
|
||||
id: Schema.String,
|
||||
urls: Schema.optional(Schema.Array(Schema.String)),
|
||||
goal: Schema.optional(Schema.String),
|
||||
}),
|
||||
[JsonObject],
|
||||
)
|
||||
const Body = Schema.Struct({
|
||||
...OpenResponses.coreFields,
|
||||
input: Schema.Array(Schema.Union([OpenResponses.InputItem, WebExtractorItem])),
|
||||
tools: optionalArray(Schema.Union([OpenResponses.Tool, NativeTool])),
|
||||
enable_thinking: Options.fields.enableThinking,
|
||||
previous_response_id: Options.fields.previousResponseId,
|
||||
conversation: Options.fields.conversation,
|
||||
stream: Schema.Literal(true),
|
||||
})
|
||||
const adapter = {
|
||||
id: "alibaba-responses",
|
||||
name: "Alibaba Responses",
|
||||
nativeTool: (native) => ProviderShared.validateWith(Schema.decodeUnknownEffect(NativeTool))(native.alibaba),
|
||||
restoreHostedToolItem: (item: unknown) => (Schema.is(WebExtractorItem)(item) ? item : undefined),
|
||||
} satisfies OpenResponses.ProviderAdapter
|
||||
|
||||
const tools = {
|
||||
web_search_call: { name: "web_search", input: (item) => item.action ?? {} },
|
||||
code_interpreter_call: { name: "code_interpreter", input: (item) => ({ code: item.code }) },
|
||||
} satisfies ResponsesHostedTools.Definitions
|
||||
|
||||
export const protocol = Protocol.make({
|
||||
id: adapter.id,
|
||||
body: {
|
||||
schema: Body,
|
||||
from: Effect.fn("AlibabaResponses.fromRequest")(function* (req) {
|
||||
const opts = yield* ProviderShared.validateWith(Schema.decodeUnknownEffect(Options))(req.providerOptions ?? {})
|
||||
const body = yield* OpenResponses.fromRequestWithAdapter(req, adapter)
|
||||
const choice = body.tool_choice
|
||||
return yield* ProviderShared.validateWith(Schema.decodeUnknownEffect(Body))({
|
||||
...body,
|
||||
enable_thinking: opts.enableThinking,
|
||||
previous_response_id: opts.previousResponseId,
|
||||
conversation: opts.conversation,
|
||||
// Model Studio expresses named selection through allowed_tools.
|
||||
tool_choice:
|
||||
typeof choice === "object" && choice.type === "function"
|
||||
? { type: "allowed_tools" as const, mode: "required" as const, tools: [choice] }
|
||||
: choice,
|
||||
})
|
||||
}),
|
||||
},
|
||||
stream: {
|
||||
event: OpenResponses.protocol.stream.event,
|
||||
initial: (req) => OpenResponses.initial(req, adapter),
|
||||
step: (state, input) =>
|
||||
Effect.gen(function* () {
|
||||
const event = OpenResponses.normalize(state, input)
|
||||
if (event.type !== "response.output_item.done" || !event.item) return yield* OpenResponses.step(state, event)
|
||||
if (event.item.type === "web_extractor_call") {
|
||||
const item = yield* Schema.decodeUnknownEffect(WebExtractorItem)(event.item).pipe(
|
||||
Effect.mapError((cause) =>
|
||||
ProviderShared.eventError(
|
||||
adapter.id,
|
||||
"Alibaba returned an invalid web extraction item",
|
||||
ProviderShared.encodeJson(event),
|
||||
cause,
|
||||
),
|
||||
),
|
||||
)
|
||||
return yield* ResponsesHostedTools.onDone(state, item, {
|
||||
web_extractor_call: { name: "web_extractor", input: () => ({ urls: item.urls, goal: item.goal }) },
|
||||
})
|
||||
}
|
||||
if (ResponsesHostedTools.isItem(event.item, tools))
|
||||
return yield* ResponsesHostedTools.onDone(state, event.item, tools)
|
||||
return yield* OpenResponses.step(state, event)
|
||||
}),
|
||||
terminal: OpenResponses.terminal,
|
||||
},
|
||||
})
|
||||
|
||||
export * as AlibabaResponses from "./alibaba-responses.js"
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Buffer } from "node:buffer"
|
||||
import { Effect, Option, Schema } from "effect"
|
||||
import { Tool } from "@opencode-ai/schema/tool"
|
||||
import { Tool } from "@opencode/schema/tool"
|
||||
import { Route } from "../route/client.js"
|
||||
import { Auth } from "../route/auth.js"
|
||||
import { Endpoint } from "../route/endpoint.js"
|
||||
@@ -50,15 +50,24 @@ const SSE_EVENTS = new Set([
|
||||
])
|
||||
export const framing = Framing.sseEvents(SSE_EVENTS)
|
||||
|
||||
export type ThinkingBlockBinding = {
|
||||
readonly prefix_mismatch_behavior?: "error" | "drop_block" | (string & {})
|
||||
}
|
||||
|
||||
export type ThinkingInput =
|
||||
| {
|
||||
readonly type: "adaptive"
|
||||
readonly display?: "summarized" | "omitted"
|
||||
readonly block_binding?: ThinkingBlockBinding
|
||||
}
|
||||
| {
|
||||
readonly type: "disabled"
|
||||
}
|
||||
| ({ readonly type: "enabled"; readonly display?: "summarized" | "omitted" } & (
|
||||
| ({
|
||||
readonly type: "enabled"
|
||||
readonly display?: "summarized" | "omitted"
|
||||
readonly block_binding?: ThinkingBlockBinding
|
||||
} & (
|
||||
| { readonly budgetTokens: number; readonly budget_tokens?: number }
|
||||
| { readonly budgetTokens?: number; readonly budget_tokens: number }
|
||||
))
|
||||
@@ -301,20 +310,27 @@ const AnthropicToolChoice = Schema.Union([
|
||||
}),
|
||||
])
|
||||
|
||||
const AnthropicThinkingBlockBinding = Schema.Struct({
|
||||
prefix_mismatch_behavior: Schema.optional(Schema.String),
|
||||
})
|
||||
|
||||
const AnthropicThinking = Schema.Union([
|
||||
Schema.Struct({
|
||||
type: Schema.tag("enabled"),
|
||||
budget_tokens: Schema.Number,
|
||||
display: Schema.optional(Schema.Literals(["summarized", "omitted"])),
|
||||
block_binding: Schema.optional(AnthropicThinkingBlockBinding),
|
||||
}),
|
||||
Schema.Struct({
|
||||
type: Schema.tag("adaptive"),
|
||||
display: Schema.optional(Schema.Literals(["summarized", "omitted"])),
|
||||
block_binding: Schema.optional(AnthropicThinkingBlockBinding),
|
||||
}),
|
||||
Schema.Struct({
|
||||
type: Schema.tag("disabled"),
|
||||
}),
|
||||
])
|
||||
type AnthropicThinking = typeof AnthropicThinking.Type
|
||||
|
||||
// SDK OutputConfig:2684 {effort?: "low"|"medium"|"high"|"xhigh"|"max"|null, format?: JSONOutputFormat:2399}
|
||||
const AnthropicJsonOutputFormat = Schema.Struct({
|
||||
@@ -455,7 +471,7 @@ type AnthropicEvent = Schema.Schema.Type<typeof AnthropicEvent>
|
||||
interface ParserState {
|
||||
readonly provider: LLMRequest["model"]["provider"]
|
||||
readonly compactions: Readonly<Record<number, string | null>>
|
||||
readonly routeID: string
|
||||
readonly providerMetadataKey: string
|
||||
readonly tools: ToolStream.State<number>
|
||||
readonly reasoningSignatures: Readonly<Record<number, string>>
|
||||
readonly usage?: Usage
|
||||
@@ -563,14 +579,14 @@ const serverToolResultType = (name: string): AnthropicServerToolResultType | und
|
||||
|
||||
const lowerServerToolResult = Effect.fn("AnthropicMessages.lowerServerToolResult")(function* (
|
||||
part: ToolResultPart,
|
||||
routeID: string,
|
||||
providerMetadataKey: string,
|
||||
) {
|
||||
const wireType = serverToolResultType(part.name)
|
||||
if (!wireType)
|
||||
return yield* invalid(`Anthropic Messages does not know how to round-trip server tool result for ${part.name}`)
|
||||
// Prefer the provider-owned replay payload; fall back to the result value for
|
||||
// histories constructed directly from provider events.
|
||||
const payload = part.providerMetadata?.[routeID]?.["result"] ?? part.result.value
|
||||
const payload = part.providerMetadata?.[providerMetadataKey]?.["result"] ?? part.result.value
|
||||
return {
|
||||
type: wireType,
|
||||
tool_use_id: scrubToolCallID(part.id),
|
||||
@@ -857,7 +873,7 @@ const lowerMessages = Effect.fn("AnthropicMessages.lowerMessages")(function* (
|
||||
breakpoints: Cache.Breakpoints,
|
||||
) {
|
||||
const messages: AnthropicMessage[] = []
|
||||
const routeID = request.model.route.id
|
||||
const providerMetadataKey = request.model.route.providerMetadataKey ?? String(request.model.provider)
|
||||
|
||||
for (const [index, message] of request.messages.entries()) {
|
||||
if (message.role === "system") {
|
||||
@@ -911,8 +927,8 @@ const lowerMessages = Effect.fn("AnthropicMessages.lowerMessages")(function* (
|
||||
if (part.type === "reasoning") {
|
||||
// A signature marks visible thinking; only signature-less parts carrying
|
||||
// redactedData round-trip as opaque redacted_thinking blocks.
|
||||
const signature = part.encrypted ?? signatureFromMetadata(part.providerMetadata, routeID)
|
||||
const redactedData = redactedDataFromMetadata(part.providerMetadata, routeID)
|
||||
const signature = part.encrypted ?? signatureFromMetadata(part.providerMetadata, providerMetadataKey)
|
||||
const redactedData = redactedDataFromMetadata(part.providerMetadata, providerMetadataKey)
|
||||
if (signature === undefined && redactedData !== undefined) {
|
||||
content.push({ type: "redacted_thinking", data: redactedData })
|
||||
continue
|
||||
@@ -941,7 +957,7 @@ const lowerMessages = Effect.fn("AnthropicMessages.lowerMessages")(function* (
|
||||
continue
|
||||
}
|
||||
if (part.type === "tool-result" && part.providerExecuted) {
|
||||
content.push(yield* lowerServerToolResult(part, routeID))
|
||||
content.push(yield* lowerServerToolResult(part, providerMetadataKey))
|
||||
continue
|
||||
}
|
||||
return yield* invalid(
|
||||
@@ -1025,8 +1041,9 @@ const resolveOptions = Effect.fn("AnthropicMessages.resolveOptions")(function* (
|
||||
...(outputConfigEffort === undefined ? {} : { effort: outputConfigEffort }),
|
||||
...(outputConfigFormat === undefined ? {} : { format: outputConfigFormat }),
|
||||
}
|
||||
const thinking = yield* resolveThinking(input?.thinking)
|
||||
return {
|
||||
thinking: yield* resolveThinking(input?.thinking),
|
||||
thinking: applyThinkingBindingDefault(request.model, thinking),
|
||||
effort: outputConfigEffort,
|
||||
output_config,
|
||||
service_tier,
|
||||
@@ -1037,15 +1054,41 @@ const resolveOptions = Effect.fn("AnthropicMessages.resolveOptions")(function* (
|
||||
}
|
||||
})
|
||||
|
||||
const supportsThinkingBlockBinding = (model: LLMRequest["model"]) => {
|
||||
const override = model.compatibility?.supportsThinkingBlockBinding
|
||||
if (override !== undefined) return override
|
||||
// Accept gateway namespaces and Vertex suffixes without treating a snapshot date as a minor version.
|
||||
const version = /(?:^|[./])claude-[a-z]+-(?<major>\d+)(?:[.-](?<minor>\d{1,2}))?(?:$|[-:@])/i.exec(model.id)?.groups
|
||||
if (!version) return false
|
||||
const major = Number(version.major)
|
||||
const minor = Number(version.minor ?? 0)
|
||||
return major > 5 || (major === 5 && minor >= 1)
|
||||
}
|
||||
|
||||
const applyThinkingBindingDefault = (model: LLMRequest["model"], thinking: AnthropicThinking | undefined) => {
|
||||
if (thinking?.type === "disabled") return thinking
|
||||
if (!supportsThinkingBlockBinding(model)) return thinking
|
||||
return {
|
||||
...(thinking ?? { type: "adaptive" as const }),
|
||||
block_binding: {
|
||||
prefix_mismatch_behavior: "drop_block",
|
||||
...thinking?.block_binding,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const resolveThinking = Effect.fn("AnthropicMessages.resolveThinking")(function* (input: unknown) {
|
||||
if (!ProviderShared.isRecord(input)) return undefined
|
||||
if (input.type === "disabled") return { type: "disabled" as const }
|
||||
if (input.type !== "adaptive" && input.type !== "enabled") return undefined
|
||||
const block_binding = yield* ProviderShared.validateWith(
|
||||
Schema.decodeUnknownEffect(Schema.UndefinedOr(AnthropicThinkingBlockBinding)),
|
||||
)(input.block_binding)
|
||||
const display =
|
||||
input.display === "summarized" || input.display === "omitted"
|
||||
? (input.display as "summarized" | "omitted")
|
||||
: undefined
|
||||
if (input.type === "adaptive") return { type: "adaptive" as const, ...(display === undefined ? {} : { display }) }
|
||||
if (input.type === "disabled") return { type: "disabled" as const }
|
||||
if (input.type !== "enabled") return undefined
|
||||
if (input.type === "adaptive") return { type: "adaptive" as const, display, block_binding }
|
||||
const budget =
|
||||
typeof input.budgetTokens === "number"
|
||||
? input.budgetTokens
|
||||
@@ -1054,7 +1097,7 @@ const resolveThinking = Effect.fn("AnthropicMessages.resolveThinking")(function*
|
||||
: undefined
|
||||
if (budget === undefined)
|
||||
return yield* ProviderShared.invalidRequest("Anthropic thinking provider option requires budgetTokens")
|
||||
return { type: "enabled" as const, budget_tokens: budget, ...(display === undefined ? {} : { display }) }
|
||||
return { type: "enabled" as const, budget_tokens: budget, display, block_binding }
|
||||
})
|
||||
|
||||
const fromRequest = Effect.fn("AnthropicMessages.fromRequest")(function* (request: LLMRequest) {
|
||||
@@ -1148,7 +1191,7 @@ const mapFinishReason = (reason: string | null | undefined): FinishReason => {
|
||||
// inclusive `inputTokens` the rest of the contract expects. Extended
|
||||
// thinking tokens are included in `output_tokens`; newer responses also
|
||||
// expose that subset through `output_tokens_details.thinking_tokens`.
|
||||
const mapUsage = (usage: AnthropicUsage | undefined, routeID: string): Usage | undefined => {
|
||||
const mapUsage = (usage: AnthropicUsage | undefined, providerMetadataKey: string): Usage | undefined => {
|
||||
if (!usage) return undefined
|
||||
const iterations = usage.iterations?.length ? usage.iterations : [usage]
|
||||
const last = usage.iterations?.at(-1)
|
||||
@@ -1175,7 +1218,7 @@ const mapUsage = (usage: AnthropicUsage | undefined, routeID: string): Usage | u
|
||||
cacheWriteInputTokens: cacheWrite,
|
||||
reasoningTokens: ProviderShared.sumTokens(...iterations.map((item) => item.output_tokens_details?.thinking_tokens)),
|
||||
totalTokens: ProviderShared.totalTokens(inputTokens, outputTokens, undefined),
|
||||
providerMetadata: { [routeID]: usage },
|
||||
providerMetadata: { [providerMetadataKey]: usage },
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1184,7 +1227,7 @@ const mapUsage = (usage: AnthropicUsage | undefined, routeID: string): Usage | u
|
||||
// field prefers `right` when defined, falls back to `left`. `inputTokens` is
|
||||
// recomputed from the merged breakdown so the inclusive total stays
|
||||
// consistent with `nonCached + cacheRead + cacheWrite`.
|
||||
const mergeUsage = (left: Usage | undefined, right: Usage | undefined, routeID: string) => {
|
||||
const mergeUsage = (left: Usage | undefined, right: Usage | undefined, providerMetadataKey: string) => {
|
||||
if (!left) return right
|
||||
if (!right) return left
|
||||
const nonCachedInputTokens = right.nonCachedInputTokens ?? left.nonCachedInputTokens
|
||||
@@ -1203,7 +1246,9 @@ const mergeUsage = (left: Usage | undefined, right: Usage | undefined, routeID:
|
||||
reasoningTokens,
|
||||
totalTokens: ProviderShared.totalTokens(inputTokens, outputTokens, undefined),
|
||||
providerMetadata: {
|
||||
[routeID]: mergeJsonRecords(left.providerMetadata?.[routeID], right.providerMetadata?.[routeID]) ?? {},
|
||||
[providerMetadataKey]:
|
||||
mergeJsonRecords(left.providerMetadata?.[providerMetadataKey], right.providerMetadata?.[providerMetadataKey]) ??
|
||||
{},
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -1221,7 +1266,7 @@ const SERVER_TOOL_RESULT_NAMES: Record<AnthropicServerToolResultType, string> =
|
||||
|
||||
const isServerToolResultType = (type: string): type is AnthropicServerToolResultType => type in SERVER_TOOL_RESULT_NAMES
|
||||
|
||||
const serverToolResultEvent = (block: AnthropicStreamBlock, routeID: string): LLMEvent | undefined => {
|
||||
const serverToolResultEvent = (block: AnthropicStreamBlock, providerMetadataKey: string): LLMEvent | undefined => {
|
||||
if (!block.type || !isServerToolResultType(block.type)) return undefined
|
||||
const errorPayload =
|
||||
typeof block.content === "object" && block.content !== null && "type" in block.content
|
||||
@@ -1235,7 +1280,7 @@ const serverToolResultEvent = (block: AnthropicStreamBlock, routeID: string): LL
|
||||
providerExecuted: true,
|
||||
// The complete payload is irreducible provider replay state: subsequent
|
||||
// stateless requests must round-trip the typed result block verbatim.
|
||||
providerMetadata: providerMetadata(routeID, { blockType: block.type, result: block.content }),
|
||||
providerMetadata: providerMetadata(providerMetadataKey, { blockType: block.type, result: block.content }),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1244,8 +1289,8 @@ type StepResult = readonly [ParserState, ReadonlyArray<LLMEvent>]
|
||||
const NO_EVENTS: StepResult["1"] = []
|
||||
|
||||
const onMessageStart = (state: ParserState, event: AnthropicEvent): StepResult => {
|
||||
const usage = mapUsage(event.message?.usage, state.routeID)
|
||||
return [usage ? { ...state, usage: mergeUsage(state.usage, usage, state.routeID) } : state, NO_EVENTS]
|
||||
const usage = mapUsage(event.message?.usage, state.providerMetadataKey)
|
||||
return [usage ? { ...state, usage: mergeUsage(state.usage, usage, state.providerMetadataKey) } : state, NO_EVENTS]
|
||||
}
|
||||
|
||||
const onContentBlockStart = (
|
||||
@@ -1297,7 +1342,9 @@ const onContentBlockStart = (
|
||||
const events: LLMEvent[] = []
|
||||
const id = `reasoning-${event.index ?? 0}`
|
||||
const metadata =
|
||||
block.signature === undefined ? undefined : providerMetadata(state.routeID, { signature: block.signature })
|
||||
block.signature === undefined
|
||||
? undefined
|
||||
: providerMetadata(state.providerMetadataKey, { signature: block.signature })
|
||||
const lifecycle = Lifecycle.reasoningStart(state.lifecycle, events, id, metadata)
|
||||
return [
|
||||
{
|
||||
@@ -1326,14 +1373,14 @@ const onContentBlockStart = (
|
||||
state.lifecycle,
|
||||
events,
|
||||
`reasoning-${event.index ?? 0}`,
|
||||
providerMetadata(state.routeID, { redactedData: block.data }),
|
||||
providerMetadata(state.providerMetadataKey, { redactedData: block.data }),
|
||||
),
|
||||
},
|
||||
events,
|
||||
]
|
||||
}
|
||||
|
||||
const result = serverToolResultEvent(block, state.routeID)
|
||||
const result = serverToolResultEvent(block, state.providerMetadataKey)
|
||||
if (!result) return [state, NO_EVENTS]
|
||||
const events: LLMEvent[] = []
|
||||
return [{ ...state, lifecycle: Lifecycle.stepStart(state.lifecycle, events) }, [...events, result]]
|
||||
@@ -1434,7 +1481,7 @@ const onContentBlockStop = Effect.fn("AnthropicMessages.onContentBlockStop")(fun
|
||||
Lifecycle.textEnd(state.lifecycle, events, `text-${event.index}`),
|
||||
events,
|
||||
`reasoning-${event.index}`,
|
||||
signature === undefined ? undefined : providerMetadata(state.routeID, { signature }),
|
||||
signature === undefined ? undefined : providerMetadata(state.providerMetadataKey, { signature }),
|
||||
)
|
||||
events.push(...resultEvents)
|
||||
const reasoningSignatures = { ...state.reasoningSignatures }
|
||||
@@ -1446,7 +1493,7 @@ const onMessageDelta = (
|
||||
state: ParserState,
|
||||
event: AnthropicEvent & { readonly delta?: AnthropicStreamDelta },
|
||||
): StepResult => {
|
||||
const usage = mergeUsage(state.usage, mapUsage(event.usage, state.routeID), state.routeID)
|
||||
const usage = mergeUsage(state.usage, mapUsage(event.usage, state.providerMetadataKey), state.providerMetadataKey)
|
||||
const pendingFinish = (() => {
|
||||
const stopReason = event.delta?.stop_reason
|
||||
if (stopReason === null || stopReason === undefined) return state.pendingFinish
|
||||
@@ -1455,7 +1502,7 @@ const onMessageDelta = (
|
||||
const finishMetadata =
|
||||
stopSequence === null || stopSequence === undefined
|
||||
? state.pendingFinish?.providerMetadata
|
||||
: providerMetadata(state.routeID, { stopSequence })
|
||||
: providerMetadata(state.providerMetadataKey, { stopSequence })
|
||||
return {
|
||||
reason: {
|
||||
normalized: mapFinishReason(stopReason),
|
||||
@@ -1483,7 +1530,12 @@ const onMessageStop = Effect.fn("AnthropicMessages.onMessageStop")(function* (st
|
||||
events.push(...result.events)
|
||||
const closed = Object.entries(state.reasoningSignatures).reduce(
|
||||
(current, [index, signature]) =>
|
||||
Lifecycle.reasoningEnd(current, events, `reasoning-${index}`, providerMetadata(state.routeID, { signature })),
|
||||
Lifecycle.reasoningEnd(
|
||||
current,
|
||||
events,
|
||||
`reasoning-${index}`,
|
||||
providerMetadata(state.providerMetadataKey, { signature }),
|
||||
),
|
||||
lifecycle,
|
||||
)
|
||||
const finished = Lifecycle.finish(closed, events, {
|
||||
@@ -1618,7 +1670,7 @@ export const protocol = Protocol.make({
|
||||
initial: (request) => ({
|
||||
provider: request.model.provider,
|
||||
compactions: {},
|
||||
routeID: request.model.route.id,
|
||||
providerMetadataKey: request.model.route.providerMetadataKey ?? String(request.model.provider),
|
||||
tools: ToolStream.empty<number>(),
|
||||
reasoningSignatures: {},
|
||||
lifecycle: Lifecycle.initial(),
|
||||
@@ -1627,24 +1679,21 @@ export const protocol = Protocol.make({
|
||||
},
|
||||
})
|
||||
|
||||
export const transport = <Body extends Pick<AnthropicMessagesBody, "messages" | "context_management">>() => {
|
||||
export const transport = <
|
||||
Body extends Pick<AnthropicMessagesBody, "messages" | "context_management" | "thinking">,
|
||||
>() => {
|
||||
const http = HttpTransport.httpJson<Body, string>({ framing })
|
||||
return {
|
||||
...http,
|
||||
prepare: (input: Parameters<typeof http.prepare>[0]) => {
|
||||
if (
|
||||
!input.body.context_management?.edits.length &&
|
||||
!input.body.messages.some((message) => message.content.some((block) => block.type === "compaction"))
|
||||
)
|
||||
return http.prepare(input)
|
||||
const requiredBetas = requiredBetaHeaders(input.body)
|
||||
if (requiredBetas.length === 0) return http.prepare(input)
|
||||
const headers = Headers.fromInput(input.request.http?.headers)
|
||||
const betas = new Set(
|
||||
(headers["anthropic-beta"] ?? "")
|
||||
.split(",")
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean),
|
||||
)
|
||||
betas.add("compact-2026-01-12")
|
||||
const existingBetas = (headers["anthropic-beta"] ?? "")
|
||||
.split(",")
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean)
|
||||
const betas = new Set([...existingBetas, ...requiredBetas])
|
||||
return http.prepare({
|
||||
...input,
|
||||
request: LLMRequest.update(input.request, {
|
||||
@@ -1658,9 +1707,24 @@ export const transport = <Body extends Pick<AnthropicMessagesBody, "messages" |
|
||||
}
|
||||
}
|
||||
|
||||
function requiredBetaHeaders(body: Pick<AnthropicMessagesBody, "messages" | "context_management" | "thinking">) {
|
||||
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"),
|
||||
)
|
||||
if (requestsCompaction || replaysCompaction) betas.push("compact-2026-01-12")
|
||||
|
||||
const thinking = body.thinking
|
||||
if (thinking && thinking.type !== "disabled" && thinking.block_binding)
|
||||
betas.push("thinking-binding-controls-2026-08-01")
|
||||
return betas
|
||||
}
|
||||
|
||||
export const route = Route.make({
|
||||
id: ADAPTER,
|
||||
provider: "anthropic",
|
||||
providerMetadataKey: "anthropic",
|
||||
protocol,
|
||||
endpoint: Endpoint.path((input) => (input.request.model.provider === "anthropic" ? `${PATH}?beta=true` : PATH), {
|
||||
baseURL: DEFAULT_BASE_URL,
|
||||
|
||||
@@ -260,14 +260,14 @@ const lowerToolChoice = (toolChoice: NonNullable<LLMRequest["toolChoice"]>) =>
|
||||
|
||||
const providerMetadata = (key: string, metadata: Record<string, unknown>): ProviderMetadata => ({ [key]: metadata })
|
||||
|
||||
const reasoningSignature = (part: ReasoningPart, routeID: string) => {
|
||||
const metadata = part.providerMetadata?.[routeID]
|
||||
const reasoningSignature = (part: ReasoningPart, providerMetadataKey: string) => {
|
||||
const metadata = part.providerMetadata?.[providerMetadataKey]
|
||||
if (part.encrypted !== undefined) return part.encrypted
|
||||
if (ProviderShared.isRecord(metadata) && typeof metadata.signature === "string") return metadata.signature
|
||||
}
|
||||
|
||||
const reasoningRedactedData = (part: ReasoningPart, routeID: string) => {
|
||||
const metadata = part.providerMetadata?.[routeID]
|
||||
const reasoningRedactedData = (part: ReasoningPart, providerMetadataKey: string) => {
|
||||
const metadata = part.providerMetadata?.[providerMetadataKey]
|
||||
if (ProviderShared.isRecord(metadata) && typeof metadata.redactedData === "string") return metadata.redactedData
|
||||
}
|
||||
|
||||
@@ -324,7 +324,7 @@ const lowerMessages = Effect.fn("BedrockConverse.lowerMessages")(function* (
|
||||
breakpoints: BedrockCache.Breakpoints,
|
||||
) {
|
||||
const messages: BedrockMessage[] = []
|
||||
const routeID = request.model.route.id
|
||||
const providerMetadataKey = request.model.route.providerMetadataKey ?? String(request.model.provider)
|
||||
|
||||
for (const message of request.messages) {
|
||||
if (message.role === "system") {
|
||||
@@ -372,8 +372,8 @@ const lowerMessages = Effect.fn("BedrockConverse.lowerMessages")(function* (
|
||||
continue
|
||||
}
|
||||
if (part.type === "reasoning") {
|
||||
const signature = reasoningSignature(part, routeID)
|
||||
const redactedData = reasoningRedactedData(part, routeID)
|
||||
const signature = reasoningSignature(part, providerMetadataKey)
|
||||
const redactedData = reasoningRedactedData(part, providerMetadataKey)
|
||||
if (signature === undefined && redactedData !== undefined) {
|
||||
content.push({ reasoningContent: { redactedContent: redactedData } })
|
||||
continue
|
||||
@@ -485,7 +485,7 @@ const mapFinishReason = (reason: string): FinishReason => {
|
||||
|
||||
// AWS reports inputTokens separately from cache reads and writes.
|
||||
// Bedrock does not break reasoning out of outputTokens for current models.
|
||||
const mapUsage = (usage: BedrockUsageSchema | undefined, routeID: string): Usage | undefined => {
|
||||
const mapUsage = (usage: BedrockUsageSchema | undefined, providerMetadataKey: string): Usage | undefined => {
|
||||
if (!usage) return undefined
|
||||
const inputTokens = ProviderShared.sumTokens(
|
||||
usage.inputTokens,
|
||||
@@ -499,12 +499,12 @@ const mapUsage = (usage: BedrockUsageSchema | undefined, routeID: string): Usage
|
||||
cacheReadInputTokens: usage.cacheReadInputTokens,
|
||||
cacheWriteInputTokens: usage.cacheWriteInputTokens,
|
||||
totalTokens: ProviderShared.totalTokens(inputTokens, usage.outputTokens, usage.totalTokens),
|
||||
providerMetadata: { [routeID]: usage },
|
||||
providerMetadata: { [providerMetadataKey]: usage },
|
||||
})
|
||||
}
|
||||
|
||||
interface ParserState {
|
||||
readonly routeID: string
|
||||
readonly providerMetadataKey: string
|
||||
readonly tools: ToolStream.State<number>
|
||||
// Bedrock splits the finish into `messageStop` (carries `stopReason`) and
|
||||
// `metadata` (carries usage). Hold both in state so `onHalt` can emit exactly
|
||||
@@ -587,8 +587,8 @@ const step = (state: ParserState, event: BedrockEvent) =>
|
||||
})()
|
||||
const redactedData = redactedChunks === undefined ? reasoning.data : encodeRedactedContent(redactedChunks)
|
||||
const metadata = (() => {
|
||||
if (reasoning.signature) return providerMetadata(state.routeID, { signature: reasoning.signature })
|
||||
if (redactedData !== undefined) return providerMetadata(state.routeID, { redactedData })
|
||||
if (reasoning.signature) return providerMetadata(state.providerMetadataKey, { signature: reasoning.signature })
|
||||
if (redactedData !== undefined) return providerMetadata(state.providerMetadataKey, { redactedData })
|
||||
})()
|
||||
const lifecycle = (() => {
|
||||
if (reasoning.text === undefined && metadata === undefined) return state.lifecycle
|
||||
@@ -640,10 +640,10 @@ const step = (state: ParserState, event: BedrockEvent) =>
|
||||
if (resultEvents.length) return Lifecycle.stepStart(state.lifecycle, events)
|
||||
const metadata = (() => {
|
||||
const signature = state.reasoningSignatures[index]
|
||||
if (signature) return providerMetadata(state.routeID, { signature })
|
||||
if (signature) return providerMetadata(state.providerMetadataKey, { signature })
|
||||
const redactedContent = state.reasoningRedactedContent[index]
|
||||
if (redactedContent)
|
||||
return providerMetadata(state.routeID, {
|
||||
return providerMetadata(state.providerMetadataKey, {
|
||||
redactedData: encodeRedactedContent(redactedContent),
|
||||
})
|
||||
})()
|
||||
@@ -697,7 +697,7 @@ const step = (state: ParserState, event: BedrockEvent) =>
|
||||
}
|
||||
|
||||
if (event.metadata) {
|
||||
const usage = mapUsage(event.metadata.usage, state.routeID) ?? state.usage
|
||||
const usage = mapUsage(event.metadata.usage, state.providerMetadataKey) ?? state.usage
|
||||
return [
|
||||
{
|
||||
...state,
|
||||
@@ -757,7 +757,7 @@ export const protocol = Protocol.make({
|
||||
stream: {
|
||||
event: BedrockEvent,
|
||||
initial: (request) => ({
|
||||
routeID: request.model.route.id,
|
||||
providerMetadataKey: request.model.route.providerMetadataKey ?? String(request.model.provider),
|
||||
tools: ToolStream.empty<number>(),
|
||||
finishReason: undefined,
|
||||
usage: undefined,
|
||||
@@ -774,6 +774,7 @@ export const protocol = Protocol.make({
|
||||
export const route = Route.make({
|
||||
id: ADAPTER,
|
||||
provider: "bedrock",
|
||||
providerMetadataKey: "bedrock",
|
||||
protocol,
|
||||
// Bedrock's URL embeds the region in the route endpoint host and the
|
||||
// validated modelId in the path. We read the validated body so the URL
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Effect, Option, Schema } from "effect"
|
||||
import { Tool } from "@opencode-ai/schema/tool"
|
||||
import { Tool } from "@opencode/schema/tool"
|
||||
import { Route } from "../route/client.js"
|
||||
import { Auth } from "../route/auth.js"
|
||||
import { Endpoint } from "../route/endpoint.js"
|
||||
@@ -232,7 +232,7 @@ type GeminiEvent = Schema.Schema.Type<typeof GeminiEvent>
|
||||
|
||||
interface ParserState {
|
||||
readonly route: string
|
||||
readonly routeID: string
|
||||
readonly providerMetadataKey: string
|
||||
readonly finishReason?: string
|
||||
readonly hasToolCalls: boolean
|
||||
readonly promptFeedback?: GeminiPromptFeedback
|
||||
@@ -309,7 +309,7 @@ const lowerToolCall = (part: ToolCallPart, omitIds: boolean, metadataKey: string
|
||||
|
||||
const lowerMessages = Effect.fn("Gemini.lowerMessages")(function* (request: LLMRequest) {
|
||||
const contents: GeminiContent[] = []
|
||||
const metadataKey = request.model.route.id
|
||||
const metadataKey = request.model.route.providerMetadataKey ?? String(request.model.provider)
|
||||
const omitCallIds = omitsFunctionCallIds(request.model.id)
|
||||
const legacyToolMedia = routesLegacyToolMedia(request.model.id)
|
||||
let pendingMedia: GeminiInlineDataPart[] | undefined
|
||||
@@ -583,7 +583,7 @@ const finish = (state: ParserState): ReadonlyArray<LLMEvent> => {
|
||||
state.reasoningId,
|
||||
state.reasoningSignature === undefined
|
||||
? undefined
|
||||
: providerMetadata(state.routeID, { thoughtSignature: state.reasoningSignature }),
|
||||
: providerMetadata(state.providerMetadataKey, { thoughtSignature: state.reasoningSignature }),
|
||||
)
|
||||
if (state.textId !== undefined)
|
||||
lifecycle = Lifecycle.textEnd(
|
||||
@@ -592,7 +592,7 @@ const finish = (state: ParserState): ReadonlyArray<LLMEvent> => {
|
||||
state.textId,
|
||||
state.textSignature === undefined
|
||||
? undefined
|
||||
: providerMetadata(state.routeID, { thoughtSignature: state.textSignature }),
|
||||
: providerMetadata(state.providerMetadataKey, { thoughtSignature: state.textSignature }),
|
||||
)
|
||||
Lifecycle.finish(lifecycle, events, {
|
||||
reason: {
|
||||
@@ -604,7 +604,7 @@ const finish = (state: ParserState): ReadonlyArray<LLMEvent> => {
|
||||
providerMetadata:
|
||||
state.promptFeedback === undefined
|
||||
? undefined
|
||||
: providerMetadata(state.routeID, { promptFeedback: state.promptFeedback }),
|
||||
: providerMetadata(state.providerMetadataKey, { promptFeedback: state.promptFeedback }),
|
||||
})
|
||||
return events
|
||||
}
|
||||
@@ -634,7 +634,9 @@ const step = (state: ParserState, event: GeminiEvent) => {
|
||||
const nextState = {
|
||||
...state,
|
||||
promptFeedback: event.promptFeedback ?? state.promptFeedback,
|
||||
usage: event.usageMetadata ? (mapUsage(event.usageMetadata, state.routeID) ?? state.usage) : state.usage,
|
||||
usage: event.usageMetadata
|
||||
? (mapUsage(event.usageMetadata, state.providerMetadataKey) ?? state.usage)
|
||||
: state.usage,
|
||||
}
|
||||
const candidate = event.candidates?.[0]
|
||||
if (candidate?.finishReason && mapFinishReason(candidate.finishReason, state.hasToolCalls) === "error")
|
||||
@@ -690,7 +692,9 @@ const step = (state: ParserState, event: GeminiEvent) => {
|
||||
lifecycle,
|
||||
events,
|
||||
textId,
|
||||
textSignature ? providerMetadata(state.routeID, { thoughtSignature: textSignature }) : undefined,
|
||||
textSignature
|
||||
? providerMetadata(state.providerMetadataKey, { thoughtSignature: textSignature })
|
||||
: undefined,
|
||||
)
|
||||
textId = undefined
|
||||
textSignature = undefined
|
||||
@@ -704,7 +708,7 @@ const step = (state: ParserState, event: GeminiEvent) => {
|
||||
events,
|
||||
reasoningId,
|
||||
part.text,
|
||||
signature ? providerMetadata(state.routeID, { thoughtSignature: signature }) : undefined,
|
||||
signature ? providerMetadata(state.providerMetadataKey, { thoughtSignature: signature }) : undefined,
|
||||
)
|
||||
continue
|
||||
}
|
||||
@@ -713,7 +717,9 @@ const step = (state: ParserState, event: GeminiEvent) => {
|
||||
lifecycle,
|
||||
events,
|
||||
reasoningId,
|
||||
reasoningSignature ? providerMetadata(state.routeID, { thoughtSignature: reasoningSignature }) : undefined,
|
||||
reasoningSignature
|
||||
? providerMetadata(state.providerMetadataKey, { thoughtSignature: reasoningSignature })
|
||||
: undefined,
|
||||
)
|
||||
reasoningId = undefined
|
||||
reasoningSignature = undefined
|
||||
@@ -727,7 +733,7 @@ const step = (state: ParserState, event: GeminiEvent) => {
|
||||
events,
|
||||
textId,
|
||||
part.text,
|
||||
textSignature ? providerMetadata(state.routeID, { thoughtSignature: textSignature }) : undefined,
|
||||
textSignature ? providerMetadata(state.providerMetadataKey, { thoughtSignature: textSignature }) : undefined,
|
||||
)
|
||||
textSignature = undefined
|
||||
continue
|
||||
@@ -748,7 +754,9 @@ const step = (state: ParserState, event: GeminiEvent) => {
|
||||
lifecycle,
|
||||
events,
|
||||
reasoningId,
|
||||
reasoningSignature ? providerMetadata(state.routeID, { thoughtSignature: reasoningSignature }) : undefined,
|
||||
reasoningSignature
|
||||
? providerMetadata(state.providerMetadataKey, { thoughtSignature: reasoningSignature })
|
||||
: undefined,
|
||||
)
|
||||
reasoningId = undefined
|
||||
reasoningSignature = undefined
|
||||
@@ -758,7 +766,7 @@ const step = (state: ParserState, event: GeminiEvent) => {
|
||||
lifecycle,
|
||||
events,
|
||||
textId,
|
||||
textSignature ? providerMetadata(state.routeID, { thoughtSignature: textSignature }) : undefined,
|
||||
textSignature ? providerMetadata(state.providerMetadataKey, { thoughtSignature: textSignature }) : undefined,
|
||||
)
|
||||
textId = undefined
|
||||
textSignature = undefined
|
||||
@@ -770,7 +778,7 @@ const step = (state: ParserState, event: GeminiEvent) => {
|
||||
name: part.functionCall.name,
|
||||
input,
|
||||
providerMetadata: part.thoughtSignature
|
||||
? providerMetadata(state.routeID, { thoughtSignature: part.thoughtSignature })
|
||||
? providerMetadata(state.providerMetadataKey, { thoughtSignature: part.thoughtSignature })
|
||||
: undefined,
|
||||
}),
|
||||
)
|
||||
@@ -813,7 +821,7 @@ export const protocol = Protocol.make({
|
||||
event: Protocol.jsonEvent(GeminiEvent),
|
||||
initial: (request) => ({
|
||||
route: `${request.model.provider}/${request.model.route.id}`,
|
||||
routeID: request.model.route.id,
|
||||
providerMetadataKey: request.model.route.providerMetadataKey ?? String(request.model.provider),
|
||||
hasToolCalls: false,
|
||||
lifecycle: Lifecycle.initial(),
|
||||
nextReasoningId: 0,
|
||||
@@ -827,6 +835,7 @@ export const protocol = Protocol.make({
|
||||
export const route = Route.make({
|
||||
id: ADAPTER,
|
||||
provider: "google",
|
||||
providerMetadataKey: "google",
|
||||
protocol,
|
||||
// Gemini's path embeds the model id and pins SSE framing at the URL level.
|
||||
endpoint: Endpoint.path(({ request }) => `/models/${request.model.id}:streamGenerateContent?alt=sse`, {
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
import { Effect, Encoding, Schema } from "effect"
|
||||
import { Headers, HttpClientRequest } from "effect/unstable/http"
|
||||
import { GeneratedImage, ImageModel, ImageResponse, type ImageRequestFor, type ImageRoute } from "../image.js"
|
||||
import { Auth } from "../route/auth.js"
|
||||
import { Usage, mergeHttpOptions, mergeJsonRecords, type HttpOptions } from "../schema/index.js"
|
||||
import { JsonObject, ProviderShared, optionalNull } from "./shared.js"
|
||||
import { ImageInputs } from "./utils/image-input.js"
|
||||
|
||||
type OpenString<Known extends string> = Known | (string & {})
|
||||
export type ImageOptions = {
|
||||
readonly n?: number
|
||||
/** Aspect ratio hint, not an exact output resolution. */
|
||||
readonly size?: string
|
||||
readonly outputFormat?: OpenString<"webp" | "png" | "jpeg">
|
||||
readonly responseFormat?: OpenString<"b64_json" | "url">
|
||||
readonly reasoningStrength?: OpenString<"low" | "high">
|
||||
readonly toolEnablement?: {
|
||||
readonly enable_image_search?: boolean
|
||||
readonly enable_web_search?: boolean
|
||||
readonly enable_shell?: boolean
|
||||
}
|
||||
readonly [key: string]: unknown
|
||||
}
|
||||
|
||||
const Body = Schema.StructWithRest(
|
||||
Schema.Struct({
|
||||
model: Schema.String,
|
||||
prompt: Schema.String,
|
||||
images: Schema.optional(Schema.Array(JsonObject)),
|
||||
n: Schema.optional(Schema.Number),
|
||||
size: Schema.optional(Schema.String),
|
||||
output_format: Schema.optional(Schema.String),
|
||||
response_format: Schema.optional(Schema.String),
|
||||
reasoning_strength: Schema.optional(Schema.String),
|
||||
tool_enablement: Schema.optional(Schema.Record(Schema.String, Schema.Boolean)),
|
||||
}),
|
||||
[JsonObject],
|
||||
)
|
||||
|
||||
const Response = Schema.Struct({
|
||||
data: Schema.Array(Schema.Struct({ b64_json: optionalNull(Schema.String), url: optionalNull(Schema.String) })),
|
||||
output_format: Schema.optional(Schema.String),
|
||||
usage: Schema.optional(
|
||||
Schema.Struct({
|
||||
input_tokens: Schema.optional(Schema.Number),
|
||||
output_tokens: Schema.optional(Schema.Number),
|
||||
total_tokens: Schema.optional(Schema.Number),
|
||||
}),
|
||||
),
|
||||
})
|
||||
|
||||
export const model = (input: {
|
||||
readonly id: string
|
||||
readonly auth: Auth.Definition
|
||||
readonly baseURL: string
|
||||
readonly headers?: Record<string, string>
|
||||
readonly http?: HttpOptions
|
||||
}) => {
|
||||
const route: ImageRoute<ImageOptions> = {
|
||||
id: "meta-images",
|
||||
generate: Effect.fn("MetaImages.generate")(function* (request: ImageRequestFor<ImageOptions>, execute) {
|
||||
const http = mergeHttpOptions(request.model.http, request.http)
|
||||
const images = yield* Effect.forEach(request.images ?? [], (image) => {
|
||||
if (image.type === "bytes") return Effect.succeed({ image_url: ImageInputs.dataUrl(image) })
|
||||
if (image.type === "url") return Effect.succeed({ image_url: image.url })
|
||||
return ImageInputs.invalid("Meta Images accepts image bytes and URLs")
|
||||
})
|
||||
const { outputFormat, responseFormat, reasoningStrength, toolEnablement, ...native } = request.options ?? {}
|
||||
const payload = yield* ProviderShared.validateWith(Schema.decodeUnknownEffect(Body))(
|
||||
mergeJsonRecords(
|
||||
{
|
||||
model: request.model.id,
|
||||
prompt: request.prompt,
|
||||
images: images.length === 0 ? undefined : images,
|
||||
output_format: outputFormat,
|
||||
response_format: responseFormat,
|
||||
reasoning_strength: reasoningStrength,
|
||||
tool_enablement: toolEnablement,
|
||||
},
|
||||
native,
|
||||
http?.body,
|
||||
),
|
||||
)
|
||||
const body = ProviderShared.encodeJson(payload)
|
||||
const url = new URL(`${input.baseURL.replace(/\/$/, "")}/images/${images.length === 0 ? "generations" : "edits"}`)
|
||||
Object.entries(http?.query ?? {}).forEach(([key, value]) => url.searchParams.set(key, value))
|
||||
const headers = yield* Auth.toEffect(input.auth)({
|
||||
request,
|
||||
method: "POST",
|
||||
url: url.toString(),
|
||||
body,
|
||||
headers: Headers.fromInput({ ...input.headers, ...http?.headers }),
|
||||
})
|
||||
const response = yield* execute(
|
||||
HttpClientRequest.post(url.toString()).pipe(
|
||||
HttpClientRequest.setHeaders(headers),
|
||||
HttpClientRequest.bodyText(body, "application/json"),
|
||||
),
|
||||
)
|
||||
const output = yield* ProviderShared.imageResponse("meta-images", "Meta Images", response)
|
||||
const decoded = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(Response))(output.body).pipe(
|
||||
Effect.mapError((cause) => output.invalid("Meta Images returned an invalid response", cause)),
|
||||
)
|
||||
const format = decoded.output_format ?? payload.output_format ?? "webp"
|
||||
const generated = yield* Effect.forEach(decoded.data, (item, index) => {
|
||||
if (item.b64_json)
|
||||
return Effect.fromResult(Encoding.decodeBase64(item.b64_json)).pipe(
|
||||
Effect.mapError((cause) => output.invalid(`Meta Images result ${index} contains invalid base64`, cause)),
|
||||
Effect.map((data) => new GeneratedImage({ mediaType: `image/${format}`, data })),
|
||||
)
|
||||
if (item.url) return Effect.succeed(new GeneratedImage({ mediaType: `image/${format}`, data: item.url }))
|
||||
return output.invalid(`Meta Images result ${index} has neither image data nor a URL`)
|
||||
})
|
||||
if (generated.length === 0) return yield* output.invalid("Meta Images returned no images")
|
||||
return new ImageResponse({
|
||||
images: generated,
|
||||
usage:
|
||||
decoded.usage === undefined
|
||||
? undefined
|
||||
: new Usage({
|
||||
inputTokens: decoded.usage.input_tokens,
|
||||
outputTokens: decoded.usage.output_tokens,
|
||||
totalTokens: decoded.usage.total_tokens,
|
||||
providerMetadata: { meta: decoded.usage },
|
||||
}),
|
||||
providerMetadata: { meta: { outputFormat: format } },
|
||||
})
|
||||
}),
|
||||
}
|
||||
return ImageModel.make<ImageOptions>({ id: input.id, provider: "meta", route, http: input.http })
|
||||
}
|
||||
|
||||
export * as MetaImages from "./meta-images.js"
|
||||
@@ -0,0 +1,52 @@
|
||||
import { Effect, Schema } from "effect"
|
||||
import { Protocol } from "../route/protocol.js"
|
||||
import type { LLMRequest } from "../schema/index.js"
|
||||
import { AnthropicMessages } from "./anthropic-messages.js"
|
||||
import { MetaResponses } from "./meta-responses.js"
|
||||
import { JsonObject, optionalArray, ProviderShared } from "./shared.js"
|
||||
|
||||
const WebSearch = Schema.Struct({
|
||||
type: Schema.Literal("web_search"),
|
||||
name: Schema.Literal("web_search"),
|
||||
user_location: MetaResponses.WebSearch.fields.user_location,
|
||||
})
|
||||
const Body = Schema.Struct({
|
||||
...AnthropicMessages.AnthropicMessagesBody.fields,
|
||||
tools: optionalArray(
|
||||
Schema.Union([
|
||||
Schema.Struct({ name: Schema.String, description: Schema.String, input_schema: JsonObject }),
|
||||
WebSearch,
|
||||
]),
|
||||
),
|
||||
})
|
||||
|
||||
const fromRequest = Effect.fn("MetaMessages.fromRequest")(function* (request: LLMRequest) {
|
||||
const projected = ProviderShared.flattenToolRequest(request)
|
||||
const body = yield* AnthropicMessages.protocol.body.from(projected.request)
|
||||
return {
|
||||
...body,
|
||||
tools:
|
||||
body.tools === undefined
|
||||
? undefined
|
||||
: yield* Effect.forEach(body.tools, (tool, index) =>
|
||||
Effect.gen(function* () {
|
||||
const native = projected.tools[index]?.native
|
||||
if (native === undefined) return tool
|
||||
const search = yield* ProviderShared.validateWith(Schema.decodeUnknownEffect(MetaResponses.WebSearch))(
|
||||
native.meta,
|
||||
)
|
||||
if (search.search_context_size !== undefined)
|
||||
return yield* ProviderShared.invalidRequest("Meta Messages does not support searchContextSize")
|
||||
return { type: "web_search" as const, name: "web_search" as const, user_location: search.user_location }
|
||||
}),
|
||||
),
|
||||
}
|
||||
})
|
||||
|
||||
export const protocol = Protocol.make({
|
||||
id: "meta-messages",
|
||||
body: { schema: Body, from: fromRequest },
|
||||
stream: AnthropicMessages.protocol.stream,
|
||||
})
|
||||
|
||||
export * as MetaMessages from "./meta-messages.js"
|
||||
@@ -0,0 +1,238 @@
|
||||
import { Effect, Encoding, Schema } from "effect"
|
||||
import { Protocol } from "../route/protocol.js"
|
||||
import { HttpTransport } from "../route/transport/index.js"
|
||||
import { LLMEvent, LLMRequest, Message, ToolResultPart } from "../schema/index.js"
|
||||
import { OpenResponses } from "./open-responses.js"
|
||||
import { JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared.js"
|
||||
import { ResponsesHostedTools } from "./utils/responses-hosted-tools.js"
|
||||
import { ToolSchemaProjection } from "./utils/tool-schema.js"
|
||||
import { MetaImage } from "./utils/meta-image.js"
|
||||
|
||||
const ADAPTER = "meta-responses"
|
||||
const NAME = "Meta Responses"
|
||||
|
||||
export const WebSearch = Schema.Struct({
|
||||
type: Schema.Literal("web_search"),
|
||||
search_context_size: Schema.optional(Schema.String),
|
||||
user_location: Schema.optional(
|
||||
Schema.Struct({
|
||||
type: Schema.Literal("approximate"),
|
||||
city: Schema.optional(Schema.String),
|
||||
region: Schema.optional(Schema.String),
|
||||
country: Schema.optional(Schema.String),
|
||||
timezone: Schema.optional(Schema.String),
|
||||
}),
|
||||
),
|
||||
})
|
||||
|
||||
export const ImageGeneration = Schema.Struct({
|
||||
type: Schema.Literal("image_generation"),
|
||||
size: Schema.optional(Schema.String),
|
||||
output_format: Schema.optional(Schema.String),
|
||||
reasoning_strength: Schema.optional(Schema.String),
|
||||
enable_image_search: Schema.optional(Schema.Boolean),
|
||||
enable_web_search: Schema.optional(Schema.Boolean),
|
||||
enable_shell: Schema.optional(Schema.Boolean),
|
||||
})
|
||||
|
||||
const NativeTool = Schema.Union([WebSearch, ImageGeneration])
|
||||
const ImageItem = Schema.Struct({
|
||||
type: Schema.Literal("image_generation_call"),
|
||||
id: Schema.String,
|
||||
status: Schema.optional(Schema.String),
|
||||
result: optionalNull(Schema.String),
|
||||
output_format: Schema.optional(Schema.String),
|
||||
error: Schema.optional(Schema.Unknown),
|
||||
})
|
||||
|
||||
const Body = Schema.Struct({
|
||||
...OpenResponses.coreFields,
|
||||
input: Schema.Array(Schema.Union([OpenResponses.InputItem, ImageItem])),
|
||||
tools: optionalArray(Schema.Union([OpenResponses.Tool, NativeTool])),
|
||||
stream: Schema.Literal(true),
|
||||
})
|
||||
|
||||
const MessageAnnotations = Schema.Struct({
|
||||
content: Schema.Array(Schema.Struct({ annotations: optionalArray(JsonObject) })),
|
||||
})
|
||||
|
||||
interface ParserState extends OpenResponses.ParserState {
|
||||
readonly completedItems: ReadonlySet<string>
|
||||
}
|
||||
|
||||
const adapter = {
|
||||
id: ADAPTER,
|
||||
name: NAME,
|
||||
restoreHostedToolItem: (item: unknown) => (Schema.is(ImageItem)(item) ? item : undefined),
|
||||
} satisfies OpenResponses.ProviderAdapter
|
||||
|
||||
const fromRequest = Effect.fn("MetaResponses.fromRequest")(function* (request: LLMRequest) {
|
||||
const key = request.model.route.providerMetadataKey ?? String(request.model.provider)
|
||||
const projected = ProviderShared.flattenToolRequest(
|
||||
LLMRequest.update(request, {
|
||||
messages: request.messages.map((message) =>
|
||||
Message.make({
|
||||
...message,
|
||||
content: message.content.map((part) => {
|
||||
if (
|
||||
part.type !== "tool-result" ||
|
||||
!part.providerExecuted ||
|
||||
part.name !== "image_generation" ||
|
||||
part.result.type !== "content" ||
|
||||
part.providerMetadata?.[key]?.itemId !== part.id
|
||||
)
|
||||
return part
|
||||
// Meta's signed image ID carries edit state; replay the handle, not the image bytes as a user message.
|
||||
return ToolResultPart.make({
|
||||
...part,
|
||||
result: {
|
||||
type: "json",
|
||||
value: { type: "image_generation_call", id: part.id, status: "completed", result: null },
|
||||
},
|
||||
})
|
||||
}),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
)
|
||||
return yield* ProviderShared.validateWith(Schema.decodeUnknownEffect(Body))({
|
||||
...(yield* OpenResponses.lowerConversation(projected.request, adapter)),
|
||||
...OpenResponses.lowerGeneration(request),
|
||||
tools:
|
||||
projected.tools.length === 0
|
||||
? undefined
|
||||
: yield* Effect.forEach(projected.tools, (tool) =>
|
||||
Effect.gen(function* () {
|
||||
if (tool.native === undefined)
|
||||
return yield* OpenResponses.lowerTool(
|
||||
NAME,
|
||||
tool,
|
||||
ToolSchemaProjection.modelCompatibility(tool.inputSchema, request.model.compatibility?.toolSchema),
|
||||
)
|
||||
return yield* ProviderShared.validateWith(Schema.decodeUnknownEffect(NativeTool))(tool.native.meta)
|
||||
}),
|
||||
),
|
||||
tool_choice:
|
||||
OpenResponses.allowedToolChoice(request) ??
|
||||
(request.toolChoice ? yield* OpenResponses.lowerToolChoice(NAME, request.toolChoice) : undefined),
|
||||
})
|
||||
})
|
||||
|
||||
const HOSTED_TOOLS = {
|
||||
web_search_call: { name: "web_search", input: (item) => item.action ?? {} },
|
||||
image_generation_call: {
|
||||
name: "image_generation",
|
||||
input: () => ({}),
|
||||
result: Effect.fn("MetaResponses.imageResult")(function* (raw: ResponsesHostedTools.Item) {
|
||||
const item = yield* Schema.decodeUnknownEffect(ImageItem)(raw).pipe(
|
||||
Effect.mapError((cause) =>
|
||||
ProviderShared.eventError(
|
||||
ADAPTER,
|
||||
"Meta returned an invalid image item",
|
||||
ProviderShared.encodeJson(raw),
|
||||
cause,
|
||||
),
|
||||
),
|
||||
)
|
||||
if (item.error !== undefined && item.error !== null) return { type: "error" as const, value: item.error }
|
||||
if (!item.result)
|
||||
return yield* ProviderShared.eventError(
|
||||
ADAPTER,
|
||||
"Meta returned an image without data",
|
||||
ProviderShared.encodeJson(raw),
|
||||
)
|
||||
const data = yield* Effect.fromResult(Encoding.decodeBase64(item.result)).pipe(
|
||||
Effect.mapError((cause) =>
|
||||
ProviderShared.eventError(
|
||||
ADAPTER,
|
||||
"Meta returned invalid image base64",
|
||||
ProviderShared.encodeJson(raw),
|
||||
cause,
|
||||
),
|
||||
),
|
||||
)
|
||||
const mime = MetaImage.mediaType(data, item.output_format)
|
||||
return {
|
||||
type: "content" as const,
|
||||
value: [{ type: "file" as const, uri: `data:${mime};base64,${item.result}`, mime }],
|
||||
}
|
||||
}),
|
||||
},
|
||||
} satisfies ResponsesHostedTools.Definitions
|
||||
|
||||
const onEvent = Effect.fn("MetaResponses.onEvent")(function* (
|
||||
state: OpenResponses.ParserState,
|
||||
input: OpenResponses.Event,
|
||||
) {
|
||||
const event = OpenResponses.normalize(state, input)
|
||||
if (event.type === "response.output_item.done" && event.item && ResponsesHostedTools.isItem(event.item, HOSTED_TOOLS))
|
||||
return yield* ResponsesHostedTools.onDone(state, event.item, HOSTED_TOOLS)
|
||||
const result = yield* OpenResponses.step(state, event)
|
||||
if (event.type !== "response.output_item.done" || event.item?.type !== "message") return result
|
||||
const message = yield* Schema.decodeUnknownEffect(MessageAnnotations)(event.item).pipe(
|
||||
Effect.mapError((cause) =>
|
||||
ProviderShared.eventError(
|
||||
ADAPTER,
|
||||
"Meta returned invalid message annotations",
|
||||
ProviderShared.encodeJson(event),
|
||||
cause,
|
||||
),
|
||||
),
|
||||
)
|
||||
const annotations = message.content.flatMap((part) => part.annotations ?? [])
|
||||
if (annotations.length === 0) return result
|
||||
return [
|
||||
result[0],
|
||||
result[1].map((item) =>
|
||||
LLMEvent.is.textEnd(item)
|
||||
? LLMEvent.textEnd({
|
||||
...item,
|
||||
providerMetadata: {
|
||||
...item.providerMetadata,
|
||||
[state.providerMetadataKey]: { ...item.providerMetadata?.[state.providerMetadataKey], annotations },
|
||||
},
|
||||
})
|
||||
: item,
|
||||
),
|
||||
] satisfies OpenResponses.StepResult
|
||||
})
|
||||
|
||||
const step = Effect.fn("MetaResponses.step")(function* (state: ParserState, input: OpenResponses.Event) {
|
||||
const completedItems = new Set(state.completedItems)
|
||||
const event = OpenResponses.normalize(state, input)
|
||||
if (event.type === "response.output_item.done" && event.item && completedItems.has(event.item.id))
|
||||
return [state, []] as const
|
||||
const events: LLMEvent[] = []
|
||||
let current: OpenResponses.ParserState = state
|
||||
// Muse Image delivers its image and optional summary only in response.completed.
|
||||
// Recover terminal-only items in order, without duplicating Spark's streamed items.
|
||||
if (event.type === "response.completed") {
|
||||
for (const [index, item] of (event.response?.output ?? []).entries()) {
|
||||
const done = OpenResponses.normalize(current, { type: "response.output_item.done", item, output_index: index })
|
||||
// Spark changes reasoning IDs in the terminal snapshot; output indices still identify the streamed items.
|
||||
if (!done.item || completedItems.has(done.item.id) || completedItems.has(state.outputItems[index] ?? "")) continue
|
||||
const result = yield* onEvent(current, done)
|
||||
current = result[0]
|
||||
events.push(...result[1])
|
||||
completedItems.add(done.item.id)
|
||||
}
|
||||
}
|
||||
const result = yield* onEvent(current, event)
|
||||
if (event.type === "response.output_item.done" && event.item) completedItems.add(event.item.id)
|
||||
return [{ ...result[0], completedItems }, [...events, ...result[1]]] as const
|
||||
})
|
||||
|
||||
export const protocol = Protocol.make({
|
||||
id: ADAPTER,
|
||||
body: { schema: Body, from: fromRequest },
|
||||
stream: {
|
||||
event: OpenResponses.protocol.stream.event,
|
||||
initial: (request): ParserState => ({ ...OpenResponses.initial(request, adapter), completedItems: new Set() }),
|
||||
step,
|
||||
terminal: OpenResponses.terminal,
|
||||
},
|
||||
})
|
||||
|
||||
export const httpTransport = HttpTransport.sseJson.with<Schema.Schema.Type<typeof Body>>()
|
||||
|
||||
export * as MetaResponses from "./meta-responses.js"
|
||||
@@ -287,10 +287,9 @@ const lowerAssistant = Effect.fn("MistralChat.lowerAssistant")(function* (
|
||||
message: LLMRequest["messages"][number],
|
||||
normalizeID: (id: string) => string,
|
||||
prefix: boolean,
|
||||
routeID: string,
|
||||
) {
|
||||
const structured = message.content.some(
|
||||
(part) => part.type === "reasoning" && isMistralThinkingContent(part.providerMetadata?.[routeID]?.thinking),
|
||||
(part) => part.type === "reasoning" && isMistralThinkingContent(part.providerMetadata?.mistral?.thinking),
|
||||
)
|
||||
const content: Array<Schema.Schema.Type<typeof MistralTextContent> | MistralThinkingContent> = []
|
||||
const text: string[] = []
|
||||
@@ -302,7 +301,7 @@ const lowerAssistant = Effect.fn("MistralChat.lowerAssistant")(function* (
|
||||
continue
|
||||
}
|
||||
if (part.type === "reasoning") {
|
||||
const native = part.providerMetadata?.[routeID]?.thinking
|
||||
const native = part.providerMetadata?.mistral?.thinking
|
||||
if (structured && isMistralThinkingContent(native)) content.push(native)
|
||||
else if (structured) content.push({ type: "text", text: part.text })
|
||||
else text.push(part.text)
|
||||
@@ -379,22 +378,13 @@ const lowerMessages = Effect.fn("MistralChat.lowerMessages")(function* (request:
|
||||
if (message.role === "assistant") {
|
||||
const hasToolCalls = message.content.some((part) => part.type === "tool-call")
|
||||
const hasNativeThinking = message.content.some(
|
||||
(part) =>
|
||||
part.type === "reasoning" &&
|
||||
isMistralThinkingContent(part.providerMetadata?.[request.model.route.id]?.thinking),
|
||||
(part) => part.type === "reasoning" && isMistralThinkingContent(part.providerMetadata?.mistral?.thinking),
|
||||
)
|
||||
const text = message.content
|
||||
.flatMap((part) => (part.type === "text" || part.type === "reasoning" ? [part.text] : []))
|
||||
.join("")
|
||||
if (!hasToolCalls && !hasNativeThinking && text.trim() === "") continue
|
||||
messages.push(
|
||||
yield* lowerAssistant(
|
||||
message,
|
||||
normalizeID,
|
||||
!hasToolCalls && message === request.messages.at(-1),
|
||||
request.model.route.id,
|
||||
),
|
||||
)
|
||||
messages.push(yield* lowerAssistant(message, normalizeID, !hasToolCalls && message === request.messages.at(-1)))
|
||||
continue
|
||||
}
|
||||
messages.push(...(yield* lowerToolResults(message, normalizeID)))
|
||||
@@ -465,7 +455,6 @@ interface ActiveContent {
|
||||
}
|
||||
|
||||
export interface ParserState {
|
||||
readonly routeID: string
|
||||
readonly tools: ToolStream.State<ToolKey>
|
||||
readonly pendingTools: Partial<Record<ToolKey, PendingTool>>
|
||||
readonly toolIDs: ReadonlyMap<string, string>
|
||||
@@ -480,7 +469,7 @@ export interface ParserState {
|
||||
readonly finishReason?: FinishReasonDetails
|
||||
}
|
||||
|
||||
const mapUsage = (usage: MistralEvent["usage"], routeID: string): Usage | undefined => {
|
||||
const mapUsage = (usage: MistralEvent["usage"]): Usage | undefined => {
|
||||
if (!usage) return undefined
|
||||
const input = usage.prompt_tokens ?? undefined
|
||||
const reported =
|
||||
@@ -496,7 +485,7 @@ const mapUsage = (usage: MistralEvent["usage"], routeID: string): Usage | undefi
|
||||
nonCachedInputTokens: ProviderShared.subtractTokens(input, cached),
|
||||
cacheReadInputTokens: cached,
|
||||
totalTokens: ProviderShared.totalTokens(input, output, usage.total_tokens ?? undefined),
|
||||
providerMetadata: { [routeID]: usage },
|
||||
providerMetadata: { mistral: usage },
|
||||
})
|
||||
}
|
||||
|
||||
@@ -528,7 +517,7 @@ const thinkingUnits = (value: unknown): ReadonlyArray<MistralThinkingUnit> => {
|
||||
const thinkingText = (thinking: ReadonlyArray<MistralThinkingUnit>) =>
|
||||
thinking.flatMap((unit) => (typeof unit.text === "string" ? [unit.text] : [])).join("")
|
||||
|
||||
const thinkingMetadata = (thinking: MistralThinkingContent, routeID: string) => ({ [routeID]: { thinking } })
|
||||
const thinkingMetadata = (thinking: MistralThinkingContent) => ({ mistral: { thinking } })
|
||||
|
||||
const closeActive = (state: ParserState, events: LLMEvent[]) => {
|
||||
if (!state.active) return state
|
||||
@@ -539,7 +528,7 @@ const closeActive = (state: ParserState, events: LLMEvent[]) => {
|
||||
state.lifecycle,
|
||||
events,
|
||||
state.active.id,
|
||||
thinkingMetadata(state.active.thinking ?? { type: "thinking", thinking: [] }, state.routeID),
|
||||
thinkingMetadata(state.active.thinking ?? { type: "thinking", thinking: [] }),
|
||||
thinkingText(state.active.thinking?.thinking ?? []),
|
||||
)
|
||||
return { ...state, lifecycle, active: undefined }
|
||||
@@ -572,14 +561,8 @@ const appendThinking = (state: ParserState, events: LLMEvent[], part: MistralOut
|
||||
...current,
|
||||
lifecycle:
|
||||
text.length > 0
|
||||
? Lifecycle.reasoningDelta(
|
||||
current.lifecycle,
|
||||
events,
|
||||
active.id,
|
||||
text,
|
||||
thinkingMetadata(thinking, state.routeID),
|
||||
)
|
||||
: Lifecycle.reasoningStart(current.lifecycle, events, active.id, thinkingMetadata(thinking, state.routeID)),
|
||||
? Lifecycle.reasoningDelta(current.lifecycle, events, active.id, text, thinkingMetadata(thinking))
|
||||
: Lifecycle.reasoningStart(current.lifecycle, events, active.id, thinkingMetadata(thinking)),
|
||||
active: { ...active, thinking },
|
||||
nextContent: current.active ? current.nextContent : current.nextContent + 1,
|
||||
}
|
||||
@@ -700,7 +683,7 @@ const step = Effect.fn("MistralChat.step")(function* (state: ParserState, event:
|
||||
})
|
||||
}
|
||||
const events: LLMEvent[] = []
|
||||
const usage = mapUsage(event.usage, state.routeID) ?? state.usage
|
||||
const usage = mapUsage(event.usage) ?? state.usage
|
||||
if (state.finishReason) {
|
||||
if (hasLateContent(event))
|
||||
return yield* ProviderShared.eventError(
|
||||
@@ -778,8 +761,7 @@ export const protocol = Protocol.make({
|
||||
body: { schema: MistralBody, from: fromRequest },
|
||||
stream: {
|
||||
event: MistralStreamEvent,
|
||||
initial: (request): ParserState => ({
|
||||
routeID: request.model.route.id,
|
||||
initial: (): ParserState => ({
|
||||
tools: ToolStream.empty<ToolKey>(),
|
||||
pendingTools: {},
|
||||
toolIDs: new Map(),
|
||||
@@ -801,6 +783,7 @@ export const httpTransport = HttpTransport.sseJson.with<MistralBody>().with({ fr
|
||||
export const route = Route.make({
|
||||
id: ADAPTER,
|
||||
provider: "mistral",
|
||||
providerMetadataKey: "mistral",
|
||||
protocol,
|
||||
endpoint: Endpoint.path(PATH, { baseURL: DEFAULT_BASE_URL }),
|
||||
auth: Auth.none,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Effect, Option, Schema } from "effect"
|
||||
import type { Content } from "@opencode-ai/schema/tool"
|
||||
import type { Content } from "@opencode/schema/tool"
|
||||
import { HttpTransport } from "../route/transport/index.js"
|
||||
import { Protocol } from "../route/protocol.js"
|
||||
import {
|
||||
@@ -408,6 +408,9 @@ export type NormalizedEvent = Event & { readonly item?: OutputItem | null }
|
||||
export interface ProviderAdapter {
|
||||
readonly id: string
|
||||
readonly name: string
|
||||
readonly nativeTool?: (
|
||||
native: NonNullable<ToolDefinition["native"]>,
|
||||
) => Effect.Effect<{ readonly type: string }, AIError>
|
||||
readonly lowerMedia?: (input: {
|
||||
readonly part: MediaPart
|
||||
readonly media: ProviderShared.NormalizedMedia
|
||||
@@ -423,7 +426,7 @@ export interface ParserState {
|
||||
readonly completedCompactions: ReadonlySet<string>
|
||||
readonly id: string
|
||||
readonly name: string
|
||||
readonly routeID: string
|
||||
readonly providerMetadataKey: string
|
||||
readonly tools: ToolStream.State<string>
|
||||
readonly hasFunctionCall: boolean
|
||||
readonly lifecycle: Lifecycle.State
|
||||
@@ -476,15 +479,15 @@ export const lowerToolChoice = (protocolName: string, toolChoice: NonNullable<LL
|
||||
|
||||
// Server-issued item ids need a nonempty prefix and suffix, but the prefix is
|
||||
// provider-defined and does not necessarily identify the item's semantic type.
|
||||
const itemID = (providerMetadata: ProviderMetadata | undefined, routeID: string) => {
|
||||
const metadata = providerMetadata?.[routeID]
|
||||
const itemID = (providerMetadata: ProviderMetadata | undefined, providerMetadataKey: string) => {
|
||||
const metadata = providerMetadata?.[providerMetadataKey]
|
||||
if (!ProviderShared.isRecord(metadata) || typeof metadata.itemId !== "string") return undefined
|
||||
const separator = metadata.itemId.indexOf("_")
|
||||
return separator > 0 && separator < metadata.itemId.length - 1 ? metadata.itemId : undefined
|
||||
}
|
||||
|
||||
const lowerToolCall = (part: ToolCallPart, routeID: string): OpenResponsesInputItem => {
|
||||
const id = itemID(part.providerMetadata, routeID)
|
||||
const lowerToolCall = (part: ToolCallPart, providerMetadataKey: string): OpenResponsesInputItem => {
|
||||
const id = itemID(part.providerMetadata, providerMetadataKey)
|
||||
return {
|
||||
type: "function_call",
|
||||
...(id === undefined ? {} : { id }),
|
||||
@@ -495,10 +498,10 @@ const lowerToolCall = (part: ToolCallPart, routeID: string): OpenResponsesInputI
|
||||
}
|
||||
}
|
||||
|
||||
const lowerReasoning = (part: ReasoningPart, routeID: string): OpenResponsesReasoningInput | undefined => {
|
||||
const metadata = part.providerMetadata?.[routeID]
|
||||
const lowerReasoning = (part: ReasoningPart, providerMetadataKey: string): OpenResponsesReasoningInput | undefined => {
|
||||
const metadata = part.providerMetadata?.[providerMetadataKey]
|
||||
if (!ProviderShared.isRecord(metadata)) return undefined
|
||||
const id = itemID(part.providerMetadata, routeID)
|
||||
const id = itemID(part.providerMetadata, providerMetadataKey)
|
||||
const encryptedContent =
|
||||
typeof metadata.reasoningEncryptedContent === "string" || metadata.reasoningEncryptedContent === null
|
||||
? metadata.reasoningEncryptedContent
|
||||
@@ -521,7 +524,7 @@ const lowerMedia = Effect.fn("OpenResponses.lowerMedia")(function* (
|
||||
const providerMedia = adapter.lowerMedia?.({ part, media, request })
|
||||
if (providerMedia) return providerMedia
|
||||
const detail = yield* ProviderShared.validateWith(Schema.decodeUnknownEffect(OpenResponsesInputImage.fields.detail))(
|
||||
part.providerMetadata?.[request.model.route.id]?.detail,
|
||||
part.providerMetadata?.[metadataKey(request.model)]?.detail,
|
||||
)
|
||||
const url =
|
||||
typeof part.data === "string" && (part.data.startsWith("https://") || part.data.startsWith("http://"))
|
||||
@@ -608,12 +611,12 @@ const lowerMessages = Effect.fn("OpenResponses.lowerMessages")(function* (
|
||||
adapter: ProviderAdapter,
|
||||
) {
|
||||
const input: LoweredInputItem[] = []
|
||||
const routeID = request.model.route.id
|
||||
const providerMetadataKey = metadataKey(request.model)
|
||||
|
||||
for (const message of request.messages) {
|
||||
const metadata = yield* ProviderShared.validateWith(
|
||||
Schema.decodeUnknownEffect(Schema.UndefinedOr(MessageMetadata)),
|
||||
)(message.providerMetadata?.[routeID])
|
||||
)(message.providerMetadata?.[providerMetadataKey])
|
||||
if (message.role === "system") {
|
||||
input.push({
|
||||
role: "developer",
|
||||
@@ -638,8 +641,8 @@ const lowerMessages = Effect.fn("OpenResponses.lowerMessages")(function* (
|
||||
const groups = content.reduce<
|
||||
Array<{ id: string | undefined; phase: MessagePhase | null | undefined; parts: TextPart[] }>
|
||||
>((groups, part) => {
|
||||
const partMetadata = part.providerMetadata?.[routeID]
|
||||
const id = itemID(part.providerMetadata, routeID) ?? metadata?.itemId
|
||||
const partMetadata = part.providerMetadata?.[providerMetadataKey]
|
||||
const id = itemID(part.providerMetadata, providerMetadataKey) ?? metadata?.itemId
|
||||
const partPhase = messagePhase(partMetadata?.phase)
|
||||
const phase = partPhase === undefined ? metadata?.phase : partPhase
|
||||
const group = groups.at(-1)
|
||||
@@ -675,7 +678,7 @@ const lowerMessages = Effect.fn("OpenResponses.lowerMessages")(function* (
|
||||
}
|
||||
if (part.type === "reasoning") {
|
||||
flushText()
|
||||
const reasoning = lowerReasoning(part, routeID)
|
||||
const reasoning = lowerReasoning(part, providerMetadataKey)
|
||||
if (!reasoning) continue
|
||||
const existing = reasoning.id === undefined ? undefined : reasoningItems[reasoning.id]
|
||||
if (existing) {
|
||||
@@ -691,12 +694,12 @@ const lowerMessages = Effect.fn("OpenResponses.lowerMessages")(function* (
|
||||
if (part.type === "tool-call") {
|
||||
flushText()
|
||||
if (part.providerExecuted === true) continue
|
||||
input.push(lowerToolCall(part, routeID))
|
||||
input.push(lowerToolCall(part, providerMetadataKey))
|
||||
continue
|
||||
}
|
||||
if (part.type === "tool-result" && part.providerExecuted === true) {
|
||||
flushText()
|
||||
const id = itemID(part.providerMetadata, routeID)
|
||||
const id = itemID(part.providerMetadata, providerMetadataKey)
|
||||
const hosted =
|
||||
part.result.type !== "json"
|
||||
? undefined
|
||||
@@ -819,11 +822,13 @@ export const fromRequestWithAdapter = Effect.fn("OpenResponses.fromRequestWithAd
|
||||
projected.tools.length === 0
|
||||
? undefined
|
||||
: yield* Effect.forEach(projected.tools, (tool) =>
|
||||
lowerTool(
|
||||
adapter.name,
|
||||
tool,
|
||||
ToolSchemaProjection.modelCompatibility(tool.inputSchema, toolSchemaCompatibility),
|
||||
),
|
||||
tool.native !== undefined && adapter.nativeTool
|
||||
? adapter.nativeTool(tool.native)
|
||||
: lowerTool(
|
||||
adapter.name,
|
||||
tool,
|
||||
ToolSchemaProjection.modelCompatibility(tool.inputSchema, toolSchemaCompatibility),
|
||||
),
|
||||
),
|
||||
tool_choice:
|
||||
allowedToolChoice(request) ??
|
||||
@@ -844,7 +849,7 @@ export const fromRequest = Effect.fn("OpenResponses.fromRequest")(function* (req
|
||||
// cached-read and cache-write subsets, and `output_tokens` (inclusive total)
|
||||
// with a `reasoning_tokens` subset. Pass the totals through and derive the
|
||||
// non-cached breakdown.
|
||||
export const mapUsage = (usage: OpenResponsesUsage | null | undefined, routeID: string) => {
|
||||
export const mapUsage = (usage: OpenResponsesUsage | null | undefined, providerMetadataKey: string) => {
|
||||
if (!usage) return undefined
|
||||
const cached = usage.input_tokens_details?.cached_tokens
|
||||
const cacheWrite = usage.input_tokens_details?.cache_write_tokens
|
||||
@@ -858,7 +863,7 @@ export const mapUsage = (usage: OpenResponsesUsage | null | undefined, routeID:
|
||||
cacheWriteInputTokens: cacheWrite,
|
||||
reasoningTokens: reasoning,
|
||||
totalTokens: ProviderShared.totalTokens(usage.input_tokens, usage.output_tokens, usage.total_tokens),
|
||||
providerMetadata: { [routeID]: usage },
|
||||
providerMetadata: { [providerMetadataKey]: usage },
|
||||
})
|
||||
}
|
||||
|
||||
@@ -874,8 +879,10 @@ const mapFinishReason = (event: Event, hasFunctionCall: boolean): FinishReason =
|
||||
return hasFunctionCall ? "tool-calls" : "unknown"
|
||||
}
|
||||
|
||||
export const metadataKey = (model: LLMRequest["model"]) => model.route.providerMetadataKey ?? "openresponses"
|
||||
|
||||
export const providerMetadata = (state: ParserState, metadata: Record<string, unknown>): ProviderMetadata => ({
|
||||
[state.routeID]: metadata,
|
||||
[state.providerMetadataKey]: metadata,
|
||||
})
|
||||
|
||||
export type StepResult = readonly [ParserState, ReadonlyArray<LLMEvent>]
|
||||
@@ -1347,7 +1354,7 @@ const onResponseFinish = Effect.fn("OpenResponses.onResponseFinish")(function* (
|
||||
normalized: mapFinishReason(event, current.hasFunctionCall),
|
||||
raw: event.response?.incomplete_details?.reason,
|
||||
},
|
||||
usage: mapUsage(event.response?.usage, current.routeID),
|
||||
usage: mapUsage(event.response?.usage, current.providerMetadataKey),
|
||||
providerMetadata:
|
||||
event.response?.id || event.response?.service_tier
|
||||
? providerMetadata(current, {
|
||||
@@ -1484,7 +1491,7 @@ export const initial = (request: LLMRequest, adapter: ProviderAdapter = BASE_ADA
|
||||
completedCompactions: new Set<string>(),
|
||||
id: adapter.id,
|
||||
name: adapter.name,
|
||||
routeID: request.model.route.id,
|
||||
providerMetadataKey: metadataKey(request.model),
|
||||
hasFunctionCall: false,
|
||||
tools: ToolStream.empty<string>(),
|
||||
lifecycle: Lifecycle.initial(),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Effect, Schema } from "effect"
|
||||
import { Tool } from "@opencode-ai/schema/tool"
|
||||
import { Tool } from "@opencode/schema/tool"
|
||||
import { Route } from "../route/client.js"
|
||||
import { Auth } from "../route/auth.js"
|
||||
import { Endpoint } from "../route/endpoint.js"
|
||||
@@ -257,7 +257,7 @@ interface PendingToolDelta {
|
||||
}
|
||||
|
||||
export interface ParserState {
|
||||
readonly routeID: string
|
||||
readonly providerMetadataKey: string
|
||||
readonly tools: ToolStream.State<number>
|
||||
readonly pendingTools: Partial<Record<number, PendingToolDelta>>
|
||||
readonly toolCallEvents: ReadonlyArray<LLMEvent>
|
||||
@@ -329,17 +329,18 @@ const lowerMedia = Effect.fn("OpenAIChat.lowerMedia")(function* (part: MediaPart
|
||||
const openAICompatibleReasoningContent = (native: unknown) =>
|
||||
isRecord(native) && typeof native.reasoning_content === "string" ? native.reasoning_content : undefined
|
||||
|
||||
const reasoningField = (part: ReasoningPart, routeID: string) => {
|
||||
const field = part.providerMetadata?.[routeID]?.reasoningField
|
||||
const reasoningField = (part: ReasoningPart, providerMetadataKey: string) => {
|
||||
const field = part.providerMetadata?.[providerMetadataKey]?.reasoningField
|
||||
return typeof field === "string" ? field : undefined
|
||||
}
|
||||
|
||||
const reasoningDetails = (parts: ReadonlyArray<ReasoningPart>, native: unknown, routeID: string) => {
|
||||
const reasoningDetails = (parts: ReadonlyArray<ReasoningPart>, native: unknown, providerMetadataKey: string) => {
|
||||
const observed = parts.flatMap((part) => {
|
||||
const details = part.providerMetadata?.[routeID]?.reasoningDetails
|
||||
const details = part.providerMetadata?.[providerMetadataKey]?.reasoningDetails
|
||||
return Array.isArray(details) ? details : []
|
||||
})
|
||||
if (parts.some((part) => Array.isArray(part.providerMetadata?.[routeID]?.reasoningDetails))) return observed
|
||||
if (parts.some((part) => Array.isArray(part.providerMetadata?.[providerMetadataKey]?.reasoningDetails)))
|
||||
return observed
|
||||
if (isRecord(native) && Array.isArray(native.reasoning_details)) return native.reasoning_details
|
||||
}
|
||||
|
||||
@@ -371,7 +372,7 @@ const lowerAssistantMessage = Effect.fn("OpenAIChat.lowerAssistantMessage")(func
|
||||
message: OpenAIChatRequestMessage,
|
||||
configuredField: string | undefined,
|
||||
requireReasoning: boolean,
|
||||
options: LoweringOptions & { readonly routeID: string },
|
||||
options: LoweringOptions & { readonly providerMetadataKey: string },
|
||||
) {
|
||||
const content: TextPart[] = []
|
||||
const reasoning: ReasoningPart[] = []
|
||||
@@ -393,13 +394,13 @@ const lowerAssistantMessage = Effect.fn("OpenAIChat.lowerAssistantMessage")(func
|
||||
}
|
||||
}
|
||||
const text = reasoning.map((part) => part.text).join("")
|
||||
const details = reasoningDetails(reasoning, message.native?.openaiCompatible, options.routeID)
|
||||
const details = reasoningDetails(reasoning, message.native?.openaiCompatible, options.providerMetadataKey)
|
||||
const observedField = reasoning
|
||||
.map((part) => reasoningField(part, options.routeID))
|
||||
.map((part) => reasoningField(part, options.providerMetadataKey))
|
||||
.find((value) => value !== undefined)
|
||||
const nativeReasoning = openAICompatibleReasoningContent(message.native?.openaiCompatible)
|
||||
const fullyStructured = reasoning.every((part) =>
|
||||
Array.isArray(part.providerMetadata?.[options.routeID]?.reasoningDetails),
|
||||
Array.isArray(part.providerMetadata?.[options.providerMetadataKey]?.reasoningDetails),
|
||||
)
|
||||
const field = (() => {
|
||||
if (configuredField !== undefined && (requireReasoning || reasoning.length > 0 || nativeReasoning !== undefined))
|
||||
@@ -468,7 +469,7 @@ const lowerMessage = Effect.fn("OpenAIChat.lowerMessage")(function* (
|
||||
message: OpenAIChatRequestMessage,
|
||||
reasoningField: string | undefined,
|
||||
requireReasoning: boolean,
|
||||
options: LoweringOptions & { readonly routeID: string },
|
||||
options: LoweringOptions & { readonly providerMetadataKey: string },
|
||||
) {
|
||||
if (message.role === "user") return [yield* lowerUserMessage(message, options)]
|
||||
if (message.role === "assistant")
|
||||
@@ -504,7 +505,7 @@ const lowerMessages = Effect.fn("OpenAIChat.lowerMessages")(function* (request:
|
||||
const mistral = ["mistral", "devstral", "codestral", "pixtral", "mixtral"].some((family) => modelID.includes(family))
|
||||
const lowering = {
|
||||
...options,
|
||||
routeID: request.model.route.id,
|
||||
providerMetadataKey: request.model.route.providerMetadataKey ?? String(request.model.provider),
|
||||
toolCallID: (id: string) => {
|
||||
if (mistral)
|
||||
return id
|
||||
@@ -832,7 +833,7 @@ const mapFinishReason = Effect.fn("OpenAIChat.mapFinishReason")(function* (event
|
||||
// Providers differ on cache-hit location: OpenAI uses
|
||||
// `prompt_tokens_details.cached_tokens`, DeepSeek uses
|
||||
// `prompt_cache_hit_tokens`, and Zai uses top-level `cached_tokens`.
|
||||
const mapUsage = (usage: OpenAIChatEvent["usage"], routeID: string): Usage | undefined => {
|
||||
const mapUsage = (usage: OpenAIChatEvent["usage"], providerMetadataKey: string): Usage | undefined => {
|
||||
if (!usage) return undefined
|
||||
const input = usage.prompt_tokens ?? undefined
|
||||
const output = usage.completion_tokens ?? undefined
|
||||
@@ -851,7 +852,7 @@ const mapUsage = (usage: OpenAIChatEvent["usage"], routeID: string): Usage | und
|
||||
cacheWriteInputTokens: cacheWrite,
|
||||
reasoningTokens: reasoning,
|
||||
totalTokens: ProviderShared.totalTokens(input, output, usage.total_tokens ?? undefined),
|
||||
providerMetadata: { [routeID]: usage },
|
||||
providerMetadata: { [providerMetadataKey]: usage },
|
||||
})
|
||||
}
|
||||
|
||||
@@ -926,11 +927,11 @@ const conflictingDetailValue = (previous: unknown, current: unknown) =>
|
||||
previous !== undefined && previous !== null && current !== undefined && current !== null && previous !== current
|
||||
|
||||
const reasoningMetadata = (
|
||||
routeID: string,
|
||||
providerMetadataKey: string,
|
||||
field: ParserState["reasoningField"],
|
||||
details?: ReadonlyArray<unknown>,
|
||||
) => ({
|
||||
[routeID]: {
|
||||
[providerMetadataKey]: {
|
||||
...(field ? { reasoningField: field } : {}),
|
||||
...(details ? { reasoningDetails: details } : {}),
|
||||
},
|
||||
@@ -954,8 +955,8 @@ const step = (state: ParserState, event: OpenAIChatEvent) =>
|
||||
// `choice.usage` instead of the top-level `usage` field.
|
||||
const choiceUsage = (choice as unknown as { usage?: OpenAIChatEvent["usage"] })?.usage
|
||||
const usage =
|
||||
mapUsage(event.usage, state.routeID) ??
|
||||
(choiceUsage ? mapUsage(choiceUsage, state.routeID) : undefined) ??
|
||||
mapUsage(event.usage, state.providerMetadataKey) ??
|
||||
(choiceUsage ? mapUsage(choiceUsage, state.providerMetadataKey) : undefined) ??
|
||||
state.usage
|
||||
const rawFinishReason = choice?.finish_reason
|
||||
const finishReason = rawFinishReason
|
||||
@@ -994,7 +995,7 @@ const step = (state: ParserState, event: OpenAIChatEvent) =>
|
||||
const detailDelta = Array.isArray(delta?.reasoning_details) ? delta.reasoning_details : undefined
|
||||
if (detailDelta !== undefined) appendReasoningDetails(state.reasoningDetails, detailDelta)
|
||||
const reasoningDetailsObserved = state.reasoningDetailsObserved || detailDelta !== undefined
|
||||
const deltaMetadata = reasoningMetadata(state.routeID, reasoningField)
|
||||
const deltaMetadata = reasoningMetadata(state.providerMetadataKey, reasoningField)
|
||||
const text = detailDelta?.length ? (detailText(detailDelta) ?? reasoning?.text) : reasoning?.text
|
||||
if (text !== undefined) lifecycle = Lifecycle.reasoningDelta(lifecycle, events, "reasoning-0", text, deltaMetadata)
|
||||
else if (
|
||||
@@ -1083,7 +1084,7 @@ const step = (state: ParserState, event: OpenAIChatEvent) =>
|
||||
|
||||
return [
|
||||
{
|
||||
routeID: state.routeID,
|
||||
providerMetadataKey: state.providerMetadataKey,
|
||||
tools: finished?.tools ?? tools,
|
||||
pendingTools,
|
||||
toolCallEvents: finished?.events ?? state.toolCallEvents,
|
||||
@@ -1127,7 +1128,7 @@ const finishEvents = Effect.fn("OpenAIChat.finishEvents")(function* (state: Pars
|
||||
// Snapshot details at publish time so the emitted event never observes later
|
||||
// mutation of the accumulated `reasoningDetails` array.
|
||||
const metadata = reasoningMetadata(
|
||||
state.routeID,
|
||||
state.providerMetadataKey,
|
||||
state.reasoningField,
|
||||
state.reasoningDetailsObserved ? [...state.reasoningDetails] : undefined,
|
||||
)
|
||||
@@ -1137,7 +1138,7 @@ const finishEvents = Effect.fn("OpenAIChat.finishEvents")(function* (state: Pars
|
||||
state.lifecycle,
|
||||
events,
|
||||
"reasoning-0",
|
||||
reasoningMetadata(state.routeID, state.reasoningField),
|
||||
reasoningMetadata(state.providerMetadataKey, state.reasoningField),
|
||||
)
|
||||
: state.lifecycle
|
||||
const ended = Lifecycle.reasoningEnd(started, events, "reasoning-0", metadata)
|
||||
@@ -1165,7 +1166,7 @@ export const protocol = Protocol.make({
|
||||
stream: {
|
||||
event: OpenAIChatStreamEvent,
|
||||
initial: (request) => ({
|
||||
routeID: request.model.route.id,
|
||||
providerMetadataKey: request.model.route.providerMetadataKey ?? String(request.model.provider),
|
||||
tools: ToolStream.empty<number>(),
|
||||
pendingTools: {},
|
||||
toolCallEvents: [],
|
||||
@@ -1189,6 +1190,7 @@ export const httpTransport = HttpTransport.sseJson.with<OpenAIChatBody>().with({
|
||||
export const route = Route.make({
|
||||
id: ADAPTER,
|
||||
provider: "openai",
|
||||
providerMetadataKey: "openai",
|
||||
protocol,
|
||||
endpoint: Endpoint.path(PATH, { baseURL: DEFAULT_BASE_URL }),
|
||||
auth: Auth.none,
|
||||
|
||||
@@ -15,6 +15,7 @@ export type OpenAICompatibleChatLanguageModelInput = RouteRoutedLanguageModelInp
|
||||
*/
|
||||
export const route = Route.make({
|
||||
id: ADAPTER,
|
||||
providerMetadataKey: "openai",
|
||||
protocol: OpenAIChat.protocol,
|
||||
endpoint: Endpoint.path("/chat/completions"),
|
||||
framing: OpenAIChat.framing,
|
||||
|
||||
@@ -13,6 +13,7 @@ export type OpenAICompatibleResponsesLanguageModelInput = RouteRoutedLanguageMod
|
||||
*/
|
||||
export const route = Route.make({
|
||||
id: ADAPTER,
|
||||
providerMetadataKey: "openresponses",
|
||||
protocol: OpenResponses.protocol,
|
||||
endpoint: Endpoint.path(OpenResponses.PATH),
|
||||
transport: OpenResponses.httpTransport,
|
||||
|
||||
@@ -313,6 +313,7 @@ export const route = Route.make({
|
||||
compact: { endpoint: ResponsesCompaction.make(adapter), trigger: ResponsesCheckpoint.make(checkpointBody) },
|
||||
id: ADAPTER,
|
||||
provider: "openai",
|
||||
providerMetadataKey: "openai",
|
||||
protocol,
|
||||
endpoint,
|
||||
auth,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Buffer } from "node:buffer"
|
||||
import { Tool } from "@opencode-ai/schema/tool"
|
||||
import { Tool } from "@opencode/schema/tool"
|
||||
import { Effect, Schema, Stream } from "effect"
|
||||
import * as Sse from "effect/unstable/encoding/Sse"
|
||||
import { Headers, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
// Responses image items can omit output_format, including when PNG/JPEG was requested.
|
||||
export const mediaType = (data: Uint8Array, format?: string) => {
|
||||
if (format !== undefined) return `image/${format}`
|
||||
if (data[0] === 137 && data[1] === 80 && data[2] === 78 && data[3] === 71) return "image/png"
|
||||
if (data[0] === 255 && data[1] === 216 && data[2] === 255) return "image/jpeg"
|
||||
if (new TextDecoder().decode(data.slice(0, 4)) === "RIFF" && new TextDecoder().decode(data.slice(8, 12)) === "WEBP")
|
||||
return "image/webp"
|
||||
return "application/octet-stream"
|
||||
}
|
||||
|
||||
export * as MetaImage from "./meta-image.js"
|
||||
@@ -93,7 +93,7 @@ export const make = <Body>(body: RouteBody<Body>): TriggerCompactOperation =>
|
||||
result = new CompactionCheckpointResponse({
|
||||
checkpoint,
|
||||
responseID,
|
||||
usage: OpenResponses.mapUsage(event.response?.usage, request.model.route.id),
|
||||
usage: OpenResponses.mapUsage(event.response?.usage, OpenResponses.metadataKey(request.model)),
|
||||
})
|
||||
return [next, [LLMEvent.finish({ reason: { normalized: "stop" } })]] as const
|
||||
}),
|
||||
@@ -102,6 +102,7 @@ export const make = <Body>(body: RouteBody<Body>): TriggerCompactOperation =>
|
||||
const route = Route.make({
|
||||
id: source.id,
|
||||
provider: source.provider,
|
||||
providerMetadataKey: source.providerMetadataKey,
|
||||
protocol,
|
||||
endpoint: source.endpoint,
|
||||
auth: source.auth,
|
||||
|
||||
@@ -125,7 +125,7 @@ export const make = (adapter: OpenResponses.ProviderAdapter): CompactOperation =
|
||||
return yield* invalid("Compaction response did not contain a checkpoint")
|
||||
return new CompactionResponse({
|
||||
replacement: result.output.map((item) => toMessage(item, request.model)),
|
||||
usage: OpenResponses.mapUsage(result.usage, request.model.route.id),
|
||||
usage: OpenResponses.mapUsage(result.usage, OpenResponses.metadataKey(request.model)),
|
||||
})
|
||||
})
|
||||
|
||||
@@ -135,7 +135,7 @@ function toMessage(item: (typeof Response.Type.output)[number], model: LLMReques
|
||||
CompactionPart.make({ provider: model.provider, id: item.id ?? undefined, encrypted: item.encrypted_content }),
|
||||
)
|
||||
|
||||
const key = model.route.id
|
||||
const key = OpenResponses.metadataKey(model)
|
||||
if (item.type === "reasoning") {
|
||||
const summary = item.summary.length ? item.summary : [{ text: "" }]
|
||||
return Message.assistant(
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import { Effect, Schema } from "effect"
|
||||
import { Protocol } from "../route/protocol.js"
|
||||
import type { LanguageModelCompatibility, LLMRequest } from "../schema/index.js"
|
||||
import { OpenAIChat } from "./openai-chat.js"
|
||||
import { ProviderShared } from "./shared.js"
|
||||
|
||||
export type ReasoningEffort = "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max" | (string & {})
|
||||
|
||||
export type OptionsInput = {
|
||||
readonly reasoningEffort?: ReasoningEffort
|
||||
readonly thinking?: {
|
||||
readonly type?: "enabled" | "disabled" | (string & {})
|
||||
/** False retains historical reasoning; omission preserves the endpoint's default. */
|
||||
readonly clear_thinking?: boolean
|
||||
}
|
||||
readonly toolStream?: boolean
|
||||
readonly doSample?: boolean
|
||||
readonly responseFormat?: { readonly type: "text" | "json_object" | (string & {}) }
|
||||
readonly requestID?: string
|
||||
readonly userID?: string
|
||||
}
|
||||
|
||||
const Options = Schema.Struct({
|
||||
reasoningEffort: Schema.optional(Schema.String),
|
||||
thinking: Schema.optional(
|
||||
Schema.Struct({ type: Schema.optional(Schema.String), clear_thinking: Schema.optional(Schema.Boolean) }),
|
||||
),
|
||||
toolStream: Schema.optional(Schema.Boolean),
|
||||
doSample: Schema.optional(Schema.Boolean),
|
||||
responseFormat: Schema.optional(Schema.Struct({ type: Schema.String })),
|
||||
requestID: Schema.optional(Schema.String),
|
||||
userID: Schema.optional(Schema.String),
|
||||
})
|
||||
|
||||
const Body = Schema.Struct({
|
||||
...OpenAIChat.bodyFields,
|
||||
thinking: Options.fields.thinking,
|
||||
do_sample: Options.fields.doSample,
|
||||
response_format: Options.fields.responseFormat,
|
||||
request_id: Options.fields.requestID,
|
||||
user_id: Options.fields.userID,
|
||||
})
|
||||
|
||||
const fromRequest = Effect.fn("ZAIChat.fromRequest")(function* (request: LLMRequest) {
|
||||
const options = yield* ProviderShared.validateWith(Schema.decodeUnknownEffect(Options))(request.providerOptions ?? {})
|
||||
const body = yield* OpenAIChat.protocol.body.from(request)
|
||||
return {
|
||||
...body,
|
||||
thinking: options.thinking,
|
||||
// Tool streaming was introduced in GLM-4.6; older models must not receive the opt-in.
|
||||
tool_stream:
|
||||
options.toolStream ??
|
||||
(body.tools?.length && /^glm-(?:4\.[67]|5(?:[.-]|$))/i.test(request.model.id) ? true : undefined),
|
||||
do_sample: options.doSample,
|
||||
response_format: options.responseFormat,
|
||||
request_id: options.requestID,
|
||||
user_id: options.userID,
|
||||
}
|
||||
})
|
||||
|
||||
export const compatibility = {
|
||||
maxTokensField: "max_tokens",
|
||||
supportsStore: false,
|
||||
supportsStrictMode: false,
|
||||
reasoningField: "reasoning_content",
|
||||
zaiToolStream: false,
|
||||
} satisfies LanguageModelCompatibility
|
||||
|
||||
export const protocol = Protocol.make({
|
||||
id: "zai-chat",
|
||||
body: { schema: Body, from: fromRequest },
|
||||
stream: OpenAIChat.protocol.stream,
|
||||
})
|
||||
|
||||
export * as ZAIChat from "./zai-chat.js"
|
||||
@@ -0,0 +1,39 @@
|
||||
import { Effect, Schema } from "effect"
|
||||
import { Protocol } from "../route/protocol.js"
|
||||
import { LLMRequest } from "../schema/index.js"
|
||||
import { AnthropicMessages } from "./anthropic-messages.js"
|
||||
import { ProviderShared } from "./shared.js"
|
||||
import type { ZAIChat } from "./zai-chat.js"
|
||||
|
||||
export type OptionsInput = {
|
||||
readonly effort?: ZAIChat.ReasoningEffort
|
||||
readonly thinking?: { readonly type: "enabled" | "adaptive" | "disabled" | (string & {}) }
|
||||
}
|
||||
|
||||
const Options = Schema.Struct({
|
||||
effort: Schema.optional(Schema.String),
|
||||
thinking: Schema.optional(Schema.Struct({ type: Schema.String })),
|
||||
})
|
||||
const Body = Schema.Struct({
|
||||
...AnthropicMessages.AnthropicMessagesBody.fields,
|
||||
thinking: Options.fields.thinking,
|
||||
})
|
||||
|
||||
const fromRequest = Effect.fn("ZAIMessages.fromRequest")(function* (request: LLMRequest) {
|
||||
const options = yield* ProviderShared.validateWith(Schema.decodeUnknownEffect(Options))(request.providerOptions ?? {})
|
||||
// Z.AI accepts enabled thinking without Anthropic's mandatory token budget.
|
||||
const body = yield* AnthropicMessages.protocol.body.from(
|
||||
LLMRequest.update(request, {
|
||||
providerOptions: { ...request.providerOptions, thinking: undefined },
|
||||
}),
|
||||
)
|
||||
return { ...body, thinking: options.thinking }
|
||||
})
|
||||
|
||||
export const protocol = Protocol.make({
|
||||
id: "zai-messages",
|
||||
body: { schema: Body, from: fromRequest },
|
||||
stream: AnthropicMessages.protocol.stream,
|
||||
})
|
||||
|
||||
export * as ZAIMessages from "./zai-messages.js"
|
||||
@@ -0,0 +1,134 @@
|
||||
import type { ProviderPackage } from "../provider-package.js"
|
||||
import { AlibabaChat } from "../protocols/alibaba-chat.js"
|
||||
import { AlibabaMessages } from "../protocols/alibaba-messages.js"
|
||||
import { AlibabaResponses } from "../protocols/alibaba-responses.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 { Framing } from "../route/framing.js"
|
||||
import { ProviderID, ToolDefinition, type ModelID } from "../schema/index.js"
|
||||
|
||||
export const id = ProviderID.make("alibaba")
|
||||
|
||||
export type Region =
|
||||
| "ap-southeast-1"
|
||||
| "cn-beijing"
|
||||
| "cn-hongkong"
|
||||
| "us-east-1"
|
||||
| "eu-central-1"
|
||||
| "ap-northeast-1"
|
||||
| (string & {})
|
||||
export type ChatOptionsInput = AlibabaChat.OptionsInput
|
||||
export type MessagesOptionsInput = AlibabaMessages.OptionsInput
|
||||
export type ResponsesOptionsInput = AlibabaResponses.OptionsInput
|
||||
|
||||
type Location = AtLeastOne<{
|
||||
readonly region: Region
|
||||
/** Overrides the selected API's complete base URL, including its version prefix. */
|
||||
readonly baseURL: string
|
||||
}> & { readonly workspaceID?: string }
|
||||
|
||||
export type Config = Location &
|
||||
Omit<RouteDefaultsInput, "providerOptions"> &
|
||||
ProviderAuthOption<"optional"> & {
|
||||
readonly providerOptions?: ChatOptionsInput | MessagesOptionsInput | ResponsesOptionsInput
|
||||
}
|
||||
export type Settings<Options = ChatOptionsInput> = Location &
|
||||
ProviderPackage.Settings & {
|
||||
readonly apiKey?: string
|
||||
readonly providerOptions?: Options
|
||||
}
|
||||
|
||||
const hosts = new Map<string, string>([
|
||||
["ap-southeast-1", "dashscope-intl.aliyuncs.com"],
|
||||
["cn-beijing", "dashscope.aliyuncs.com"],
|
||||
["cn-hongkong", "cn-hongkong.dashscope.aliyuncs.com"],
|
||||
["us-east-1", "dashscope-us.aliyuncs.com"],
|
||||
])
|
||||
const chatRoute = Route.make({
|
||||
id: "alibaba-chat",
|
||||
provider: id,
|
||||
providerMetadataKey: "alibaba",
|
||||
protocol: AlibabaChat.protocol,
|
||||
endpoint: Endpoint.path("/chat/completions"),
|
||||
framing: Framing.sse,
|
||||
})
|
||||
const messagesRoute = Route.make({
|
||||
id: "alibaba-messages",
|
||||
provider: id,
|
||||
providerMetadataKey: "alibaba",
|
||||
protocol: AlibabaMessages.protocol,
|
||||
endpoint: Endpoint.path("/messages"),
|
||||
framing: Framing.sse,
|
||||
headers: () => ({ "anthropic-version": "2023-06-01" }),
|
||||
})
|
||||
const responsesRoute = Route.make({
|
||||
id: "alibaba-responses",
|
||||
provider: id,
|
||||
providerMetadataKey: "alibaba",
|
||||
protocol: AlibabaResponses.protocol,
|
||||
endpoint: Endpoint.path("/responses"),
|
||||
framing: Framing.sse,
|
||||
})
|
||||
|
||||
export const routes = [chatRoute, messagesRoute, responsesRoute]
|
||||
|
||||
export const configure = (input: Config) => {
|
||||
const { apiKey: _key, auth: _auth, region, workspaceID, baseURL, ...rest } = input
|
||||
const host =
|
||||
region === undefined
|
||||
? undefined
|
||||
: workspaceID === undefined
|
||||
? hosts.get(region)
|
||||
: `${workspaceID}.${region}.maas.aliyuncs.com`
|
||||
if (baseURL === undefined) {
|
||||
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` } }
|
||||
const chat = (id: string | ModelID) =>
|
||||
chatRoute.with(common).model<ChatOptionsInput>({ id, compatibility: AlibabaChat.compatibility })
|
||||
const messages = (id: string | ModelID) =>
|
||||
messagesRoute
|
||||
.with({
|
||||
...opts,
|
||||
endpoint: { baseURL: baseURL ?? `https://${host}/apps/anthropic/v1` },
|
||||
})
|
||||
.model<MessagesOptionsInput>({ id, compatibility: { requireSignature: false } })
|
||||
const responses = (id: string | ModelID) => responsesRoute.with(common).model<ResponsesOptionsInput>({ id })
|
||||
return { id, model: chat, chat, messages, responses, configure }
|
||||
}
|
||||
|
||||
export const provider = { id, configure }
|
||||
|
||||
export const model: ProviderPackage.Definition<Settings, ChatOptionsInput>["model"] = (id, input) =>
|
||||
fromSettings(input).chat(id)
|
||||
export const messagesModel: ProviderPackage.Definition<
|
||||
Settings<MessagesOptionsInput>,
|
||||
MessagesOptionsInput
|
||||
>["model"] = (id, input) => fromSettings(input).messages(id)
|
||||
export const responsesModel: ProviderPackage.Definition<
|
||||
Settings<ResponsesOptionsInput>,
|
||||
ResponsesOptionsInput
|
||||
>["model"] = (id, input) => fromSettings(input).responses(id)
|
||||
|
||||
function fromSettings(input: Settings<Config["providerOptions"]>) {
|
||||
const { body, ...rest } = input
|
||||
return configure({ ...rest, http: body === undefined ? undefined : { body } })
|
||||
}
|
||||
|
||||
export const webSearch = () => hostedTool("web_search", "Search the web with Alibaba's hosted search tool.")
|
||||
export const webExtractor = () => hostedTool("web_extractor", "Extract web page content with Alibaba's hosted tool.")
|
||||
export const codeInterpreter = () => hostedTool("code_interpreter", "Execute code with Alibaba's hosted interpreter.")
|
||||
|
||||
function hostedTool(type: "web_search" | "web_extractor" | "code_interpreter", description: string) {
|
||||
return ToolDefinition.make({
|
||||
name: type,
|
||||
description,
|
||||
inputSchema: { type: "object", properties: {} },
|
||||
native: { alibaba: { type } },
|
||||
})
|
||||
}
|
||||
|
||||
export * as Alibaba from "./alibaba.js"
|
||||
@@ -0,0 +1 @@
|
||||
export { model, type Settings } from "../alibaba.js"
|
||||
@@ -0,0 +1,3 @@
|
||||
import type { Alibaba } from "../alibaba.js"
|
||||
export { messagesModel as model } from "../alibaba.js"
|
||||
export type Settings = Alibaba.Settings<Alibaba.MessagesOptionsInput>
|
||||
@@ -0,0 +1,3 @@
|
||||
import type { Alibaba } from "../alibaba.js"
|
||||
export { responsesModel as model } from "../alibaba.js"
|
||||
export type Settings = Alibaba.Settings<Alibaba.ResponsesOptionsInput>
|
||||
@@ -36,6 +36,7 @@ export interface Settings extends ProviderPackage.Settings {
|
||||
const responsesRoute = Route.make({
|
||||
id: "bedrock-mantle-responses",
|
||||
provider: id,
|
||||
providerMetadataKey: "mantle",
|
||||
protocol: OpenAIResponses.protocol,
|
||||
endpoint: OpenAIResponses.route.endpoint,
|
||||
auth: OpenAIResponses.route.auth,
|
||||
@@ -46,6 +47,7 @@ const responsesRoute = Route.make({
|
||||
const chatRoute = OpenAIChat.route.with({
|
||||
id: "bedrock-mantle-chat",
|
||||
provider: id,
|
||||
providerMetadataKey: "mantle",
|
||||
})
|
||||
|
||||
export const routes = [responsesRoute, chatRoute]
|
||||
|
||||
@@ -45,6 +45,7 @@ const configuredRoute = (input: Config) => {
|
||||
return BedrockConverse.route.with({
|
||||
...rest,
|
||||
provider: id,
|
||||
providerMetadataKey: "bedrock",
|
||||
endpoint: { baseURL: baseURL ?? bedrockBaseURL(resolvedRegion) },
|
||||
auth: BedrockAuth.resolveAuth({ apiKey, credentials, profile }, resolvedRegion, { mode: auth }),
|
||||
})
|
||||
|
||||
@@ -24,6 +24,7 @@ export interface Settings extends ProviderPackage.Settings {
|
||||
export const route = Route.make({
|
||||
id: "baseten-chat",
|
||||
provider: id,
|
||||
providerMetadataKey: "baseten",
|
||||
protocol: OpenAIChat.protocol,
|
||||
endpoint: Endpoint.path("/chat/completions", { baseURL }),
|
||||
framing: OpenAIChat.framing,
|
||||
|
||||
@@ -24,6 +24,7 @@ export interface Settings extends ProviderPackage.Settings {
|
||||
export const route = Route.make({
|
||||
id: "cerebras-chat",
|
||||
provider: id,
|
||||
providerMetadataKey: "cerebras",
|
||||
protocol: OpenAIChat.protocol,
|
||||
endpoint: Endpoint.path("/chat/completions", { baseURL }),
|
||||
framing: OpenAIChat.framing,
|
||||
|
||||
@@ -53,6 +53,7 @@ const auth = (input: LanguageModelOptions) => {
|
||||
export const route = Route.make({
|
||||
id: "cloudflare-ai-gateway",
|
||||
provider: id,
|
||||
providerMetadataKey: "cloudflare-ai-gateway",
|
||||
protocol: OpenAIChat.protocol,
|
||||
endpoint: Endpoint.path("/chat/completions"),
|
||||
framing: OpenAIChat.framing,
|
||||
|
||||
@@ -35,6 +35,7 @@ export const baseURL = (input: WorkersAIURL) => {
|
||||
export const route = Route.make({
|
||||
id: "cloudflare-workers-ai",
|
||||
provider: id,
|
||||
providerMetadataKey: "cloudflare-workers-ai",
|
||||
protocol: OpenAIChat.protocol,
|
||||
endpoint: Endpoint.path("/chat/completions"),
|
||||
framing: OpenAIChat.framing,
|
||||
|
||||
@@ -24,6 +24,7 @@ export interface Settings extends ProviderPackage.Settings {
|
||||
export const route = Route.make({
|
||||
id: "deepinfra-chat",
|
||||
provider: id,
|
||||
providerMetadataKey: "deepinfra",
|
||||
protocol: OpenAIChat.protocol,
|
||||
endpoint: Endpoint.path("/chat/completions", { baseURL }),
|
||||
framing: OpenAIChat.framing,
|
||||
|
||||
@@ -24,6 +24,7 @@ export interface Settings extends ProviderPackage.Settings {
|
||||
export const route = Route.make({
|
||||
id: "deepseek-chat",
|
||||
provider: id,
|
||||
providerMetadataKey: "deepseek",
|
||||
protocol: OpenAIChat.protocol,
|
||||
endpoint: Endpoint.path("/chat/completions", { baseURL }),
|
||||
framing: OpenAIChat.framing,
|
||||
|
||||
@@ -24,6 +24,7 @@ export interface Settings extends ProviderPackage.Settings {
|
||||
export const route = Route.make({
|
||||
id: "fireworks-chat",
|
||||
provider: id,
|
||||
providerMetadataKey: "fireworks",
|
||||
protocol: OpenAIChat.protocol,
|
||||
endpoint: Endpoint.path("/chat/completions", { baseURL }),
|
||||
framing: OpenAIChat.framing,
|
||||
|
||||
@@ -28,6 +28,7 @@ export interface Settings extends ProviderPackage.Settings {
|
||||
const route = Route.make({
|
||||
id: "google-vertex-chat",
|
||||
provider: id,
|
||||
providerMetadataKey: "vertex",
|
||||
protocol: OpenAIChat.protocol,
|
||||
endpoint: Endpoint.path("/chat/completions"),
|
||||
framing: OpenAIChat.framing,
|
||||
|
||||
@@ -37,6 +37,7 @@ export interface Settings extends ProviderPackage.Settings {
|
||||
const route = Route.make({
|
||||
id: "google-vertex-messages",
|
||||
provider: id,
|
||||
providerMetadataKey: "anthropic",
|
||||
protocol: Protocol.make({
|
||||
id: AnthropicMessages.protocol.id,
|
||||
body: {
|
||||
|
||||
@@ -28,6 +28,7 @@ export interface Settings extends ProviderPackage.Settings {
|
||||
const route = Route.make({
|
||||
id: "google-vertex-responses",
|
||||
provider: id,
|
||||
providerMetadataKey: "vertex",
|
||||
protocol: OpenResponses.protocol,
|
||||
endpoint: Endpoint.path(OpenResponses.PATH),
|
||||
transport: OpenResponses.httpTransport,
|
||||
|
||||
@@ -68,6 +68,7 @@ const protocol = {
|
||||
const route = Route.make({
|
||||
id: "google-vertex-gemini",
|
||||
provider: id,
|
||||
providerMetadataKey: "vertex",
|
||||
protocol,
|
||||
endpoint: Endpoint.path(({ request }) => {
|
||||
const model = String(request.model.id)
|
||||
|
||||
@@ -71,6 +71,7 @@ export const protocol = Protocol.make({
|
||||
export const route = Route.make({
|
||||
id: "groq-chat",
|
||||
provider: id,
|
||||
providerMetadataKey: "openai",
|
||||
protocol,
|
||||
endpoint: Endpoint.path("/chat/completions", { baseURL }),
|
||||
framing: OpenAIChat.framing,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * as Alibaba from "./alibaba.js"
|
||||
export * as Anthropic from "./anthropic.js"
|
||||
export * as AnthropicCompatible from "./anthropic-compatible.js"
|
||||
export * as AmazonBedrock from "./amazon-bedrock.js"
|
||||
@@ -16,7 +17,10 @@ export * as GoogleVertexChat from "./google-vertex-chat.js"
|
||||
export * as GoogleVertexMessages from "./google-vertex-messages.js"
|
||||
export * as GoogleVertexResponses from "./google-vertex-responses.js"
|
||||
export * as Groq from "./groq.js"
|
||||
export * as Meta from "./meta.js"
|
||||
export * as MiniMax from "./minimax.js"
|
||||
export * as Mistral from "./mistral.js"
|
||||
export * as Moonshot from "./moonshot.js"
|
||||
export * as OpenAI from "./openai.js"
|
||||
export * as OpenAICompatible from "./openai-compatible.js"
|
||||
export * as OpenAICompatibleResponses from "./openai-compatible-responses.js"
|
||||
@@ -24,3 +28,4 @@ export * as OpenRouter from "./openrouter.js"
|
||||
export * as TogetherAI from "./togetherai.js"
|
||||
export * as XAI from "./xai.js"
|
||||
export * as ZAI from "./zai.js"
|
||||
export * as ZAICodingPlan from "./zai-coding-plan.js"
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
import type { ProviderPackage } from "../provider-package.js"
|
||||
import { OpenAIChat } from "../protocols/openai-chat.js"
|
||||
import { AnthropicMessages } from "../protocols/anthropic-messages.js"
|
||||
import { MetaResponses } from "../protocols/meta-responses.js"
|
||||
import { MetaMessages } from "../protocols/meta-messages.js"
|
||||
import { MetaImages } from "../protocols/meta-images.js"
|
||||
import { AuthOptions, type ProviderAuthOption } from "../route/auth-options.js"
|
||||
import { Route, type RouteDefaultsInput } from "../route/client.js"
|
||||
import { Endpoint } from "../route/endpoint.js"
|
||||
import { HttpOptions, ProviderID, ToolDefinition, type ModelID } from "../schema/index.js"
|
||||
import type { OpenResponsesProviderOptionsInput } from "./open-responses-options.js"
|
||||
|
||||
export const id = ProviderID.make("meta")
|
||||
const baseURL = "https://api.meta.ai/v1"
|
||||
|
||||
export type ProviderOptionsInput = OpenResponsesProviderOptionsInput &
|
||||
Pick<AnthropicMessages.OptionsInput, "thinking" | "effort">
|
||||
export type MessagesOptionsInput = Pick<
|
||||
AnthropicMessages.OptionsInput,
|
||||
"thinking" | "effort" | "outputConfig" | "output_config" | "serviceTier" | "service_tier" | "metadata"
|
||||
> & { readonly [key: string]: unknown }
|
||||
export type ImageOptions = MetaImages.ImageOptions
|
||||
|
||||
export interface WebSearchOptions {
|
||||
readonly searchContextSize?: "low" | "medium" | "high" | (string & {})
|
||||
readonly userLocation?: {
|
||||
readonly city?: string
|
||||
readonly region?: string
|
||||
readonly country?: string
|
||||
readonly timezone?: string
|
||||
}
|
||||
}
|
||||
|
||||
export const webSearch = (options: WebSearchOptions = {}) =>
|
||||
ToolDefinition.make({
|
||||
name: "web_search",
|
||||
description: "Search the web with Meta's hosted search tool.",
|
||||
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
||||
native: {
|
||||
meta: {
|
||||
type: "web_search",
|
||||
search_context_size: options.searchContextSize,
|
||||
user_location:
|
||||
options.userLocation === undefined ? undefined : { type: "approximate", ...options.userLocation },
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export interface ImageGenerationOptions {
|
||||
readonly size?: string
|
||||
readonly outputFormat?: "webp" | "png" | "jpeg" | (string & {})
|
||||
readonly reasoningStrength?: "low" | "high" | (string & {})
|
||||
readonly enableImageSearch?: boolean
|
||||
readonly enableWebSearch?: boolean
|
||||
readonly enableShell?: boolean
|
||||
}
|
||||
|
||||
export const imageGeneration = (options: ImageGenerationOptions = {}) =>
|
||||
ToolDefinition.make({
|
||||
name: "image_generation",
|
||||
description: "Generate or edit an image with Muse Image.",
|
||||
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
||||
native: {
|
||||
meta: {
|
||||
type: "image_generation",
|
||||
size: options.size,
|
||||
output_format: options.outputFormat,
|
||||
reasoning_strength: options.reasoningStrength,
|
||||
enable_image_search: options.enableImageSearch,
|
||||
enable_web_search: options.enableWebSearch,
|
||||
enable_shell: options.enableShell,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export type LanguageModelOptions = Omit<RouteDefaultsInput, "providerOptions"> &
|
||||
ProviderAuthOption<"optional"> & {
|
||||
readonly baseURL?: string
|
||||
readonly providerOptions?: ProviderOptionsInput
|
||||
}
|
||||
|
||||
export interface Settings extends ProviderPackage.Settings {
|
||||
readonly apiKey?: string
|
||||
readonly baseURL?: string
|
||||
readonly providerOptions?: ProviderOptionsInput
|
||||
}
|
||||
|
||||
const responsesRoute = Route.make({
|
||||
id: "meta-responses",
|
||||
provider: id,
|
||||
providerMetadataKey: "meta",
|
||||
protocol: MetaResponses.protocol,
|
||||
endpoint: Endpoint.path("/responses", { baseURL }),
|
||||
// Meta Responses does not support WebSocket upgrades; always use HTTP/SSE.
|
||||
transport: MetaResponses.httpTransport,
|
||||
defaults: { providerOptions: { store: false, include: ["reasoning.encrypted_content"] } },
|
||||
})
|
||||
|
||||
const chatRoute = Route.make({
|
||||
id: "meta-chat",
|
||||
provider: id,
|
||||
providerMetadataKey: "meta",
|
||||
protocol: OpenAIChat.protocol,
|
||||
endpoint: Endpoint.path("/chat/completions", { baseURL }),
|
||||
framing: OpenAIChat.framing,
|
||||
})
|
||||
|
||||
const messagesRoute = Route.make({
|
||||
id: "meta-messages",
|
||||
provider: id,
|
||||
providerMetadataKey: "meta",
|
||||
protocol: MetaMessages.protocol,
|
||||
endpoint: Endpoint.path("/messages", { baseURL }),
|
||||
framing: AnthropicMessages.framing,
|
||||
defaults: { providerOptions: { thinking: { type: "adaptive", display: "omitted" } } },
|
||||
})
|
||||
|
||||
export const routes = [responsesRoute, chatRoute, messagesRoute]
|
||||
|
||||
export const configure = (input: LanguageModelOptions = {}) => {
|
||||
const { apiKey: _apiKey, auth: _auth, baseURL: endpoint, ...defaults } = input
|
||||
const options = {
|
||||
...defaults,
|
||||
endpoint: { baseURL: endpoint ?? baseURL },
|
||||
auth: AuthOptions.bearer(input, "META_API_KEY"),
|
||||
}
|
||||
const configuredResponses = responsesRoute.with(options)
|
||||
const configuredChat = chatRoute.with(options)
|
||||
const configuredMessages = messagesRoute.with(options)
|
||||
const responses = (modelID: string | ModelID) =>
|
||||
configuredResponses.model<OpenResponsesProviderOptionsInput>({ id: modelID })
|
||||
const chat = (modelID: string | ModelID) =>
|
||||
configuredChat.model<OpenResponsesProviderOptionsInput>({
|
||||
id: modelID,
|
||||
compatibility: { maxTokensField: "max_completion_tokens", supportsStore: false },
|
||||
})
|
||||
const messages = (modelID: string | ModelID) =>
|
||||
configuredMessages.model<MessagesOptionsInput>({
|
||||
id: modelID,
|
||||
compatibility: { requireSignature: false },
|
||||
})
|
||||
const image = (modelID: string | ModelID) =>
|
||||
MetaImages.model({
|
||||
id: modelID,
|
||||
baseURL: endpoint ?? baseURL,
|
||||
auth: options.auth,
|
||||
headers: input.headers,
|
||||
http: input.http === undefined ? undefined : HttpOptions.make(input.http),
|
||||
})
|
||||
return { id, model: responses, responses, chat, messages, image, configure }
|
||||
}
|
||||
|
||||
export const provider = configure()
|
||||
export const responses = provider.responses
|
||||
export const chat = provider.chat
|
||||
export const messages = provider.messages
|
||||
export const image = provider.image
|
||||
|
||||
export const model: ProviderPackage.Definition<Settings, OpenResponsesProviderOptionsInput>["model"] = (
|
||||
modelID,
|
||||
settings,
|
||||
) => fromSettings(settings).responses(modelID)
|
||||
|
||||
export const chatModel: ProviderPackage.Definition<Settings, OpenResponsesProviderOptionsInput>["model"] = (
|
||||
modelID,
|
||||
settings,
|
||||
) => fromSettings(settings).chat(modelID)
|
||||
|
||||
export const messagesModel: ProviderPackage.Definition<Settings, MessagesOptionsInput>["model"] = (modelID, settings) =>
|
||||
fromSettings(settings).messages(modelID)
|
||||
|
||||
function fromSettings(settings: Settings) {
|
||||
return configure({
|
||||
apiKey: settings.apiKey,
|
||||
baseURL: settings.baseURL,
|
||||
headers: settings.headers,
|
||||
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
|
||||
providerOptions: settings.providerOptions,
|
||||
})
|
||||
}
|
||||
|
||||
export * as Meta from "./meta.js"
|
||||
@@ -0,0 +1,2 @@
|
||||
export { chatModel as model } from "../meta.js"
|
||||
export type { Settings } from "../meta.js"
|
||||
@@ -0,0 +1,2 @@
|
||||
export { messagesModel as model } from "../meta.js"
|
||||
export type { Settings } from "../meta.js"
|
||||
@@ -0,0 +1,2 @@
|
||||
export { model } from "../meta.js"
|
||||
export type { Settings } from "../meta.js"
|
||||
@@ -0,0 +1,144 @@
|
||||
import { Effect, Schema } from "effect"
|
||||
import type { ProviderPackage } from "../provider-package.js"
|
||||
import { AnthropicMessages } from "../protocols/anthropic-messages.js"
|
||||
import { OpenAIChat } from "../protocols/openai-chat.js"
|
||||
import { OpenResponses } from "../protocols/open-responses.js"
|
||||
import { ProviderShared } from "../protocols/shared.js"
|
||||
import { AuthOptions, type ProviderAuthOption } from "../route/auth-options.js"
|
||||
import { Route, type RouteDefaultsInput } from "../route/client.js"
|
||||
import { Endpoint } from "../route/endpoint.js"
|
||||
import { Framing } from "../route/framing.js"
|
||||
import { Protocol } from "../route/protocol.js"
|
||||
import { ProviderID, type LLMRequest, type ModelID } from "../schema/index.js"
|
||||
|
||||
export const id = ProviderID.make("minimax")
|
||||
|
||||
export type MessagesOptionsInput = {
|
||||
/** M3 defaults to disabled; M2.x always thinks. */
|
||||
readonly thinking?: { readonly type: "adaptive" | "disabled" }
|
||||
readonly metadata?: AnthropicMessages.OptionsInput["metadata"]
|
||||
}
|
||||
|
||||
export type ChatOptionsInput = {
|
||||
/** M3 defaults to adaptive; M2.x always thinks. */
|
||||
readonly thinking?: { readonly type: "adaptive" | "disabled" | (string & {}) }
|
||||
/** Separates reasoning from text. Defaults to true. */
|
||||
readonly reasoningSplit?: boolean
|
||||
}
|
||||
|
||||
export type ResponsesOptionsInput = {
|
||||
/** M3 defaults to none. Other supported values enable thinking without changing its depth. */
|
||||
readonly reasoningEffort?: "none" | "minimal" | "low" | "medium" | "high" | (string & {})
|
||||
}
|
||||
|
||||
export type ProviderOptionsInput = MessagesOptionsInput | ChatOptionsInput | ResponsesOptionsInput
|
||||
|
||||
export type Config = Omit<RouteDefaultsInput, "providerOptions"> &
|
||||
ProviderAuthOption<"optional"> & {
|
||||
/** Overrides the selected API's base URL, including its version prefix. */
|
||||
readonly baseURL?: string
|
||||
readonly providerOptions?: ProviderOptionsInput
|
||||
}
|
||||
|
||||
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 })),
|
||||
reasoningSplit: Schema.optional(Schema.Boolean),
|
||||
})
|
||||
|
||||
const chatProtocol = Protocol.make({
|
||||
id: "minimax-chat",
|
||||
body: {
|
||||
schema: Schema.Struct({
|
||||
...OpenAIChat.bodyFields,
|
||||
thinking: ChatOptions.fields.thinking,
|
||||
reasoning_split: Schema.Boolean,
|
||||
}),
|
||||
from: Effect.fn("MiniMax.chatFromRequest")(function* (request: LLMRequest) {
|
||||
const options = yield* ProviderShared.validateWith(Schema.decodeUnknownEffect(ChatOptions))(
|
||||
request.providerOptions ?? {},
|
||||
)
|
||||
return {
|
||||
...(yield* OpenAIChat.protocol.body.from(request)),
|
||||
thinking: options.thinking,
|
||||
// MiniMax otherwise embeds <think> tags in ordinary assistant text.
|
||||
reasoning_split: options.reasoningSplit ?? true,
|
||||
}
|
||||
}),
|
||||
},
|
||||
stream: OpenAIChat.protocol.stream,
|
||||
})
|
||||
|
||||
const messagesRoute = Route.make({
|
||||
id: "minimax-messages",
|
||||
provider: id,
|
||||
providerMetadataKey: "minimax",
|
||||
protocol: AnthropicMessages.protocol,
|
||||
endpoint: Endpoint.path("/messages", { baseURL: "https://api.minimax.io/anthropic/v1" }),
|
||||
framing: AnthropicMessages.framing,
|
||||
headers: () => ({ "anthropic-version": "2023-06-01" }),
|
||||
})
|
||||
|
||||
const chatRoute = Route.make({
|
||||
id: "minimax-chat",
|
||||
provider: id,
|
||||
providerMetadataKey: "minimax",
|
||||
protocol: chatProtocol,
|
||||
endpoint: Endpoint.path("/chat/completions", { baseURL: "https://api.minimax.io/v1" }),
|
||||
framing: OpenAIChat.framing,
|
||||
})
|
||||
|
||||
const responsesRoute = Route.make({
|
||||
id: "minimax-responses",
|
||||
provider: id,
|
||||
providerMetadataKey: "minimax",
|
||||
protocol: OpenResponses.protocol,
|
||||
endpoint: Endpoint.path("/responses", { baseURL: "https://api.minimax.io/v1" }),
|
||||
framing: Framing.sse,
|
||||
})
|
||||
|
||||
export const routes = [messagesRoute, chatRoute, responsesRoute]
|
||||
|
||||
export const configure = (input: Config = {}) => {
|
||||
const { apiKey: _apiKey, auth: _auth, baseURL, ...rest } = input
|
||||
const defaults = {
|
||||
...rest,
|
||||
endpoint: baseURL === undefined ? undefined : { baseURL },
|
||||
auth: AuthOptions.bearer(input, "MINIMAX_API_KEY"),
|
||||
}
|
||||
const messages = (modelID: string | ModelID) =>
|
||||
messagesRoute.with(defaults).model<MessagesOptionsInput>({ id: modelID })
|
||||
const chat = (modelID: string | ModelID) =>
|
||||
chatRoute.with(defaults).model<ChatOptionsInput>({
|
||||
id: modelID,
|
||||
compatibility: { supportsStore: false, supportsStrictMode: false },
|
||||
})
|
||||
const responses = (modelID: string | ModelID) =>
|
||||
responsesRoute.with(defaults).model<ResponsesOptionsInput>({ id: modelID })
|
||||
return { id, model: messages, messages, chat, responses, configure }
|
||||
}
|
||||
|
||||
export const provider = configure()
|
||||
|
||||
export const model: ProviderPackage.Definition<Settings<MessagesOptionsInput>, MessagesOptionsInput>["model"] = (
|
||||
modelID,
|
||||
settings,
|
||||
) =>
|
||||
configure({
|
||||
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
|
||||
export const chat = provider.chat
|
||||
export const responses = provider.responses
|
||||
|
||||
export * as MiniMax from "./minimax.js"
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { ProviderPackage } from "../../provider-package.js"
|
||||
import { MiniMax } from "../minimax.js"
|
||||
|
||||
export type Settings = MiniMax.Settings<MiniMax.ChatOptionsInput>
|
||||
|
||||
export const model: ProviderPackage.Definition<Settings, MiniMax.ChatOptionsInput>["model"] = (modelID, settings) =>
|
||||
MiniMax.configure({
|
||||
apiKey: settings.apiKey,
|
||||
baseURL: settings.baseURL,
|
||||
headers: settings.headers,
|
||||
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
|
||||
providerOptions: settings.providerOptions,
|
||||
}).chat(modelID)
|
||||
@@ -0,0 +1 @@
|
||||
export { model, type Settings, type MessagesOptionsInput } from "../minimax.js"
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { ProviderPackage } from "../../provider-package.js"
|
||||
import { MiniMax } from "../minimax.js"
|
||||
|
||||
export type Settings = MiniMax.Settings<MiniMax.ResponsesOptionsInput>
|
||||
|
||||
export const model: ProviderPackage.Definition<Settings, MiniMax.ResponsesOptionsInput>["model"] = (
|
||||
modelID,
|
||||
settings,
|
||||
) =>
|
||||
MiniMax.configure({
|
||||
apiKey: settings.apiKey,
|
||||
baseURL: settings.baseURL,
|
||||
headers: settings.headers,
|
||||
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
|
||||
providerOptions: settings.providerOptions,
|
||||
}).responses(modelID)
|
||||
@@ -0,0 +1,145 @@
|
||||
import { Effect, Schema } from "effect"
|
||||
import type { ProviderPackage } from "../provider-package.js"
|
||||
import { AnthropicMessages } from "../protocols/anthropic-messages.js"
|
||||
import { OpenAIChat } from "../protocols/openai-chat.js"
|
||||
import { OpenResponses } from "../protocols/open-responses.js"
|
||||
import { ProviderShared } from "../protocols/shared.js"
|
||||
import { AuthOptions, type ProviderAuthOption } from "../route/auth-options.js"
|
||||
import { Route, type RouteDefaultsInput } from "../route/client.js"
|
||||
import { Endpoint } from "../route/endpoint.js"
|
||||
import { Framing } from "../route/framing.js"
|
||||
import { Protocol } from "../route/protocol.js"
|
||||
import { ProviderID, type LLMRequest, type ModelID } from "../schema/index.js"
|
||||
|
||||
export const id = ProviderID.make("moonshotai")
|
||||
|
||||
export type ReasoningEffort = "low" | "high" | "max" | (string & {})
|
||||
|
||||
export type ChatOptionsInput = {
|
||||
/** K3 always reasons; omitted effort uses the model's default. */
|
||||
readonly reasoningEffort?: ReasoningEffort
|
||||
/** K2.6 supports disabling thinking; K2.7 Code always thinks and preserves reasoning. */
|
||||
readonly thinking?: {
|
||||
readonly type: "enabled" | "disabled" | (string & {})
|
||||
readonly keep?: "all" | (string & {}) | null
|
||||
}
|
||||
}
|
||||
|
||||
export type MessagesOptionsInput = {
|
||||
readonly effort?: ReasoningEffort
|
||||
readonly metadata?: AnthropicMessages.OptionsInput["metadata"]
|
||||
}
|
||||
|
||||
export type ResponsesOptionsInput = {
|
||||
readonly reasoningEffort?: ReasoningEffort
|
||||
readonly safetyIdentifier?: string
|
||||
}
|
||||
|
||||
export type Config = Omit<RouteDefaultsInput, "providerOptions"> &
|
||||
ProviderAuthOption<"optional"> & {
|
||||
/** Overrides the selected API's base URL, including its version prefix. */
|
||||
readonly baseURL?: string
|
||||
readonly providerOptions?: ChatOptionsInput | MessagesOptionsInput | ResponsesOptionsInput
|
||||
}
|
||||
|
||||
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),
|
||||
thinking: Schema.optional(
|
||||
Schema.Struct({ type: Schema.String, keep: Schema.optional(Schema.NullOr(Schema.String)) }),
|
||||
),
|
||||
})
|
||||
|
||||
const chatProtocol = Protocol.make({
|
||||
id: "moonshot-chat",
|
||||
body: {
|
||||
schema: Schema.Struct({ ...OpenAIChat.bodyFields, thinking: ChatOptions.fields.thinking }),
|
||||
from: Effect.fn("Moonshot.chatFromRequest")(function* (request: LLMRequest) {
|
||||
const options = yield* ProviderShared.validateWith(Schema.decodeUnknownEffect(ChatOptions))(
|
||||
request.providerOptions ?? {},
|
||||
)
|
||||
return { ...(yield* OpenAIChat.protocol.body.from(request)), thinking: options.thinking }
|
||||
}),
|
||||
},
|
||||
stream: OpenAIChat.protocol.stream,
|
||||
})
|
||||
|
||||
const chatRoute = Route.make({
|
||||
id: "moonshot-chat",
|
||||
provider: id,
|
||||
providerMetadataKey: "moonshot",
|
||||
protocol: chatProtocol,
|
||||
endpoint: Endpoint.path("/chat/completions", { baseURL: "https://api.moonshot.ai/v1" }),
|
||||
framing: OpenAIChat.framing,
|
||||
})
|
||||
|
||||
const messagesRoute = Route.make({
|
||||
id: "moonshot-messages",
|
||||
provider: id,
|
||||
providerMetadataKey: "moonshot",
|
||||
protocol: AnthropicMessages.protocol,
|
||||
endpoint: Endpoint.path("/messages", { baseURL: "https://api.moonshot.ai/anthropic/v1" }),
|
||||
framing: AnthropicMessages.framing,
|
||||
})
|
||||
|
||||
const responsesRoute = Route.make({
|
||||
id: "moonshot-responses",
|
||||
provider: id,
|
||||
providerMetadataKey: "moonshot",
|
||||
protocol: OpenResponses.protocol,
|
||||
endpoint: Endpoint.path("/responses", { baseURL: "https://api.moonshot.ai/v1" }),
|
||||
framing: Framing.sse,
|
||||
})
|
||||
|
||||
export const routes = [chatRoute, messagesRoute, responsesRoute]
|
||||
|
||||
export const configure = (input: Config = {}) => {
|
||||
const { apiKey: _apiKey, auth: _auth, baseURL, ...rest } = input
|
||||
const defaults = {
|
||||
...rest,
|
||||
endpoint: baseURL === undefined ? undefined : { baseURL },
|
||||
auth: AuthOptions.bearer(input, ["MOONSHOT_API_KEY", "MOONSHOTAI_API_KEY"]),
|
||||
}
|
||||
const chat = (modelID: string | ModelID) =>
|
||||
chatRoute.with(defaults).model<ChatOptionsInput>({
|
||||
id: modelID,
|
||||
compatibility: {
|
||||
maxTokensField: "max_tokens",
|
||||
supportsStore: false,
|
||||
supportsStrictMode: false,
|
||||
toolSchema: "moonshot",
|
||||
reasoningField: "reasoning_content",
|
||||
},
|
||||
})
|
||||
const messages = (modelID: string | ModelID) =>
|
||||
messagesRoute.with(defaults).model<MessagesOptionsInput>({
|
||||
id: modelID,
|
||||
compatibility: { requireSignature: false, toolSchema: "moonshot" },
|
||||
})
|
||||
const responses = (modelID: string | ModelID) =>
|
||||
responsesRoute
|
||||
.with(defaults)
|
||||
.model<ResponsesOptionsInput>({ id: modelID, compatibility: { toolSchema: "moonshot" } })
|
||||
return { id, model: chat, chat, messages, responses, configure }
|
||||
}
|
||||
|
||||
export const provider = configure()
|
||||
export const chat = provider.chat
|
||||
export const messages = provider.messages
|
||||
export const responses = provider.responses
|
||||
|
||||
export const model: ProviderPackage.Definition<Settings, ChatOptionsInput>["model"] = (modelID, settings) =>
|
||||
configure({
|
||||
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"
|
||||
@@ -0,0 +1 @@
|
||||
export { model, type Settings } from "../moonshot.js"
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { ProviderPackage } from "../../provider-package.js"
|
||||
import { Moonshot } from "../moonshot.js"
|
||||
|
||||
export type Settings = Moonshot.Settings<Moonshot.MessagesOptionsInput>
|
||||
|
||||
export const model: ProviderPackage.Definition<Settings, Moonshot.MessagesOptionsInput>["model"] = (
|
||||
modelID,
|
||||
settings,
|
||||
) =>
|
||||
Moonshot.configure({
|
||||
apiKey: settings.apiKey,
|
||||
baseURL: settings.baseURL,
|
||||
headers: settings.headers,
|
||||
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
|
||||
providerOptions: settings.providerOptions,
|
||||
}).messages(modelID)
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { ProviderPackage } from "../../provider-package.js"
|
||||
import { Moonshot } from "../moonshot.js"
|
||||
|
||||
export type Settings = Moonshot.Settings<Moonshot.ResponsesOptionsInput>
|
||||
|
||||
export const model: ProviderPackage.Definition<Settings, Moonshot.ResponsesOptionsInput>["model"] = (
|
||||
modelID,
|
||||
settings,
|
||||
) =>
|
||||
Moonshot.configure({
|
||||
apiKey: settings.apiKey,
|
||||
baseURL: settings.baseURL,
|
||||
headers: settings.headers,
|
||||
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
|
||||
providerOptions: settings.providerOptions,
|
||||
}).responses(modelID)
|
||||
@@ -162,6 +162,7 @@ const bodyOptions = (input: unknown) => {
|
||||
export const route = Route.make({
|
||||
id: ADAPTER,
|
||||
provider: id,
|
||||
providerMetadataKey: "openrouter",
|
||||
protocol,
|
||||
endpoint: Endpoint.path("/chat/completions", { baseURL }),
|
||||
framing: OpenAIChat.framing,
|
||||
|
||||
@@ -24,6 +24,7 @@ export interface Settings extends ProviderPackage.Settings {
|
||||
export const route = Route.make({
|
||||
id: "togetherai-chat",
|
||||
provider: id,
|
||||
providerMetadataKey: "togetherai",
|
||||
protocol: OpenAIChat.protocol,
|
||||
endpoint: Endpoint.path("/chat/completions", { baseURL }),
|
||||
framing: OpenAIChat.framing,
|
||||
|
||||
@@ -32,12 +32,13 @@ const RESPONSES_WEBSOCKET_ROTATE_AFTER_MS = 24 * 60 * 1000
|
||||
|
||||
const responsesRoute = Route.make({
|
||||
compact: { endpoint: XAIResponses.compact },
|
||||
id: "xai-responses",
|
||||
id: "openai-responses",
|
||||
provider: id,
|
||||
providerMetadataKey: "xai",
|
||||
protocol: XAIResponses.protocol,
|
||||
endpoint: Endpoint.path("/responses", { baseURL }),
|
||||
transport: OpenResponsesChannel.transport({
|
||||
id: "xai-responses",
|
||||
id: "openai-responses",
|
||||
name: "xAI Responses",
|
||||
rotateAfterMs: RESPONSES_WEBSOCKET_ROTATE_AFTER_MS,
|
||||
}),
|
||||
@@ -45,8 +46,9 @@ const responsesRoute = Route.make({
|
||||
})
|
||||
|
||||
const chatRoute = Route.make({
|
||||
id: "xai-chat",
|
||||
id: "openai-compatible-chat",
|
||||
provider: id,
|
||||
providerMetadataKey: "xai",
|
||||
protocol: OpenAIChat.protocol,
|
||||
endpoint: Endpoint.path("/chat/completions", { baseURL }),
|
||||
framing: OpenAIChat.framing,
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import type { ProviderPackage } from "../provider-package.js"
|
||||
import { AnthropicMessages } from "../protocols/anthropic-messages.js"
|
||||
import { OpenAIChat } from "../protocols/openai-chat.js"
|
||||
import { OpenResponses } from "../protocols/open-responses.js"
|
||||
import { ZAIChat } from "../protocols/zai-chat.js"
|
||||
import { ZAIMessages } from "../protocols/zai-messages.js"
|
||||
import { AuthOptions, type ProviderAuthOption } from "../route/auth-options.js"
|
||||
import { Route, type RouteDefaultsInput } from "../route/client.js"
|
||||
import { Endpoint } from "../route/endpoint.js"
|
||||
import { Framing } from "../route/framing.js"
|
||||
import { ProviderID, type ModelID } from "../schema/index.js"
|
||||
|
||||
export const id = ProviderID.make("zai-coding-plan")
|
||||
|
||||
export type ChatOptionsInput = ZAIChat.OptionsInput
|
||||
export type MessagesOptionsInput = ZAIMessages.OptionsInput
|
||||
export type ResponsesOptionsInput = { readonly reasoningEffort?: ZAIChat.ReasoningEffort }
|
||||
|
||||
export type Config = Omit<RouteDefaultsInput, "providerOptions"> &
|
||||
ProviderAuthOption<"optional"> & {
|
||||
/** Overrides the selected API's complete base URL. */
|
||||
readonly baseURL?: string
|
||||
readonly providerOptions?: ChatOptionsInput | MessagesOptionsInput | ResponsesOptionsInput
|
||||
}
|
||||
|
||||
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",
|
||||
provider: id,
|
||||
providerMetadataKey: "zai",
|
||||
protocol: ZAIChat.protocol,
|
||||
endpoint: Endpoint.path("/chat/completions", { baseURL: "https://api.z.ai/api/coding/paas/v4" }),
|
||||
framing: OpenAIChat.framing,
|
||||
})
|
||||
const messagesRoute = Route.make({
|
||||
id: "zai-coding-messages",
|
||||
provider: id,
|
||||
providerMetadataKey: "zai",
|
||||
protocol: ZAIMessages.protocol,
|
||||
endpoint: Endpoint.path("/messages", { baseURL: "https://api.z.ai/api/anthropic/v1" }),
|
||||
framing: AnthropicMessages.framing,
|
||||
headers: () => ({ "anthropic-version": "2023-06-01" }),
|
||||
})
|
||||
const responsesRoute = Route.make({
|
||||
id: "zai-coding-responses",
|
||||
provider: id,
|
||||
providerMetadataKey: "zai",
|
||||
protocol: OpenResponses.protocol,
|
||||
endpoint: Endpoint.path("/responses", { baseURL: "https://api.z.ai/api/v1" }),
|
||||
framing: Framing.sse,
|
||||
})
|
||||
|
||||
export const routes = [chatRoute, messagesRoute, responsesRoute]
|
||||
|
||||
export const configure = (input: Config = {}) => {
|
||||
const { apiKey: _apiKey, auth: _auth, baseURL, ...rest } = input
|
||||
const defaults = {
|
||||
...rest,
|
||||
endpoint: baseURL === undefined ? undefined : { baseURL },
|
||||
auth: AuthOptions.bearer(input, "ZAI_API_KEY"),
|
||||
}
|
||||
const chat = (modelID: string | ModelID) =>
|
||||
chatRoute.with(defaults).model<ChatOptionsInput>({ id: modelID, compatibility: ZAIChat.compatibility })
|
||||
const messages = (modelID: string | ModelID) =>
|
||||
messagesRoute
|
||||
.with(defaults)
|
||||
.model<MessagesOptionsInput>({ id: modelID, compatibility: { requireSignature: false } })
|
||||
const responses = (modelID: string | ModelID) =>
|
||||
responsesRoute.with(defaults).model<ResponsesOptionsInput>({ id: modelID })
|
||||
return { id, model: chat, chat, messages, responses, configure }
|
||||
}
|
||||
|
||||
export const provider = configure()
|
||||
export const chat = provider.chat
|
||||
export const messages = provider.messages
|
||||
export const responses = provider.responses
|
||||
|
||||
export const model: ProviderPackage.Definition<Settings, ChatOptionsInput>["model"] = (modelID, settings) =>
|
||||
configure({
|
||||
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"
|
||||
@@ -0,0 +1 @@
|
||||
export { model, type Settings } from "../zai-coding-plan.js"
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { ProviderPackage } from "../../provider-package.js"
|
||||
import { ZAICodingPlan } from "../zai-coding-plan.js"
|
||||
|
||||
export type Settings = ZAICodingPlan.Settings<ZAICodingPlan.MessagesOptionsInput>
|
||||
|
||||
export const model: ProviderPackage.Definition<Settings, ZAICodingPlan.MessagesOptionsInput>["model"] = (
|
||||
modelID,
|
||||
settings,
|
||||
) =>
|
||||
ZAICodingPlan.configure({
|
||||
apiKey: settings.apiKey,
|
||||
baseURL: settings.baseURL,
|
||||
headers: settings.headers,
|
||||
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
|
||||
providerOptions: settings.providerOptions,
|
||||
}).messages(modelID)
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { ProviderPackage } from "../../provider-package.js"
|
||||
import { ZAICodingPlan } from "../zai-coding-plan.js"
|
||||
|
||||
export type Settings = ZAICodingPlan.Settings<ZAICodingPlan.ResponsesOptionsInput>
|
||||
|
||||
export const model: ProviderPackage.Definition<Settings, ZAICodingPlan.ResponsesOptionsInput>["model"] = (
|
||||
modelID,
|
||||
settings,
|
||||
) =>
|
||||
ZAICodingPlan.configure({
|
||||
apiKey: settings.apiKey,
|
||||
baseURL: settings.baseURL,
|
||||
headers: settings.headers,
|
||||
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
|
||||
providerOptions: settings.providerOptions,
|
||||
}).responses(modelID)
|
||||
@@ -1,20 +1,53 @@
|
||||
import type { ProviderPackage } from "../provider-package.js"
|
||||
import { ZAIChat } from "../protocols/zai-chat.js"
|
||||
import { ZAIImages } from "../protocols/zai-images.js"
|
||||
import { OpenAIChat } from "../protocols/openai-chat.js"
|
||||
import { AuthOptions, type ProviderAuthOption } from "../route/auth-options.js"
|
||||
import { Route, type RouteDefaultsInput } from "../route/client.js"
|
||||
import { Endpoint } from "../route/endpoint.js"
|
||||
import { HttpOptions, ProviderID, type ModelID } from "../schema/index.js"
|
||||
|
||||
export const id = ProviderID.make("zai")
|
||||
|
||||
export type Config = ProviderAuthOption<"optional"> & {
|
||||
export type ChatOptionsInput = ZAIChat.OptionsInput
|
||||
|
||||
export type Config = Omit<RouteDefaultsInput, "providerOptions"> &
|
||||
ProviderAuthOption<"optional"> & {
|
||||
readonly baseURL?: string
|
||||
readonly providerOptions?: ChatOptionsInput
|
||||
}
|
||||
|
||||
export interface Settings extends ProviderPackage.Settings {
|
||||
readonly apiKey?: string
|
||||
readonly baseURL?: string
|
||||
readonly headers?: Record<string, string>
|
||||
readonly http?: HttpOptions.Input
|
||||
readonly providerOptions?: ChatOptionsInput
|
||||
}
|
||||
|
||||
export type { ZAIImageOptions } from "../protocols/zai-images.js"
|
||||
|
||||
const auth = (options: ProviderAuthOption<"optional">) => AuthOptions.bearer(options, "ZAI_API_KEY")
|
||||
|
||||
const chatRoute = Route.make({
|
||||
id: "zai-chat",
|
||||
provider: id,
|
||||
providerMetadataKey: "zai",
|
||||
protocol: ZAIChat.protocol,
|
||||
endpoint: Endpoint.path("/chat/completions", { baseURL: "https://api.z.ai/api/paas/v4" }),
|
||||
framing: OpenAIChat.framing,
|
||||
})
|
||||
|
||||
export const routes = [chatRoute]
|
||||
|
||||
export const configure = (input: Config = {}) => {
|
||||
const { apiKey: _apiKey, auth: _auth, baseURL, ...rest } = input
|
||||
const chat = (modelID: string | ModelID) =>
|
||||
chatRoute
|
||||
.with({
|
||||
...rest,
|
||||
endpoint: baseURL === undefined ? undefined : { baseURL },
|
||||
auth: auth(input),
|
||||
})
|
||||
.model<ChatOptionsInput>({ id: modelID, compatibility: ZAIChat.compatibility })
|
||||
const image = (modelID: string | ModelID) =>
|
||||
ZAIImages.model({
|
||||
id: modelID,
|
||||
@@ -26,6 +59,8 @@ export const configure = (input: Config = {}) => {
|
||||
|
||||
return {
|
||||
id,
|
||||
model: chat,
|
||||
chat,
|
||||
image,
|
||||
configure,
|
||||
}
|
||||
@@ -33,3 +68,15 @@ export const configure = (input: Config = {}) => {
|
||||
|
||||
export const provider = configure()
|
||||
export const image = provider.image
|
||||
export const chat = provider.chat
|
||||
|
||||
export const model: ProviderPackage.Definition<Settings, ChatOptionsInput>["model"] = (modelID, settings) =>
|
||||
configure({
|
||||
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"
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { model, type Settings } from "../zai.js"
|
||||
@@ -42,9 +42,10 @@ export interface Route<
|
||||
Compact extends CompactionOperations | undefined = CompactionOperations | undefined,
|
||||
> {
|
||||
readonly compact: Compact
|
||||
/** Route identity and namespace for metadata emitted and replayed by this route. */
|
||||
readonly id: string
|
||||
readonly provider?: ProviderID
|
||||
/** ProviderMetadata namespace emitted and consumed by this route. */
|
||||
readonly providerMetadataKey?: string
|
||||
readonly protocol: ProtocolID
|
||||
readonly endpoint: Endpoint.Definition<Body>
|
||||
readonly auth: Auth.Definition
|
||||
@@ -112,6 +113,7 @@ export interface RoutePatch<Body, Prepared> extends RouteDefaultsInput {
|
||||
readonly compact?: CompactionOperations
|
||||
readonly id?: string
|
||||
readonly provider?: string | ProviderID
|
||||
readonly providerMetadataKey?: string
|
||||
readonly auth?: Auth.Definition
|
||||
readonly transport?: Transport<Body, Prepared, unknown>
|
||||
readonly endpoint?: EndpointPatch<Body>
|
||||
@@ -274,10 +276,12 @@ const resolveRequestOptions = (request: LLMRequest) => {
|
||||
|
||||
export interface MakeInput<Body, Frame, Event, State> {
|
||||
readonly compact?: CompactionOperations
|
||||
/** Route identity, also used as the namespace for emitted and replayed ProviderMetadata. */
|
||||
/** Route id used in diagnostics and prepared request metadata. */
|
||||
readonly id: string
|
||||
/** Provider identity for route-owned model construction. */
|
||||
readonly provider?: string | ProviderID
|
||||
/** ProviderMetadata namespace emitted and consumed by this route. */
|
||||
readonly providerMetadataKey?: string
|
||||
/** Semantic API contract — owns body construction, body schema, and parsing. */
|
||||
readonly protocol: Protocol<Body, Frame, Event, State>
|
||||
/** Where the request is sent. */
|
||||
@@ -294,10 +298,12 @@ export interface MakeInput<Body, Frame, Event, State> {
|
||||
|
||||
export interface MakeTransportInput<Body, Prepared, Frame, Event, State> {
|
||||
readonly compact?: CompactionOperations
|
||||
/** Route identity, also used as the namespace for emitted and replayed ProviderMetadata. */
|
||||
/** Route id used in diagnostics and prepared request metadata. */
|
||||
readonly id: string
|
||||
/** Provider identity for route-owned model construction. */
|
||||
readonly provider?: string | ProviderID
|
||||
/** ProviderMetadata namespace emitted and consumed by this route. */
|
||||
readonly providerMetadataKey?: string
|
||||
/** Semantic API contract — owns body construction, body schema, and parsing. */
|
||||
readonly protocol: Protocol<Body, Frame, Event, State>
|
||||
/** Where the request is sent. */
|
||||
@@ -370,6 +376,7 @@ function makeFromTransport<Body, Prepared, Frame, Event, State>(
|
||||
compact: routeInput.compact,
|
||||
id: routeInput.id,
|
||||
provider: routeInput.provider === undefined ? undefined : ProviderID.make(routeInput.provider),
|
||||
providerMetadataKey: routeInput.providerMetadataKey,
|
||||
protocol: protocol.id,
|
||||
endpoint: routeInput.endpoint,
|
||||
auth: routeInput.auth ?? Auth.none,
|
||||
@@ -378,12 +385,17 @@ function makeFromTransport<Body, Prepared, Frame, Event, State>(
|
||||
defaults: routeInput.defaults ?? {},
|
||||
body: protocol.body,
|
||||
with: (patch: RoutePatch<Body, Prepared>) => {
|
||||
const { compact, id, provider, auth, transport, endpoint, ...defaults } = patch
|
||||
const { compact, id, provider, providerMetadataKey, auth, transport, endpoint, ...defaults } = patch
|
||||
return build({
|
||||
...routeInput,
|
||||
compact: "compact" in patch ? compact : routeInput.compact,
|
||||
id: id ?? routeInput.id,
|
||||
provider: provider ?? routeInput.provider,
|
||||
providerMetadataKey:
|
||||
providerMetadataKey ??
|
||||
(provider !== undefined && String(provider) !== String(routeInput.provider)
|
||||
? String(provider)
|
||||
: routeInput.providerMetadataKey),
|
||||
auth: auth ?? routeInput.auth,
|
||||
endpoint: endpoint ? Endpoint.merge(routeInput.endpoint, endpoint) : routeInput.endpoint,
|
||||
transport: (transport as Transport<Body, Prepared, Frame> | undefined) ?? routeInput.transport,
|
||||
@@ -524,6 +536,7 @@ export function make<Body, Prepared, Frame, Event, State>(
|
||||
compact: input.compact,
|
||||
id: input.id,
|
||||
provider: input.provider,
|
||||
providerMetadataKey: input.providerMetadataKey,
|
||||
protocol,
|
||||
endpoint: input.endpoint,
|
||||
auth: input.auth,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Schema } from "effect"
|
||||
import { Tool } from "@opencode-ai/schema/tool"
|
||||
import { Tool } from "@opencode/schema/tool"
|
||||
import { ModelID, ProviderID, RouteID } from "./ids.js"
|
||||
|
||||
export const ProviderFailureClassification = Schema.Literals(["context-overflow", "payload-too-large"])
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Schema } from "effect"
|
||||
import { LLM } from "@opencode-ai/schema/llm"
|
||||
import { LLM } from "@opencode/schema/llm"
|
||||
import { ContentBlockID, ToolCallID } from "./ids.js"
|
||||
import {
|
||||
Message,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Schema } from "effect"
|
||||
import { Tool } from "@opencode-ai/schema/tool"
|
||||
import { Tool } from "@opencode/schema/tool"
|
||||
import {
|
||||
CacheHint,
|
||||
CachePolicy,
|
||||
@@ -15,7 +15,6 @@ import { ProviderID } from "./ids.js"
|
||||
export const MessageRole = Schema.Literals(["system", "user", "assistant", "tool"])
|
||||
export type MessageRole = Schema.Schema.Type<typeof MessageRole>
|
||||
|
||||
/** Provider-specific data; LLM routes emit and replay it under their route ID. */
|
||||
export const ProviderMetadata = Schema.Record(Schema.String, Schema.Record(Schema.String, Schema.Unknown)).annotate({
|
||||
identifier: "LLM.ProviderMetadata",
|
||||
})
|
||||
|
||||
@@ -163,6 +163,8 @@ export class LanguageModelCompatibility extends Schema.Class<LanguageModelCompat
|
||||
supportsStrictMode: Schema.optional(Schema.Boolean),
|
||||
zaiToolStream: Schema.optional(Schema.Boolean),
|
||||
requireSignature: Schema.optional(Schema.Boolean),
|
||||
/** Supports Anthropic's thinking-prefix mismatch controls. Overrides model-ID detection. */
|
||||
supportsThinkingBlockBinding: Schema.optional(Schema.Boolean),
|
||||
}) {}
|
||||
|
||||
export namespace LanguageModelCompatibility {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Effect, JsonSchema, Schema } from "effect"
|
||||
import { Tool } from "@opencode-ai/schema/tool"
|
||||
import { Tool } from "@opencode/schema/tool"
|
||||
import type {
|
||||
ToolCallPart,
|
||||
ToolDefinition as ToolDefinitionClass,
|
||||
|
||||
@@ -66,14 +66,14 @@ export function continuationRequest(input: {
|
||||
firstAssistant.push({
|
||||
type: "reasoning",
|
||||
text: "I inspected the previous turn.",
|
||||
providerMetadata: { [input.model.route.id]: { signature: "sig_continuation_1" } },
|
||||
providerMetadata: { anthropic: { signature: "sig_continuation_1" } },
|
||||
})
|
||||
if (features.has("encrypted-reasoning"))
|
||||
firstAssistant.push({
|
||||
type: "reasoning",
|
||||
text: "I inspected the previous turn.",
|
||||
providerMetadata: {
|
||||
[input.model.route.id]: {
|
||||
openai: {
|
||||
itemId: "rs_continuation_1",
|
||||
reasoningEncryptedContent: "encrypted-continuation-state",
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { AIError, ImageInput, LanguageModel, LLM, LLMClient, Provider } from "@opencode-ai/ai"
|
||||
import { Route, Protocol, WebSocketTransport } from "@opencode-ai/ai/route"
|
||||
import { Provider as ProviderSubpath } from "@opencode-ai/ai/provider"
|
||||
import { AIError, ImageInput, LanguageModel, LLM, LLMClient, Provider } from "@opencode/ai"
|
||||
import { Route, Protocol, WebSocketTransport } from "@opencode/ai/route"
|
||||
import { Provider as ProviderSubpath } from "@opencode/ai/provider"
|
||||
import {
|
||||
Baseten,
|
||||
CloudflareAIGateway,
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
OpenAICompatible,
|
||||
OpenRouter,
|
||||
XAI,
|
||||
} from "@opencode-ai/ai/providers"
|
||||
} from "@opencode/ai/providers"
|
||||
import {
|
||||
OpenAIChat,
|
||||
OpenAICompatibleChat,
|
||||
@@ -20,9 +20,9 @@ import {
|
||||
OpenAIResponses,
|
||||
OpenResponses,
|
||||
OpenResponsesChannel,
|
||||
} from "@opencode-ai/ai/protocols"
|
||||
import * as AnthropicMessages from "@opencode-ai/ai/protocols/anthropic-messages"
|
||||
import { TestLLM } from "@opencode-ai/ai/testing"
|
||||
} from "@opencode/ai/protocols"
|
||||
import * as AnthropicMessages from "@opencode/ai/protocols/anthropic-messages"
|
||||
import { TestLLM } from "@opencode/ai/testing"
|
||||
|
||||
describe("public exports", () => {
|
||||
test("root exposes app-facing runtime APIs", () => {
|
||||
@@ -46,7 +46,7 @@ describe("public exports", () => {
|
||||
})
|
||||
|
||||
test("provider barrels expose user-facing facades", async () => {
|
||||
const { OpenAICompatibleResponses } = await import("@opencode-ai/ai/providers")
|
||||
const { OpenAICompatibleResponses } = await import("@opencode/ai/providers")
|
||||
|
||||
expect(OpenAI.model).toBeFunction()
|
||||
expect(OpenAI.provider.responses).toBe(OpenAI.responses)
|
||||
@@ -69,8 +69,8 @@ describe("public exports", () => {
|
||||
expect(XAI.model).toBeFunction()
|
||||
expect(XAI.provider.responses).toBe(XAI.responses)
|
||||
expect(XAI.provider.chat).toBe(XAI.chat)
|
||||
expect(XAI.configure({ apiKey: "fixture" }).responses("grok-4.3").route.id).toBe("xai-responses")
|
||||
expect(XAI.configure({ apiKey: "fixture" }).chat("grok-4.3").route.id).toBe("xai-chat")
|
||||
expect(XAI.configure({ apiKey: "fixture" }).responses("grok-4.3").route.id).toBe("openai-responses")
|
||||
expect(XAI.configure({ apiKey: "fixture" }).chat("grok-4.3").route.id).toBe("openai-compatible-chat")
|
||||
})
|
||||
|
||||
test("protocol barrels expose supported low-level routes", () => {
|
||||
|
||||
Vendored
+35
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"version": 1,
|
||||
"metadata": {
|
||||
"tags": [
|
||||
"prefix:alibaba-chat",
|
||||
"provider:alibaba",
|
||||
"protocol:alibaba-chat",
|
||||
"region:ap-southeast-1",
|
||||
"thinking",
|
||||
"usage"
|
||||
],
|
||||
"name": "alibaba-chat/qwen-3-7-plus-streams-thinking-disabled",
|
||||
"recordedAt": "2026-09-08T03:10:42.782Z"
|
||||
},
|
||||
"interactions": [
|
||||
{
|
||||
"transport": "http",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "https://dashscope-intl.aliyuncs.com/compatible-mode/v1/chat/completions",
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": "{\"model\":\"qwen3.7-plus\",\"messages\":[{\"role\":\"user\",\"content\":\"What is 173 multiplied by 219? Reply with only the final integer.\"}],\"stream\":true,\"stream_options\":{\"include_usage\":true},\"max_completion_tokens\":4096,\"enable_thinking\":false}"
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"content-type": "text/event-stream;charset=utf-8"
|
||||
},
|
||||
"body": "data: {\"model\":\"qwen3.7-plus\",\"id\":\"chatcmpl-047bcb67-b193-9a4f-9d77-ea0325be6d7c\",\"created\":1788837041,\"object\":\"chat.completion.chunk\",\"usage\":null,\"choices\":[{\"logprobs\":null,\"index\":0,\"delta\":{\"content\":\"\",\"role\":\"assistant\"},\"finish_reason\":null}]}\n\ndata: {\"model\":\"qwen3.7-plus\",\"id\":\"chatcmpl-047bcb67-b193-9a4f-9d77-ea0325be6d7c\",\"choices\":[{\"delta\":{\"content\":\"3\"},\"index\":0,\"finish_reason\":null,\"logprobs\":null}],\"created\":1788837041,\"object\":\"chat.completion.chunk\",\"usage\":null}\n\ndata: {\"model\":\"qwen3.7-plus\",\"id\":\"chatcmpl-047bcb67-b193-9a4f-9d77-ea0325be6d7c\",\"choices\":[{\"delta\":{\"content\":\"7887\"},\"index\":0,\"finish_reason\":null,\"logprobs\":null}],\"created\":1788837041,\"object\":\"chat.completion.chunk\",\"usage\":null}\n\ndata: {\"model\":\"qwen3.7-plus\",\"id\":\"chatcmpl-047bcb67-b193-9a4f-9d77-ea0325be6d7c\",\"choices\":[{\"delta\":{\"content\":\"\"},\"index\":0,\"finish_reason\":\"stop\",\"logprobs\":null}],\"created\":1788837041,\"object\":\"chat.completion.chunk\",\"usage\":null}\n\ndata: {\"choices\":[],\"created\":1788837041,\"id\":\"chatcmpl-047bcb67-b193-9a4f-9d77-ea0325be6d7c\",\"model\":\"qwen3.7-plus\",\"object\":\"chat.completion.chunk\",\"usage\":{\"completion_tokens\":5,\"prompt_tokens\":32,\"prompt_tokens_details\":{\"cached_tokens\":0,\"text_tokens\":32},\"total_tokens\":37}}\n\ndata: [DONE]\n\n"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
+35
File diff suppressed because one or more lines are too long
+28
File diff suppressed because one or more lines are too long
Vendored
+35
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"version": 1,
|
||||
"metadata": {
|
||||
"tags": [
|
||||
"prefix:alibaba-chat",
|
||||
"provider:alibaba",
|
||||
"protocol:alibaba-chat",
|
||||
"region:ap-southeast-1",
|
||||
"tool",
|
||||
"tool-choice"
|
||||
],
|
||||
"name": "alibaba-chat/qwen-3-8-max-obeys-named-tool-choice",
|
||||
"recordedAt": "2026-09-08T03:10:56.576Z"
|
||||
},
|
||||
"interactions": [
|
||||
{
|
||||
"transport": "http",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "https://dashscope-intl.aliyuncs.com/compatible-mode/v1/chat/completions",
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": "{\"model\":\"qwen3.8-max\",\"messages\":[{\"role\":\"user\",\"content\":\"Find the current weather in Paris.\"}],\"tools\":[{\"type\":\"function\",\"function\":{\"name\":\"get_weather\",\"description\":\"Get weather in a city\",\"parameters\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\",\"enum\":[\"Paris\"]}},\"required\":[\"city\"]}}}],\"tool_choice\":{\"type\":\"function\",\"function\":{\"name\":\"get_weather\"}},\"stream\":true,\"stream_options\":{\"include_usage\":true},\"reasoning_effort\":\"none\",\"max_completion_tokens\":4096}"
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"content-type": "text/event-stream;charset=utf-8"
|
||||
},
|
||||
"body": "data: {\"model\":\"qwen3.8-max\",\"id\":\"chatcmpl-681dfa5a-ae08-98da-ba0d-4b1d0d364b6b\",\"created\":1788837055,\"object\":\"chat.completion.chunk\",\"usage\":null,\"choices\":[{\"logprobs\":null,\"index\":0,\"delta\":{\"content\":\"\",\"role\":\"assistant\",\"tool_calls\":[{\"index\":0,\"id\":\"call_4eb823cb28d141c8befbb331\",\"type\":\"function\",\"function\":{\"name\":\"get_weather\",\"arguments\":\"\"}}]},\"finish_reason\":null}]}\n\ndata: {\"model\":\"qwen3.8-max\",\"id\":\"chatcmpl-681dfa5a-ae08-98da-ba0d-4b1d0d364b6b\",\"choices\":[{\"delta\":{\"content\":\"\",\"tool_calls\":[{\"index\":0,\"id\":\"\",\"type\":\"function\",\"function\":{\"arguments\":\"\"}}]},\"index\":0,\"finish_reason\":null,\"logprobs\":null}],\"created\":1788837055,\"object\":\"chat.completion.chunk\",\"usage\":null}\n\ndata: {\"model\":\"qwen3.8-max\",\"id\":\"chatcmpl-681dfa5a-ae08-98da-ba0d-4b1d0d364b6b\",\"choices\":[{\"delta\":{\"content\":\"\",\"tool_calls\":[{\"type\":\"function\",\"index\":0,\"function\":{\"arguments\":\"{\\\"city\\\": \\\"Paris\"}}]},\"index\":0,\"finish_reason\":null,\"logprobs\":null}],\"created\":1788837055,\"object\":\"chat.completion.chunk\",\"usage\":null}\n\ndata: {\"model\":\"qwen3.8-max\",\"id\":\"chatcmpl-681dfa5a-ae08-98da-ba0d-4b1d0d364b6b\",\"choices\":[{\"delta\":{\"content\":\"\",\"tool_calls\":[{\"type\":\"function\",\"index\":0,\"function\":{\"arguments\":\"\\\"\"}}]},\"index\":0,\"finish_reason\":null,\"logprobs\":null}],\"created\":1788837055,\"object\":\"chat.completion.chunk\",\"usage\":null}\n\ndata: {\"model\":\"qwen3.8-max\",\"id\":\"chatcmpl-681dfa5a-ae08-98da-ba0d-4b1d0d364b6b\",\"choices\":[{\"delta\":{\"content\":\"\",\"tool_calls\":[{\"type\":\"function\",\"index\":0,\"function\":{\"arguments\":\"}\"}}]},\"index\":0,\"finish_reason\":null,\"logprobs\":null}],\"created\":1788837055,\"object\":\"chat.completion.chunk\",\"usage\":null}\n\ndata: {\"model\":\"qwen3.8-max\",\"id\":\"chatcmpl-681dfa5a-ae08-98da-ba0d-4b1d0d364b6b\",\"choices\":[{\"delta\":{\"content\":\"\",\"tool_calls\":[{\"type\":\"function\",\"index\":0,\"function\":{\"arguments\":\"\"}}]},\"index\":0,\"finish_reason\":null,\"logprobs\":null}],\"created\":1788837055,\"object\":\"chat.completion.chunk\",\"usage\":null}\n\ndata: {\"model\":\"qwen3.8-max\",\"id\":\"chatcmpl-681dfa5a-ae08-98da-ba0d-4b1d0d364b6b\",\"choices\":[{\"delta\":{\"tool_calls\":[{\"function\":{\"arguments\":\"\"},\"index\":0,\"id\":null,\"type\":\"function\"}],\"content\":\"\"},\"index\":0,\"finish_reason\":null,\"logprobs\":null}],\"created\":1788837055,\"object\":\"chat.completion.chunk\",\"usage\":null}\n\ndata: {\"model\":\"qwen3.8-max\",\"id\":\"chatcmpl-681dfa5a-ae08-98da-ba0d-4b1d0d364b6b\",\"choices\":[{\"delta\":{},\"index\":0,\"finish_reason\":\"stop\",\"logprobs\":null}],\"created\":1788837055,\"object\":\"chat.completion.chunk\",\"usage\":null}\n\ndata: {\"choices\":[],\"created\":1788837055,\"id\":\"chatcmpl-681dfa5a-ae08-98da-ba0d-4b1d0d364b6b\",\"model\":\"qwen3.8-max\",\"object\":\"chat.completion.chunk\",\"usage\":{\"completion_tokens\":19,\"prompt_tokens\":288,\"prompt_tokens_details\":{\"cached_tokens\":0,\"text_tokens\":288},\"total_tokens\":307}}\n\ndata: [DONE]\n\n"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+73
File diff suppressed because one or more lines are too long
+34
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"version": 1,
|
||||
"metadata": {
|
||||
"tags": [
|
||||
"prefix:alibaba-chat",
|
||||
"provider:alibaba",
|
||||
"protocol:alibaba-chat",
|
||||
"region:ap-southeast-1",
|
||||
"structured-output"
|
||||
],
|
||||
"name": "alibaba-chat/qwen-3-8-max-returns-a-json-object",
|
||||
"recordedAt": "2026-09-08T03:11:29.462Z"
|
||||
},
|
||||
"interactions": [
|
||||
{
|
||||
"transport": "http",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "https://dashscope-intl.aliyuncs.com/compatible-mode/v1/chat/completions",
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": "{\"model\":\"qwen3.8-max\",\"messages\":[{\"role\":\"user\",\"content\":\"Return a JSON object with one key \\\"city\\\" set to the capital city of France.\"}],\"stream\":true,\"stream_options\":{\"include_usage\":true},\"reasoning_effort\":\"none\",\"max_completion_tokens\":1024,\"response_format\":{\"type\":\"json_object\"}}"
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"content-type": "text/event-stream;charset=utf-8"
|
||||
},
|
||||
"body": "data: {\"model\":\"qwen3.8-max\",\"id\":\"chatcmpl-de34af77-7b1e-9999-a48f-e564c03d4b6b\",\"created\":1788837088,\"object\":\"chat.completion.chunk\",\"usage\":null,\"choices\":[{\"logprobs\":null,\"index\":0,\"delta\":{\"content\":\"\",\"role\":\"assistant\"},\"finish_reason\":null}]}\n\ndata: {\"model\":\"qwen3.8-max\",\"id\":\"chatcmpl-de34af77-7b1e-9999-a48f-e564c03d4b6b\",\"choices\":[{\"delta\":{\"content\":\"{\\\"\"},\"index\":0,\"finish_reason\":null,\"logprobs\":null}],\"created\":1788837088,\"object\":\"chat.completion.chunk\",\"usage\":null}\n\ndata: {\"model\":\"qwen3.8-max\",\"id\":\"chatcmpl-de34af77-7b1e-9999-a48f-e564c03d4b6b\",\"choices\":[{\"delta\":{\"content\":\"city\\\":\"},\"index\":0,\"finish_reason\":null,\"logprobs\":null}],\"created\":1788837088,\"object\":\"chat.completion.chunk\",\"usage\":null}\n\ndata: {\"model\":\"qwen3.8-max\",\"id\":\"chatcmpl-de34af77-7b1e-9999-a48f-e564c03d4b6b\",\"choices\":[{\"delta\":{\"content\":\" \\\"Paris\\\"}\"},\"index\":0,\"finish_reason\":null,\"logprobs\":null}],\"created\":1788837088,\"object\":\"chat.completion.chunk\",\"usage\":null}\n\ndata: {\"model\":\"qwen3.8-max\",\"id\":\"chatcmpl-de34af77-7b1e-9999-a48f-e564c03d4b6b\",\"choices\":[{\"delta\":{\"content\":\"\"},\"index\":0,\"finish_reason\":\"stop\",\"logprobs\":null}],\"created\":1788837088,\"object\":\"chat.completion.chunk\",\"usage\":null}\n\ndata: {\"choices\":[],\"created\":1788837088,\"id\":\"chatcmpl-de34af77-7b1e-9999-a48f-e564c03d4b6b\",\"model\":\"qwen3.8-max\",\"object\":\"chat.completion.chunk\",\"usage\":{\"completion_tokens\":6,\"prompt_tokens\":32,\"prompt_tokens_details\":{\"cached_tokens\":0,\"text_tokens\":32},\"total_tokens\":38}}\n\ndata: [DONE]\n\n"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user