Compare commits

...
56 Commits
Author SHA1 Message Date
David HillandLukeParkerDev 9128e847bd fix(app): refine browser toolbar and empty state (#48061)
Co-authored-by: LukeParkerDev <10430890+Hona@users.noreply.github.com>
2026-09-09 07:17:11 +00:00
David Hill 8b92833624 fix(app): constrain worktree dropdown spacing (#48062) 2026-09-09 16:57:59 +10:00
David Hill f4dd76913f fix(session-ui): simplify file search styling (#48063) 2026-09-09 16:57:41 +10:00
David Hill ef88566d61 fix(app): smooth right panel transitions (#48086) 2026-09-09 16:57:31 +10:00
David Hill b3f765c17d fix(app): reorder and rename browser experiment (#48087) 2026-09-09 16:56:29 +10:00
David Hill ab2366de2e fix(app): improve queued message previews (#48091) 2026-09-09 16:56:16 +10:00
Aiden Cline c0aa963c13 refactor(codemode): split the interpreter into a per-execution Runtime and per-call Frame (#48082) 2026-09-09 00:43:48 -05:00
Luke Parker d55d941f3c fix(app): restore selected review pane tab per session (#48079) 2026-09-09 05:42:40 +00:00
Luke Parker 2dea1f3d0e fix(browser): suspend and restore idle desktop tabs (#48080) 2026-09-09 15:15:11 +10:00
Aiden Cline fcafe82cdc fix(core): hide Bedrock bare IDs that require inference profiles (#48078) 2026-09-09 00:06:05 -05:00
Aiden Cline 1fe06bb4ed refactor(codemode): define built-in globals as host functions and namespaces (#48072) 2026-09-08 23:57:03 -05:00
Aiden Cline c1f4beaf40 feat(ai): add Alibaba inference provider (#47874) 2026-09-08 23:29:02 -05:00
Aiden Cline e628143448 fix(core): drop retained-tail reference from compaction rules (#48074) 2026-09-08 23:23:09 -05:00
Aiden Cline 148042ab81 fix(core): trim compaction summary prompt (#48058) 2026-09-08 22:56:54 -05:00
Brendan Allan b7aea8b0ef fix(session-ui): fade in streamed inline code (#44899) 2026-09-09 03:42:46 +00:00
opencode-agent[bot]andrekram1-node 1623ac3ba9 test(cli): update auth account coverage (#48066)
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
2026-09-08 22:28:48 -05:00
opencode-agent[bot]andBrendonovich f02c5f8648 fix(app): follow rendered model order with arrow keys (#47912)
Co-authored-by: Brendonovich <14191578+Brendonovich@users.noreply.github.com>
2026-09-09 11:23:38 +08:00
Aiden Cline 0da0772bc0 refactor(codemode): use acorn's typed AST in the interpreter (#48060) 2026-09-08 22:20:12 -05:00
Aiden Cline 08e28fb915 refactor(codemode): name the data boundary and prepare tools once (#48021) 2026-09-08 21:35:36 -05:00
Dax 74b0fa9d1f feat(cli): add V2 uninstall command (#48056) 2026-09-08 22:34:35 -04:00
Dax dbd9b18f3d refactor(cli): move import and export under session (#48055) 2026-09-08 21:59:58 -04:00
opencode-agent[bot]andBrendonovich b43e1c682b test(app): remove subagent link coverage (#48053)
Co-authored-by: Brendonovich <14191578+Brendonovich@users.noreply.github.com>
2026-09-09 01:54:59 +00:00
opencode-agent[bot]andBrendonovich d11f5916ee test(app): remove subagent link coverage (#48052)
Co-authored-by: Brendonovich <14191578+Brendonovich@users.noreply.github.com>
2026-09-09 01:52:09 +00:00
opencode-agent[bot]andBrendonovich f1ce69d2ce fix(app): link background subagents to their sessions (#47455)
Co-authored-by: Brendonovich <14191578+Brendonovich@users.noreply.github.com>
2026-09-09 09:39:22 +08:00
Brendan Allan d39290fbb9 feat(desktop): add SSH server connections (#47753) 2026-09-09 09:32:38 +08:00
Aiden Cline 6ee2ed7510 refactor(session): remove message content mutation API (#48043) 2026-09-08 20:27:22 -05:00
Dax 594635b5ae feat(cli): add auth account switching and targeted logout (#48050) 2026-09-08 21:27:21 -04:00
David Hill 18b05e86fb feat(app): add browser shortcut to new-tab menu (#48044) 2026-09-09 00:03:10 +00:00
Dax cc501650c6 refactor(cli): remove console command (#48040) 2026-09-08 19:54:47 -04:00
David HillandLukeParkerDev 37b6fbc5bf fix(app): move vertical tab update button to footer (#48030)
Co-authored-by: LukeParkerDev <10430890+Hona@users.noreply.github.com>
2026-09-09 09:41:10 +10:00
Aiden Cline d24f8b0810 fix(core): discourage polling background shell commands (#48041) 2026-09-08 18:34:59 -05:00
Aiden Cline 79a6a90862 fix(plugin): normalize promise API inputs (#48039) 2026-09-08 18:34:49 -05:00
David Hill 8c5eca5bb2 fix(session-ui): show one count before used (#48038) 2026-09-09 09:15:24 +10:00
David Hill f3128fa241 fix(ui): unify composer submit button styles (#48032) 2026-09-09 09:07:41 +10:00
David Hill 883d16d2ad fix(session-ui): emphasize mentions and soften at prefixes (#48029) 2026-09-09 09:05:13 +10:00
Dax Raad 5c30292daa fix(update): restore next channel compatibility alias 2026-09-08 16:57:16 -04:00
Aiden Cline 11aee1b49f fix(codegen): preserve equivalent suffixed type references (#48024) 2026-09-08 15:21:17 -05:00
Dax Raad a322d7f98f fix(update): bind migrated singular D1 database 2026-09-08 15:57:51 -04:00
Aiden Cline 546b9fb3c1 refactor(codemode): consolidate duplicated interpreter helpers (#48009) 2026-09-08 14:46:16 -05:00
Aiden Cline 83288921f6 fix(core): map Bedrock OpenAI reasoning effort for inference-profile model IDs (#48006) 2026-09-08 14:42:33 -05:00
opencode-agent[bot] 510b771946 chore: update nix node_modules hashes 2026-09-08 18:28:38 +00:00
Dax Raad 78915a0f17 refactor(update): rename updates service to update 2026-09-08 14:10:28 -04:00
Adam 1ead17547b fix(core): name hosted provider opencode web search (#48001) 2026-09-08 12:28:12 -05:00
Aiden Cline 0c1bf08ca6 feat(core): add Poe browser OAuth (#47883) 2026-09-08 12:15:17 -05:00
Aiden Cline 8b09f6415a refactor(codemode): export runtime value classes as Values (#48000) 2026-09-08 12:11:34 -05:00
Adam e791afdfa3 feat(core): add console web search (#47293) 2026-09-08 12:08:53 -05:00
Dax Raad e655fed6c3 fix(updates): reject retired next channel 2026-09-08 13:06:08 -04:00
ccbc018072 fix(tui): honor configured worktree strategy (#47991)
Co-authored-by: nexxeln <95541290+nexxeln@users.noreply.github.com>
Co-authored-by: Hona <10430890+Hona@users.noreply.github.com>
2026-09-08 12:46:57 -04:00
James Long 4432956490 fix(worktree): accept the strategy's returned directory (#47997) 2026-09-08 12:45:57 -04:00
Dax Raad 375bf4908f feat(updates): configure per-channel gradual rollouts 2026-09-08 12:34:59 -04:00
James Long cc6bff39a0 feat(tui): manage worktrees and explicitly move sessions (#47984) 2026-09-08 12:20:25 -04:00
James Long 8a5709324f fix(worktree): resolve configured paths relative to project (#47990) 2026-09-08 12:03:53 -04:00
James Long be58ca806c fix(cli): recover dev hot reloads and preserve routes (#47979) 2026-09-08 11:19:51 -04:00
Dax 9e42e5cc4c feat(core): refresh console provider config periodically (#47980) 2026-09-08 11:17:12 -04:00
opencode-agent[bot] c2a1649dd4 chore: update nix node_modules hashes 2026-09-08 14:19:25 +00:00
James Long ded9c7e505 feat(cli): add Vite-powered TUI development entrypoint (#47950) 2026-09-08 10:00:16 -04:00
346 changed files with 13814 additions and 4578 deletions
+1 -1
View File
@@ -48,7 +48,7 @@ jobs:
- name: Deploy update service
if: github.ref_name == 'v2'
working-directory: services/updates
working-directory: services/update
run: bun run deploy
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
+71 -70
View File
@@ -168,6 +168,7 @@
"@yuuang/ffi-rs-linux-x64-gnu": "1.3.2",
"@yuuang/ffi-rs-win32-arm64-msvc": "1.3.2",
"@yuuang/ffi-rs-win32-x64-msvc": "1.3.2",
"solid-refresh": "0.6.3",
"vite": "catalog:",
"vite-plugin-solid": "catalog:",
},
@@ -1037,8 +1038,8 @@
"wrangler": "4.110.0",
},
},
"services/updates": {
"name": "@opencode/updates",
"services/update": {
"name": "@opencode/update",
"version": "1.18.4",
"dependencies": {
"jose": "6.0.11",
@@ -2245,7 +2246,7 @@
"@opencode/ui": ["@opencode/ui@workspace:packages/ui"],
"@opencode/updates": ["@opencode/updates@workspace:services/updates"],
"@opencode/update": ["@opencode/update@workspace:services/update"],
"@opencode/util": ["@opencode/util@workspace:packages/util"],
@@ -6419,11 +6420,11 @@
"@opencode/ui/vite-plugin-solid": ["vite-plugin-solid@2.11.10", "", { "dependencies": { "@babel/core": "^7.23.3", "@types/babel__core": "^7.20.4", "babel-preset-solid": "^1.8.4", "merge-anything": "^5.1.7", "solid-refresh": "^0.6.3", "vitefu": "^1.0.4" }, "peerDependencies": { "@testing-library/jest-dom": "^5.16.6 || ^5.17.0 || ^6.*", "solid-js": "^1.7.2", "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" }, "optionalPeers": ["@testing-library/jest-dom"] }, "sha512-Yr1dQybmtDtDAHkii6hXuc1oVH9CPcS/Zb2jN/P36qqcrkNnVPsMTzQ06jyzFPFjj3U1IYKMVt/9ZqcwGCEbjw=="],
"@opencode/updates/@types/semver": ["@types/semver@7.7.1", "", {}, "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA=="],
"@opencode/update/@types/semver": ["@types/semver@7.7.1", "", {}, "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA=="],
"@opencode/updates/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
"@opencode/update/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
"@opencode/updates/wrangler": ["wrangler@4.110.0", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.5.0", "@cloudflare/unenv-preset": "2.16.1", "blake3-wasm": "2.1.5", "esbuild": "0.28.1", "miniflare": "4.20260708.1", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", "workerd": "1.20260708.1" }, "optionalDependencies": { "fsevents": "2.3.3" }, "peerDependencies": { "@cloudflare/workers-types": "^5.20260708.1" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js", "cf-wrangler": "bin/cf-wrangler.js" } }, "sha512-xZeXKYi7hxQRF5anL+v77RkufJNpF9f3Eqeyqq2QBsETpLZgh0Agj0jJ6JPtkbgn6ukZdh8OK5egsGPWIditgg=="],
"@opencode/update/wrangler": ["wrangler@4.110.0", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.5.0", "@cloudflare/unenv-preset": "2.16.1", "blake3-wasm": "2.1.5", "esbuild": "0.28.1", "miniflare": "4.20260708.1", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", "workerd": "1.20260708.1" }, "optionalDependencies": { "fsevents": "2.3.3" }, "peerDependencies": { "@cloudflare/workers-types": "^5.20260708.1" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js", "cf-wrangler": "bin/cf-wrangler.js" } }, "sha512-xZeXKYi7hxQRF5anL+v77RkufJNpF9f3Eqeyqq2QBsETpLZgh0Agj0jJ6JPtkbgn6ukZdh8OK5egsGPWIditgg=="],
"@opencode/web/@astrojs/cloudflare": ["@astrojs/cloudflare@12.6.3", "", { "dependencies": { "@astrojs/internal-helpers": "0.7.1", "@astrojs/underscore-redirects": "1.0.0", "@cloudflare/workers-types": "^4.20250507.0", "tinyglobby": "^0.2.13", "vite": "^6.3.5", "wrangler": "^4.14.1" }, "peerDependencies": { "astro": "^5.0.0" } }, "sha512-xhJptF5tU2k5eo70nIMyL1Udma0CqmUEnGSlGyFflLqSY82CRQI6nWZ/xZt0ZvmXuErUjIx0YYQNfZsz5CNjLQ=="],
@@ -7317,15 +7318,15 @@
"@opencode/storybook/@tailwindcss/vite/tailwindcss": ["tailwindcss@4.1.11", "", {}, "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA=="],
"@opencode/updates/wrangler/@cloudflare/kv-asset-handler": ["@cloudflare/kv-asset-handler@0.5.0", "", {}, "sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg=="],
"@opencode/update/wrangler/@cloudflare/kv-asset-handler": ["@cloudflare/kv-asset-handler@0.5.0", "", {}, "sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg=="],
"@opencode/updates/wrangler/@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.16.1", "", { "peerDependencies": { "unenv": "2.0.0-rc.24", "workerd": ">1.20260305.0 <2.0.0-0" }, "optionalPeers": ["workerd"] }, "sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw=="],
"@opencode/update/wrangler/@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.16.1", "", { "peerDependencies": { "unenv": "2.0.0-rc.24", "workerd": ">1.20260305.0 <2.0.0-0" }, "optionalPeers": ["workerd"] }, "sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw=="],
"@opencode/updates/wrangler/esbuild": ["esbuild@0.28.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.1", "@esbuild/android-arm": "0.28.1", "@esbuild/android-arm64": "0.28.1", "@esbuild/android-x64": "0.28.1", "@esbuild/darwin-arm64": "0.28.1", "@esbuild/darwin-x64": "0.28.1", "@esbuild/freebsd-arm64": "0.28.1", "@esbuild/freebsd-x64": "0.28.1", "@esbuild/linux-arm": "0.28.1", "@esbuild/linux-arm64": "0.28.1", "@esbuild/linux-ia32": "0.28.1", "@esbuild/linux-loong64": "0.28.1", "@esbuild/linux-mips64el": "0.28.1", "@esbuild/linux-ppc64": "0.28.1", "@esbuild/linux-riscv64": "0.28.1", "@esbuild/linux-s390x": "0.28.1", "@esbuild/linux-x64": "0.28.1", "@esbuild/netbsd-arm64": "0.28.1", "@esbuild/netbsd-x64": "0.28.1", "@esbuild/openbsd-arm64": "0.28.1", "@esbuild/openbsd-x64": "0.28.1", "@esbuild/openharmony-arm64": "0.28.1", "@esbuild/sunos-x64": "0.28.1", "@esbuild/win32-arm64": "0.28.1", "@esbuild/win32-ia32": "0.28.1", "@esbuild/win32-x64": "0.28.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw=="],
"@opencode/update/wrangler/esbuild": ["esbuild@0.28.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.1", "@esbuild/android-arm": "0.28.1", "@esbuild/android-arm64": "0.28.1", "@esbuild/android-x64": "0.28.1", "@esbuild/darwin-arm64": "0.28.1", "@esbuild/darwin-x64": "0.28.1", "@esbuild/freebsd-arm64": "0.28.1", "@esbuild/freebsd-x64": "0.28.1", "@esbuild/linux-arm": "0.28.1", "@esbuild/linux-arm64": "0.28.1", "@esbuild/linux-ia32": "0.28.1", "@esbuild/linux-loong64": "0.28.1", "@esbuild/linux-mips64el": "0.28.1", "@esbuild/linux-ppc64": "0.28.1", "@esbuild/linux-riscv64": "0.28.1", "@esbuild/linux-s390x": "0.28.1", "@esbuild/linux-x64": "0.28.1", "@esbuild/netbsd-arm64": "0.28.1", "@esbuild/netbsd-x64": "0.28.1", "@esbuild/openbsd-arm64": "0.28.1", "@esbuild/openbsd-x64": "0.28.1", "@esbuild/openharmony-arm64": "0.28.1", "@esbuild/sunos-x64": "0.28.1", "@esbuild/win32-arm64": "0.28.1", "@esbuild/win32-ia32": "0.28.1", "@esbuild/win32-x64": "0.28.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw=="],
"@opencode/updates/wrangler/miniflare": ["miniflare@4.20260708.1", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "sharp": "0.34.5", "undici": "7.28.0", "workerd": "1.20260708.1", "ws": "8.21.0", "youch": "4.1.0-beta.10" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-c94O9zRDISdqO18EHt6l0iF/fWgWt8p18PJvRsA/L/NJZ9Cfke3s/F5Blg1XXF7WDutVRzWVWy8Vy4LaT5ifsA=="],
"@opencode/update/wrangler/miniflare": ["miniflare@4.20260708.1", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "sharp": "0.34.5", "undici": "7.28.0", "workerd": "1.20260708.1", "ws": "8.21.0", "youch": "4.1.0-beta.10" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-c94O9zRDISdqO18EHt6l0iF/fWgWt8p18PJvRsA/L/NJZ9Cfke3s/F5Blg1XXF7WDutVRzWVWy8Vy4LaT5ifsA=="],
"@opencode/updates/wrangler/workerd": ["workerd@1.20260708.1", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260708.1", "@cloudflare/workerd-darwin-arm64": "1.20260708.1", "@cloudflare/workerd-linux-64": "1.20260708.1", "@cloudflare/workerd-linux-arm64": "1.20260708.1", "@cloudflare/workerd-windows-64": "1.20260708.1" }, "bin": { "workerd": "bin/workerd" } }, "sha512-WAK+Kt/VVCSldH2qSr8lx46XCJ4Q+bdlHNaFqUtOHthBEIB8C1N8HVW+VOLrxDoTCk0NGNv0zajnBeQK4JOB9w=="],
"@opencode/update/wrangler/workerd": ["workerd@1.20260708.1", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260708.1", "@cloudflare/workerd-darwin-arm64": "1.20260708.1", "@cloudflare/workerd-linux-64": "1.20260708.1", "@cloudflare/workerd-linux-arm64": "1.20260708.1", "@cloudflare/workerd-windows-64": "1.20260708.1" }, "bin": { "workerd": "bin/workerd" } }, "sha512-WAK+Kt/VVCSldH2qSr8lx46XCJ4Q+bdlHNaFqUtOHthBEIB8C1N8HVW+VOLrxDoTCk0NGNv0zajnBeQK4JOB9w=="],
"@opencode/web/@astrojs/cloudflare/@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.7.1", "", {}, "sha512-7dwEVigz9vUWDw3nRwLQ/yH/xYovlUA0ZD86xoeKEBmkz9O6iELG1yri67PgAPW6VLL/xInA4t7H0CK6VmtkKQ=="],
@@ -8253,71 +8254,71 @@
"@opencode/storybook/@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.11", "", { "os": "win32", "cpu": "x64" }, "sha512-YfHoggn1j0LK7wR82TOucWc5LDCguHnoS879idHekmmiR7g9HUtMw9MI0NHatS28u/Xlkfi9w5RJWgz2Dl+5Qg=="],
"@opencode/updates/wrangler/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ=="],
"@opencode/update/wrangler/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ=="],
"@opencode/updates/wrangler/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.28.1", "", { "os": "android", "cpu": "arm" }, "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ=="],
"@opencode/update/wrangler/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.28.1", "", { "os": "android", "cpu": "arm" }, "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ=="],
"@opencode/updates/wrangler/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.1", "", { "os": "android", "cpu": "arm64" }, "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg=="],
"@opencode/update/wrangler/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.1", "", { "os": "android", "cpu": "arm64" }, "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg=="],
"@opencode/updates/wrangler/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.28.1", "", { "os": "android", "cpu": "x64" }, "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng=="],
"@opencode/update/wrangler/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.28.1", "", { "os": "android", "cpu": "x64" }, "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng=="],
"@opencode/updates/wrangler/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q=="],
"@opencode/update/wrangler/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q=="],
"@opencode/updates/wrangler/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ=="],
"@opencode/update/wrangler/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ=="],
"@opencode/updates/wrangler/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw=="],
"@opencode/update/wrangler/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw=="],
"@opencode/updates/wrangler/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ=="],
"@opencode/update/wrangler/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ=="],
"@opencode/updates/wrangler/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.1", "", { "os": "linux", "cpu": "arm" }, "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ=="],
"@opencode/update/wrangler/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.1", "", { "os": "linux", "cpu": "arm" }, "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ=="],
"@opencode/updates/wrangler/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g=="],
"@opencode/update/wrangler/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g=="],
"@opencode/updates/wrangler/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.1", "", { "os": "linux", "cpu": "ia32" }, "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w=="],
"@opencode/update/wrangler/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.1", "", { "os": "linux", "cpu": "ia32" }, "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w=="],
"@opencode/updates/wrangler/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg=="],
"@opencode/update/wrangler/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg=="],
"@opencode/updates/wrangler/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ=="],
"@opencode/update/wrangler/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ=="],
"@opencode/updates/wrangler/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ=="],
"@opencode/update/wrangler/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ=="],
"@opencode/updates/wrangler/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ=="],
"@opencode/update/wrangler/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ=="],
"@opencode/updates/wrangler/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag=="],
"@opencode/update/wrangler/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag=="],
"@opencode/updates/wrangler/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.1", "", { "os": "linux", "cpu": "x64" }, "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA=="],
"@opencode/update/wrangler/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.1", "", { "os": "linux", "cpu": "x64" }, "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA=="],
"@opencode/updates/wrangler/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw=="],
"@opencode/update/wrangler/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw=="],
"@opencode/updates/wrangler/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.1", "", { "os": "none", "cpu": "x64" }, "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg=="],
"@opencode/update/wrangler/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.1", "", { "os": "none", "cpu": "x64" }, "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg=="],
"@opencode/updates/wrangler/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.1", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q=="],
"@opencode/update/wrangler/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.1", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q=="],
"@opencode/updates/wrangler/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw=="],
"@opencode/update/wrangler/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw=="],
"@opencode/updates/wrangler/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg=="],
"@opencode/update/wrangler/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg=="],
"@opencode/updates/wrangler/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.1", "", { "os": "sunos", "cpu": "x64" }, "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ=="],
"@opencode/update/wrangler/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.1", "", { "os": "sunos", "cpu": "x64" }, "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ=="],
"@opencode/updates/wrangler/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA=="],
"@opencode/update/wrangler/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA=="],
"@opencode/updates/wrangler/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg=="],
"@opencode/update/wrangler/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg=="],
"@opencode/updates/wrangler/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A=="],
"@opencode/update/wrangler/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A=="],
"@opencode/updates/wrangler/miniflare/sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="],
"@opencode/update/wrangler/miniflare/sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="],
"@opencode/updates/wrangler/miniflare/undici": ["undici@7.28.0", "", {}, "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA=="],
"@opencode/update/wrangler/miniflare/undici": ["undici@7.28.0", "", {}, "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA=="],
"@opencode/updates/wrangler/workerd/@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20260708.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-HXFCvhS1wpg3uXO0CLUwmwC41i2loM5FSK69EUchOBpmYBAXxT1oHLm6EOA5lqhTk5Mu9kjRiQYxa1GwKPwfJg=="],
"@opencode/update/wrangler/workerd/@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20260708.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-HXFCvhS1wpg3uXO0CLUwmwC41i2loM5FSK69EUchOBpmYBAXxT1oHLm6EOA5lqhTk5Mu9kjRiQYxa1GwKPwfJg=="],
"@opencode/updates/wrangler/workerd/@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20260708.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-JVlJaKDoRTVKSroHIlf8g3UCPjKj4iDbMZE2CNYht5qQ+2rL0FAUiVlV82G3BqKnnw9kHYnnsMzC08b9zVtdzA=="],
"@opencode/update/wrangler/workerd/@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20260708.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-JVlJaKDoRTVKSroHIlf8g3UCPjKj4iDbMZE2CNYht5qQ+2rL0FAUiVlV82G3BqKnnw9kHYnnsMzC08b9zVtdzA=="],
"@opencode/updates/wrangler/workerd/@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20260708.1", "", { "os": "linux", "cpu": "x64" }, "sha512-3daE60YdD7YX0Jtuzc9DE/r/qMkmx8ZvHTkF8Mzmp3F5tbzlV0DAzmu5PFUPF2WuvtKbAhZKbvC2cHmWpQYxnA=="],
"@opencode/update/wrangler/workerd/@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20260708.1", "", { "os": "linux", "cpu": "x64" }, "sha512-3daE60YdD7YX0Jtuzc9DE/r/qMkmx8ZvHTkF8Mzmp3F5tbzlV0DAzmu5PFUPF2WuvtKbAhZKbvC2cHmWpQYxnA=="],
"@opencode/updates/wrangler/workerd/@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20260708.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-VLdNYOx5Hj+9C6isy0ACWZsbMtSxex2DIJWEe7cZxUdlphZ58ZT8zxNXK8yunFiowd34hn3VwGMopdvdj8lvmA=="],
"@opencode/update/wrangler/workerd/@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20260708.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-VLdNYOx5Hj+9C6isy0ACWZsbMtSxex2DIJWEe7cZxUdlphZ58ZT8zxNXK8yunFiowd34hn3VwGMopdvdj8lvmA=="],
"@opencode/updates/wrangler/workerd/@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20260708.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bC/aSAwLy16Vjo24i9XU3aWH+eRgz7NeR5xPKavGbembO18ZywYTQbXh14eXtY6fAqN3RzRG8psijTdhX4xydA=="],
"@opencode/update/wrangler/workerd/@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20260708.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bC/aSAwLy16Vjo24i9XU3aWH+eRgz7NeR5xPKavGbembO18ZywYTQbXh14eXtY6fAqN3RzRG8psijTdhX4xydA=="],
"@opencode/web/@astrojs/cloudflare/wrangler/@cloudflare/kv-asset-handler": ["@cloudflare/kv-asset-handler@0.5.0", "", {}, "sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg=="],
@@ -8921,55 +8922,55 @@
"@opencode/storybook/@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@opencode/updates/wrangler/miniflare/sharp/@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="],
"@opencode/update/wrangler/miniflare/sharp/@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="],
"@opencode/updates/wrangler/miniflare/sharp/@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="],
"@opencode/update/wrangler/miniflare/sharp/@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="],
"@opencode/updates/wrangler/miniflare/sharp/@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="],
"@opencode/update/wrangler/miniflare/sharp/@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="],
"@opencode/updates/wrangler/miniflare/sharp/@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="],
"@opencode/update/wrangler/miniflare/sharp/@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="],
"@opencode/updates/wrangler/miniflare/sharp/@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="],
"@opencode/update/wrangler/miniflare/sharp/@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="],
"@opencode/updates/wrangler/miniflare/sharp/@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="],
"@opencode/update/wrangler/miniflare/sharp/@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="],
"@opencode/updates/wrangler/miniflare/sharp/@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.2.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA=="],
"@opencode/update/wrangler/miniflare/sharp/@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.2.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA=="],
"@opencode/updates/wrangler/miniflare/sharp/@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.2.4", "", { "os": "linux", "cpu": "none" }, "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA=="],
"@opencode/update/wrangler/miniflare/sharp/@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.2.4", "", { "os": "linux", "cpu": "none" }, "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA=="],
"@opencode/updates/wrangler/miniflare/sharp/@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ=="],
"@opencode/update/wrangler/miniflare/sharp/@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ=="],
"@opencode/updates/wrangler/miniflare/sharp/@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="],
"@opencode/update/wrangler/miniflare/sharp/@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="],
"@opencode/updates/wrangler/miniflare/sharp/@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="],
"@opencode/update/wrangler/miniflare/sharp/@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="],
"@opencode/updates/wrangler/miniflare/sharp/@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="],
"@opencode/update/wrangler/miniflare/sharp/@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="],
"@opencode/updates/wrangler/miniflare/sharp/@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" }, "os": "linux", "cpu": "arm" }, "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw=="],
"@opencode/update/wrangler/miniflare/sharp/@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" }, "os": "linux", "cpu": "arm" }, "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw=="],
"@opencode/updates/wrangler/miniflare/sharp/@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="],
"@opencode/update/wrangler/miniflare/sharp/@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="],
"@opencode/updates/wrangler/miniflare/sharp/@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.4" }, "os": "linux", "cpu": "ppc64" }, "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA=="],
"@opencode/update/wrangler/miniflare/sharp/@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.4" }, "os": "linux", "cpu": "ppc64" }, "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA=="],
"@opencode/updates/wrangler/miniflare/sharp/@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.2.4" }, "os": "linux", "cpu": "none" }, "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw=="],
"@opencode/update/wrangler/miniflare/sharp/@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.2.4" }, "os": "linux", "cpu": "none" }, "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw=="],
"@opencode/updates/wrangler/miniflare/sharp/@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.4" }, "os": "linux", "cpu": "s390x" }, "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg=="],
"@opencode/update/wrangler/miniflare/sharp/@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.4" }, "os": "linux", "cpu": "s390x" }, "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg=="],
"@opencode/updates/wrangler/miniflare/sharp/@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="],
"@opencode/update/wrangler/miniflare/sharp/@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="],
"@opencode/updates/wrangler/miniflare/sharp/@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="],
"@opencode/update/wrangler/miniflare/sharp/@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="],
"@opencode/updates/wrangler/miniflare/sharp/@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="],
"@opencode/update/wrangler/miniflare/sharp/@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="],
"@opencode/updates/wrangler/miniflare/sharp/@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="],
"@opencode/update/wrangler/miniflare/sharp/@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="],
"@opencode/updates/wrangler/miniflare/sharp/@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g=="],
"@opencode/update/wrangler/miniflare/sharp/@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g=="],
"@opencode/updates/wrangler/miniflare/sharp/@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg=="],
"@opencode/update/wrangler/miniflare/sharp/@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg=="],
"@opencode/updates/wrangler/miniflare/sharp/@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="],
"@opencode/update/wrangler/miniflare/sharp/@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="],
"@opencode/updates/wrangler/miniflare/sharp/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
"@opencode/update/wrangler/miniflare/sharp/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
"@opencode/web/@astrojs/cloudflare/wrangler/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ=="],
@@ -9153,7 +9154,7 @@
"@opencode/storybook/@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="],
"@opencode/updates/wrangler/miniflare/sharp/@img/sharp-wasm32/@emnapi/runtime": ["@emnapi/runtime@1.11.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="],
"@opencode/update/wrangler/miniflare/sharp/@img/sharp-wasm32/@emnapi/runtime": ["@emnapi/runtime@1.11.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="],
"@opencode/web/@astrojs/cloudflare/wrangler/miniflare/sharp/@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="],
+4 -4
View File
@@ -1,8 +1,8 @@
{
"nodeModules": {
"x86_64-linux": "sha256-EKhY3iZDrbNrBhntWpSdtLcmNLte6yVBxpIrCxr1uNM=",
"aarch64-linux": "sha256-0OjDGZHgcnnk6IxkfK6ogeeqsGTqY/dcaZ/XzT23sgA=",
"aarch64-darwin": "sha256-Zk51gnOicaLtPuqCYfgARhm2TjL222w1Y0Em288o0YY=",
"x86_64-darwin": "sha256-hvDZ9zCV6zOSx6i7JZ1kVUMht+JI/jc8/y+aYrNHQ1E="
"x86_64-linux": "sha256-yzCk746pospz8EVakHRcDhYJkhGYGSt9dHOPbzO4OYo=",
"aarch64-linux": "sha256-MFJVLos4v2r9jazmmr3ldVCJKLrO+Qp/BpGpyM/1lf8=",
"aarch64-darwin": "sha256-k8r/HVSgdRSTlJ1lI7EebqbxeA3AElnaw1sDYOPEdQw=",
"x86_64-darwin": "sha256-ACJdJfz12xLBQvWIkbuve86znSoGJ2PLDQbgh0cT6/g="
}
}
+2
View File
@@ -9,6 +9,8 @@
"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",
+72
View File
@@ -29,6 +29,78 @@ 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;
+92
View File
@@ -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"
+10 -5
View File
@@ -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
@@ -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) ??
+134
View File
@@ -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>
+1
View File
@@ -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"
@@ -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"
}
}
]
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -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"
}
}
]
}
File diff suppressed because one or more lines are too long
@@ -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"
}
}
]
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,36 @@
{
"version": 1,
"metadata": {
"tags": [
"prefix:alibaba-chat",
"provider:alibaba",
"protocol:alibaba-chat",
"region:ap-southeast-1",
"text",
"reasoning",
"usage"
],
"name": "alibaba-chat/qwen-3-8-max-streams-none-effort",
"recordedAt": "2026-09-08T03:09:27.584Z"
},
"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\":\"What is 173 multiplied by 219? Reply with only the final integer.\"}],\"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-d8fbc8fe-4d5a-9dc9-84c6-ed0c7b9dd3bb\",\"created\":1788836967,\"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-d8fbc8fe-4d5a-9dc9-84c6-ed0c7b9dd3bb\",\"choices\":[{\"delta\":{\"content\":\"3\"},\"index\":0,\"finish_reason\":null,\"logprobs\":null}],\"created\":1788836967,\"object\":\"chat.completion.chunk\",\"usage\":null}\n\ndata: {\"model\":\"qwen3.8-max\",\"id\":\"chatcmpl-d8fbc8fe-4d5a-9dc9-84c6-ed0c7b9dd3bb\",\"choices\":[{\"delta\":{\"content\":\"78\"},\"index\":0,\"finish_reason\":null,\"logprobs\":null}],\"created\":1788836967,\"object\":\"chat.completion.chunk\",\"usage\":null}\n\ndata: {\"model\":\"qwen3.8-max\",\"id\":\"chatcmpl-d8fbc8fe-4d5a-9dc9-84c6-ed0c7b9dd3bb\",\"choices\":[{\"delta\":{\"content\":\"87\"},\"index\":0,\"finish_reason\":null,\"logprobs\":null}],\"created\":1788836967,\"object\":\"chat.completion.chunk\",\"usage\":null}\n\ndata: {\"model\":\"qwen3.8-max\",\"id\":\"chatcmpl-d8fbc8fe-4d5a-9dc9-84c6-ed0c7b9dd3bb\",\"choices\":[{\"delta\":{\"content\":\"\"},\"index\":0,\"finish_reason\":\"stop\",\"logprobs\":null}],\"created\":1788836967,\"object\":\"chat.completion.chunk\",\"usage\":null}\n\ndata: {\"choices\":[],\"created\":1788836967,\"id\":\"chatcmpl-d8fbc8fe-4d5a-9dc9-84c6-ed0c7b9dd3bb\",\"model\":\"qwen3.8-max\",\"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"
}
}
]
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,35 @@
{
"version": 1,
"metadata": {
"tags": [
"prefix:alibaba-messages",
"provider:alibaba",
"protocol:alibaba-messages",
"region:ap-southeast-1",
"thinking",
"usage"
],
"name": "alibaba-messages/qwen-3-7-plus-streams-thinking-disabled",
"recordedAt": "2026-09-08T03:10:57.819Z"
},
"interactions": [
{
"transport": "http",
"request": {
"method": "POST",
"url": "https://dashscope-intl.aliyuncs.com/apps/anthropic/v1/messages",
"headers": {
"content-type": "application/json"
},
"body": "{\"model\":\"qwen3.7-plus\",\"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"What is 173 multiplied by 219? Reply with only the final integer.\"}]}],\"stream\":true,\"max_tokens\":4096,\"thinking\":{\"type\":\"disabled\"}}"
},
"response": {
"status": 200,
"headers": {
"content-type": "text/event-stream"
},
"body": "event:ping\ndata:{\"type\":\"ping\"}\n\nevent:message_start\ndata:{\"message\":{\"model\":\"qwen3.7-plus\",\"id\":\"msg_c4d58b4f-a61d-9d0c-a9e4-cb45d34b1120\",\"role\":\"assistant\",\"type\":\"message\",\"content\":[],\"usage\":{\"input_tokens\":20,\"output_tokens\":0}},\"type\":\"message_start\"}\n\nevent:content_block_start\ndata:{\"type\":\"content_block_start\",\"content_block\":{\"type\":\"text\",\"text\":\"\"},\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"text_delta\",\"text\":\"3\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"text_delta\",\"text\":\"7887\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_stop\ndata:{\"type\":\"content_block_stop\",\"index\":0}\n\nevent:message_delta\ndata:{\"delta\":{\"stop_reason\":\"end_turn\"},\"type\":\"message_delta\",\"usage\":{\"output_tokens\":5,\"cache_creation_input_tokens\":0,\"input_tokens\":32,\"cache_read_input_tokens\":0,\"prompt_tokens_details\":{\"cached_tokens\":0}}}\n\nevent:message_stop\ndata:{\"type\":\"message_stop\"}\n\n"
}
}
]
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,34 @@
{
"version": 1,
"metadata": {
"tags": [
"prefix:alibaba-messages",
"provider:alibaba",
"protocol:alibaba-messages",
"region:ap-southeast-1",
"structured-output"
],
"name": "alibaba-messages/qwen-3-8-max-follows-a-json-schema",
"recordedAt": "2026-09-08T03:11:30.530Z"
},
"interactions": [
{
"transport": "http",
"request": {
"method": "POST",
"url": "https://dashscope-intl.aliyuncs.com/apps/anthropic/v1/messages",
"headers": {
"content-type": "application/json"
},
"body": "{\"model\":\"qwen3.8-max\",\"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"Return a JSON object with one key \\\"city\\\" set to the capital city of France.\"}]}],\"stream\":true,\"max_tokens\":1024,\"thinking\":{\"type\":\"disabled\"},\"output_config\":{\"format\":{\"type\":\"json_schema\",\"schema\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"],\"additionalProperties\":false}}}}"
},
"response": {
"status": 200,
"headers": {
"content-type": "text/event-stream"
},
"body": "event:ping\ndata:{\"type\":\"ping\"}\n\nevent:message_start\ndata:{\"message\":{\"model\":\"qwen3.8-max\",\"id\":\"msg_16e067c1-984b-94d3-9abb-11792d794271\",\"role\":\"assistant\",\"type\":\"message\",\"content\":[],\"usage\":{\"input_tokens\":18,\"output_tokens\":0}},\"type\":\"message_start\"}\n\nevent:content_block_start\ndata:{\"type\":\"content_block_start\",\"content_block\":{\"type\":\"text\",\"text\":\"\"},\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"text_delta\",\"text\":\"{\\\"\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"text_delta\",\"text\":\"city\\\":\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"text_delta\",\"text\":\" \\\"Paris\\\"}\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_stop\ndata:{\"type\":\"content_block_stop\",\"index\":0}\n\nevent:message_delta\ndata:{\"delta\":{\"stop_reason\":\"end_turn\"},\"type\":\"message_delta\",\"usage\":{\"output_tokens\":6,\"cache_creation_input_tokens\":0,\"input_tokens\":32,\"cache_read_input_tokens\":0,\"prompt_tokens_details\":{\"cached_tokens\":0}}}\n\nevent:message_stop\ndata:{\"type\":\"message_stop\"}\n\n"
}
}
]
}
@@ -0,0 +1,35 @@
{
"version": 1,
"metadata": {
"tags": [
"prefix:alibaba-messages",
"provider:alibaba",
"protocol:alibaba-messages",
"region:ap-southeast-1",
"tool",
"tool-choice"
],
"name": "alibaba-messages/qwen-3-8-max-obeys-named-tool-choice",
"recordedAt": "2026-09-08T03:11:06.590Z"
},
"interactions": [
{
"transport": "http",
"request": {
"method": "POST",
"url": "https://dashscope-intl.aliyuncs.com/apps/anthropic/v1/messages",
"headers": {
"content-type": "application/json"
},
"body": "{\"model\":\"qwen3.8-max\",\"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"Find the current weather in Paris.\"}]}],\"tools\":[{\"name\":\"get_weather\",\"description\":\"Get weather in a city\",\"input_schema\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\",\"enum\":[\"Paris\"]}},\"required\":[\"city\"]}}],\"tool_choice\":{\"type\":\"tool\",\"name\":\"get_weather\"},\"stream\":true,\"max_tokens\":4096,\"thinking\":{\"type\":\"disabled\"}}"
},
"response": {
"status": 200,
"headers": {
"content-type": "text/event-stream"
},
"body": "event:ping\ndata:{\"type\":\"ping\"}\n\nevent:message_start\ndata:{\"message\":{\"model\":\"qwen3.8-max\",\"id\":\"msg_0e8f1abf-f2bc-9a86-a6a7-14804ac17eac\",\"role\":\"assistant\",\"type\":\"message\",\"content\":[],\"usage\":{\"input_tokens\":45,\"output_tokens\":0}},\"type\":\"message_start\"}\n\nevent:content_block_start\ndata:{\"type\":\"content_block_start\",\"content_block\":{\"name\":\"get_weather\",\"input\":{},\"id\":\"toolu_cf9cab33261f4709ae096d8a\",\"type\":\"tool_use\"},\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"partial_json\":\"\",\"type\":\"input_json_delta\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"partial_json\":\"{\\\"city\\\": \\\"Paris\",\"type\":\"input_json_delta\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"partial_json\":\"\\\"\",\"type\":\"input_json_delta\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"partial_json\":\"}\",\"type\":\"input_json_delta\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_stop\ndata:{\"type\":\"content_block_stop\",\"index\":0}\n\nevent:message_delta\ndata:{\"delta\":{\"stop_reason\":\"end_turn\"},\"type\":\"message_delta\",\"usage\":{\"output_tokens\":19,\"cache_creation_input_tokens\":0,\"input_tokens\":288,\"cache_read_input_tokens\":0,\"prompt_tokens_details\":{\"cached_tokens\":0}}}\n\nevent:message_stop\ndata:{\"type\":\"message_stop\"}\n\n"
}
}
]
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,36 @@
{
"version": 1,
"metadata": {
"tags": [
"prefix:alibaba-messages",
"provider:alibaba",
"protocol:alibaba-messages",
"region:ap-southeast-1",
"text",
"reasoning",
"usage"
],
"name": "alibaba-messages/qwen-3-8-max-streams-max-effort",
"recordedAt": "2026-09-08T03:12:36.479Z"
},
"interactions": [
{
"transport": "http",
"request": {
"method": "POST",
"url": "https://dashscope-intl.aliyuncs.com/apps/anthropic/v1/messages",
"headers": {
"content-type": "application/json"
},
"body": "{\"model\":\"qwen3.8-max\",\"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"What is 173 multiplied by 219? Reply with only the final integer.\"}]}],\"stream\":true,\"max_tokens\":4096,\"output_config\":{\"effort\":\"max\"}}"
},
"response": {
"status": 200,
"headers": {
"content-type": "text/event-stream"
},
"body": "event:ping\ndata:{\"type\":\"ping\"}\n\nevent:message_start\ndata:{\"message\":{\"model\":\"qwen3.8-max\",\"id\":\"msg_7952446b-0176-9526-ad7d-b1f0184bc106\",\"role\":\"assistant\",\"type\":\"message\",\"content\":[],\"usage\":{\"input_tokens\":20,\"output_tokens\":0}},\"type\":\"message_start\"}\n\nevent:content_block_start\ndata:{\"type\":\"content_block_start\",\"content_block\":{\"type\":\"thinking\",\"signature\":\"\",\"thinking\":\"\"},\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"We\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" need answer user\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"'s simple multiplication with\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" only final integer.\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" Need compute 1\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"73*2\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"19. \"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"173*\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"200=\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"3460\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"0; 1\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"73*1\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"9=32\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"87 (\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"173*\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"20=3\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"460-\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"173=\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"3287\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"); sum=3\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"7887\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\". Final only integer\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\".\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"signature_delta\",\"signature\":\"\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_stop\ndata:{\"type\":\"content_block_stop\",\"index\":0}\n\nevent:content_block_start\ndata:{\"type\":\"content_block_start\",\"content_block\":{\"type\":\"text\",\"text\":\"\"},\"index\":1}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"text_delta\",\"text\":\"37\"},\"type\":\"content_block_delta\",\"index\":1}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"text_delta\",\"text\":\"887\"},\"type\":\"content_block_delta\",\"index\":1}\n\nevent:content_block_stop\ndata:{\"type\":\"content_block_stop\",\"index\":1}\n\nevent:message_delta\ndata:{\"delta\":{\"stop_reason\":\"end_turn\"},\"type\":\"message_delta\",\"usage\":{\"output_tokens\":92,\"cache_creation_input_tokens\":0,\"input_tokens\":81,\"cache_read_input_tokens\":0,\"prompt_tokens_details\":{\"cached_tokens\":0}}}\n\nevent:message_stop\ndata:{\"type\":\"message_stop\"}\n\n"
}
}
]
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,36 @@
{
"version": 1,
"metadata": {
"tags": [
"prefix:alibaba-messages",
"provider:alibaba",
"protocol:alibaba-messages",
"region:ap-southeast-1",
"text",
"reasoning",
"usage"
],
"name": "alibaba-messages/qwen-3-8-max-streams-xhigh-effort",
"recordedAt": "2026-09-08T03:10:04.633Z"
},
"interactions": [
{
"transport": "http",
"request": {
"method": "POST",
"url": "https://dashscope-intl.aliyuncs.com/apps/anthropic/v1/messages",
"headers": {
"content-type": "application/json"
},
"body": "{\"model\":\"qwen3.8-max\",\"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"What is 173 multiplied by 219? Reply with only the final integer.\"}]}],\"stream\":true,\"max_tokens\":4096,\"output_config\":{\"effort\":\"xhigh\"}}"
},
"response": {
"status": 200,
"headers": {
"content-type": "text/event-stream"
},
"body": "event:ping\ndata:{\"type\":\"ping\"}\n\nevent:message_start\ndata:{\"message\":{\"model\":\"qwen3.8-max\",\"id\":\"msg_a57b8563-71fb-9248-a31e-c49d6ba38717\",\"role\":\"assistant\",\"type\":\"message\",\"content\":[],\"usage\":{\"input_tokens\":20,\"output_tokens\":0}},\"type\":\"message_start\"}\n\nevent:content_block_start\ndata:{\"type\":\"content_block_start\",\"content_block\":{\"type\":\"thinking\",\"signature\":\"\",\"thinking\":\"\"},\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"We\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" need answer simple multiplication\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\". We\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" already call\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\". Need compute \"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"173*\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"219.\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" 173\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"*200\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"=346\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"00; *\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"19=3\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"287;\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" sum 37\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"88\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"7. Final only\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" integer. Ensure\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" no extra.\\n\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"signature_delta\",\"signature\":\"\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_stop\ndata:{\"type\":\"content_block_stop\",\"index\":0}\n\nevent:content_block_start\ndata:{\"type\":\"content_block_start\",\"content_block\":{\"type\":\"text\",\"text\":\"\"},\"index\":1}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"text_delta\",\"text\":\"378\"},\"type\":\"content_block_delta\",\"index\":1}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"text_delta\",\"text\":\"87\"},\"type\":\"content_block_delta\",\"index\":1}\n\nevent:content_block_stop\ndata:{\"type\":\"content_block_stop\",\"index\":1}\n\nevent:message_delta\ndata:{\"delta\":{\"stop_reason\":\"end_turn\"},\"type\":\"message_delta\",\"usage\":{\"output_tokens\":69,\"cache_creation_input_tokens\":0,\"input_tokens\":81,\"cache_read_input_tokens\":0,\"prompt_tokens_details\":{\"cached_tokens\":0}}}\n\nevent:message_stop\ndata:{\"type\":\"message_stop\"}\n\n"
}
}
]
}
@@ -0,0 +1,35 @@
{
"version": 1,
"metadata": {
"tags": [
"prefix:alibaba-responses",
"provider:alibaba",
"protocol:alibaba-responses",
"region:ap-southeast-1",
"thinking",
"usage"
],
"name": "alibaba-responses/qwen-3-7-plus-streams-thinking-disabled",
"recordedAt": "2026-09-08T03:11:07.495Z"
},
"interactions": [
{
"transport": "http",
"request": {
"method": "POST",
"url": "https://dashscope-intl.aliyuncs.com/compatible-mode/v1/responses",
"headers": {
"content-type": "application/json"
},
"body": "{\"model\":\"qwen3.7-plus\",\"input\":[{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"What is 173 multiplied by 219? Reply with only the final integer.\"}]}],\"max_output_tokens\":4096,\"enable_thinking\":false,\"stream\":true}"
},
"response": {
"status": 200,
"headers": {
"content-type": "text/event-stream;charset=UTF-8"
},
"body": "id:1\nevent:response.created\n:HTTP_STATUS/200\ndata:{\"sequence_number\":0,\"type\":\"response.created\",\"response\":{\"top_logprobs\":0,\"metadata\":{},\"presence_penalty\":0.0,\"created_at\":1788837067,\"store\":true,\"tools\":[],\"output\":[],\"top_p\":1.0,\"completed_at\":1788837067,\"frequency_penalty\":0.0,\"parallel_tool_calls\":true,\"background\":false,\"temperature\":1.0,\"tool_choice\":\"auto\",\"model\":\"qwen3.7-plus\",\"service_tier\":\"default\",\"id\":\"resp_493607bf-baef-9ced-9e36-2e2a583b6177\",\"max_output_tokens\":4096,\"object\":\"response\",\"status\":\"queued\"}}\n\nid:2\nevent:response.in_progress\n:HTTP_STATUS/200\ndata:{\"sequence_number\":1,\"type\":\"response.in_progress\",\"response\":{\"top_logprobs\":0,\"metadata\":{},\"presence_penalty\":0.0,\"created_at\":1788837067,\"store\":true,\"tools\":[],\"output\":[],\"top_p\":1.0,\"completed_at\":1788837067,\"frequency_penalty\":0.0,\"parallel_tool_calls\":true,\"background\":false,\"temperature\":1.0,\"tool_choice\":\"auto\",\"model\":\"qwen3.7-plus\",\"service_tier\":\"default\",\"id\":\"resp_493607bf-baef-9ced-9e36-2e2a583b6177\",\"max_output_tokens\":4096,\"object\":\"response\",\"status\":\"in_progress\"}}\n\nid:3\nevent:response.output_item.added\n:HTTP_STATUS/200\ndata:{\"sequence_number\":2,\"item\":{\"id\":\"msg_509c6dec-e527-41b5-a9c9-7f02fecb7a3f\",\"role\":\"assistant\",\"type\":\"message\",\"content\":[],\"status\":\"in_progress\"},\"output_index\":0,\"type\":\"response.output_item.added\"}\n\nid:4\nevent:response.content_part.added\n:HTTP_STATUS/200\ndata:{\"sequence_number\":3,\"output_index\":0,\"type\":\"response.content_part.added\",\"content_index\":0,\"item_id\":\"msg_509c6dec-e527-41b5-a9c9-7f02fecb7a3f\",\"part\":{\"type\":\"output_text\",\"annotations\":[],\"text\":\"\"}}\n\nid:5\nevent:response.output_text.delta\n:HTTP_STATUS/200\ndata:{\"sequence_number\":4,\"content_index\":0,\"item_id\":\"msg_509c6dec-e527-41b5-a9c9-7f02fecb7a3f\",\"delta\":\"3\",\"output_index\":0,\"type\":\"response.output_text.delta\",\"logprobs\":[]}\n\nid:6\nevent:response.output_text.delta\n:HTTP_STATUS/200\ndata:{\"sequence_number\":5,\"content_index\":0,\"item_id\":\"msg_509c6dec-e527-41b5-a9c9-7f02fecb7a3f\",\"delta\":\"7887\",\"output_index\":0,\"type\":\"response.output_text.delta\",\"logprobs\":[]}\n\nid:7\nevent:response.output_text.delta\n:HTTP_STATUS/200\ndata:{\"sequence_number\":6,\"content_index\":0,\"item_id\":\"msg_509c6dec-e527-41b5-a9c9-7f02fecb7a3f\",\"delta\":\"\",\"output_index\":0,\"type\":\"response.output_text.delta\",\"logprobs\":[]}\n\nid:8\nevent:response.output_text.done\n:HTTP_STATUS/200\ndata:{\"sequence_number\":7,\"content_index\":0,\"item_id\":\"msg_509c6dec-e527-41b5-a9c9-7f02fecb7a3f\",\"text\":\"37887\",\"output_index\":0,\"type\":\"response.output_text.done\",\"logprobs\":[]}\n\nid:9\nevent:response.content_part.done\n:HTTP_STATUS/200\ndata:{\"sequence_number\":8,\"output_index\":0,\"type\":\"response.content_part.done\",\"content_index\":0,\"item_id\":\"msg_509c6dec-e527-41b5-a9c9-7f02fecb7a3f\",\"part\":{\"type\":\"output_text\",\"annotations\":[],\"text\":\"37887\"}}\n\nid:10\nevent:response.output_item.done\n:HTTP_STATUS/200\ndata:{\"sequence_number\":9,\"item\":{\"id\":\"msg_509c6dec-e527-41b5-a9c9-7f02fecb7a3f\",\"role\":\"assistant\",\"type\":\"message\",\"content\":[{\"type\":\"output_text\",\"annotations\":[],\"text\":\"37887\"}],\"status\":\"completed\"},\"output_index\":0,\"type\":\"response.output_item.done\"}\n\nid:11\nevent:response.completed\n:HTTP_STATUS/200\ndata:{\"sequence_number\":10,\"type\":\"response.completed\",\"response\":{\"top_logprobs\":0,\"metadata\":{},\"presence_penalty\":0.0,\"usage\":{\"total_tokens\":73,\"input_tokens_details\":{\"cached_tokens\":0},\"output_tokens\":5,\"input_tokens\":68,\"output_tokens_details\":{\"reasoning_tokens\":0},\"x_details\":[{\"total_tokens\":73,\"x_billing_type\":\"response_api\",\"output_tokens\":5,\"input_tokens\":68,\"prompt_tokens_details\":{\"cached_tokens\":0}}]},\"created_at\":1788837067,\"store\":true,\"tools\":[],\"output\":[{\"id\":\"msg_509c6dec-e527-41b5-a9c9-7f02fecb7a3f\",\"role\":\"assistant\",\"type\":\"message\",\"content\":[{\"type\":\"output_text\",\"annotations\":[],\"text\":\"37887\"}],\"status\":\"completed\"}],\"top_p\":1.0,\"completed_at\":1788837067,\"frequency_penalty\":0.0,\"parallel_tool_calls\":true,\"background\":false,\"temperature\":1.0,\"tool_choice\":\"auto\",\"model\":\"qwen3.7-plus\",\"service_tier\":\"default\",\"id\":\"resp_493607bf-baef-9ced-9e36-2e2a583b6177\",\"max_output_tokens\":4096,\"object\":\"response\",\"status\":\"completed\"}}\n\n"
}
}
]
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,53 @@
{
"version": 1,
"metadata": {
"tags": [
"prefix:alibaba-responses",
"provider:alibaba",
"protocol:alibaba-responses",
"region:ap-southeast-1",
"continuation",
"storage"
],
"name": "alibaba-responses/qwen-3-8-max-continues-a-stored-response",
"recordedAt": "2026-09-08T03:12:42.429Z"
},
"interactions": [
{
"transport": "http",
"request": {
"method": "POST",
"url": "https://dashscope-intl.aliyuncs.com/compatible-mode/v1/responses",
"headers": {
"content-type": "application/json"
},
"body": "{\"model\":\"qwen3.8-max\",\"input\":[{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"Remember the password word apricot. Reply OK.\"}]}],\"store\":true,\"reasoning\":{\"effort\":\"none\"},\"max_output_tokens\":1024,\"stream\":true}"
},
"response": {
"status": 200,
"headers": {
"content-type": "text/event-stream;charset=UTF-8"
},
"body": "id:1\nevent:response.created\n:HTTP_STATUS/200\ndata:{\"sequence_number\":0,\"type\":\"response.created\",\"response\":{\"top_logprobs\":0,\"metadata\":{},\"presence_penalty\":0.0,\"reasoning\":{\"effort\":\"none\"},\"created_at\":1788837161,\"store\":true,\"tools\":[],\"output\":[],\"top_p\":1.0,\"completed_at\":1788837161,\"frequency_penalty\":0.0,\"parallel_tool_calls\":true,\"background\":false,\"temperature\":1.0,\"tool_choice\":\"auto\",\"model\":\"qwen3.8-max\",\"service_tier\":\"default\",\"id\":\"resp_97f8cd28-c7ab-9d51-9c14-4fc531decdc8\",\"max_output_tokens\":1024,\"object\":\"response\",\"status\":\"queued\"}}\n\nid:2\nevent:response.in_progress\n:HTTP_STATUS/200\ndata:{\"sequence_number\":1,\"type\":\"response.in_progress\",\"response\":{\"top_logprobs\":0,\"metadata\":{},\"presence_penalty\":0.0,\"reasoning\":{\"effort\":\"none\"},\"created_at\":1788837161,\"store\":true,\"tools\":[],\"output\":[],\"top_p\":1.0,\"completed_at\":1788837161,\"frequency_penalty\":0.0,\"parallel_tool_calls\":true,\"background\":false,\"temperature\":1.0,\"tool_choice\":\"auto\",\"model\":\"qwen3.8-max\",\"service_tier\":\"default\",\"id\":\"resp_97f8cd28-c7ab-9d51-9c14-4fc531decdc8\",\"max_output_tokens\":1024,\"object\":\"response\",\"status\":\"in_progress\"}}\n\nid:3\nevent:response.output_item.added\n:HTTP_STATUS/200\ndata:{\"sequence_number\":2,\"item\":{\"id\":\"msg_a908ad8e-6220-444b-880c-02aecbaf8fac\",\"role\":\"assistant\",\"type\":\"message\",\"content\":[],\"status\":\"in_progress\"},\"output_index\":0,\"type\":\"response.output_item.added\"}\n\nid:4\nevent:response.content_part.added\n:HTTP_STATUS/200\ndata:{\"sequence_number\":3,\"output_index\":0,\"type\":\"response.content_part.added\",\"content_index\":0,\"item_id\":\"msg_a908ad8e-6220-444b-880c-02aecbaf8fac\",\"part\":{\"type\":\"output_text\",\"annotations\":[],\"text\":\"\"}}\n\nid:5\nevent:response.output_text.delta\n:HTTP_STATUS/200\ndata:{\"sequence_number\":4,\"content_index\":0,\"item_id\":\"msg_a908ad8e-6220-444b-880c-02aecbaf8fac\",\"delta\":\"OK\",\"output_index\":0,\"type\":\"response.output_text.delta\",\"logprobs\":[]}\n\nid:6\nevent:response.output_text.delta\n:HTTP_STATUS/200\ndata:{\"sequence_number\":5,\"content_index\":0,\"item_id\":\"msg_a908ad8e-6220-444b-880c-02aecbaf8fac\",\"delta\":\".\",\"output_index\":0,\"type\":\"response.output_text.delta\",\"logprobs\":[]}\n\nid:7\nevent:response.output_text.delta\n:HTTP_STATUS/200\ndata:{\"sequence_number\":6,\"content_index\":0,\"item_id\":\"msg_a908ad8e-6220-444b-880c-02aecbaf8fac\",\"delta\":\"\",\"output_index\":0,\"type\":\"response.output_text.delta\",\"logprobs\":[]}\n\nid:8\nevent:response.output_text.done\n:HTTP_STATUS/200\ndata:{\"sequence_number\":7,\"content_index\":0,\"item_id\":\"msg_a908ad8e-6220-444b-880c-02aecbaf8fac\",\"text\":\"OK.\",\"output_index\":0,\"type\":\"response.output_text.done\",\"logprobs\":[]}\n\nid:9\nevent:response.content_part.done\n:HTTP_STATUS/200\ndata:{\"sequence_number\":8,\"output_index\":0,\"type\":\"response.content_part.done\",\"content_index\":0,\"item_id\":\"msg_a908ad8e-6220-444b-880c-02aecbaf8fac\",\"part\":{\"type\":\"output_text\",\"annotations\":[],\"text\":\"OK.\"}}\n\nid:10\nevent:response.output_item.done\n:HTTP_STATUS/200\ndata:{\"sequence_number\":9,\"item\":{\"id\":\"msg_a908ad8e-6220-444b-880c-02aecbaf8fac\",\"role\":\"assistant\",\"type\":\"message\",\"content\":[{\"type\":\"output_text\",\"annotations\":[],\"text\":\"OK.\"}],\"status\":\"completed\"},\"output_index\":0,\"type\":\"response.output_item.done\"}\n\nid:11\nevent:response.completed\n:HTTP_STATUS/200\ndata:{\"sequence_number\":10,\"type\":\"response.completed\",\"response\":{\"top_logprobs\":0,\"metadata\":{},\"presence_penalty\":0.0,\"reasoning\":{\"effort\":\"none\"},\"usage\":{\"total_tokens\":60,\"input_tokens_details\":{\"cached_tokens\":0},\"output_tokens\":2,\"input_tokens\":58,\"output_tokens_details\":{\"reasoning_tokens\":0},\"x_details\":[{\"total_tokens\":60,\"x_billing_type\":\"response_api\",\"output_tokens\":2,\"input_tokens\":58,\"prompt_tokens_details\":{\"cached_tokens\":0}}]},\"created_at\":1788837161,\"store\":true,\"tools\":[],\"output\":[{\"id\":\"msg_a908ad8e-6220-444b-880c-02aecbaf8fac\",\"role\":\"assistant\",\"type\":\"message\",\"content\":[{\"type\":\"output_text\",\"annotations\":[],\"text\":\"OK.\"}],\"status\":\"completed\"}],\"top_p\":1.0,\"completed_at\":1788837161,\"frequency_penalty\":0.0,\"parallel_tool_calls\":true,\"background\":false,\"temperature\":1.0,\"tool_choice\":\"auto\",\"model\":\"qwen3.8-max\",\"service_tier\":\"default\",\"id\":\"resp_97f8cd28-c7ab-9d51-9c14-4fc531decdc8\",\"max_output_tokens\":1024,\"object\":\"response\",\"status\":\"completed\"}}\n\n"
}
},
{
"transport": "http",
"request": {
"method": "POST",
"url": "https://dashscope-intl.aliyuncs.com/compatible-mode/v1/responses",
"headers": {
"content-type": "application/json"
},
"body": "{\"model\":\"qwen3.8-max\",\"input\":[{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"What word did I ask you to remember? Reply with only the word.\"}]}],\"store\":true,\"reasoning\":{\"effort\":\"none\"},\"max_output_tokens\":1024,\"previous_response_id\":\"resp_97f8cd28-c7ab-9d51-9c14-4fc531decdc8\",\"stream\":true}"
},
"response": {
"status": 200,
"headers": {
"content-type": "text/event-stream;charset=UTF-8"
},
"body": "id:1\nevent:response.created\n:HTTP_STATUS/200\ndata:{\"sequence_number\":0,\"type\":\"response.created\",\"response\":{\"top_logprobs\":0,\"metadata\":{},\"presence_penalty\":0.0,\"reasoning\":{\"effort\":\"none\"},\"created_at\":1788837162,\"store\":true,\"tools\":[],\"output\":[],\"top_p\":1.0,\"completed_at\":1788837162,\"previous_response_id\":\"resp_97f8cd28-c7ab-9d51-9c14-4fc531decdc8\",\"frequency_penalty\":0.0,\"parallel_tool_calls\":true,\"background\":false,\"temperature\":1.0,\"tool_choice\":\"auto\",\"model\":\"qwen3.8-max\",\"service_tier\":\"default\",\"id\":\"resp_1d81d34c-d3ec-9f98-ab59-5d4641d4217a\",\"max_output_tokens\":1024,\"object\":\"response\",\"status\":\"queued\"}}\n\nid:2\nevent:response.in_progress\n:HTTP_STATUS/200\ndata:{\"sequence_number\":1,\"type\":\"response.in_progress\",\"response\":{\"top_logprobs\":0,\"metadata\":{},\"presence_penalty\":0.0,\"reasoning\":{\"effort\":\"none\"},\"created_at\":1788837162,\"store\":true,\"tools\":[],\"output\":[],\"top_p\":1.0,\"completed_at\":1788837162,\"previous_response_id\":\"resp_97f8cd28-c7ab-9d51-9c14-4fc531decdc8\",\"frequency_penalty\":0.0,\"parallel_tool_calls\":true,\"background\":false,\"temperature\":1.0,\"tool_choice\":\"auto\",\"model\":\"qwen3.8-max\",\"service_tier\":\"default\",\"id\":\"resp_1d81d34c-d3ec-9f98-ab59-5d4641d4217a\",\"max_output_tokens\":1024,\"object\":\"response\",\"status\":\"in_progress\"}}\n\nid:3\nevent:response.output_item.added\n:HTTP_STATUS/200\ndata:{\"sequence_number\":2,\"item\":{\"id\":\"msg_7bc50c33-8fbd-4b10-91fc-4f2533bb2091\",\"role\":\"assistant\",\"type\":\"message\",\"content\":[],\"status\":\"in_progress\"},\"output_index\":0,\"type\":\"response.output_item.added\"}\n\nid:4\nevent:response.content_part.added\n:HTTP_STATUS/200\ndata:{\"sequence_number\":3,\"output_index\":0,\"type\":\"response.content_part.added\",\"content_index\":0,\"item_id\":\"msg_7bc50c33-8fbd-4b10-91fc-4f2533bb2091\",\"part\":{\"type\":\"output_text\",\"annotations\":[],\"text\":\"\"}}\n\nid:5\nevent:response.output_text.delta\n:HTTP_STATUS/200\ndata:{\"sequence_number\":4,\"content_index\":0,\"item_id\":\"msg_7bc50c33-8fbd-4b10-91fc-4f2533bb2091\",\"delta\":\"ap\",\"output_index\":0,\"type\":\"response.output_text.delta\",\"logprobs\":[]}\n\nid:6\nevent:response.output_text.delta\n:HTTP_STATUS/200\ndata:{\"sequence_number\":5,\"content_index\":0,\"item_id\":\"msg_7bc50c33-8fbd-4b10-91fc-4f2533bb2091\",\"delta\":\"ricot\",\"output_index\":0,\"type\":\"response.output_text.delta\",\"logprobs\":[]}\n\nid:7\nevent:response.output_text.delta\n:HTTP_STATUS/200\ndata:{\"sequence_number\":6,\"content_index\":0,\"item_id\":\"msg_7bc50c33-8fbd-4b10-91fc-4f2533bb2091\",\"delta\":\"\",\"output_index\":0,\"type\":\"response.output_text.delta\",\"logprobs\":[]}\n\nid:8\nevent:response.output_text.done\n:HTTP_STATUS/200\ndata:{\"sequence_number\":7,\"content_index\":0,\"item_id\":\"msg_7bc50c33-8fbd-4b10-91fc-4f2533bb2091\",\"text\":\"apricot\",\"output_index\":0,\"type\":\"response.output_text.done\",\"logprobs\":[]}\n\nid:9\nevent:response.content_part.done\n:HTTP_STATUS/200\ndata:{\"sequence_number\":8,\"output_index\":0,\"type\":\"response.content_part.done\",\"content_index\":0,\"item_id\":\"msg_7bc50c33-8fbd-4b10-91fc-4f2533bb2091\",\"part\":{\"type\":\"output_text\",\"annotations\":[],\"text\":\"apricot\"}}\n\nid:10\nevent:response.output_item.done\n:HTTP_STATUS/200\ndata:{\"sequence_number\":9,\"item\":{\"id\":\"msg_7bc50c33-8fbd-4b10-91fc-4f2533bb2091\",\"role\":\"assistant\",\"type\":\"message\",\"content\":[{\"type\":\"output_text\",\"annotations\":[],\"text\":\"apricot\"}],\"status\":\"completed\"},\"output_index\":0,\"type\":\"response.output_item.done\"}\n\nid:11\nevent:response.completed\n:HTTP_STATUS/200\ndata:{\"sequence_number\":10,\"type\":\"response.completed\",\"response\":{\"top_logprobs\":0,\"metadata\":{},\"presence_penalty\":0.0,\"reasoning\":{\"effort\":\"none\"},\"usage\":{\"total_tokens\":92,\"input_tokens_details\":{\"cached_tokens\":0},\"output_tokens\":3,\"input_tokens\":89,\"output_tokens_details\":{\"reasoning_tokens\":0},\"x_details\":[{\"total_tokens\":92,\"x_billing_type\":\"response_api\",\"output_tokens\":3,\"input_tokens\":89,\"prompt_tokens_details\":{\"cached_tokens\":0}}]},\"created_at\":1788837162,\"store\":true,\"tools\":[],\"output\":[{\"id\":\"msg_7bc50c33-8fbd-4b10-91fc-4f2533bb2091\",\"role\":\"assistant\",\"type\":\"message\",\"content\":[{\"type\":\"output_text\",\"annotations\":[],\"text\":\"apricot\"}],\"status\":\"completed\"}],\"top_p\":1.0,\"completed_at\":1788837162,\"previous_response_id\":\"resp_97f8cd28-c7ab-9d51-9c14-4fc531decdc8\",\"frequency_penalty\":0.0,\"parallel_tool_calls\":true,\"background\":false,\"temperature\":1.0,\"tool_choice\":\"auto\",\"model\":\"qwen3.8-max\",\"service_tier\":\"default\",\"id\":\"resp_1d81d34c-d3ec-9f98-ab59-5d4641d4217a\",\"max_output_tokens\":1024,\"object\":\"response\",\"status\":\"completed\"}}\n\n"
}
}
]
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,39 @@
import { LLM } from "../../src/index.js"
import { Alibaba } from "../../src/providers.js"
const provider = Alibaba.configure({ region: "ap-southeast-1" })
Alibaba.configure({ baseURL: "https://gateway.example/v1" })
Alibaba.configure({ region: "eu-central-1", workspaceID: "llm-workspace" })
LLM.request({
model: provider.chat("qwen3.8-max"),
providerOptions: { reasoningEffort: "future", enableThinking: true, preserveThinking: false, toolStream: true },
})
LLM.request({
model: provider.messages("qwen3.8-max"),
providerOptions: { effort: "xhigh", thinking: { type: "enabled" } },
})
LLM.request({
model: provider.messages("qwen3.7-plus"),
providerOptions: { thinking: { type: "enabled", budgetTokens: 512 } },
})
LLM.request({
model: provider.responses("qwen3.8-max"),
providerOptions: { reasoningEffort: "low", enableThinking: true, store: true, previousResponseId: "resp_previous" },
})
// @ts-expect-error Region or complete base URL is required.
Alibaba.configure({ apiKey: "fixture" })
LLM.request({
model: provider.chat("qwen3.8-max"),
// @ts-expect-error Thinking toggle is a boolean.
providerOptions: { enableThinking: "true" },
})
LLM.request({
model: provider.messages("qwen3.8-max"),
// @ts-expect-error Messages uses effort.
providerOptions: { reasoningEffort: "high" },
})
LLM.request({
model: provider.responses("qwen3.8-max"),
// @ts-expect-error Responses does not use Chat thinking budgets.
providerOptions: { thinkingBudget: 512 },
})
+32
View File
@@ -51,6 +51,10 @@ describe("provider package entrypoints", () => {
import("@opencode/ai/providers/zai-coding-plan/chat"),
import("@opencode/ai/providers/zai-coding-plan/messages"),
import("@opencode/ai/providers/zai-coding-plan/responses"),
import("@opencode/ai/providers/alibaba"),
import("@opencode/ai/providers/alibaba/chat"),
import("@opencode/ai/providers/alibaba/messages"),
import("@opencode/ai/providers/alibaba/responses"),
])
for (const module of modules) expect(module.model).toBeFunction()
@@ -61,6 +65,34 @@ describe("provider package entrypoints", () => {
expect(modules[19].model).not.toBe(modules[20].model)
})
test("maps Alibaba API entrypoints onto explicit regional routes", async () => {
const modules = await Promise.all([
import("@opencode/ai/providers/alibaba"),
import("@opencode/ai/providers/alibaba/chat"),
import("@opencode/ai/providers/alibaba/messages"),
import("@opencode/ai/providers/alibaba/responses"),
])
expect(modules[0].model).toBe(modules[1].model)
const settings = {
region: "eu-central-1",
workspaceID: "llm-fixture",
apiKey: "fixture",
headers: { "x-test": "fixture" },
body: { extension: true },
}
const routes = ["alibaba-chat", "alibaba-chat", "alibaba-messages", "alibaba-responses"]
modules.forEach((module, index) => {
const model = module.model("qwen3.8-max", settings)
expect(model.provider).toBe("alibaba")
expect(model.route.id).toBe(routes[index])
expect(model.route.endpoint.baseURL).toBe(
`https://llm-fixture.eu-central-1.maas.aliyuncs.com/${index === 2 ? "apps/anthropic/v1" : "compatible-mode/v1"}`,
)
expect(model.route.defaults.headers).toEqual(settings.headers)
expect(model.route.defaults.http?.body).toEqual(settings.body)
})
})
test("maps Moonshot API entrypoints onto provider-owned routes", async () => {
const modules = await Promise.all([
import("@opencode/ai/providers/moonshot"),
@@ -0,0 +1,254 @@
import { describe, expect } from "bun:test"
import { Effect } from "effect"
import { LLM, LLMEvent, LLMRequest, Message, ToolDefinition } from "../../src/index.js"
import { Alibaba } from "../../src/providers.js"
import { LLMClient } from "../../src/route.js"
import { compileRequest } from "../../src/route/client.js"
import { recordedTests } from "../recorded-test.js"
const alibaba = Alibaba.configure({ region: "ap-southeast-1", apiKey: process.env.ALIBABA_API_KEY ?? "fixture" })
const record = (api: "chat" | "messages" | "responses") =>
recordedTests({
prefix: `alibaba-${api}`,
provider: "alibaba",
protocol: `alibaba-${api}`,
requires: ["ALIBABA_API_KEY"],
tags: ["region:ap-southeast-1"],
})
for (const api of ["chat", "messages", "responses"] as const) {
const recorded = record(api)
describe(`Alibaba ${api} capabilities`, () => {
for (const enabled of [false, true]) {
recorded.effect.with(
`Qwen 3.7 Plus streams thinking ${enabled ? "enabled" : "disabled"}`,
{ tags: ["thinking", "usage"] },
() =>
Effect.gen(function* () {
const request = LLM.request({
model: alibaba[api]("qwen3.7-plus"),
providerOptions:
api === "messages"
? { thinking: { type: enabled ? "enabled" : "disabled", ...(enabled ? { budgetTokens: 1024 } : {}) } }
: api === "chat"
? { enableThinking: enabled, ...(enabled ? { thinkingBudget: 1024 } : {}) }
: { enableThinking: enabled },
prompt: "What is 173 multiplied by 219? Reply with only the final integer.",
generation: { maxTokens: 4096 },
})
const compiled = yield* compileRequest(request)
expect(api === "messages" ? compiled.body.thinking.type : compiled.body.enable_thinking).toBe(
api === "messages" ? (enabled ? "enabled" : "disabled") : enabled,
)
const response = yield* LLMClient.generate(request)
expect(response.text.replaceAll(",", "")).toContain("37887")
expect(response.reasoning.length > 0).toBe(enabled)
expect(response.finishReason.normalized).toBe("stop")
expect(response.usage?.inputTokens).toBeGreaterThan(0)
expect(response.usage?.outputTokens).toBeGreaterThan(0)
}),
120_000,
)
}
recorded.effect.with(
"Qwen 3.8 Flash reads image bytes",
{ tags: ["image"] },
() =>
Effect.gen(function* () {
const bytes = yield* Effect.promise(() =>
Bun.file(new URL("../fixtures/media/restroom.png", import.meta.url)).bytes(),
)
const response = yield* LLMClient.generate(
LLM.request({
model: alibaba[api]("qwen3.8-flash"),
providerOptions: api === "messages" ? { thinking: { type: "disabled" } } : { enableThinking: false },
messages: [
Message.user([
{ type: "text", text: "Read the three words in this image. Reply with only the words in order." },
{ type: "media", mediaType: "image/png", data: bytes },
]),
],
generation: { maxTokens: 4096 },
}),
)
expect(response.text.toLowerCase()).toContain("jiggling restroom prison")
expect(response.finishReason.normalized).toBe("stop")
}),
120_000,
)
recorded.effect.with(
"Qwen 3.8 Max obeys named tool choice",
{ tags: ["tool", "tool-choice"] },
() =>
Effect.gen(function* () {
const response = yield* LLMClient.generate(
LLM.request({
model: alibaba[api]("qwen3.8-max"),
prompt: "Find the current weather in Paris.",
providerOptions: api === "messages" ? { thinking: { type: "disabled" } } : { reasoningEffort: "none" },
tools: [
ToolDefinition.make({
name: "get_weather",
description: "Get weather in a city",
inputSchema: {
type: "object",
properties: { city: { type: "string", enum: ["Paris"] } },
required: ["city"],
},
}),
],
toolChoice: { type: "tool", name: "get_weather" },
generation: { maxTokens: 4096 },
}),
)
expect(response.toolCalls).toMatchObject([{ name: "get_weather", input: { city: "Paris" } }])
expect(response.finishReason.normalized).toBe(api === "messages" ? "stop" : "tool-calls")
if (api === "messages") expect(response.finishReason.raw).toBe("end_turn")
}),
120_000,
)
})
}
record("chat").effect.with(
"Qwen 3.8 Max returns a JSON object",
{ tags: ["structured-output"] },
() =>
Effect.gen(function* () {
const response = yield* LLMClient.generate(
LLM.request({
model: alibaba.chat("qwen3.8-max"),
prompt: 'Return a JSON object with one key "city" set to the capital city of France.',
providerOptions: { reasoningEffort: "none", responseFormat: { type: "json_object" } },
generation: { maxTokens: 1024 },
}),
)
expect(JSON.parse(response.text)).toEqual({ city: "Paris" })
expect(response.finishReason.normalized).toBe("stop")
}),
120_000,
)
record("messages").effect.with(
"Qwen 3.8 Max follows a JSON schema",
{ tags: ["structured-output"] },
() =>
Effect.gen(function* () {
const response = yield* LLMClient.generate(
LLM.request({
model: alibaba.messages("qwen3.8-max"),
prompt: 'Return a JSON object with one key "city" set to the capital city of France.',
providerOptions: {
thinking: { type: "disabled" },
outputConfig: {
format: {
type: "json_schema",
schema: {
type: "object",
properties: { city: { type: "string" } },
required: ["city"],
additionalProperties: false,
},
},
},
},
generation: { maxTokens: 1024 },
}),
)
expect(JSON.parse(response.text)).toEqual({ city: "Paris" })
expect(response.finishReason.normalized).toBe("stop")
}),
120_000,
)
const responses = record("responses")
responses.effect.with(
"Qwen 3.8 Max continues a stored response",
{ tags: ["continuation", "storage"] },
() =>
Effect.gen(function* () {
const first = yield* LLMClient.generate(
LLM.request({
model: alibaba.responses("qwen3.8-max"),
prompt: "Remember the password word apricot. Reply OK.",
providerOptions: { store: true, reasoningEffort: "none" },
generation: { maxTokens: 1024 },
}),
)
const id = first.events.find(LLMEvent.is.finish)?.providerMetadata?.alibaba?.responseId
expect(id).toBeString()
if (typeof id !== "string") throw new Error("Missing Alibaba response ID")
const second = yield* LLMClient.generate(
LLM.request({
model: alibaba.responses("qwen3.8-max"),
prompt: "What word did I ask you to remember? Reply with only the word.",
providerOptions: { previousResponseId: id, store: true, reasoningEffort: "none" },
generation: { maxTokens: 1024 },
}),
)
expect(second.text.toLowerCase()).toContain("apricot")
expect(second.finishReason.normalized).toBe("stop")
}),
120_000,
)
responses.effect.with(
"Qwen 3.8 Max uses hosted web search and extraction",
{ tags: ["hosted-tool", "web-search", "web-extractor"] },
() =>
Effect.gen(function* () {
const request = LLM.request({
model: alibaba.responses("qwen3.8-max"),
prompt:
"Use web search to find Alibaba Cloud Model Studio's official documentation, then use web_extractor to read the page. Give a brief summary with the source URL.",
tools: [Alibaba.webSearch(), Alibaba.webExtractor()],
providerOptions: { reasoningEffort: "low", store: false },
generation: { maxTokens: 4096 },
})
const response = yield* LLMClient.generate(request)
expect(response.toolCalls).toEqual(
expect.arrayContaining([
expect.objectContaining({ name: "web_search", providerExecuted: true }),
expect.objectContaining({ name: "web_extractor", providerExecuted: true }),
]),
)
expect(response.text.toLowerCase()).toContain("alibaba")
expect(response.events.some(LLMEvent.is.toolResult)).toBe(true)
const replay = yield* compileRequest(
LLMRequest.update(request, {
messages: [...request.messages, response.message, Message.user("Summarize in one sentence.")],
}),
)
expect(replay.body.input).toEqual(
expect.arrayContaining([
expect.objectContaining({ type: "web_search_call" }),
expect.objectContaining({ type: "web_extractor_call" }),
]),
)
}),
180_000,
)
responses.effect.with(
"Qwen 3.8 Max uses hosted code interpreter",
{ tags: ["hosted-tool", "code-interpreter"] },
() =>
Effect.gen(function* () {
const response = yield* LLMClient.generate(
LLM.request({
model: alibaba.responses("qwen3.8-max"),
prompt:
"Use the code interpreter to compute the SHA-256 hash of the UTF-8 string hello (no newline). Reply with only the hash.",
tools: [Alibaba.codeInterpreter()],
providerOptions: { reasoningEffort: "low" },
generation: { maxTokens: 4096 },
}),
)
expect(response.toolCalls).toEqual(
expect.arrayContaining([expect.objectContaining({ name: "code_interpreter", providerExecuted: true })]),
)
expect(response.text).toContain("2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824")
expect(response.events.some(LLMEvent.is.toolResult)).toBe(true)
}),
180_000,
)
@@ -0,0 +1,163 @@
import { describe, expect } from "bun:test"
import { Effect } from "effect"
import { LLM, LLMEvent, LLMRequest, Message, ToolDefinition, type LLMResponse } from "../../src/index.js"
import { Alibaba } from "../../src/providers.js"
import { LLMClient } from "../../src/route.js"
import { compileRequest } from "../../src/route/client.js"
import { recordedTests } from "../recorded-test.js"
const alibaba = Alibaba.configure({ region: "ap-southeast-1", apiKey: process.env.ALIBABA_API_KEY ?? "fixture" })
const weather = ToolDefinition.make({
name: "get_weather",
description: "Get the current weather in a city",
inputSchema: {
type: "object",
properties: { city: { type: "string", enum: ["Paris"] } },
required: ["city"],
additionalProperties: false,
},
})
for (const api of ["chat", "messages", "responses"] as const) {
const recorded = recordedTests({
prefix: `alibaba-${api}`,
provider: "alibaba",
protocol: `alibaba-${api}`,
requires: ["ALIBABA_API_KEY"],
tags: ["region:ap-southeast-1"],
})
describe(`Alibaba ${api}`, () => {
for (const effort of api === "messages"
? [undefined, "low", "medium", "high", "xhigh", "max"]
: [undefined, "none", "minimal", "low", "medium", "high", "xhigh", "max"]) {
recorded.effect.with(
`Qwen 3.8 Max streams ${effort ?? "default"} effort`,
{ tags: ["text", "reasoning", "usage"] },
() =>
Effect.gen(function* () {
const request = LLM.request({
model: alibaba[api]("qwen3.8-max"),
prompt: "What is 173 multiplied by 219? Reply with only the final integer.",
providerOptions: api === "messages" ? { effort } : { reasoningEffort: effort },
generation: { maxTokens: 4096 },
})
const compiled = yield* compileRequest(request)
expect(compiled.body.enable_thinking).toBeUndefined()
expect(compiled.body.thinking).toBeUndefined()
expect(
api === "chat"
? compiled.body.reasoning_effort
: api === "messages"
? compiled.body.output_config?.effort
: compiled.body.reasoning?.effort,
).toBe(effort)
const response = yield* LLMClient.generate(request)
expect(response.text.replaceAll(",", "")).toContain("37887")
expect(response.reasoning.length > 0).toBe(effort !== "none")
expect(response.finishReason.normalized).toBe("stop")
expectUsage(response)
}),
120_000,
)
}
recorded.effect.with(
"Qwen 3.8 Max replays reasoning through a tool loop and follow-up",
{ tags: ["tool", "tool-loop", "reasoning", "continuation"] },
() =>
Effect.gen(function* () {
const request = LLM.request({
model: alibaba[api]("qwen3.8-max"),
providerOptions:
api === "messages"
? { effort: "medium" }
: api === "chat"
? { reasoningEffort: "medium", preserveThinking: true, toolStream: true }
: { reasoningEffort: "medium", store: false },
prompt:
"We have a budget of 38000 dollars for 219 trips costing 173 dollars each. Calculate whether that is affordable. If it is, use get_weather to look up the current weather in Paris. After receiving the result, report the weather in one short sentence.",
tools: [weather],
generation: { maxTokens: 4096 },
})
const first = yield* LLMClient.generate(request)
expect(first.toolCalls).toMatchObject([{ name: "get_weather", input: { city: "Paris" } }])
expect(first.finishReason.normalized).toBe("tool-calls")
expect(first.reasoning.length).toBeGreaterThan(0)
expect(first.events.some(LLMEvent.is.toolInputDelta)).toBe(true)
expectUsage(first)
const continuation = LLMRequest.update(request, {
messages: [
...request.messages,
first.message,
...first.toolCalls.map((call) =>
Message.tool({ id: call.id, name: call.name, result: { condition: "sunny", temperature: "18C" } }),
),
],
})
expectReasoning(api, (yield* compileRequest(continuation)).body, first)
const second = yield* LLMClient.generate(continuation)
expect(second.text.toLowerCase()).toContain("sunny")
expect(second.toolCalls).toHaveLength(0)
expect(second.finishReason.normalized).toBe("stop")
expectUsage(second)
const followUp = LLMRequest.update(continuation, {
messages: [
...continuation.messages,
second.message,
Message.user("What temperature did the tool report? Reply with only the temperature."),
],
})
expectReasoning(api, (yield* compileRequest(followUp)).body, first)
const third = yield* LLMClient.generate(followUp)
expect(third.text).toContain("18")
expect(third.toolCalls).toHaveLength(0)
expect(third.finishReason.normalized).toBe("stop")
expectUsage(third)
}),
180_000,
)
})
}
function expectUsage(response: LLMResponse) {
expect(response.usage?.inputTokens).toBeGreaterThan(0)
expect(response.usage?.outputTokens).toBeGreaterThan(0)
expect(response.events.filter(LLMEvent.is.finish)).toHaveLength(1)
}
function expectReasoning(
api: "chat" | "messages" | "responses",
body: Readonly<Record<string, unknown>>,
response: LLMResponse,
) {
if (api === "chat") {
expect(body.messages).toEqual(
expect.arrayContaining([expect.objectContaining({ role: "assistant", reasoning_content: response.reasoning })]),
)
return
}
for (const part of response.message.content.filter((part) => part.type === "reasoning")) {
if (api === "messages") {
expect(body.messages).toEqual(
expect.arrayContaining([
expect.objectContaining({
role: "assistant",
content: expect.arrayContaining([
{ type: "thinking", thinking: part.text, signature: part.providerMetadata?.alibaba?.signature ?? "" },
]),
}),
]),
)
continue
}
expect(body.input).toEqual(
expect.arrayContaining([
expect.objectContaining({
type: "reasoning",
id: part.providerMetadata?.alibaba?.itemId,
summary: expect.arrayContaining([{ type: "summary_text", text: part.text }]),
}),
]),
)
}
}
+376
View File
@@ -0,0 +1,376 @@
import { expect, test } from "bun:test"
import { ConfigProvider, Effect } from "effect"
import { Headers } from "effect/unstable/http"
import { Auth, LLM, LLMClient, LLMRequest, Message, ReasoningPart, ToolDefinition } from "../../src/index.js"
import { Alibaba } from "../../src/providers.js"
import { compileRequest } from "../../src/route/client.js"
import { Endpoint } from "../../src/route/endpoint.js"
import { it } from "../lib/effect.js"
import { dynamicResponse } from "../lib/http.js"
import { sseEvents } from "../lib/sse.js"
const tool = ToolDefinition.make({
name: "lookup",
description: "Look up a value",
inputSchema: { type: "object", properties: { key: { type: "string" } } },
})
const paths = {
chat: "/compatible-mode/v1/chat/completions",
messages: "/apps/anthropic/v1/messages",
responses: "/compatible-mode/v1/responses",
}
it.effect("Alibaba owns regional shared and workspace-specific endpoints", () =>
Effect.gen(function* () {
for (const region of [
"ap-southeast-1",
"cn-beijing",
"cn-hongkong",
"us-east-1",
"eu-central-1",
"ap-northeast-1",
]) {
const provider = Alibaba.configure({ region, workspaceID: "llm-fixture", apiKey: "fixture" })
expect(provider.model).toBe(provider.chat)
for (const api of ["chat", "messages", "responses"] as const) {
const model = provider[api]("qwen-plus-us")
const request = LLM.request({ model, prompt: "Hello" })
const compiled = yield* compileRequest(request)
expect(Endpoint.render(model.route.endpoint, { request, body: compiled.body }).toString()).toBe(
`https://llm-fixture.${region}.maas.aliyuncs.com${paths[api]}`,
)
expect(model.provider).toBe("alibaba")
expect(model.route.id).toBe(`alibaba-${api}`)
expect(compiled.body.model).toBe("qwen-plus-us")
for (const field of [
"thinking",
"enable_thinking",
"thinking_budget",
"preserve_thinking",
"reasoning_effort",
"reasoning",
"output_config",
"store",
"tool_stream",
])
expect(compiled.body[field]).toBeUndefined()
}
}
for (const [region, host] of [
["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 provider = Alibaba.configure({ region, apiKey: "fixture" })
for (const api of ["chat", "messages", "responses"] as const) {
const request = LLM.request({ model: provider[api]("qwen3.8-max") })
const compiled = yield* compileRequest(request)
expect(Endpoint.render(request.model.route.endpoint, { request, body: compiled.body }).toString()).toBe(
`https://${host}${paths[api]}`,
)
}
}
}),
)
test("Alibaba requires explicit placement and supports complete base URL overrides", () => {
for (const region of ["eu-central-1", "ap-northeast-1", "future-region"])
expect(() => Alibaba.configure({ region })).toThrow("requires workspaceID or baseURL")
for (const config of [
{ baseURL: "https://gateway.example/prefix" },
{ region: "future-region", workspaceID: "ignored", baseURL: "https://gateway.example/prefix" },
]) {
const provider = Alibaba.configure(config)
for (const api of ["chat", "messages", "responses"] as const)
expect(provider[api]("unchanged-id").route.endpoint.baseURL).toBe(config.baseURL)
}
expect(
Alibaba.configure({ region: "future-region", workspaceID: "llm-fixture" }).chat("new-model").route.endpoint.baseURL,
).toBe("https://llm-fixture.future-region.maas.aliyuncs.com/compatible-mode/v1")
})
it.effect("Alibaba resolves explicit auth, API keys, and regional environment credentials", () =>
Effect.gen(function* () {
for (const item of [
{
config: {},
env: { DASHSCOPE_API_KEY: "primary", ALIBABA_API_KEY: "fallback" },
headers: { authorization: "Bearer primary" },
},
{ config: {}, env: { ALIBABA_API_KEY: "fallback" }, headers: { authorization: "Bearer fallback" } },
{
config: { apiKey: "explicit" },
env: { DASHSCOPE_API_KEY: "primary" },
headers: { authorization: "Bearer explicit" },
},
{
config: { auth: Auth.header("x-custom-key", "custom") },
env: { DASHSCOPE_API_KEY: "primary" },
headers: { "x-custom-key": "custom" },
},
]) {
const provider = Alibaba.configure({ region: "ap-southeast-1", ...item.config })
for (const api of ["chat", "messages", "responses"] as const) {
const request = LLM.request({ model: provider[api]("qwen3.8-max") })
const headers = yield* request.model.route.auth
.apply({ request, method: "POST", url: "https://fixture", body: "{}", headers: Headers.empty })
.pipe(Effect.provide(ConfigProvider.layer(ConfigProvider.fromEnv({ env: item.env }))))
expect(headers).toEqual(expect.objectContaining(item.headers))
}
}
}),
)
it.effect("Alibaba keeps native reasoning controls and future efforts on their selected API", () =>
Effect.gen(function* () {
const provider = Alibaba.configure({ region: "ap-southeast-1", apiKey: "fixture" })
for (const effort of ["none", "minimal", "low", "medium", "high", "xhigh", "max", "future-effort"]) {
const chat = yield* compileRequest(
LLM.request({ model: provider.chat("qwen3.8-max"), providerOptions: { reasoningEffort: effort } }),
)
const messages = yield* compileRequest(
LLM.request({ model: provider.messages("qwen3.8-max"), providerOptions: { effort } }),
)
const responses = yield* compileRequest(
LLM.request({ model: provider.responses("qwen3.8-max"), providerOptions: { reasoningEffort: effort } }),
)
expect(chat.body.reasoning_effort).toBe(effort)
expect(messages.body.output_config).toEqual({ effort })
expect(responses.body.reasoning).toEqual({ effort })
for (const result of [chat, messages, responses]) {
expect(result.body.thinking).toBeUndefined()
expect(result.body.enable_thinking).toBeUndefined()
}
}
for (const enableThinking of [true, false]) {
const chat = yield* compileRequest(
LLM.request({
model: provider.chat("qwen3.7-plus"),
tools: [tool],
generation: { maxTokens: 1234, topK: 20 },
providerOptions: {
enableThinking,
thinkingBudget: 512,
preserveThinking: false,
clearThinking: false,
toolStream: false,
parallelToolCalls: false,
repetitionPenalty: 1.1,
responseFormat: { type: "json_object" },
enableSearch: true,
searchOptions: { forced_search: true, search_strategy: "future-strategy", enable_search_extension: false },
},
}),
)
expect(chat.body).toMatchObject({
enable_thinking: enableThinking,
thinking_budget: 512,
preserve_thinking: false,
clear_thinking: false,
tool_stream: false,
parallel_tool_calls: false,
repetition_penalty: 1.1,
max_completion_tokens: 1234,
top_k: 20,
response_format: { type: "json_object" },
enable_search: true,
search_options: { forced_search: true, search_strategy: "future-strategy", enable_search_extension: false },
})
expect(chat.body.max_tokens).toBeUndefined()
expect(chat.body.tools).toEqual([
expect.objectContaining({ function: expect.not.objectContaining({ strict: expect.anything() }) }),
])
const responses = yield* compileRequest(
LLM.request({
model: provider.responses("qwen3.7-plus"),
providerOptions: { enableThinking, store: false, previousResponseId: "resp_previous" },
}),
)
expect(responses.body).toMatchObject({
enable_thinking: enableThinking,
store: false,
previous_response_id: "resp_previous",
})
}
for (const thinking of [
{ type: "enabled" },
{ type: "disabled" },
{ type: "enabled", budgetTokens: 512 },
{ type: "future", budget_tokens: 4096 },
]) {
const messages = yield* compileRequest(
LLM.request({ model: provider.messages("qwen3.7-plus"), providerOptions: { thinking } }),
)
expect(messages.body.thinking).toEqual({
type: thinking.type,
budget_tokens: thinking.budgetTokens ?? thinking.budget_tokens,
})
}
}),
)
it.effect("Alibaba validates malformed options before execution", () =>
Effect.gen(function* () {
const provider = Alibaba.configure({ region: "ap-southeast-1", apiKey: "fixture" })
for (const [api, providerOptions] of [
["chat", { preserveThinking: "false" }],
["messages", { thinking: { type: "enabled", budgetTokens: "512" } }],
["responses", { enableThinking: "false" }],
] as const) {
const model = provider[api]("qwen3.8-max").route.with({ providerOptions }).model({ id: "qwen3.8-max" })
const error = yield* compileRequest(LLM.request({ model })).pipe(Effect.flip)
expect(error.reason._tag).toBe("InvalidRequest")
}
}),
)
it.effect("Alibaba preserves unsigned and signed Messages thinking without a budget requirement", () =>
Effect.gen(function* () {
const result = yield* compileRequest(
LLM.request({
model: Alibaba.configure({ region: "ap-southeast-1", apiKey: "fixture" }).messages("qwen3.8-max"),
providerOptions: { thinking: { type: "enabled" }, effort: "low" },
messages: [
Message.user("Hello"),
Message.assistant([
ReasoningPart.make({ type: "reasoning", text: "unsigned" }),
ReasoningPart.make({
type: "reasoning",
text: "signed",
providerMetadata: { alibaba: { signature: "opaque" } },
}),
]),
],
}),
)
expect(result.body.thinking).toEqual({ type: "enabled" })
expect(result.body.messages).toEqual(
expect.arrayContaining([
expect.objectContaining({
role: "assistant",
content: [
{ type: "thinking", thinking: "unsigned", signature: "" },
{ type: "thinking", thinking: "signed", signature: "opaque" },
],
}),
]),
)
}),
)
it.effect("Alibaba serializes Messages configuration, per-request controls, and final HTTP overlays", () =>
LLMClient.generate(
LLM.request({
model: Alibaba.configure({
baseURL: "https://gateway.example/v1",
apiKey: "fixture",
providerOptions: { thinking: { type: "enabled", budgetTokens: 512 }, effort: "high" },
}).messages("qwen3.8-max"),
prompt: "Hello",
providerOptions: { effort: "low" },
http: { body: { output_config: { effort: "medium" }, extension: true } },
}),
).pipe(
Effect.provide(
dynamicResponse((input) =>
Effect.sync(() => {
expect(input.request.url).toBe("https://gateway.example/v1/messages")
expect(input.request.headers.authorization).toBe("Bearer fixture")
expect(input.request.headers["anthropic-version"]).toBe("2023-06-01")
expect(JSON.parse(input.text)).toMatchObject({
thinking: { type: "enabled", budget_tokens: 512 },
output_config: { effort: "medium" },
extension: true,
})
return input.respond(
sseEvents(
{
type: "message_start",
message: { id: "msg_fixture", content: [], usage: { input_tokens: 1, output_tokens: 0 } },
},
{ type: "message_delta", delta: { stop_reason: "end_turn" }, usage: { output_tokens: 1 } },
{ type: "message_stop" },
),
{ headers: { "content-type": "text/event-stream" } },
)
}),
),
),
),
)
it.effect("Alibaba Responses lowers hosted tools and named selection using HTTP even with a WebSocket executor", () =>
Effect.gen(function* () {
const request = LLM.request({
model: Alibaba.configure({ region: "ap-southeast-1", apiKey: "fixture" }).responses("qwen3.8-max"),
tools: [Alibaba.webSearch(), Alibaba.webExtractor(), Alibaba.codeInterpreter(), tool],
prompt: "Hello",
})
const compiled = yield* compileRequest(request)
expect(compiled.body.tools).toMatchObject([
{ type: "web_search" },
{ type: "web_extractor" },
{ type: "code_interpreter" },
{ type: "function", name: "lookup" },
])
const named = yield* compileRequest(
LLMRequest.update(request, { tools: [tool], toolChoice: { type: "tool", name: "lookup" } }),
)
expect(named.body.tool_choice).toEqual({
type: "allowed_tools",
mode: "required",
tools: [{ type: "function", name: "lookup" }],
})
const response = yield* LLMClient.generate(request, {
webSocket: { execute: () => Effect.die("Unexpected WebSocket") },
})
expect(response.toolCalls).toMatchObject([
{
name: "web_extractor",
providerExecuted: true,
input: { urls: ["https://example.com"], goal: "Read the page" },
},
])
const replay = yield* compileRequest(
LLMRequest.update(request, { messages: [...request.messages, response.message, Message.user("Continue")] }),
)
expect(replay.body.input).toEqual(
expect.arrayContaining([
expect.objectContaining({
type: "web_extractor_call",
id: "extract_1",
urls: ["https://example.com"],
goal: "Read the page",
result: { text: "fixture" },
}),
]),
)
}).pipe(
Effect.provide(
dynamicResponse((input) =>
Effect.sync(() => {
expect(input.request.url).toBe("https://dashscope-intl.aliyuncs.com/compatible-mode/v1/responses")
return input.respond(
sseEvents(
{
type: "response.output_item.done",
output_index: 0,
item: {
type: "web_extractor_call",
id: "extract_1",
status: "completed",
urls: ["https://example.com"],
goal: "Read the page",
result: { text: "fixture" },
},
},
{ type: "response.completed", response: {} },
),
{ headers: { "content-type": "text/event-stream" } },
)
}),
),
),
),
)
@@ -0,0 +1,67 @@
import { expect, story } from "../../storybook/playwright/story"
story("settings menu reconnect retains its prompt handler across server updates", async ({ mount, page }) => {
const component = await mount("app-dialog-ssh--settings-reconnect")
await component.getByRole("button", { name: "More options" }).click()
await page.getByRole("menuitem", { name: "Connect", exact: true }).click()
const dialog = page.getByRole("dialog")
await expect(dialog.getByRole("textbox")).toBeVisible()
await dialog.getByRole("textbox").fill("fixture-password")
await dialog.getByRole("button", { name: "Continue" }).click()
await expect(dialog.getByRole("textbox", { name: "Verification code:" })).toBeVisible()
await dialog.getByRole("textbox").fill("123456")
await dialog.getByRole("button", { name: "Continue" }).click()
await expect(dialog).toHaveCount(0)
await expect(component.getByRole("button", { name: "Authenticate", exact: true })).toHaveCount(0)
})
story("cancelling a version mismatch permits reconnecting again", async ({ mount, page }) => {
const component = await mount("app-dialog-ssh--incompatible-session")
await component.getByRole("button", { name: "Reconnect", exact: true }).click()
const dialog = page.getByRole("dialog")
await expect(dialog.getByRole("alert")).toBeVisible()
await dialog.getByRole("button", { name: "Cancel", exact: true }).click()
await expect(dialog).toHaveCount(0)
await component.getByRole("button", { name: "Reconnect", exact: true }).click()
await expect(dialog.getByRole("alert")).toBeVisible()
await dialog.getByRole("button", { name: "Cancel", exact: true }).click()
await expect(dialog).toHaveCount(0)
})
story("adding a server keeps all SSH challenges in the original connection dialog", async ({ mount, page }) => {
await mount("app-dialog-ssh--host")
const dialog = page.getByRole("dialog")
await dialog.getByRole("textbox", { name: "Host or SSH command" }).fill("ssh devbox")
await dialog.getByRole("button", { name: "Add server", exact: true }).click()
await expect(dialog).toHaveCount(1)
await expect(dialog.getByRole("button", { name: "Cancel", exact: true })).toBeFocused()
await dialog.getByRole("button", { name: "Trust and connect" }).click()
await dialog.getByRole("textbox").fill("fixture-password")
await dialog.getByRole("button", { name: "Continue", exact: true }).click()
await dialog.getByRole("textbox", { name: "Verification code:" }).fill("123456")
await dialog.getByRole("button", { name: "Continue", exact: true }).click()
await expect(dialog).toHaveCount(0)
})
story("updating an incompatible connection continues authentication in the same dialog", async ({ mount, page }) => {
const component = await mount("app-dialog-ssh--incompatible-session")
await component.getByRole("button", { name: "Reconnect", exact: true }).click()
const dialog = page.getByRole("dialog")
await dialog.getByRole("button", { name: "Update and reconnect", exact: true }).click()
await expect(dialog).toHaveCount(1)
await dialog.getByRole("textbox").fill("fixture-password")
await dialog.getByRole("button", { name: "Continue", exact: true }).click()
await dialog.getByRole("textbox", { name: "Verification code:" }).fill("123456")
await dialog.getByRole("button", { name: "Continue", exact: true }).click()
await expect(dialog).toHaveCount(0)
await expect(component.getByText("Session connected")).toBeVisible()
})
story("key-based reconnect completes without opening an authentication dialog", async ({ mount, page }) => {
const component = await mount("app-dialog-ssh--key-reconnect")
await component.getByRole("button", { name: "Reconnect", exact: true }).click()
await expect(component.getByRole("button", { name: "Connecting to SSH server" })).toBeDisabled()
await expect(page.getByRole("dialog")).toHaveCount(0)
await expect(component.getByText("Session connected")).toBeVisible()
await expect(page.getByRole("dialog")).toHaveCount(0)
})
@@ -39,7 +39,7 @@ for (const width of [1400, 390]) {
reducedMotion: true,
viewport: { width, height: 900 },
})
await page.getByRole("button", { name: "Used 1 Patch", exact: true }).click()
await page.getByRole("button", { name: "1 used Patch", exact: true }).click()
const patch = page.locator('[data-component="apply-patch-tool"]')
const trigger = patch.getByRole("button", { name: /patch-border.ts/ })
await expect(trigger).toHaveAttribute("aria-expanded", "false")
@@ -39,6 +39,51 @@ test("restores review mode and selected file per session", async ({ page }) => {
await expectSelectedFile(page, "gamma.ts")
})
for (const tab of ["Context", "Open file", "README.md"]) {
test(`restores the selected ${tab} pane tab after switching sessions and reloading`, async ({ page }) => {
await setup(page)
await page.goto(sessionHref(sessionA))
await expectSessionTitle(page, titleA)
await page.getByRole("button", { name: "Toggle review" }).click()
const panel = page.locator("#review-panel")
if (tab === "Context") await page.getByRole("button", { name: "View context usage" }).click()
if (tab !== "Context") await panel.getByRole("button", { name: "Open file" }).click()
if (tab === "README.md") await panel.getByRole("button", { name: "README.md" }).click()
await expect(panel.getByRole("tab", { name: tab, selected: true })).toBeVisible()
await switchSession(page, titleB)
await page.getByRole("button", { name: "Toggle review" }).click()
await expect(panel.locator("#session-side-panel-review-tab")).toHaveAttribute("aria-selected", "true")
await switchSession(page, titleA)
await expect(panel.getByRole("tab", { name: tab, selected: true })).toBeVisible()
await page.reload()
await expectSessionTitle(page, titleA)
await expect(panel.getByRole("tab", { name: tab, selected: true })).toBeVisible()
const selected = panel.getByRole("tab", { name: tab })
const review = panel.locator("#session-side-panel-review-tab")
await selected.press("Home")
await expect(review).toHaveAttribute("aria-selected", "true")
await review.press("End")
await expect(selected).toHaveAttribute("aria-selected", "true")
await review.click()
await expect(review).toHaveAttribute("aria-selected", "true")
await selected.click()
await expect(selected).toHaveAttribute("aria-selected", "true")
await switchSession(page, titleB)
await expect(review).toHaveAttribute("aria-selected", "true")
await switchSession(page, titleA)
await expect(selected).toHaveAttribute("aria-selected", "true")
await selected.press("Control+w")
await expect(selected).toHaveCount(0)
await expect(review).toHaveAttribute("aria-selected", "true")
})
}
async function selectFile(page: Page, file: string) {
await page.getByRole("button", { name: file }).click()
await expectSelectedFile(page, file)
@@ -76,6 +121,10 @@ async function setup(page: Page) {
default: { providerID: "opencode", modelID: "test" },
},
sessions: [session(sessionA, titleA, 1700000000000), session(sessionB, titleB, 1700000001000)],
fileList: () => [
{ name: "README.md", path: "README.md", absolute: `${directory}/README.md`, type: "file", ignored: false },
],
fileContent: (path) => ({ type: "text", content: `contents:${path}` }),
pageMessages: () => ({ items: [] }),
})
await page.route(/\/api\/vcs(?:\?.*)?$/, (route) =>
@@ -1,5 +1,5 @@
import { base64Encode } from "@opencode/util/encode"
import { expect, test, type Locator } from "@playwright/test"
import { expect, test, type Locator, type Page } from "@playwright/test"
import { mockOpenCodeServer } from "../utils/mock-server"
import { expectSessionTitle } from "../utils/waits"
@@ -85,6 +85,38 @@ for (const width of [1000, 1440]) {
await expect.poll(() => toggle.boundingBox()).toEqual(closed)
})
test(`keeps moving header content out of the toggle area (${width}px, ${direction})`, async ({ page }) => {
await page.setViewportSize({ width, height: 900 })
await page.goto(`/server/${base64Encode(server)}/session/${sessionID}`)
await expectSessionTitle(page, "Review toggle position")
await page.locator("html").evaluate((element, dir) => element.setAttribute("dir", dir), direction)
const toggle = page.getByRole("button", { name: "Toggle review", exact: true })
await expect(toggle).toHaveAttribute("aria-expanded", "false")
for (const opened of [true, false]) {
// Pause in the same task as the click so even the first painted state can be inspected.
await toggle.evaluate((element) => {
;(element as HTMLButtonElement).click()
document
.getAnimations()
.filter((animation) => animation.timeline instanceof DocumentTimeline)
.forEach((animation) => animation.pause())
})
await expect(toggle).toHaveAttribute("aria-expanded", String(opened))
await expect(page.locator("#review-panel")).toHaveAttribute("aria-hidden", String(!opened))
for (const progress of [0.08, 0.16, 0.25, 0.5, 0.8, 0.96]) {
await expectHeaderClearOfToggle(page, toggle, progress)
}
await page.evaluate(() => {
document
.getAnimations()
.filter((animation) => animation.timeline instanceof DocumentTimeline)
.forEach((animation) => animation.finish())
})
}
await expect(page.locator("#review-panel")).toBeHidden()
})
test(`keeps terminal controls clear of the review toggle (${width}px, ${direction})`, async ({ page }) => {
await page.setViewportSize({ width, height: 900 })
const ptys: { id: string; title: string }[] = []
@@ -166,10 +198,84 @@ for (const width of [1000, 1440]) {
await expect(toggle).toBeFocused()
await expect.poll(() => toggle.boundingBox()).toEqual(position)
await expectTerminalControlsAligned(terminal, toggle)
// Closing the terminal clears the region's animation flag while retaining the review contents.
await page.keyboard.press("Control+Backquote")
await expect(terminal).toBeHidden()
await expect
.poll(() =>
page
.locator('[data-slot="session-chat-panel"]')
.evaluate((element) => element.getAnimations().every((animation) => animation.playState === "finished")),
)
.toBe(true)
await expect(page.locator('[data-slot="session-review-content"]')).toHaveCSS("opacity", "0")
await toggle.evaluate((element) => {
;(element as HTMLButtonElement).click()
document
.getAnimations()
.filter((animation) => animation.timeline instanceof DocumentTimeline)
.forEach((animation) => animation.pause())
})
await expect(toggle).toHaveAttribute("aria-expanded", "true")
await expectHeaderClearOfToggle(page, toggle, 0.25)
})
}
}
async function expectHeaderClearOfToggle(page: Page, toggle: Locator, progress: number) {
const geometry = await page.locator('[data-slot="session-chat-panel"]').evaluate((chat, progress) => {
const row = chat.parentElement!
const animations = row
.getAnimations({ subtree: true })
.filter((animation) => animation.timeline instanceof DocumentTimeline)
const width = animations.find(
(animation) => animation instanceof CSSTransition && animation.transitionProperty === "width",
)!
animations.forEach((animation) => {
animation.pause()
animation.currentTime = Number(width.effect!.getTiming().duration) * progress
})
const chatBounds = chat.getBoundingClientRect()
const panelBounds = document.querySelector("#review-panel")!.getBoundingClientRect()
const summaryBounds = document
.querySelector('[data-session-title] button[aria-label="Session details"]')!
.getBoundingClientRect()
return {
row: row.getBoundingClientRect().width,
panelWidth: panelBounds.width,
timelineControlInset:
getComputedStyle(row).direction === "rtl"
? summaryBounds.left - chatBounds.left
: chatBounds.right - summaryBounds.right,
gap:
getComputedStyle(row).direction === "rtl"
? chatBounds.left - panelBounds.right
: panelBounds.left - chatBounds.right,
contentOpacity: Number(getComputedStyle(document.querySelector('[data-slot="session-review-content"]')!).opacity),
panels: chatBounds.width + panelBounds.width + parseFloat(getComputedStyle(row).columnGap),
}
}, progress)
expect(geometry.gap).toBeCloseTo(8, 1)
// Reserve the fixed toggle's 28px width, the 8px control gap, and the 12px header inset.
expect(geometry.timelineControlInset).toBeCloseTo(48, 1)
if (geometry.panelWidth > 0) expect(Math.abs(geometry.row - geometry.panels)).toBeLessThanOrEqual(1)
if (progress === 0.25) {
expect(geometry.contentOpacity).toBeGreaterThan(0)
expect(geometry.contentOpacity).toBeLessThan(1)
}
const clip = await toggle.boundingBox()
if (!clip) throw new Error("Review toggle bounds are unavailable")
// Header contents must make no difference to the pixels behind the fixed toggle.
expect(await page.screenshot({ clip })).toEqual(
await page.screenshot({
clip,
style: ".session-review-v2-tabs-bar { visibility: hidden !important; }",
}),
)
}
async function expectTerminalControlsAligned(terminal: Locator, toggle: Locator) {
await expect
.poll(async () => {
@@ -0,0 +1,44 @@
import { expect, test } from "@playwright/test"
import { setupTimeline } from "../performance/timeline-stability/fixture"
for (const reducedMotion of [false, true]) {
test(`suppresses the scrollbar from toggle press until timeline interaction (reduced motion: ${reducedMotion})`, async ({
page,
}) => {
await setupTimeline(page, { seedHistory: true, reducedMotion })
const chat = page.locator('[data-slot="session-chat-panel"]')
const scroll = page.locator('[data-slot="session-timeline-scroll"]')
const viewport = scroll.locator(".scroll-view__viewport")
const thumb = scroll.locator('.scroll-view__thumb[data-orientation="vertical"]')
const toggle = page.getByRole("button", { name: "Toggle review", exact: true })
await expect(thumb).toHaveCount(1)
await scroll.hover()
await expect(thumb).toHaveAttribute("data-visible", "true")
await expect(thumb).toHaveCSS("visibility", "visible")
for (const opened of [true, false]) {
await toggle.hover()
await page.mouse.down()
await expect(thumb).toHaveCSS("visibility", "hidden")
await page.mouse.up()
await expect(toggle).toHaveAttribute("aria-expanded", String(opened))
await chat.evaluate(async (element) => {
await Promise.all(element.getAnimations().map((animation) => animation.finished))
})
await expect(chat).toHaveAttribute("data-width-animating", "false")
await expect(thumb).toHaveCSS("visibility", "hidden")
// Late scroll anchoring must not bring the thumb back after the panel has settled.
await viewport.evaluate(
(element) =>
new Promise<void>((resolve) => {
element.addEventListener("scroll", () => resolve(), { once: true })
element.scrollTop += element.scrollTop > 0 ? -1 : 1
}),
)
await expect(thumb).toHaveCSS("visibility", "hidden")
await scroll.hover()
await expect(thumb).toHaveAttribute("data-visible", "true")
await expect(thumb).toHaveCSS("visibility", "visible")
}
})
}
@@ -308,8 +308,8 @@ for (const delivery of ["steer", "queue"] as const) {
})
const tools = page.locator('[data-timeline-part-ids="tool_queue_read,tool_queue_grep"]')
await expect(tools).toBeVisible()
await expect(tools).toHaveText(/^Used\s*1 Read, 1 Grep$/)
await expect(tools.locator('[data-slot="basic-tool-tool-title"]')).toHaveText("1 Read, 1 Grep")
await expect(tools).toHaveText(/^2 used\s*Read, Grep$/)
await expect(tools.locator('[data-slot="basic-tool-tool-title"]')).toHaveText("Read, Grep")
await expect(thinking).toHaveCount(0)
await expect(pending).toBeVisible()
expect(mock.rows.map((row) => ({ id: row.id, delivery: row.delivery }))).toEqual([
@@ -318,7 +318,7 @@ for (const delivery of ["steer", "queue"] as const) {
await transcript.screenshot({ path: testInfo.outputPath("pending-steer.png") })
// Soft assertions let delivery run too, even when the pending ordering regresses.
await expect.soft(tools.or(pending)).toHaveText([/^Used\s*1 Read, 1 Grep$/, /U2: Also check the retry path\./])
await expect.soft(tools.or(pending)).toHaveText([/^2 used\s*Read, Grep$/, /U2: Also check the retry path\./])
await expect
.soft(transcript.locator('[data-timeline-row="AssistantPart"]').filter({ has: tools }))
.toHaveAttribute("data-message-id", userID)
@@ -350,7 +350,7 @@ for (const delivery of ["steer", "queue"] as const) {
await expect(response).toHaveAttribute("data-message-id", inboxID)
await expect(thinking).toHaveCount(0)
await expect(tools.or(pending).or(response)).toHaveText([
/^Used\s*1 Read, 1 Grep$/,
/^2 used\s*Read, Grep$/,
/U2: Also check the retry path\./,
/A3: Now checking the retry path for U2\./,
])
@@ -25,7 +25,7 @@ test("space activates a focused timeline button instead of scrolling", async ({
seedHistory: true,
})
const scroller = page.locator(".scroll-view__viewport", { has: page.locator("[data-timeline-row]") })
const trigger = page.getByRole("button", { name: "Used 1 Shell", exact: true })
const trigger = page.getByRole("button", { name: "1 used Shell", exact: true })
await expect
.poll(() => scroller.evaluate((element) => element.scrollHeight - element.clientHeight))
.toBeGreaterThan(300)
@@ -93,8 +93,8 @@ test.describe("regression: session timeline local row state", () => {
await expectSessionTitle(page, title)
const group = page.locator('[data-component="collapsed-tool-group"]')
const summary = group.getByRole("button", { name: /^Used \d+ Patch$/ })
await expect(summary).toHaveAccessibleName("Used 1 Patch")
const summary = group.getByRole("button", { name: /^\d+ used Patch$/ })
await expect(summary).toHaveAccessibleName("1 used Patch")
await summary.click()
await group.locator(`[data-timeline-part-id="${editPartID}"]`).evaluate((element) => {
element.setAttribute("data-disclosure-probe", "existing")
@@ -110,8 +110,8 @@ test.describe("regression: session timeline local row state", () => {
if (count === 3) await trigger.click()
const id = `prt_patch_${count}`
events.push(...toolEvents({ ...part, id, callID: id }))
await expect(summary).toHaveAccessibleName(`Used ${count} Patch`)
await expect(summary.locator('[data-slot="basic-tool-tool-title"]')).toHaveText(`${count} Patch`)
await expect(summary).toHaveAccessibleName(`${count} used Patch`)
await expect(summary.locator('[data-slot="basic-tool-tool-title"]')).toHaveText("Patch")
await expect(group).toHaveAttribute("data-timeline-part-ids", new RegExp(`${id}$`))
await expect(trigger).toHaveAttribute("aria-expanded", String(count === 2))
await expect(summary).toHaveAttribute("aria-expanded", "true")
@@ -55,7 +55,7 @@ test.describe("regression: session timeline context group resize", () => {
await devtools.send("Emulation.setCPUThrottlingRate", { rate: 4 })
const context = page.locator(`[data-timeline-part-ids="${contextIDs.join(",")}"]`).first()
await expectAppVisible(context)
await expect(context.getByRole("button")).toHaveAccessibleName("Used 1 Read, 1 Glob, 1 Grep, 1 List")
await expect(context.getByRole("button")).toHaveAccessibleName("4 used Read, Glob, Grep, List")
const contextSelector = `[data-timeline-part-ids="${contextIDs.join(",")}"]`
const regions = defineVisualRegions({
@@ -88,7 +88,7 @@ test.describe("regression: session timeline context group resize", () => {
await page.waitForTimeout(delay)
}
await expect(context.getByRole("button")).toHaveAccessibleName("Used 1 Read, 1 Glob, 1 Grep, 1 List")
await expect(context.getByRole("button")).toHaveAccessibleName("4 used Read, Glob, Grep, List")
await page.waitForTimeout(700)
const trace = await stopVisualProbe<keyof typeof regions>(page)
const labels = trace.samples
@@ -107,7 +107,7 @@ test.describe("regression: session timeline context group resize", () => {
]),
)
expect(labels).toEqual(["Used 1 Read, 1 Glob, 1 Grep, 1 List"])
expect(labels).toEqual(["4 used Read, Glob, Grep, List"])
expect(issues, JSON.stringify(trace.samples, null, 2)).toEqual([])
})
})
@@ -16,8 +16,8 @@ for (const locale of ["de", "ar"] as const) {
})
const group = page.locator(`[data-timeline-part-ids="${ids.join(",")}"]`)
const names = locale === "de" ? "1 Lesen, 1 Glob" : "1 \u0642\u0631\u0627\u0621\u0629, 1 Glob"
await expect(group.getByRole("button")).toHaveAccessibleName(`Used ${names}`)
const names = locale === "de" ? "Lesen, Glob" : "\u0642\u0631\u0627\u0621\u0629, Glob"
await expect(group.getByRole("button")).toHaveAccessibleName(`2 used ${names}`)
await expect(group.locator('[data-slot="basic-tool-tool-title"]')).toHaveText(names)
await expect(page.locator("html")).toHaveAttribute("lang", locale)
})
@@ -386,7 +386,7 @@ test("separates blocking and already-backgrounded work into two rows", async ({
const used = page
.locator('[data-timeline-part-ids="call_backgrounded,call_shell_backgrounded,call_blocking"]')
.locator(':scope > [data-component="collapsible"] > [data-slot="collapsible-trigger"]')
await expect(used).toHaveText(/^Used\s*2 Agent, 1 Shell$/)
await expect(used).toHaveText(/^3 used\s*Agent, Shell$/)
await expect(used).toHaveAttribute("aria-expanded", "false")
await used.click()
await expect(used).toHaveAttribute("aria-expanded", "true")
@@ -46,7 +46,7 @@ test("changes timeline presets and saves custom thinking details", async ({ page
.toEqual({ placement: "grouped", details: "collapsed" })
await settings.getByRole("button", { name: "Back to app", exact: true }).click()
await expect(settings).toBeHidden()
await page.getByRole("button", { name: "Used 1 Thought", exact: true }).click()
await page.getByRole("button", { name: "1 used Thought", exact: true }).click()
await expect(part.getByRole("button")).toHaveAttribute("aria-expanded", "false")
await part.getByRole("button").click()
await expect(part.getByText("The selected mode controls these details.", { exact: true })).toBeVisible()
@@ -45,10 +45,10 @@ test("expands a mixed collapsed tool stack without expanding its individual call
const group = page.locator(
'[data-timeline-part-ids="prt_stack_shell_1,prt_stack_explore,prt_stack_patch,prt_stack_shell_2"]',
)
const summary = group.getByRole("button", { name: "Used 2 Shell, 1 Agent, 1 Patch", exact: true })
const summary = group.getByRole("button", { name: "4 used Shell, Agent, Patch", exact: true })
await expect(summary).toHaveAttribute("aria-expanded", "false")
await expect(summary).toHaveCSS("height", "28px")
await expect(summary.locator('[data-slot="basic-tool-tool-title"]')).toHaveText("2 Shell, 1 Agent, 1 Patch")
await expect(summary.locator('[data-slot="basic-tool-tool-title"]')).toHaveText("Shell, Agent, Patch")
await expect(summary.locator('[data-component="tag"]')).toHaveCount(0)
await summary.click()
await expect(summary).toHaveAttribute("aria-expanded", "true")
@@ -75,8 +75,8 @@ test("leaves tools expanded by settings outside the collapsed stack", async ({ p
await expect(page.locator('[data-timeline-part-id="prt_expanded_shell"]')).toBeVisible()
const group = page.locator('[data-timeline-part-ids="prt_collapsed_patch,prt_collapsed_read"]')
await expect(group.getByRole("button", { name: "Used 1 Patch, 1 Read", exact: true })).toBeVisible()
await expect(group.locator('[data-slot="basic-tool-tool-title"]')).toHaveText("1 Patch, 1 Read")
await expect(group.getByRole("button", { name: "2 used Patch, Read", exact: true })).toBeVisible()
await expect(group.locator('[data-slot="basic-tool-tool-title"]')).toHaveText("Patch, Read")
await expect(page.locator('[data-timeline-spacing="tool"]')).toHaveCSS("padding-top", "8px")
})
@@ -114,7 +114,7 @@ test("combines follow-up patches into one three-file stack inside Used", async (
],
})
const group = page.locator('[data-component="collapsed-tool-group"]')
await group.getByRole("button", { name: "Used 1 Shell, 1 Patch", exact: true }).click()
await group.getByRole("button", { name: "2 used Shell, Patch", exact: true }).click()
await expect(group.getByText("2 files", { exact: true })).toBeVisible()
await timeline.send(
partUpdated(
@@ -129,7 +129,7 @@ test("combines follow-up patches into one three-file stack inside Used", async (
),
),
)
await expect(group.getByRole("button", { name: "Used 1 Shell, 2 Patch", exact: true })).toHaveAttribute(
await expect(group.getByRole("button", { name: "3 used Shell, Patch", exact: true })).toHaveAttribute(
"aria-expanded",
"true",
)
@@ -162,7 +162,7 @@ test("keeps failed search calls and their error cards inside the collapsed stack
await setupTimeline(page, { messages: [userMessage(), assistantMessage(parts)] })
const group = page.locator('[data-component="collapsed-tool-group"]')
const summary = group.getByRole("button", { name: "Used 1 Glob, 1 Grep", exact: true })
const summary = group.getByRole("button", { name: "2 used Glob, Grep", exact: true })
await expect(summary).toHaveAttribute("aria-expanded", "false")
await summary.click()
await expect(group.locator('[data-kind="tool-error-card"]')).toHaveCount(2)
@@ -181,7 +181,7 @@ for (const grouped of [false, true]) {
await expect(working).toHaveCount(0)
return
}
const trigger = group.getByRole("button", { name: "Used 2 Shell", exact: true, includeHidden: true })
const trigger = group.getByRole("button", { name: "2 used Shell", exact: true, includeHidden: true })
await expect(trigger).toHaveAttribute("aria-expanded", "false")
await expect(working).toBeVisible()
await trigger.click()
@@ -51,7 +51,7 @@ test("shows parent lineage while the child timeline loads", async ({ page }) =>
await page.goto(sessionHref(parentID))
await expectSessionTitle(page, parentTitle)
await page.getByRole("button", { name: "Used 1 Agent", exact: true }).click()
await page.getByRole("button", { name: "1 used Agent", exact: true }).click()
await page.locator(`a[href="${sessionHref(childID)}"]`).click()
await Promise.all([requested.promise, expect(page).toHaveURL(sessionHref(childID))])
await Promise.all([
@@ -76,7 +76,7 @@ test("keeps the parent visible while the child session resolves", async ({ page
await page.goto(sessionHref(parentID))
await expectSessionTitle(page, parentTitle)
await page.getByRole("button", { name: "Used 1 Agent", exact: true }).click()
await page.getByRole("button", { name: "1 used Agent", exact: true }).click()
await page.locator(`a[href="${sessionHref(childID)}"]`).click()
await requested.promise
await Promise.all([expect(page).toHaveURL(sessionHref(parentID)), expectSessionTitle(page, parentTitle)]).finally(
@@ -194,7 +194,7 @@ async function setup(page: Page, events?: () => OpenCodeEvent[]) {
async function openChildFromParent(page: Page) {
await page.goto(sessionHref(parentID))
await expectSessionTitle(page, parentTitle)
await page.getByRole("button", { name: "Used 1 Agent", exact: true }).click()
await page.getByRole("button", { name: "1 used Agent", exact: true }).click()
const card = page.locator(`a[href="${sessionHref(childID)}"]`)
await expect(card).toBeVisible()
@@ -99,13 +99,16 @@ test("creates a session in a new project and selects its model", async ({ page }
const modelControl = page.locator('[data-action="composer-model"]')
await expect(modelControl).toContainText("Go Model 1")
await modelControl.click()
await page.locator('[data-option-key="opencode:free-model"]').click()
const modelSearch = page.getByPlaceholder("Search models", { exact: true })
await expect(modelSearch).toBeFocused()
await modelSearch.press("ArrowDown")
await modelSearch.press("Enter")
await expect(modelControl).toContainText("Free Model")
await modelControl.click()
const goModel = page.locator('[data-option-key="opencode-go:go-model-1"]')
await expect(goModel).toBeVisible()
await goModel.click()
await expect(modelSearch).toBeFocused()
await modelSearch.press("ArrowUp")
await modelSearch.press("Enter")
await expect(modelControl).toContainText("Go Model 1")
})
+1
View File
@@ -10,6 +10,7 @@
"./i18n/desktop-native": "./src/runtime/i18n/desktop-native.ts",
"./updater": "./src/shell/updates/types.ts",
"./wsl/types": "./src/servers/wsl/types.ts",
"./ssh": "./src/servers/ssh/types.ts",
"./vite": "./vite.js",
"./index.css": "./src/index.css"
},
+6 -1
View File
@@ -17,6 +17,8 @@ import { ServerConnection, ServersProvider } from "@/runtime/server/registry"
import { SettingsProvider } from "@/settings/model"
import { TabsProvider } from "@/shell/tabs/tabs"
import { WslServersProvider } from "@/servers/wsl/context"
import { SshProvider } from "@/servers/ssh/context"
import { SshRestore } from "@/servers/ssh/restore"
import { ErrorPage } from "@/shell/errors/error"
import { AppRoutes, File, preloadRoute } from "@/shell/routes/routes"
@@ -81,7 +83,9 @@ export function AppBaseProviders(
<QueryProvider>
<WslServersProvider>
<DialogProvider>
<FileComponentProvider component={File}>{props.children}</FileComponentProvider>
<SshProvider>
<FileComponentProvider component={File}>{props.children}</FileComponentProvider>
</SshProvider>
</DialogProvider>
</WslServersProvider>
</QueryProvider>
@@ -109,6 +113,7 @@ export function AppInterface(props: {
<BodyTypography />
<CommandProvider>
<DesktopCommands />
<SshRestore />
<HighlightsProvider>
{props.children}
{rootProps.children}
+2 -2
View File
@@ -841,8 +841,8 @@ export function ComposerEditorSubmitButton(props: {
disabled={!props.stopping && props.disabled}
tabIndex={props.mode === "normal" ? undefined : -1}
icon={<Icon name={props.stopping ? "stop" : props.mode === "shell" ? "arrow-undo-down" : "arrow-up"} />}
variant="contrast"
class="size-7 rounded-md p-[6px] disabled:opacity-50"
variant="submit"
class="size-7 rounded-md p-[6px]"
aria-label={props.stopping ? props.stopLabel : props.sendLabel}
onClick={(event) => {
event.preventDefault()
@@ -37,6 +37,7 @@ export type ComposerEditorView = {
agent?: ComposerSelectControl
variant?: ComposerSelectControl
submit: {
available?: Accessor<boolean>
stopping: Accessor<boolean>
working?: Accessor<boolean>
queue?: ComposerQueue
@@ -333,6 +334,7 @@ export function createComposerEditor(input: {
draft.removeAttachment(id)
},
canSubmit() {
if (input.view.submit.available?.() === false) return false
if (input.view.draftOnly) return false
const persisted = draft.state
if (state.mode === "shell") {
@@ -365,6 +367,7 @@ export function createComposerEditor(input: {
dispatch({ type: "mode.shell" })
},
submit(options?: { alternate?: boolean }) {
if (input.view.submit.available?.() === false) return
if (input.view.draftOnly) return
input.view.submit.onSubmit(options)
dispatch({ type: "popover.close" })
+5 -1
View File
@@ -11,7 +11,7 @@ import { useLanguage } from "@/runtime/i18n/language"
import { useLayout } from "@/shell/state/layout"
import { usePlatform } from "@/runtime/platform/platform"
import { useWorkspaceLocation } from "@/workspaces/location"
import { useData } from "@/runtime/server/current"
import { useData, useServer } from "@/runtime/server/current"
import { createSessionTabs } from "@/session/helpers"
import { showToast } from "@/shell/notifications/toast"
import { formatServerError } from "@/runtime/server/errors"
@@ -30,6 +30,8 @@ export type ComposerModel = ComposerEditorModel & {
export function createComposerModel(adapter: ComposerAdapter, options?: { queue?: ComposerQueue }): ComposerModel {
const sdk = useWorkspaceLocation()
const data = useData()
const server = useServer()
const available = () => server.conn.type !== "ssh" || server.ctx.sdk.connection.status() === "connected"
const files = useFile()
const layout = useLayout()
const comments = useComments()
@@ -394,10 +396,12 @@ export function createComposerModel(adapter: ComposerAdapter, options?: { queue?
keybind: () => command.keybindParts("model.variant.cycle"),
},
submit: {
available,
stopping,
working: adapter.working,
queue: options?.queue,
onSubmit: (submitOptions) => {
if (!available()) return
const queue = options?.queue
// Confirming an edit re-admits the queued prompt instead of sending
// the composer value as a new prompt. Enter keeps it queued in
+1
View File
@@ -20,4 +20,5 @@ export { createDraftStore } from "./runtime/persistence/drafts"
export { createNamespaceStorage, type NamespaceStorage } from "./runtime/persistence/namespace"
export { flushPersisted } from "./runtime/persistence/persist"
export { useWslServers } from "./servers/wsl/context"
export { useSsh } from "./servers/ssh/context"
export { type UpdaterPlatform, type UpdaterState } from "./shell/updates/types"
+26 -9
View File
@@ -15,6 +15,7 @@ import { Persistence } from "@/runtime/persistence/schema"
import type { HomeController } from "../model"
import { useGlobal } from "@/runtime/server/runtime"
import { SessionTransfer } from "@opencode/schema/session-transfer"
import { useSshAuthenticate } from "@/servers/ssh/authenticate"
export const HomeServersSchema = Schema.Struct({
collapsed: Persistence.record(Persistence.fallback(Schema.Boolean, () => false)),
@@ -28,6 +29,7 @@ export function createHomeProjectsController(home: HomeController) {
const openSettings = useSettingsCommand()
const serverManagement = useServerActionsController()
const global = useGlobal()
const authenticate = useSshAuthenticate()
const [_state, setState, _, ready] = persisted(Persist.global("home.servers"), HomeServersSchema, { collapsed: {} })
const [state] = createResource(
() => ready.promise ?? Promise.resolve(),
@@ -42,6 +44,15 @@ export function createHomeProjectsController(home: HomeController) {
return platform.platform === "desktop" && !!platform.openPath && ServerConnection.local(conn)
}
function choose(conn: ServerConnection.Any) {
pickDirectory({
server: conn,
title: language.t("command.project.open"),
multiple: true,
onSelect: (result) => home.project.add(conn, homeProjectDirectories(result)),
})
}
return {
copy: {
language,
@@ -71,15 +82,25 @@ export function createHomeProjectsController(home: HomeController) {
void dialog.show(() => <DialogServer mode="edit" server={conn} />)
})
},
focus: home.selection.focusServer,
authenticate: (conn: ServerConnection.Any) => authenticate(conn),
focus: (conn: ServerConnection.Any) => {
if (authenticate(conn, () => home.selection.focusServer(conn))) return
home.selection.focusServer(conn)
},
},
project: {
list: home.project.list,
recentlyClosed: home.project.recentlyClosed,
homedir: home.project.homedir,
select: home.project.select,
select: (conn: ServerConnection.Any, directory: string) => {
if (authenticate(conn, () => home.project.select(conn, directory))) return
home.project.select(conn, directory)
},
add: home.project.add,
openNewSession: home.project.openProjectNewSession,
openNewSession: (conn: ServerConnection.Any, directory: string) => {
if (authenticate(conn, () => home.project.openProjectNewSession(conn, directory))) return
home.project.openProjectNewSession(conn, directory)
},
canImportSession: !!platform.openAttachmentPickerDialog,
importSession: (conn: ServerConnection.Any, project: LocalProject) => {
if (!platform.openAttachmentPickerDialog) return
@@ -125,13 +146,9 @@ export function createHomeProjectsController(home: HomeController) {
.forEach((directory) => notification.project.markViewed(directory))
},
choose: (conn: ServerConnection.Any) => {
if (authenticate(conn, () => choose(conn))) return
if (home.server.health(conn)?.healthy === false) return
pickDirectory({
server: conn,
title: language.t("command.project.open"),
multiple: true,
onSelect: (result) => home.project.add(conn, homeProjectDirectories(result)),
})
choose(conn)
},
close: (conn: ServerConnection.Any, directory: string) => {
const next = closeHomeProject(
@@ -26,6 +26,7 @@ export function HomeProjects(props: {
onWheel={props.scroll.viewport.containWheel}
onChooseProject={props.projects.project.choose}
onFocusServer={props.projects.server.focus}
onAuthenticateServer={props.projects.server.authenticate}
onToggleCollapsed={props.projects.server.toggleCollapsed}
onEditServer={props.projects.server.edit}
onSetDefaultServer={props.projects.server.setDefault}
+59
View File
@@ -0,0 +1,59 @@
/* Home server/project rows keep the label underneath the hover actions.
The actions carry a tab-style background with a fade on the left, and the
label fades out where it slides underneath. Mirrors tab-nav.css. */
[data-home-row] {
--home-row-surface: var(--v2-background-bg-base);
--home-row-background: color-mix(
in srgb,
var(--home-row-surface) var(--home-row-opacity, 100%),
var(--v2-background-bg-base)
);
background: var(--home-row-background);
}
[data-home-row]:is(:hover, :focus-within, [data-dragging="true"], :has([data-menu="true"])) {
--home-row-surface: var(--v2-background-bg-layer-01);
}
[data-home-row][data-selected] {
--home-row-surface: var(--v2-background-bg-layer-03);
}
[data-home-row][data-dimmed="true"] {
--home-row-opacity: 60%;
}
/* Keep the background outside the button's disabled-content opacity. */
[data-home-row] > button {
background: transparent;
}
[data-home-row] [data-slot="home-row-actions"] {
background: linear-gradient(to right, transparent, var(--home-row-background) 8px);
}
[data-home-row]:dir(rtl) [data-slot="home-row-actions"] {
background: linear-gradient(to left, transparent, var(--home-row-background) 8px);
}
[data-home-row]:is(:hover, :focus-within, :has([data-menu="true"])) [data-slot="home-row-label"] {
-webkit-mask-image: linear-gradient(to right, black 0, black calc(100% - 44px), transparent calc(100% - 36px));
mask-image: linear-gradient(to right, black 0, black calc(100% - 44px), transparent calc(100% - 36px));
}
[data-home-row]:is(:hover, :focus-within, :has([data-menu="true"])):dir(rtl) [data-slot="home-row-label"] {
-webkit-mask-image: linear-gradient(to left, black 0, black calc(100% - 44px), transparent calc(100% - 36px));
mask-image: linear-gradient(to left, black 0, black calc(100% - 44px), transparent calc(100% - 36px));
}
@media (hover: none) {
[data-home-row] [data-slot="home-row-label"] {
-webkit-mask-image: linear-gradient(to right, black 0, black calc(100% - 44px), transparent calc(100% - 36px));
mask-image: linear-gradient(to right, black 0, black calc(100% - 44px), transparent calc(100% - 36px));
}
[data-home-row]:dir(rtl) [data-slot="home-row-label"] {
-webkit-mask-image: linear-gradient(to left, black 0, black calc(100% - 44px), transparent calc(100% - 36px));
mask-image: linear-gradient(to left, black 0, black calc(100% - 44px), transparent calc(100% - 36px));
}
}
+74 -16
View File
@@ -10,6 +10,8 @@ import { ScrollView } from "@opencode/ui/scroll-view"
import { ProjectAvatar } from "@opencode/ui/project-avatar"
import { Icon } from "@opencode/ui/icon"
import { IconButton } from "@opencode/ui/icon-button"
import { Button } from "@opencode/ui/button"
import { Spinner } from "@opencode/ui/spinner"
import { Menu } from "@opencode/ui/menu"
import { Tooltip } from "@opencode/ui/tooltip"
import { getProjectAvatarVariant, type HomeProjectSelection, type LocalProject } from "@/shell/state/layout"
@@ -21,6 +23,7 @@ import { ServerRowMenuView, serverMenuLabels } from "@/servers/registry/row-menu
import { ServerHealthIndicator } from "@/servers/registry/row"
import { type ServerHealth } from "@/runtime/server/health"
import { fileManagerApp } from "@/home/projects/file-manager"
import "./view.css"
const HOME_PROJECT_NAV_LABEL = "min-w-0 flex-1 overflow-hidden text-ellipsis whitespace-nowrap"
@@ -46,6 +49,7 @@ export type HomeProjectsViewProps = {
onWheel: (event: WheelEvent) => void
onChooseProject: (server: ServerConnection.Any) => void
onFocusServer: (server: ServerConnection.Any) => void
onAuthenticateServer?: (server: ServerConnection.Any) => void
onToggleCollapsed: (server: ServerConnection.Any) => void
onEditServer: (server: ServerConnection.Http) => void
onSetDefaultServer: (server: ServerConnection.Any | undefined) => void
@@ -122,6 +126,10 @@ export function HomeProjectsView(props: HomeProjectsViewProps) {
props.onFocusServer(server)
setState("open", false)
}}
onAuthenticateServer={(server) => {
setState("open", false)
props.onAuthenticateServer?.(server)
}}
onChooseProject={(server) => {
setState("open", false)
props.onChooseProject(server)
@@ -196,7 +204,12 @@ function HomeProjectsPanel(props: HomeProjectsViewProps) {
</HomeProjectNavButton>
</Show>
<Show
when={props.servers.length > 1}
when={
props.servers.length > 1 ||
props.servers.some(
(server) => server.type === "ssh" && (server.authenticationRequired || server.connecting),
)
}
fallback={
<Show when={props.servers[0]}>
{(server) => (
@@ -231,6 +244,8 @@ function HomeProjectsPanel(props: HomeProjectsViewProps) {
const healthy = () => !!props.serverHealth(item)?.healthy
const hasProjects = () => projects().length > 0
const collapsed = () => props.collapsed(item)
const authentication = () => item.type === "ssh" && item.authenticationRequired
const connecting = () => item.type === "ssh" && item.connecting
return (
<div class="flex min-w-0 flex-col gap-1">
<HomeServerRow
@@ -241,7 +256,26 @@ function HomeProjectsPanel(props: HomeProjectsViewProps) {
collapsed={collapsed()}
health={props.serverHealth(item)}
/>
<Show when={healthy() && hasProjects() && !collapsed()}>
<Show when={authentication() || connecting()}>
<div class="mx-3 h-px bg-v2-border-border-base" />
<div class="px-1.5 py-1">
<Button
data-action="home-server-authenticate"
class="w-full"
size="small"
variant="neutral"
disabled={connecting()}
aria-busy={!!connecting()}
onClick={() => props.onAuthenticateServer?.(item)}
>
<Show when={connecting()}>
<Spinner class="size-3.5" />
</Show>
{props.language.t(connecting() ? "ssh.stage.connecting" : "ssh.action.authenticate")}
</Button>
</div>
</Show>
<Show when={healthy() && !authentication() && !connecting() && hasProjects() && !collapsed()}>
<div class="mx-3 h-px bg-v2-border-border-base" />
<HomeProjectList {...props} {...contextMenuProps} server={item} items={projects()} />
</Show>
@@ -314,6 +348,7 @@ function HomeServerRow(props: {
health: ServerHealth | undefined
}) {
const healthy = () => !!props.health?.healthy
const authentication = () => props.server.type === "ssh" && props.server.authenticationRequired
const incompatible = () => !!props.health?.incompatible
const canToggle = () => healthy() && props.projectsForServer(props.server).length > 0
const contextMenuID = () => serverContextMenuID(props.server)
@@ -326,16 +361,23 @@ function HomeServerRow(props: {
appearance="standard"
placement="top"
class="flex h-7 w-full min-w-0"
inactive={!incompatible()}
value={props.language.t("server.row.incompatible", { version: props.health?.version ?? "1" })}
inactive={!incompatible() && !authentication()}
value={
authentication()
? props.language.t("ssh.stage.authentication")
: props.language.t("server.row.incompatible", { version: props.health?.version ?? "1" })
}
>
<div class="group/server relative flex h-7 w-full min-w-0 items-center rounded-[6px]">
<div
class="group/server relative flex h-7 w-full min-w-0 items-center rounded-[6px]"
data-home-row
data-dimmed={!healthy() && !incompatible()}
data-selected={props.selected ? "" : undefined}
>
<HomeProjectNavButton
type="button"
class="pr-16"
classList={{ "opacity-60": !healthy() && !incompatible() }}
data-selected={props.selected ? "" : undefined}
disabled={!healthy()}
disabled={!healthy() && !authentication()}
onClick={() => props.onFocusServer(props.server)}
>
<span
@@ -369,10 +411,18 @@ function HomeServerRow(props: {
/>
</span>
<div class="flex size-4 shrink-0 items-center justify-center -mr-0.5">
<ServerHealthIndicator health={props.health} />
<ServerHealthIndicator
health={props.health}
connecting={props.server.type === "ssh" && props.server.connecting}
authenticationRequired={authentication()}
/>
</div>
<span class="flex min-w-0 items-center gap-1">
<span class={HOME_PROJECT_NAV_LABEL}>
<span
data-slot="home-row-label"
class="flex min-w-0 flex-1 items-center gap-1"
classList={{ "opacity-60": !healthy() && !incompatible() }}
>
<span class="min-w-0 overflow-hidden text-ellipsis whitespace-nowrap">
{props.server.displayName ?? new URL(props.server.http.url).host}
</span>
<Show when={props.server.label}>
@@ -390,8 +440,9 @@ function HomeServerRow(props: {
</span>
</HomeProjectNavButton>
<div
data-slot="home-row-actions"
class={`
hover-reveal absolute right-1 top-1/2 flex -translate-y-1/2 items-center gap-1
hover-reveal absolute bottom-0 right-1 top-0 flex items-center gap-1 rounded-r-[6px] pl-2
group-hover/server:opacity-100 focus-within:opacity-100 data-[menu=true]:opacity-100
`}
data-menu={props.contextMenuOpen(contextMenuID())}
@@ -418,7 +469,7 @@ function HomeServerRow(props: {
size="small"
icon={<Icon name="folder-add-left" />}
aria-label={props.language.t("home.project.add")}
disabled={props.health?.healthy === false}
disabled={props.health?.healthy === false && !authentication()}
onClick={() => props.onChooseProject(props.server)}
/>
</Tooltip>
@@ -602,6 +653,10 @@ function HomeProjectRow(
ref={sortable.ref}
class="group/project relative flex h-7 min-w-0 items-center rounded-[6px]"
classList={{ "z-10": sortable.isDragSource() }}
data-home-row
data-dimmed={serverUnreachable()}
data-dragging={sortable.isDragSource()}
data-selected={props.selected ? "" : undefined}
onContextMenu={(event) => {
event.preventDefault()
props.onSetContextMenuOpen(contextMenuID(), true)
@@ -610,7 +665,7 @@ function HomeProjectRow(
<HomeProjectNavButton
type="button"
data-component="home-project-row"
class="pr-16 disabled:opacity-60"
class="disabled:opacity-60"
classList={{
"bg-v2-background-bg-layer-01 text-v2-text-text-base": sortable.isDragSource(),
}}
@@ -647,11 +702,14 @@ function HomeProjectRow(
}}
>
<HomeProjectAvatar project={props.project} />
<span class={HOME_PROJECT_NAV_LABEL}>{displayName(props.project)}</span>
<span data-slot="home-row-label" class={HOME_PROJECT_NAV_LABEL}>
{displayName(props.project)}
</span>
</HomeProjectNavButton>
<div
data-slot="home-row-actions"
class={`
hover-reveal absolute right-1 top-1/2 flex -translate-y-1/2 items-center gap-1
hover-reveal absolute bottom-0 right-1 top-0 flex items-center gap-1 rounded-r-[6px] pl-2
group-hover/project:opacity-100 focus-within:opacity-100 data-[menu=true]:opacity-100
`}
data-menu={props.contextMenuOpen(contextMenuID())}
+63 -9
View File
@@ -38,12 +38,65 @@
}
@layer components {
[data-slot="session-chat-panel"][data-scrollbar-hidden="true"]
[data-slot="session-timeline-scroll"]
> .scroll-view__thumb {
visibility: hidden;
}
[data-slot="session-side-panel-presence"][data-opened="true"] {
animation: terminal-panel-presence-in 240ms cubic-bezier(0.22, 1, 0.36, 1);
animation: side-region-presence-in 240ms cubic-bezier(0.22, 1, 0.36, 1);
}
[data-slot="session-side-panel-presence"][data-opened="false"] {
animation: terminal-panel-presence-out 240ms cubic-bezier(0.22, 1, 0.36, 1) forwards;
animation: side-region-presence-out 240ms cubic-bezier(0.22, 1, 0.36, 1) forwards;
}
#review-panel {
container-type: inline-size;
}
/* Like the composer toolbar, fade only clipped content without reserving layout space.
The fade contracts as overflow clears; the second mask preserves the header divider. */
#review-panel .session-review-v2-tabs-bar {
--session-review-header-fade: clamp(0px, calc(100% - 100cqi), 24px);
mask-image:
linear-gradient(
to right,
#000 calc(100cqi - 40px - var(--session-review-header-fade)),
transparent calc(100cqi - 40px)
),
linear-gradient(to top, #000 1px, transparent 1px);
&:dir(rtl) {
mask-image:
linear-gradient(
to left,
#000 calc(100cqi - 40px - var(--session-review-header-fade)),
transparent calc(100cqi - 40px)
),
linear-gradient(to top, #000 1px, transparent 1px);
}
}
/* The panel's width animation supplies the slide; only fade its fixed-width contents. */
[data-slot="session-side-region-presence"][data-opened] [data-slot="session-review-content"] {
transition: opacity 200ms ease-out 40ms;
}
/* Cached contents must stay transparent even after the presence animation finishes. */
#review-panel[aria-hidden="true"] > [data-slot="session-review-content"] {
opacity: 0;
}
[data-slot="session-side-region-presence"][data-opened="false"] [data-slot="session-review-content"] {
transition: opacity 160ms ease-out;
}
@starting-style {
[data-slot="session-side-region-presence"][data-opened="true"] [data-slot="session-review-content"] {
opacity: 0;
}
}
[data-slot="session-side-region-presence"][data-opened="true"] {
@@ -82,10 +135,15 @@
[data-slot="terminal-panel-presence"],
[data-slot="side-terminal-panel-presence"],
[data-slot="session-side-panel-presence"],
[data-slot="session-review-content"],
[data-slot="session-side-region-presence"],
[data-component="terminal-panel"] {
animation: none !important;
}
[data-slot="session-review-content"] {
transition: none !important;
}
}
@keyframes terminal-panel-presence-in {
@@ -125,13 +183,9 @@
}
}
/* Presence needs an animation lifetime, but the panel frame must never fade. */
@keyframes side-region-presence-in {
from {
opacity: 0;
}
0.01% {
opacity: 0.999999;
}
from,
to {
opacity: 1;
}
@@ -142,7 +196,7 @@
opacity: 1;
}
to {
opacity: 0.999999;
opacity: 1;
visibility: hidden;
}
}
@@ -89,7 +89,7 @@ export function PromptWorkspaceSelector(props: {
contentClass={props.onboarding ? "max-w-[280px]" : undefined}
class="min-w-0"
>
<Menu placement="bottom" gutter={4} onOpenChange={onOpenChange}>
<Menu placement="bottom" gutter={4} overflowPadding={24} onOpenChange={onOpenChange}>
<Menu.Trigger
aria-description={language.t("session.new.workspace.trigger.tooltip")}
class="flex h-6 min-w-0 max-w-[203px] items-center gap-1.5 rounded-sm px-1.5 hover:bg-v2-overlay-simple-overlay-hover focus-visible:bg-v2-overlay-simple-overlay-hover focus-visible:outline-none data-[expanded]:bg-v2-overlay-simple-overlay-pressed data-[expanded]:text-v2-text-text-muted"
@@ -148,7 +148,7 @@ export function PromptWorkspaceSelector(props: {
<Menu.Sub
gutter={0}
overlap
overflowPadding={8}
overflowPadding={24}
onOpenChange={(open) => {
if (!open) {
focusSearch = false
@@ -177,7 +177,7 @@ export function PromptWorkspaceSelector(props: {
</span>
</Menu.SubTrigger>
<Menu.Portal>
<Menu.SubContent class="max-h-[224px] w-[200px] overflow-y-auto">
<Menu.SubContent class="max-h-[66.667dvh] w-[200px] overflow-y-auto !pb-0 [&>[data-component=menu-v2-item]:last-child]:mb-0.5 [@media(max-height:600px)]:max-h-[calc(100dvh-48px)]">
<Show when={props.workspaces.length >= 10}>
<div class="flex h-7 items-center gap-2 rounded-sm ps-3 pe-2 text-v2-icon-icon-muted">
<Icon name="magnifying-glass" size="small" class="shrink-0" />
@@ -314,7 +314,7 @@ function ModelSelectorPopoverView(props: {
const models = createMemo(() => props.models(store.search))
const groups = createMemo(() => props.groups(models()))
const keys = () => [...models().map(modelKey), manageKey]
const keys = () => [...groups().flatMap((group) => group.items.map(modelKey)), manageKey]
const initialActive = () => {
const selected = props.current
const options = keys()
+55 -3
View File
@@ -2,6 +2,54 @@ import { DESKTOP_NATIVE_ENGLISH } from "./desktop-native"
export const dict = {
...DESKTOP_NATIVE_ENGLISH,
"ssh.label": "SSH",
"ssh.offline": "Not connected to {{host}}. Your draft is preserved; remote work may still be running.",
"ssh.placeholder": "ssh user@example.com",
"ssh.add": "Add SSH server",
"ssh.server.menu.label": "SSH server",
"ssh.target": "Host or SSH command",
"ssh.connect": "Connect",
"ssh.connectTo": "Connect to {{host}}",
"ssh.authenticate": "SSH authentication",
"ssh.action.authenticate": "Authenticate",
"ssh.session.disconnected": "SSH connection inactive",
"ssh.session.connecting": "Connecting to SSH server",
"ssh.session.reconnectDescription":
"Reconnect to view this session and continue working. Your remote session is preserved.",
"ssh.session.reconnect": "Reconnect",
"ssh.authenticationRequired": "Authentication required for {{host}}",
"ssh.trust": "Trust and connect",
"ssh.continue": "Continue",
"ssh.retry": "Retry",
"ssh.update": "Update and reconnect",
"ssh.openProject": "Open project",
"ssh.project": "Open project on {{host}}",
"ssh.disconnect": "Disconnect",
"ssh.forget": "Forget connection",
"ssh.stage.disconnected": "Disconnected. The remote server is left running.",
"ssh.stage.connecting": "Connecting over SSH…",
"ssh.stage.checking": "Checking OpenCode…",
"ssh.stage.downloading": "Downloading server…",
"ssh.stage.uploading": "Uploading server…",
"ssh.stage.starting": "Connecting to OpenCode…",
"ssh.stage.ready": "Connected",
"ssh.stage.authentication": "Authentication required",
"ssh.stage.incompatible": "Server update required",
"ssh.stage.failed": "Connection failed",
"ssh.error.input":
"Enter a host or SSH connection command. Remote commands and unsupported SSH options arent allowed.",
"ssh.error.connection": "Could not establish the SSH connection. Check your network and SSH configuration.",
"ssh.error.platform":
"This remote platform is not supported. Automatic setup currently requires Linux or macOS on x64 or arm64.",
"ssh.error.version": "The remote service must match this Desktop version before connecting.",
"ssh.error.install":
"Could not install the remote server. Check connectivity, disk space, and that tar is installed.",
"ssh.error.unpublished":
"This Desktop version has no published remote server. For development builds, install and start V2 on the host, then retry.",
"ssh.error.service": "SSH connected, but the OpenCode server did not become ready.",
"ssh.error.host-key":
"The hosts identity could not be verified. Verify its fingerprint before updating your SSH known hosts.",
"ssh.error.ssh-missing": "OpenSSH was not found. Install an OpenSSH client and ensure ssh is available on PATH.",
"session.location.unavailable": "Session location unavailable",
"session.location.description": "Choose another directory to continue this session.",
"session.location.choose": "Choose directory",
@@ -58,6 +106,8 @@ export const dict = {
"command.session.new": "New session",
"command.file.open": "Open file",
"command.browser.open": "Open browser",
"command.browser.reload": "Reload browser page",
"command.tab.close": "Close tab",
"command.tab.reopenClosed": "Reopen closed tab",
"command.context.addSelection": "Add selection to context",
@@ -719,7 +769,8 @@ export const dict = {
"session.queue.steerTooltip": "Send without interrupting",
"session.queue.remove": "Remove",
"session.queue.reorder": "Reorder queued prompt",
"session.queue.attachments": "+ attachments",
"session.queue.attachments.one": "Plus {{count}} attachment",
"session.queue.attachments.other": "Plus {{count}} attachments",
"session.timeline.working": "Working",
"session.timeline.notice.finished": "{{actor}} finished",
"session.timeline.notice.failed": "{{actor}} failed",
@@ -846,7 +897,8 @@ export const dict = {
"session.browser.address": "Browser address",
"session.browser.replaced": "Browser control moved to another desktop window.",
"session.browser.address.placeholder": "Enter a URL",
"session.browser.suspended": "Browser suspended. Interact with this session to reconnect.",
"session.browser.address.placeholder": "Enter URL",
"titlebar.update": "Update",
"titlebar.tabs": "Tabs",
@@ -1074,7 +1126,7 @@ export const dict = {
"settings.general.row.uiFont.description": "Customise the font used throughout the interface",
"settings.general.row.showFileTree.title": "File tree",
"settings.general.row.showFileTree.description": "Show the file tree panel in sessions",
"settings.general.row.browserPane.title": "Browser pane",
"settings.general.row.browserPane.title": "Browser",
"settings.general.row.browserPane.description": "Allow agents to open and control an in-app development browser.",
"settings.general.row.showNavigation.title": "Navigation controls",
"settings.general.row.showNavigation.description": "Show the back and forward buttons in the desktop title bar",
@@ -1,7 +1,12 @@
import type { Browser } from "@opencode/plugin-browser/rpc"
export type BrowserPaneEndpoint = Readonly<{ url: string; username?: string; password?: string }>
export type BrowserPaneTarget = Readonly<{ sessionID: string; endpoint: BrowserPaneEndpoint }>
export type BrowserPaneTarget = Readonly<{
serverKey: string
sessionID: string
endpoint: BrowserPaneEndpoint
restore?: Browser.State
}>
export type BrowserPaneLayout = {
tabID: Browser.TabID
visible: boolean
@@ -4,6 +4,7 @@ import type { Accessor } from "solid-js"
import type { DesktopMenuAction } from "@/shell/commands/desktop-menu"
import { ServerConnection } from "@/runtime/server/registry"
import type { WslServersPlatform } from "@/servers/wsl/types"
import type { SshPlatform } from "@/servers/ssh/types"
import type { UpdaterPlatform } from "@/shell/updates/types"
import type { DraftStore } from "@/runtime/persistence/drafts"
import type { BrowserPanePlatform } from "./browser-pane"
@@ -86,6 +87,7 @@ type PlatformBase = {
/** Manage WSL sidecar servers (Electron on Windows only) */
wslServers?: WslServersPlatform
sshServers?: SshPlatform
/** Webview zoom level (desktop only) */
webviewZoom?: Accessor<number>
+12 -2
View File
@@ -1,7 +1,7 @@
import type { OpenCodeEvent } from "@opencode/client/promise"
import { createClientConnection, createPtyClient, type ClientConnectionStatus } from "@opencode/client/solid"
import { createGlobalEmitter } from "@solid-primitives/event-bus"
import { type Accessor, onCleanup } from "solid-js"
import { type Accessor, createEffect, on, onCleanup } from "solid-js"
import { createApiForServer, type ServerApi } from "@/runtime/server/api"
import { usePlatform } from "@/runtime/platform/platform"
import { ServerConnection } from "./registry"
@@ -74,8 +74,18 @@ type ServerSDKBase = {
function createServerSdkContextBase(server: ServerConnection.Any, scope: ServerScope): ServerSDKBase {
const platform = usePlatform()
const transport = createServerTransport({ http: server.http, fetch: platform.fetch })
if (server.type === "ssh") {
createEffect(
on(
() => `${server.http.url}\0${server.http.password ?? ""}`,
() => transport.update(server.http),
{ defer: true },
),
)
}
const events = createOpenCodeEventSource()
const reconnect = server.type === "sidecar" && server.variant === "base" ? server.reconnect : undefined
const reconnect =
server.type === "ssh" || (server.type === "sidecar" && server.variant === "base") ? server.reconnect : undefined
const connection = createClientConnection(transport.api, {
reconnect: reconnect ? async (signal) => transport.update(await reconnect(signal)) : undefined,
+56 -8
View File
@@ -5,7 +5,7 @@ import { ClientError, OpenCode } from "@opencode/client"
import { Accessor, createEffect, onCleanup } from "solid-js"
import { createStore, reconcile } from "solid-js/store"
export type ServerHealth = { healthy: boolean; version?: string; incompatible?: boolean }
export type ServerHealth = { healthy: boolean; version?: string; incompatible?: boolean; checking?: boolean }
interface CheckServerHealthOptions {
timeoutMs?: number
@@ -142,25 +142,73 @@ export function useCheckServerHealth() {
}
export const useServerHealth = (servers: Accessor<ServerConnection.Any[]>, enabled: Accessor<boolean>) => {
const checkServerHealth = useCheckServerHealth()
return createServerHealth(servers, enabled, useCheckServerHealth())
}
export function createServerHealth(
servers: Accessor<ServerConnection.Any[]>,
enabled: Accessor<boolean>,
check: (http: ServerConnection.HttpBase) => Promise<ServerHealth>,
) {
const [status, setStatus] = createStore({} as Record<ServerConnection.Key, ServerHealth | undefined>)
const endpoints = new Map<ServerConnection.Key, string>()
createEffect(() => {
if (!enabled()) {
endpoints.clear()
setStatus(reconcile({}))
return
}
const list = servers()
// Snapshot transport fields synchronously so a newly established SSH tunnel
// invalidates both the old result and any probe still using the old endpoint.
const list = servers().map((conn) => ({
key: ServerConnection.key(conn),
type: conn.type,
http: conn.http,
stage: conn.type === "ssh" ? conn.stage : undefined,
}))
for (const conn of list) {
if (conn.stage && conn.stage !== "ready") {
endpoints.delete(conn.key)
setStatus(
conn.key,
reconcile(
conn.stage === "failed"
? { healthy: false }
: conn.stage === "incompatible"
? { healthy: false, incompatible: true }
: undefined,
),
)
continue
}
const endpoint = cacheKey(conn.http)
if (conn.type === "ssh" && endpoints.get(conn.key) !== endpoint) {
setStatus(conn.key, reconcile({ healthy: false, checking: true }))
}
endpoints.set(conn.key, endpoint)
}
for (const key of endpoints.keys()) {
if (!list.some((conn) => conn.key === key)) endpoints.delete(key)
}
let dead = false
const refresh = async () => {
const results: Record<string, ServerHealth> = {}
const results: Record<string, ServerHealth | undefined> = {}
await Promise.all(
list.map(async (conn) => {
const key = ServerConnection.key(conn)
const result = await checkServerHealth(conn.http)
results[key] = result
if (!dead) setStatus(key, result)
if (conn.stage && conn.stage !== "ready") {
results[conn.key] =
conn.stage === "failed"
? { healthy: false }
: conn.stage === "incompatible"
? { healthy: false, incompatible: true }
: undefined
return
}
const result = await check(conn.http)
results[conn.key] = result
if (!dead) setStatus(conn.key, reconcile(result))
}),
)
if (dead) return
+8 -1
View File
@@ -5,6 +5,7 @@ import { Persist, persisted } from "@/runtime/persistence/storage"
import { pathKey } from "@/workspaces/path-key"
import { ServerScope } from "@/runtime/server/scope"
import { ServerHttp, ServerHttpBase, ServerKey, serverState } from "./persistence"
import type { SshItem } from "@/servers/ssh/types"
type ServerState = ReturnType<typeof serverState>["current"]["Type"]
// The store retains more history than is displayed. Consumers filter recently closed entries
@@ -24,6 +25,7 @@ export function normalizeServerUrl(input: string) {
export function serverName(conn?: ServerConnection.Any, ignoreDisplayName = false) {
if (!conn) return ""
if (conn.displayName && !ignoreDisplayName) return conn.displayName
if (conn.type === "ssh") return conn.host
return conn.http.url.replace(/^https?:\/\//, "").replace(/\/+$/, "")
}
@@ -159,9 +161,14 @@ export namespace ServerConnection {
// Remote server desktop can SSH into
export type Ssh = {
type: "ssh"
stage?: SshItem["stage"]
connecting?: boolean
authenticationRequired?: boolean
id?: string
host: string
// SSH client exposes an HTTP server for the app to use as a proxy
http: HttpBase
reconnect?: (signal: AbortSignal) => Promise<HttpBase>
} & Base
export type Any =
@@ -178,7 +185,7 @@ export namespace ServerConnection {
return Key.make("sidecar")
}
case "ssh":
return Key.make(`ssh:${conn.host}`)
return Key.make(`ssh:${conn.id ?? conn.host}`)
}
}
@@ -8,6 +8,7 @@ import { useSettings } from "@/settings/model"
import { useTabs } from "@/shell/tabs/tabs"
import { type ServerHealth } from "@/runtime/server/health"
import { showToast } from "@/shell/notifications/toast"
import { useSsh } from "../ssh/context"
function showRequestError(language: ReturnType<typeof useLanguage>, err: unknown) {
showToast({
@@ -50,6 +51,7 @@ function useDefaultServer() {
export function useServerActionsController() {
const server = useServers()
const ssh = useSsh()
const tabs = useTabs()
const platform = usePlatform()
const language = useLanguage()
@@ -58,6 +60,7 @@ export function useServerActionsController() {
const remove = async (key: ServerConnection.Key) => {
try {
if (key.startsWith("wsl:")) await platform.wslServers?.removeServer(key)
if (key.startsWith("ssh:")) await ssh.forget(key.slice(4))
tabs.removeServer(key)
server.remove(key)
if ((await platform.getDefaultServer?.()) === key) await defaults.set(null)
@@ -5,6 +5,7 @@ import { type Component, Show } from "solid-js"
import type { ServerActionsController } from "@/servers/registry/controller"
import { useLanguage } from "@/runtime/i18n/language"
import { ServerConnection } from "@/runtime/server/registry"
import { SshMenu } from "../ssh/menu"
export const ServerRowMenu: Component<{
server: ServerConnection.Any
@@ -15,6 +16,7 @@ export const ServerRowMenu: Component<{
}> = (props) => {
const language = useLanguage()
const key = ServerConnection.key(props.server)
if (props.server.type === "ssh" && props.server.id) return <SshMenu id={props.server.id} domain={props.domain} />
return (
<ServerRowMenuView
server={props.server}
@@ -0,0 +1,44 @@
import { For } from "solid-js"
import { ServerHealthIndicator } from "./row"
import type { ServerHealth } from "@/runtime/server/health"
const states: { label: string; connecting?: boolean; authenticationRequired?: boolean; health?: ServerHealth }[] = [
{
label: "Authentication required (overrides failed health)",
authenticationRequired: true,
health: { healthy: false },
},
{
label: "Authentication required (overrides pending health)",
authenticationRequired: true,
health: { healthy: false, checking: true },
},
{ label: "Connecting (previous health check failed)", connecting: true, health: { healthy: false } },
{ label: "Tunnel ready, checking its new endpoint", health: { healthy: false, checking: true } },
{ label: "Connected", health: { healthy: true } },
{ label: "Failed", health: { healthy: false } },
{ label: "Incompatible", health: { healthy: false, incompatible: true } },
{ label: "Not checked" },
]
export default { title: "App/Servers/Health indicator", id: "app-server-health" }
export const States = {
render: () => (
<div class="flex flex-col gap-4">
<For each={states}>
{(state) => (
<div class="flex items-center gap-2">
<div class="flex size-4 shrink-0 items-center justify-center">
<ServerHealthIndicator
health={state.health}
connecting={state.connecting}
authenticationRequired={state.authenticationRequired}
/>
</div>
<span>{state.label}</span>
</div>
)}
</For>
</div>
),
}
+44 -11
View File
@@ -1,5 +1,7 @@
import { Tooltip } from "@opencode/ui/tooltip"
import { Icon } from "@opencode/ui/icon"
import { Spinner } from "@opencode/ui/spinner"
import { useLanguage } from "@/runtime/i18n/language"
import { createResizeObserver } from "@solid-primitives/resize-observer"
import {
children,
@@ -102,22 +104,53 @@ export function ServerRow(props: ServerRowProps) {
)
}
export function ServerHealthIndicator(props: { health?: ServerHealth }) {
export function ServerHealthIndicator(props: {
health?: ServerHealth
connecting?: boolean
authenticationRequired?: boolean
}) {
const language = useLanguage()
return (
<Show
when={props.health?.incompatible}
when={props.authenticationRequired}
fallback={
<div
classList={{
"size-1.5 rounded-full shrink-0 my-[3.5px]": true,
"bg-icon-success-base": props.health?.healthy === true,
"bg-icon-critical-base": props.health?.healthy === false,
"bg-border-weak-base": props.health === undefined,
}}
/>
<Show
when={props.connecting || props.health?.checking}
fallback={
<Show
when={props.health?.incompatible}
fallback={
<div
classList={{
"size-1.5 rounded-full shrink-0 my-[3.5px]": true,
"bg-icon-success-base": props.health?.healthy === true,
"bg-icon-critical-base": props.health?.healthy === false,
"bg-border-weak-base": props.health === undefined,
}}
/>
}
>
<Icon name="warning" size="small" class="shrink-0 text-icon-warning-base" />
</Show>
}
>
<span
role="status"
aria-label={language.t("ssh.stage.connecting")}
class="inline-flex h-3.5 w-1.5 shrink-0 items-center justify-center text-v2-icon-icon-muted"
>
<Spinner class="size-3 shrink-0" />
</span>
</Show>
}
>
<Icon name="warning" size="small" class="shrink-0 text-icon-warning-base" />
<span
role="status"
aria-label={language.t("ssh.stage.authentication")}
class="inline-flex h-3.5 w-1.5 shrink-0 items-center justify-center text-v2-icon-icon-muted"
>
<Icon name="lock" size="small" class="shrink-0" />
</span>
</Show>
)
}

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