Compare commits

..
Author SHA1 Message Date
Kit Langton 9f06708d0e fix(plugin): load Effect from the host for plugins 2026-10-04 23:14:49 -07:00
118 changed files with 1665 additions and 2990 deletions

No files matched your search

-2
View File
@@ -5,8 +5,6 @@ on:
branches: [dev, v2]
pull_request:
branches: [dev, v2]
paths-ignore:
- services/www/src/docs/**
workflow_dispatch:
concurrency:
+9 -31
View File
@@ -27,7 +27,6 @@ jobs:
runs-on: blacksmith-4vcpu-ubuntu-2404
outputs:
app: ${{ steps.packages.outputs.app }}
docs: ${{ steps.docs.outputs.only }}
steps:
- name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
@@ -53,17 +52,6 @@ jobs:
bun x turbo@2.10.2 ls --affected --filter=@opencode/app --output=json > affected.json
bun -e 'const result = await Bun.file("affected.json").json(); console.log(`app=${result.packages.count > 0}`)' >> "$GITHUB_OUTPUT"
# A pull request that changes only documentation site files cannot affect what the unit job tests or builds; the
# typecheck job and the www workflow check those files.
- name: Find documentation-only pull request
id: docs
if: github.event_name == 'pull_request'
run: |
files=$(git diff --name-only HEAD^1 HEAD)
if [ -n "$files" ] && ! grep -qv '^services/www/src/docs/' <<< "$files"; then
echo "only=true" >> "$GITHUB_OUTPUT"
fi
unit:
name: unit (${{ matrix.settings.name }})
strategy:
@@ -74,48 +62,39 @@ jobs:
host: blacksmith-4vcpu-ubuntu-2404
- name: windows
host: blacksmith-4vcpu-windows-2025
needs: affected
runs-on: ${{ matrix.settings.host }}
env:
# Skip steps rather than the job so the required unit (linux) and unit (windows) checks still report.
UNIT_ENABLED: ${{ needs.affected.outputs.docs != 'true' }}
defaults:
run:
shell: bash
steps:
- name: Checkout repository
if: env.UNIT_ENABLED == 'true'
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
token: ${{ secrets.GITHUB_TOKEN }}
fetch-depth: 0
- name: Setup Node
if: env.UNIT_ENABLED == 'true'
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: "24"
- name: Setup Bun
if: env.UNIT_ENABLED == 'true'
uses: ./.github/actions/setup-bun
- name: Test Effect simplification rules
if: runner.os == 'Linux' && env.UNIT_ENABLED == 'true'
if: runner.os == 'Linux'
run: bun run test:effect-simplification-rules
- name: Check Effect simplifications
if: runner.os == 'Linux' && env.UNIT_ENABLED == 'true'
if: runner.os == 'Linux'
run: bun run lint:effect-simplifications
- name: Configure git identity
if: env.UNIT_ENABLED == 'true'
run: |
git config --global user.email "bot@opencode.ai"
git config --global user.name "opencode"
- name: Cache Turbo
if: env.UNIT_ENABLED == 'true'
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: node_modules/.cache/turbo
@@ -125,7 +104,6 @@ jobs:
turbo-${{ runner.os }}-
- name: Run unit tests
if: env.UNIT_ENABLED == 'true'
timeout-minutes: 20
run: |
# The runners have four vCPUs, and each Bun test process performs its own concurrent work.
@@ -140,12 +118,12 @@ jobs:
TURBO_SCM_HEAD: ${{ github.sha }}
- name: Verify published codemode package
if: runner.os == 'Linux' && env.UNIT_ENABLED == 'true'
if: runner.os == 'Linux'
working-directory: packages/codemode
run: bun run script/publish.ts --dry-run
- name: Verify packed workerd SDK
if: runner.os == 'Linux' && env.UNIT_ENABLED == 'true'
if: runner.os == 'Linux'
timeout-minutes: 15
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
@@ -158,7 +136,7 @@ jobs:
TURBO_SCM_HEAD: ${{ github.sha }}
- name: Verify compiled service lifecycle
if: always() && env.UNIT_ENABLED == 'true'
if: always()
timeout-minutes: 10
working-directory: packages/cli
env:
@@ -168,13 +146,13 @@ jobs:
bun run script/service-smoke.ts
- name: Setup Node build runtime
if: always() && env.UNIT_ENABLED == 'true'
if: always()
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: "26.4.0"
- name: Verify Node build
if: always() && env.UNIT_ENABLED == 'true'
if: always()
timeout-minutes: 15
working-directory: packages/cli
env:
@@ -184,12 +162,12 @@ jobs:
bun run script/service-smoke.ts --node
- name: Check generated client
if: runner.os == 'Linux' && env.UNIT_ENABLED == 'true'
if: runner.os == 'Linux'
working-directory: packages/client
run: bun run check:generated
- name: Check generated documentation
if: runner.os == 'Linux' && env.UNIT_ENABLED == 'true'
if: runner.os == 'Linux'
working-directory: services/www
run: bun run check:generated
-33
View File
@@ -1,33 +0,0 @@
name: www
# Builds the website the way deploy-www does, without deploying, so a pull request finds broken pages and links before
# they reach v2.
on:
pull_request:
branches: [v2]
paths:
- services/www/**
- bun.lock
- .github/workflows/www.yml
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
cancel-in-progress: true
permissions:
contents: read
jobs:
build:
name: www build
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Setup Bun
uses: ./.github/actions/setup-bun
- name: Build
working-directory: services/www
run: bun run build
+63 -36
View File
@@ -33,7 +33,7 @@
},
"packages/ai": {
"name": "@opencode/ai",
"version": "2.0.23",
"version": "2.0.22",
"dependencies": {
"@aws-sdk/credential-providers": "3.1057.0",
"@opencode/schema": "workspace:*",
@@ -55,7 +55,7 @@
},
"packages/app": {
"name": "@opencode/app",
"version": "2.0.23",
"version": "2.0.22",
"dependencies": {
"@corvu/drawer": "catalog:",
"@dnd-kit/abstract": "0.5.0",
@@ -112,7 +112,7 @@
},
"packages/cli": {
"name": "@opencode/cli",
"version": "2.0.23",
"version": "2.0.22",
"bin": {
"opencode": "./bin/opencode.cjs",
"opencode2": "./bin/opencode2.cjs",
@@ -179,7 +179,7 @@
},
"packages/client": {
"name": "@opencode/client",
"version": "2.0.23",
"version": "2.0.22",
"dependencies": {
"@opencode/protocol": "workspace:*",
"@opencode/schema": "workspace:*",
@@ -205,7 +205,7 @@
},
"packages/codemode": {
"name": "@opencode/codemode",
"version": "2.0.23",
"version": "2.0.22",
"dependencies": {
"acorn": "8.15.0",
"effect": "catalog:",
@@ -218,7 +218,7 @@
},
"packages/console/app": {
"name": "@opencode/console-app",
"version": "2.0.23",
"version": "2.0.22",
"dependencies": {
"@cloudflare/vite-plugin": "1.15.2",
"@ibm/plex": "6.4.1",
@@ -254,7 +254,7 @@
},
"packages/console/core": {
"name": "@opencode/console-core",
"version": "2.0.23",
"version": "2.0.22",
"dependencies": {
"@aws-sdk/client-sts": "3.782.0",
"@jsx-email/render": "1.1.1",
@@ -281,7 +281,7 @@
},
"packages/console/function": {
"name": "@opencode/console-function",
"version": "2.0.23",
"version": "2.0.22",
"dependencies": {
"@openauthjs/openauth": "0.0.0-20250322224806",
"@opencode/console-core": "workspace:*",
@@ -298,7 +298,7 @@
},
"packages/console/mail": {
"name": "@opencode/console-mail",
"version": "2.0.23",
"version": "2.0.22",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
@@ -322,7 +322,7 @@
},
"packages/console/support": {
"name": "@opencode/console-support",
"version": "2.0.23",
"version": "2.0.22",
"dependencies": {
"@cloudflare/vite-plugin": "1.15.2",
"@opencode/console-core": "workspace:*",
@@ -342,7 +342,7 @@
},
"packages/core": {
"name": "@opencode/core",
"version": "2.0.23",
"version": "2.0.22",
"dependencies": {
"@ai-sdk/cohere": "3.0.27",
"@ai-sdk/gateway": "3.0.104",
@@ -383,6 +383,7 @@
"mime-types": "3.0.2",
"tree-sitter-bash": "0.25.0",
"tree-sitter-powershell": "0.25.10",
"venice-ai-sdk-provider": "2.1.1",
"web-tree-sitter": "0.25.10",
"which": "6.0.1",
"zod": "catalog:",
@@ -410,7 +411,7 @@
},
"packages/desktop": {
"name": "@opencode/desktop",
"version": "2.0.23",
"version": "2.0.22",
"dependencies": {
"@zip.js/zip.js": "2.7.62",
"electron-context-menu": "5.0.0",
@@ -455,7 +456,7 @@
},
"packages/enterprise": {
"name": "@opencode/enterprise",
"version": "2.0.23",
"version": "2.0.22",
"dependencies": {
"@hono/standard-validator": "catalog:",
"@opencode-ai/sdk": "1.18.21",
@@ -492,7 +493,7 @@
},
"packages/function": {
"name": "@opencode/function",
"version": "2.0.23",
"version": "2.0.22",
"dependencies": {
"@octokit/auth-app": "8.0.1",
"@octokit/rest": "catalog:",
@@ -508,7 +509,7 @@
},
"packages/gui-extensions": {
"name": "@opencode/gui-extensions",
"version": "2.0.23",
"version": "2.0.22",
"dependencies": {
"@dnd-kit/abstract": "0.5.0",
"@dnd-kit/dom": "0.5.0",
@@ -553,7 +554,7 @@
},
"packages/http-recorder": {
"name": "@opencode/http-recorder",
"version": "2.0.23",
"version": "2.0.22",
"dependencies": {
"@effect/platform-node-shared": "4.0.0-rc.112",
},
@@ -572,7 +573,7 @@
},
"packages/httpapi-codegen": {
"name": "@opencode/httpapi-codegen",
"version": "2.0.23",
"version": "2.0.22",
"dependencies": {
"effect": "catalog:",
"prettier": "3.6.2",
@@ -585,7 +586,7 @@
},
"packages/latex": {
"name": "@opencode/latex",
"version": "2.0.23",
"version": "2.0.22",
"dependencies": {
"@opencode/plugin": "workspace:*",
"@opentui/core": "catalog:",
@@ -599,7 +600,7 @@
},
"packages/merman": {
"name": "@opencode/merman",
"version": "2.0.23",
"version": "2.0.22",
"dependencies": {
"@opencode/plugin": "workspace:*",
"@opentui/core": "catalog:",
@@ -614,7 +615,7 @@
},
"packages/plugin": {
"name": "@opencode/plugin",
"version": "2.0.23",
"version": "2.0.22",
"dependencies": {
"@ai-sdk/provider": "3.0.8",
"@opencode/ai": "workspace:*",
@@ -653,7 +654,7 @@
},
"packages/plugin-browser": {
"name": "@opencode/plugin-browser",
"version": "2.0.23",
"version": "2.0.22",
"dependencies": {
"@opencode/plugin": "workspace:*",
"@opencode/schema": "workspace:*",
@@ -683,7 +684,7 @@
},
"packages/protocol": {
"name": "@opencode/protocol",
"version": "2.0.23",
"version": "2.0.22",
"dependencies": {
"@opencode/schema": "workspace:*",
"effect": "catalog:",
@@ -698,7 +699,7 @@
},
"packages/schema": {
"name": "@opencode/schema",
"version": "2.0.23",
"version": "2.0.22",
"dependencies": {
"@standard-schema/spec": "catalog:",
"effect": "catalog:",
@@ -722,7 +723,7 @@
},
"packages/sdk": {
"name": "@opencode/sdk",
"version": "2.0.23",
"version": "2.0.22",
"dependencies": {
"@opencode/client": "workspace:*",
"@opencode/core": "workspace:*",
@@ -743,7 +744,7 @@
},
"packages/server": {
"name": "@opencode/server",
"version": "2.0.23",
"version": "2.0.22",
"dependencies": {
"@effect/platform-node": "catalog:",
"@effect/platform-node-shared": "catalog:",
@@ -765,7 +766,7 @@
},
"packages/session-ui": {
"name": "@opencode/session-ui",
"version": "2.0.23",
"version": "2.0.22",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode/client": "workspace:*",
@@ -800,7 +801,7 @@
},
"packages/simulation": {
"name": "@opencode/simulation",
"version": "2.0.23",
"version": "2.0.22",
"dependencies": {
"@opencode/ai": "workspace:*",
"@opencode/core": "workspace:*",
@@ -820,7 +821,7 @@
},
"packages/stats/app": {
"name": "@opencode/stats-app",
"version": "2.0.23",
"version": "2.0.22",
"dependencies": {
"@ibm/plex": "6.4.1",
"@kobalte/core": "catalog:",
@@ -854,7 +855,7 @@
},
"packages/stats/core": {
"name": "@opencode/stats-core",
"version": "2.0.23",
"version": "2.0.22",
"dependencies": {
"@aws-sdk/client-athena": "3.933.0",
"@planetscale/database": "1.19.0",
@@ -873,7 +874,7 @@
},
"packages/stats/server": {
"name": "@opencode/stats-server",
"version": "2.0.23",
"version": "2.0.22",
"dependencies": {
"@aws-sdk/client-firehose": "3.933.0",
"@effect/platform-node": "catalog:",
@@ -919,7 +920,7 @@
},
"packages/theme": {
"name": "@opencode/theme",
"version": "2.0.23",
"version": "2.0.22",
"dependencies": {
"@opentui/core": "catalog:",
"effect": "catalog:",
@@ -933,7 +934,7 @@
},
"packages/tui": {
"name": "@opencode/tui",
"version": "2.0.23",
"version": "2.0.22",
"dependencies": {
"@opencode/client": "workspace:*",
"@opencode/core": "workspace:*",
@@ -967,7 +968,7 @@
},
"packages/ui": {
"name": "@opencode/ui",
"version": "2.0.23",
"version": "2.0.22",
"dependencies": {
"@kobalte/core": "catalog:",
"@pierre/diffs": "catalog:",
@@ -1002,7 +1003,7 @@
},
"packages/util": {
"name": "@opencode/util",
"version": "2.0.23",
"version": "2.0.22",
"dependencies": {
"@effect/opentelemetry": "catalog:",
"@effect/platform-node": "catalog:",
@@ -1040,7 +1041,7 @@
},
"packages/web": {
"name": "@opencode/web",
"version": "2.0.23",
"version": "2.0.22",
"dependencies": {
"@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1",
@@ -1081,7 +1082,7 @@
},
"services/update": {
"name": "@opencode/update",
"version": "2.0.23",
"version": "2.0.22",
"dependencies": {
"jose": "6.0.11",
"semver": "catalog:",
@@ -3326,6 +3327,8 @@
"@webgpu/types": ["@webgpu/types@0.1.54", "", {}, "sha512-81oaalC8LFrXjhsczomEQ0u3jG+TqE6V9QHLA8GNZq/Rnot0KDugu3LhSYSlie8tSdooAN1Hov05asrUUp9qgg=="],
"@workflow/serde": ["@workflow/serde@4.1.0", "", {}, "sha512-pav4F2BoirECWR7Nf1TKt+2eETcBj7jj4cBefQ8VXQCA6NPkaKeLfj/zMgi+3zYV5ZIBT4GuUiphsj0/b9hPQQ=="],
"@xmldom/xmldom": ["@xmldom/xmldom@0.8.14", "", {}, "sha512-T4EDRUBVZYRldYApjEJiU0e1stYWaRAX7CuSnKzrpwdZKo53zGV8/pqfzV6FfwNl9YThD2OumQYvqtvjvgG7aQ=="],
"@yuuang/ffi-rs-android-arm64": ["@yuuang/ffi-rs-android-arm64@1.3.7", "", { "os": "android", "cpu": "arm64" }, "sha512-t6Wx3Xll6c07Nuk0k3xnZsxKFxlshm92i0U/BiTHc6kQbvu+fMJF+gKsj4yEj886jH51CM3EqZT9Xdhq9CdUVw=="],
@@ -3366,6 +3369,8 @@
"agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="],
"ai": ["ai@7.0.66", "", { "dependencies": { "@ai-sdk/gateway": "4.0.52", "@ai-sdk/provider": "4.0.7", "@ai-sdk/provider-utils": "5.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-wBUyoCYF3GVr+62nelBgR8YbpTSsMZrzFyOOjiwijylNSM2TFCW35C+Pml2vc59/WLMpyhS/LWZ55M+B9DAcSg=="],
"ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="],
"ajv-draft-04": ["ajv-draft-04@1.0.0", "", { "peerDependencies": { "ajv": "^8.5.0" }, "optionalPeers": ["ajv"] }, "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw=="],
@@ -5718,6 +5723,8 @@
"validate-npm-package-name": ["validate-npm-package-name@7.0.2", "", {}, "sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A=="],
"venice-ai-sdk-provider": ["venice-ai-sdk-provider@2.1.1", "", { "dependencies": { "@ai-sdk/openai-compatible": "^2.0.51", "@ai-sdk/provider": "^3.0.10", "@ai-sdk/provider-utils": "^4.0.30" }, "peerDependencies": { "ai": "^6.0.90" } }, "sha512-w3OHkuzzKZ3r2TOxER6myBYzZJNoDqol+DUHu3NnfBN/GETnUVxecZJab0CHQQ8GZc0jjzpFymepjcLDPS4SQg=="],
"vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
"vfile-location": ["vfile-location@5.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg=="],
@@ -6386,6 +6393,12 @@
"@vscode/emmet-helper/jsonc-parser": ["jsonc-parser@2.3.1", "", {}, "sha512-H8jvkz1O50L3dMZCsLqiuB2tA7muqbSg1AtGEkN0leAqGjsUzDJir3Zwr02BhqdcITPg3ei3mZ+HjMocAknhhg=="],
"ai/@ai-sdk/gateway": ["@ai-sdk/gateway@4.0.52", "", { "dependencies": { "@ai-sdk/provider": "4.0.7", "@ai-sdk/provider-utils": "5.0.27", "@vercel/oidc": "3.2.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-SXUM8jzzuTUJRq+EOgPd5to6DSx0EKslVn+IVZHbUEX6k/3vCPNrvjckbK26HnNxHU/STxm+zTSJteqrO+7Z0w=="],
"ai/@ai-sdk/provider": ["@ai-sdk/provider@4.0.7", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-6or44XprPzKbr8zkmzosowSE0pxkvJcoojBL+mCZvPUt3kvXp3XSNqeVun9golb1acEfSo6yaEBRT18h2VU+1Q=="],
"ai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@5.0.27", "", { "dependencies": { "@ai-sdk/provider": "4.0.7", "@standard-schema/spec": "^1.1.0", "@workflow/serde": "4.1.0", "eventsource-parser": "^3.0.8", "undici": "^7.28.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-EzAn4pdgG5g0xXtH6lE2zyNmfjDQIDjATkfqzuidEI35g++hh4+07vnjzkT/RmGmIClPZiRj/Q2GMPV2V7mkHw=="],
"ansi-align/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
"anymatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
@@ -6690,6 +6703,12 @@
"unzipper/fs-extra": ["fs-extra@11.3.1", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-eXvGGwZ5CL17ZSwHWd3bbgk7UUpF6IFHtP57NYYakPvHOs8GDgDe5KJI36jIJzDkJ6eJjuzRA8eBQb6SkKue0g=="],
"venice-ai-sdk-provider/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.69", "", { "dependencies": { "@ai-sdk/provider": "3.0.15", "@ai-sdk/provider-utils": "4.0.46" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-C99M0T0SpRkcCClmJxbQkpSqGmxLfh3NhTsNF3aNaUQZZ7oXN5sPWi9LGs49X5Q/r9FWxBYeZARXs15xxtGIig=="],
"venice-ai-sdk-provider/@ai-sdk/provider": ["@ai-sdk/provider@3.0.14", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-5X1k57JBJ4H7H1QjX7CnJYAB1I19r/trVZTMcSms7/kLNZ8RaU4Nt2agcwZzv82Hfx6Q7/TOLU7agAKeFfc8cA=="],
"venice-ai-sdk-provider/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.40", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-OL5IrpUm9Y8Dwy+w/vvFwPotS6m52O9W0op2oXgXdCROMJIBalBI0oro6OIBYkPxvm5Xg02GSkoQN25RlR0bnw=="],
"vite-plugin-dynamic-import/acorn": ["acorn@8.18.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ=="],
"vite-plugin-icons-spritesheet/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
@@ -7238,6 +7257,8 @@
"@vitest/expect/@vitest/utils/@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="],
"ai/@ai-sdk/provider-utils/undici": ["undici@7.29.0", "", {}, "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw=="],
"ansi-align/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
"ansi-align/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
@@ -7536,6 +7557,10 @@
"tw-to-css/tailwindcss/postcss": ["postcss@8.5.26", "", { "dependencies": { "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ=="],
"venice-ai-sdk-provider/@ai-sdk/openai-compatible/@ai-sdk/provider": ["@ai-sdk/provider@3.0.15", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-XeZW1CcDF2GMbH4wejW6xBRI2QCOgnkVYUnxoeDadB1mf85riL2bMUeDoh+6gJ/r4mjNfzUPW8OjLjvwTP0u1Q=="],
"venice-ai-sdk-provider/@ai-sdk/openai-compatible/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.46", "", { "dependencies": { "@ai-sdk/provider": "3.0.15", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8", "undici": "^6.28.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-tEtld97plCFiYevsJuOkGkeuhQndeMWFBVrJS4AjnbD5AqrNSXRCe0p+BZ3Cju/sxDeeZ9ym3q9YUV8fASA7aQ=="],
"vitest/@vitest/expect/chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="],
"vscode-languageserver/vscode-languageserver-protocol/vscode-jsonrpc": ["vscode-jsonrpc@8.2.0", "", {}, "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA=="],
@@ -8414,6 +8439,8 @@
"tw-to-css/tailwindcss/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
"venice-ai-sdk-provider/@ai-sdk/openai-compatible/@ai-sdk/provider-utils/undici": ["undici@6.28.0", "", {}, "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA=="],
"yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"@astrojs/cloudflare/@cloudflare/vite-plugin/miniflare/sharp/@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.3.1" }, "os": "darwin", "cpu": "arm64" }, "sha512-eEieHsMksAW4IiO5NzauESRl2D2qz3J/kwUxUrSfV06A93eEaRfMpHXyUb1mAqrR7i8U9A0GRqE9pjn6u1Jjpg=="],
+3 -3
View File
@@ -1,7 +1,7 @@
{
"nodeModules": {
"x86_64-linux": "sha256-4AqU8dPEwo0ukoyEX1SuDVzsXQ7G/fIPnxoL400zH4M=",
"aarch64-linux": "sha256-p75sJYtR8a4oVKb1+Dgp8Kl+1onSmUw5NRv95bbV1+E=",
"aarch64-darwin": "sha256-Psyvbw/lGulysQlhK/MqcHVIwx2aCDTr+Pfr+gRWsG4="
"x86_64-linux": "sha256-2RlbJRTEKSliuUbAE2lAktX63JFR/RKAuLkcCou8wb4=",
"aarch64-linux": "sha256-P7DAE018lTJNGmttg87U9oaTAuw/wHlnMStdnXYKCr0=",
"aarch64-darwin": "sha256-N+NfV1ObOTnW+ez7As+CS+6ci/iRGCQ6cslvyQFCp6E="
}
}
+1 -1
View File
@@ -2,7 +2,7 @@
"$schema": "https://json.schemastore.org/package.json",
"name": "opencode",
"description": "AI-powered development tool",
"version": "2.0.23",
"version": "2.0.22",
"private": true,
"type": "module",
"packageManager": "bun@1.4.2",
-35
View File
@@ -106,41 +106,6 @@ for await (const event of ai.llm.stream(ai.llm.request(input))) {
await ai.dispose()
```
## Venice AI
`Venice` provides native Chat Completions with streaming tools and reasoning. `model` and `chat`
select the same API; credentials default to `VENICE_API_KEY`.
```ts
import { LLM } from "@opencode/ai"
import { Venice } from "@opencode/ai/providers"
import { Effect } from "effect"
const program = Effect.gen(function* () {
const response = yield* LLM.generate({
model: Venice.configure({ apiKey: process.env.VENICE_API_KEY }).chat("qwen3-6-27b"),
prompt: "Explain this design.",
providerOptions: {
reasoningEffort: "high",
veniceParameters: { includeVeniceSystemPrompt: false },
},
})
console.log(response.text)
})
```
Effort lowers to `reasoning.effort`; `reasoning.enabled` and `reasoning.summary` are also available.
Supported effort levels and toggles depend on the selected model. Omitted controls preserve its defaults.
Venice's added system prompt is disabled by default, matching the previous OpenCode Venice SDK behavior.
Replay complete `response.message` values to retain signed/encrypted reasoning and Gemini thought
signatures, including per-tool signatures. Venice's encrypted scalar trailers are excluded from visible
reasoning but retained in provider metadata for replay. Cache affinity uses `promptCacheKey`, and cache-write
usage reads Venice's `cache_creation_input_tokens` field.
The native package entrypoint is `@opencode/ai/providers/venice`. Image generation, embeddings, Responses, and
client-side E2EE are not implemented by this provider.
## Experimental evaluation
Evaluation models compare shared state with typed choice, score, and boolean questions. The API is
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "2.0.23",
"version": "2.0.22",
"name": "@opencode/ai",
"type": "module",
"license": "MIT",
+43 -35
View File
@@ -584,7 +584,10 @@ const serverToolResultType = (name: string): AnthropicServerToolResultType | und
return undefined
}
const lowerServerToolResult = Effect.fnUntraced(function* (part: ToolResultPart, providerMetadataKey: string) {
const lowerServerToolResult = Effect.fnUntraced(function* (
part: ToolResultPart,
providerMetadataKey: string,
) {
const wireType = serverToolResultType(part.name)
if (!wireType)
return yield* invalid(`Anthropic Messages does not know how to round-trip server tool result for ${part.name}`)
@@ -654,7 +657,10 @@ const citationsFromMetadata = (metadata: MediaPart["metadata"]): AnthropicDocume
const isHttpUrl = (value: string) => /^https?:\/\//i.test(value.trim())
const lowerMedia = Effect.fnUntraced(function* (part: MediaPart, breakpoints?: Cache.Breakpoints) {
const lowerMedia = Effect.fnUntraced(function* (
part: MediaPart,
breakpoints?: Cache.Breakpoints,
) {
const mime = part.media.mediaType.toLowerCase()
const cacheControlValue = breakpoints ? cacheControl(breakpoints, part.cache) : undefined
const fileId = fileIdFromMetadata(part.metadata)
@@ -798,6 +804,9 @@ const requireThinkingSignature = (request: LLMRequest) => {
return true
}
// Mid-conversation system messages became available with Opus 4.8 and version
// 5 of the other supported Claude families. Treat later family versions as
// compatible without assuming that every Anthropic Messages model is Claude.
// Opus 4.8 and every Claude 5 model accept mid-conversation system messages; later versions inherit support.
const supportsNativeSystemUpdates = (request: LLMRequest) => {
const version = claudeVersion(String(request.model.id))
@@ -806,9 +815,25 @@ const supportsNativeSystemUpdates = (request: LLMRequest) => {
return version.major >= 5
}
// Native system messages must follow a user turn (tool results count) or a paused server-tool turn.
const acceptsNativeSystemAfter = (message: AnthropicMessage | undefined) =>
message?.role === "user" || (message?.role === "assistant" && message.content.at(-1)?.type === "server_tool_use")
const endsInServerToolUse = (message: LLMRequest["messages"][number]) => {
const last = message.content.at(-1)
return message.role === "assistant" && last?.type === "tool-call" && last.providerExecuted === true
}
const canUseNativeSystemUpdate = (request: LLMRequest, index: number) => {
const previous = request.messages[index - 1]
const next = request.messages[index + 1]
// Vertex currently rejects/404s for a system message after local tool results,
// so fold it into the user tool-result turn across continuations and history.
if (request.model.route.id === "google-vertex-messages" && previous?.role === "tool") return false
return (
previous !== undefined &&
previous.role !== "system" &&
(previous.role === "user" || previous.role === "tool" || endsInServerToolUse(previous)) &&
next?.role !== "system" &&
(next === undefined || next.role === "assistant")
)
}
const splitsLocalToolResults = (messages: LLMRequest["messages"], index: number) => {
const pending = new Set<string>()
@@ -837,34 +862,12 @@ const lowerNativeSystemUpdate = Effect.fnUntraced(function* (
}
})
const lowerWrappedSystemUpdate = Effect.fnUntraced(function* (
message: LLMRequest["messages"][number],
const lowerMessages = Effect.fnUntraced(function* (
request: LLMRequest,
breakpoints: Cache.Breakpoints,
) {
const part = yield* ProviderShared.wrappedSystemUpdate("Anthropic Messages", message)
return { type: "text" as const, text: part.text, cache_control: cacheControl(breakpoints, part.cache) }
})
const appendToUserTurn = (messages: AnthropicMessage[], block: AnthropicUserBlock) => {
const last = messages.at(-1)
if (last?.role === "user") messages[messages.length - 1] = { role: "user", content: [...last.content, block] }
else messages.push({ role: "user", content: [block] })
}
const lowerMessages = Effect.fnUntraced(function* (request: LLMRequest, breakpoints: Cache.Breakpoints) {
const messages: AnthropicMessage[] = []
const providerMetadataKey = request.model.route.providerMetadataKey ?? String(request.model.provider)
// Text updates stay where they are unless a user turn follows them; then they move after the latest
// user turn, the nearest spot where Anthropic accepts a native system message.
const holdUpdates = supportsNativeSystemUpdates(request)
const held: Array<LLMRequest["messages"][number]> = []
const releaseHeld = Effect.fnUntraced(function* () {
const native = acceptsNativeSystemAfter(messages.findLast((message) => message.role !== "system"))
for (const update of held.splice(0)) {
if (native) messages.push(yield* lowerNativeSystemUpdate(update, breakpoints))
else appendToUserTurn(messages, yield* lowerWrappedSystemUpdate(update, breakpoints))
}
})
for (const [index, message] of request.messages.entries()) {
if (message.role === "system") {
@@ -876,8 +879,16 @@ const lowerMessages = Effect.fnUntraced(function* (request: LLMRequest, breakpoi
}
if (splitsLocalToolResults(request.messages, index))
return yield* invalid("Anthropic Messages system updates cannot split a local tool call from its tool result")
if (holdUpdates) held.push(message)
else appendToUserTurn(messages, yield* lowerWrappedSystemUpdate(message, breakpoints))
if (supportsNativeSystemUpdates(request) && canUseNativeSystemUpdate(request, index)) {
messages.push(yield* lowerNativeSystemUpdate(message, breakpoints))
continue
}
const part = yield* ProviderShared.wrappedSystemUpdate("Anthropic Messages", message)
const block = { type: "text" as const, text: part.text, cache_control: cacheControl(breakpoints, part.cache) }
const previous = messages.at(-1)
if (previous?.role === "user")
messages[messages.length - 1] = { role: "user", content: [...previous.content, block] }
else messages.push({ role: "user", content: [block] })
continue
}
@@ -953,9 +964,7 @@ const lowerMessages = Effect.fnUntraced(function* (request: LLMRequest, breakpoi
`Anthropic Messages assistant messages only support text, reasoning, and tool-call content for now`,
)
}
if (content.length === 0) continue
yield* releaseHeld()
messages.push({ role: "assistant", content })
if (content.length > 0) messages.push({ role: "assistant", content })
continue
}
@@ -976,7 +985,6 @@ const lowerMessages = Effect.fnUntraced(function* (request: LLMRequest, breakpoi
messages[messages.length - 1] = { role: "user", content: [...previous.content, ...content] }
else messages.push({ role: "user", content })
}
yield* releaseHeld()
return messages
})
+9 -13
View File
@@ -65,7 +65,7 @@ const ExtraContent = Schema.Struct({
})
const decodeExtraContent = (value: unknown) => Option.getOrUndefined(Schema.decodeUnknownOption(ExtraContent)(value))
export const OpenAIChatAssistantToolCall = Schema.Struct({
const OpenAIChatAssistantToolCall = Schema.Struct({
id: Schema.String,
type: Schema.tag("function"),
function: Schema.Struct({
@@ -134,7 +134,7 @@ const OpenAIChatUserContent = Schema.Union([
])
type OpenAIChatUserContent = Schema.Schema.Type<typeof OpenAIChatUserContent>
export const OpenAIChatMessage = Schema.Union([
const OpenAIChatMessage = Schema.Union([
Schema.Struct({
role: Schema.Literal("system"),
content: Schema.Union([Schema.String, Schema.Array(OpenAIChatUserContent)]),
@@ -200,7 +200,7 @@ export type OpenAIChatBody = Schema.Schema.Type<typeof OpenAIChatBody>
// The event schema is one decoded SSE `data:` payload. `Framing.sse` splits the
// byte stream into strings, then `Protocol.jsonEvent` decodes each string into
// this provider-native event shape.
export const OpenAIChatUsage = Schema.StructWithRest(
const OpenAIChatUsage = Schema.StructWithRest(
Schema.Struct({
prompt_tokens: optionalNull(Schema.Number),
completion_tokens: optionalNull(Schema.Number),
@@ -238,7 +238,7 @@ const OpenAIChatToolCallDeltaFunction = Schema.Struct({
arguments: optionalNull(Schema.String),
})
export const OpenAIChatToolCallDelta = Schema.Struct({
const OpenAIChatToolCallDelta = Schema.Struct({
index: optionalNull(Schema.Number),
id: optionalNull(Schema.String),
function: optionalNull(OpenAIChatToolCallDeltaFunction),
@@ -246,7 +246,7 @@ export const OpenAIChatToolCallDelta = Schema.Struct({
})
type OpenAIChatToolCallDelta = Schema.Schema.Type<typeof OpenAIChatToolCallDelta>
export const OpenAIChatDelta = Schema.StructWithRest(
const OpenAIChatDelta = Schema.StructWithRest(
Schema.Struct({
content: optionalNull(Schema.String),
refusal: optionalNull(Schema.String),
@@ -259,7 +259,7 @@ export const OpenAIChatDelta = Schema.StructWithRest(
[Schema.Record(Schema.String, Schema.Unknown)],
)
export const OpenAIChatChoice = Schema.StructWithRest(
const OpenAIChatChoice = Schema.StructWithRest(
Schema.Struct({
delta: optionalNull(OpenAIChatDelta),
finish_reason: optionalNull(Schema.String),
@@ -326,8 +326,6 @@ export interface ParserState {
interface LoweringOptions {
readonly cacheControl?: (cache: CacheHint | undefined) => OpenAIChatCacheControl | undefined
readonly toolCallID?: (id: string) => string
/** Project provider-specific fields from the exact source, even when other messages are dropped during lowering. */
readonly assistant?: (source: LLMRequest["messages"][number], message: OpenAIChatMessage) => OpenAIChatMessage
}
const lowerTool = (tool: ToolDefinition, options: LoweringOptions, supportsStrictMode: boolean): OpenAIChatTool => ({
@@ -541,10 +539,8 @@ const lowerMessage = Effect.fnUntraced(function* (
options: LoweringOptions & { readonly providerMetadataKey: string },
) {
if (message.role === "user") return [yield* lowerUserMessage(message, options)]
if (message.role === "assistant") {
const lowered = yield* lowerAssistantMessage(message, reasoningField, requireReasoning, options)
return [options.assistant?.(message, lowered) ?? lowered]
}
if (message.role === "assistant")
return [yield* lowerAssistantMessage(message, reasoningField, requireReasoning, options)]
return (yield* lowerToolMessages(message, options)).messages
})
@@ -1218,7 +1214,7 @@ const step = (state: ParserState, event: OpenAIChatEvent) =>
] as const
})
export const finishEvents = Effect.fnUntraced(function* (state: ParserState) {
const finishEvents = Effect.fnUntraced(function* (state: ParserState) {
if (state.finishReason === undefined && state.requireFinishReason)
return yield* new AIError({
reason: new InvalidProviderOutputError({
-374
View File
@@ -1,374 +0,0 @@
import { Effect, Schema } from "effect"
import { Protocol } from "../route/protocol.js"
import { AIError, LLMEvent, type LanguageModelCompatibility, type LLMRequest } from "../schema/index.js"
import { classifyProviderFailure } from "../provider-error.js"
import { OpenAIChat } from "./openai-chat.js"
import { JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared.js"
import { cacheControl } from "./utils/cache.js"
// ---------------------------------------------------------------------------
// Public options and request body
// ---------------------------------------------------------------------------
export type ReasoningEffort = "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max" | (string & {})
const Parameters = Schema.Struct({
enableWebSearch: Schema.optional(Schema.String),
enableWebScraping: Schema.optional(Schema.Boolean),
enableWebCitations: Schema.optional(Schema.Boolean),
enableXSearch: Schema.optional(Schema.Boolean),
stripThinkingResponse: Schema.optional(Schema.Boolean),
disableThinking: Schema.optional(Schema.Boolean),
includeVeniceSystemPrompt: Schema.optional(Schema.Boolean),
characterSlug: Schema.optional(Schema.String),
includeSearchResultsInStream: Schema.optional(Schema.Boolean),
returnSearchResultsAsDocuments: Schema.optional(Schema.Boolean),
})
const Reasoning = Schema.Struct({
effort: Schema.optional(Schema.String),
enabled: Schema.optional(Schema.Boolean),
summary: Schema.optional(Schema.String),
})
export type OptionsInput = {
readonly reasoningEffort?: ReasoningEffort
readonly reasoning?: {
readonly effort?: ReasoningEffort
readonly enabled?: boolean
readonly summary?: "auto" | "concise" | "detailed" | (string & {})
}
readonly veniceParameters?: Omit<typeof Parameters.Type, "enableWebSearch"> & {
readonly enableWebSearch?: "off" | "on" | "auto" | (string & {})
}
readonly promptCacheKey?: string
readonly promptCacheRetention?: "default" | "extended" | "24h" | (string & {})
readonly parallelToolCalls?: boolean
readonly maxCompletionTokens?: number
readonly maxTokens?: number
readonly minP?: number
readonly repetitionPenalty?: number
readonly stopTokenIds?: readonly number[]
readonly logprobs?: boolean
readonly topLogprobs?: number
readonly maxTemp?: number
readonly minTemp?: number
readonly responseFormat?: Readonly<Record<string, unknown>>
readonly user?: string
}
const Options = Schema.Struct({
reasoningEffort: Schema.optional(Schema.String),
reasoning: Schema.optional(Reasoning),
veniceParameters: Schema.optional(Parameters),
promptCacheKey: Schema.optional(Schema.String),
promptCacheRetention: Schema.optional(Schema.String),
parallelToolCalls: Schema.optional(Schema.Boolean),
maxCompletionTokens: Schema.optional(Schema.Number),
maxTokens: Schema.optional(Schema.Number),
minP: Schema.optional(Schema.Number),
repetitionPenalty: Schema.optional(Schema.Number),
stopTokenIds: Schema.optional(Schema.Array(Schema.Number)),
logprobs: Schema.optional(Schema.Boolean),
topLogprobs: Schema.optional(Schema.Number),
maxTemp: Schema.optional(Schema.Number),
minTemp: Schema.optional(Schema.Number),
responseFormat: Schema.optional(JsonObject),
user: Schema.optional(Schema.String),
})
const ToolCall = Schema.Struct({
...OpenAIChat.OpenAIChatAssistantToolCall.fields,
thought_signature: Schema.optional(Schema.String),
})
const Assistant = Schema.StructWithRest(
Schema.Struct({
...OpenAIChat.OpenAIChatMessage.cases.assistant.schema.fields,
tool_calls: optionalArray(ToolCall),
thought_signature: Schema.optional(Schema.String),
}),
[Schema.Record(Schema.String, Schema.Unknown)],
)
const Messages = Schema.Array(
Schema.Union([
OpenAIChat.OpenAIChatMessage.cases.system,
OpenAIChat.OpenAIChatMessage.cases.user,
Assistant,
OpenAIChat.OpenAIChatMessage.cases.tool,
]),
)
const Body = Schema.Struct({
...OpenAIChat.bodyFields,
messages: Messages,
reasoning: Schema.optional(Reasoning),
venice_parameters: Schema.Record(Schema.String, Schema.Union([Schema.String, Schema.Boolean])),
prompt_cache_retention: Options.fields.promptCacheRetention,
parallel_tool_calls: Options.fields.parallelToolCalls,
min_p: Options.fields.minP,
top_k: Schema.optional(Schema.Number),
repetition_penalty: Options.fields.repetitionPenalty,
stop_token_ids: Options.fields.stopTokenIds,
logprobs: Options.fields.logprobs,
top_logprobs: Options.fields.topLogprobs,
max_temp: Options.fields.maxTemp,
min_temp: Options.fields.minTemp,
response_format: Options.fields.responseFormat,
user: Options.fields.user,
})
// ---------------------------------------------------------------------------
// Streaming schemas and state
// ---------------------------------------------------------------------------
const ToolDelta = Schema.Struct({
...OpenAIChat.OpenAIChatToolCallDelta.fields,
thought_signature: optionalNull(Schema.String),
})
const Delta = Schema.StructWithRest(
Schema.Struct({
...OpenAIChat.OpenAIChatDelta.schema.fields,
tool_calls: optionalNull(Schema.Array(ToolDelta)),
thought_signature: optionalNull(Schema.String),
}),
[Schema.Record(Schema.String, Schema.Unknown)],
)
const Choice = Schema.StructWithRest(
Schema.Struct({ ...OpenAIChat.OpenAIChatChoice.schema.fields, delta: optionalNull(Delta) }),
[Schema.Record(Schema.String, Schema.Unknown)],
)
const Usage = Schema.StructWithRest(
Schema.Struct({
...OpenAIChat.OpenAIChatUsage.schema.fields,
prompt_tokens_details: optionalNull(
Schema.StructWithRest(
Schema.Struct({
cached_tokens: optionalNull(Schema.Number),
cache_write_tokens: optionalNull(Schema.Number),
cache_creation_input_tokens: optionalNull(Schema.Number),
}),
[Schema.Record(Schema.String, Schema.Unknown)],
),
),
}),
[Schema.Record(Schema.String, Schema.Unknown)],
)
const Event = Schema.StructWithRest(
Schema.Struct({
...OpenAIChat.OpenAIChatEvent.schema.fields,
choices: optionalNull(Schema.Array(Choice)),
usage: optionalNull(Usage),
error: optionalNull(Schema.Union([Schema.String, OpenAIChat.OpenAIChatEvent.schema.fields.error.schema])),
issues: optionalArray(
Schema.Struct({
message: Schema.String,
path: Schema.optional(Schema.Array(Schema.Union([Schema.String, Schema.Number]))),
}),
),
}),
[Schema.Record(Schema.String, Schema.Unknown)],
)
interface State {
readonly shared: OpenAIChat.ParserState
readonly pending: string
readonly reasoning: string
readonly encrypted: boolean
readonly signature?: string
readonly tools: Readonly<Record<string, string>>
}
const MARKER = "__ENCRYPTED_REASONING__"
// ---------------------------------------------------------------------------
// Request lowering
// ---------------------------------------------------------------------------
const fromRequest = Effect.fn("VeniceChat.fromRequest")(function* (request: LLMRequest) {
const options = yield* ProviderShared.validateWith(Schema.decodeUnknownEffect(Options))(request.providerOptions ?? {})
const body = yield* OpenAIChat.fromRequest(request, {
cacheControl: cacheControl(),
assistant: (source, message) => {
if (message.role !== "assistant") return message
const parts = source.content.filter(
(part) => part.type === "text" || part.type === "reasoning" || part.type === "tool-call",
)
const calls = source.content.filter((part) => part.type === "tool-call")
const signature = parts
.map((part) => part.providerMetadata?.venice?.messageThoughtSignature)
.find((value) => typeof value === "string")
const raw = parts
.map((part) => part.providerMetadata?.venice?.encryptedReasoningContent)
.find((value) => typeof value === "string")
return {
...message,
...(signature === undefined ? {} : { thought_signature: signature }),
...(raw === undefined ? {} : { reasoning_content: raw }),
tool_calls: message.tool_calls?.map((call, index) => {
const signature = calls[index]?.providerMetadata?.venice?.thoughtSignature
return { ...call, ...(typeof signature === "string" ? { thought_signature: signature } : {}) }
}),
}
},
})
return {
...body,
max_completion_tokens: options.maxCompletionTokens ?? options.maxTokens ?? request.generation?.maxTokens,
reasoning_effort: undefined,
reasoning:
options.reasoningEffort === undefined
? options.reasoning
: { ...options.reasoning, effort: options.reasoningEffort },
// Match the previous Venice SDK default, not the gateway's added prompt.
venice_parameters: Object.fromEntries(
Object.entries({
...options.veniceParameters,
includeVeniceSystemPrompt: options.veniceParameters?.includeVeniceSystemPrompt ?? false,
})
.filter(([, value]) => value !== undefined)
.map(([key, value]) => [key.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`), value]),
),
prompt_cache_retention: options.promptCacheRetention,
parallel_tool_calls: options.parallelToolCalls,
min_p: options.minP,
top_k: request.generation?.topK,
repetition_penalty: options.repetitionPenalty,
stop_token_ids: options.stopTokenIds,
logprobs: options.logprobs,
top_logprobs: options.topLogprobs,
max_temp: options.maxTemp,
min_temp: options.minTemp,
response_format: options.responseFormat,
user: options.user,
}
})
// ---------------------------------------------------------------------------
// Venice normalization around the shared Chat state machine
// ---------------------------------------------------------------------------
const step = Effect.fn("VeniceChat.step")(function* (state: State, event: typeof Event.Type) {
if (typeof event.error === "string")
return yield* new AIError({
reason: classifyProviderFailure({
message: [
event.error,
...(event.issues ?? []).map(
(issue) => `${issue.path?.length ? `${issue.path.join(".")}: ` : ""}${issue.message}`,
),
].join("; "),
rawBody: ProviderShared.encodeJson(event),
}),
})
const delta = event.choices?.[0]?.delta
const scalar = delta?.reasoning_content ?? ""
const text = state.pending + scalar
const marker = text.indexOf(MARKER)
const pending =
marker >= 0 || state.encrypted
? 0
: (Array.from({ length: Math.min(text.length, MARKER.length - 1) }, (_, index) => index + 1)
.filter((length) => text.endsWith(MARKER.slice(0, length)))
.at(-1) ?? 0)
const visible = state.encrypted ? "" : marker >= 0 ? text.slice(0, marker) : text.slice(0, text.length - pending)
const result = yield* OpenAIChat.protocol.stream.step(state.shared, {
...event,
error: event.error,
usage: event.usage
? {
...event.usage,
prompt_tokens_details: event.usage.prompt_tokens_details
? {
...event.usage.prompt_tokens_details,
cache_write_tokens:
event.usage.prompt_tokens_details.cache_write_tokens ??
event.usage.prompt_tokens_details.cache_creation_input_tokens,
}
: event.usage.prompt_tokens_details,
}
: event.usage,
choices: event.choices?.map((choice, index) =>
index === 0 && choice.delta
? {
...choice,
delta: {
...choice.delta,
reasoning_content: visible || undefined,
// Opaque-only scalar reasoning still needs a canonical part for replay.
reasoning_details: choice.delta.reasoning_details ?? (marker >= 0 ? [] : undefined),
},
}
: choice,
),
})
const tools = { ...state.tools }
for (const call of delta?.tool_calls ?? []) {
if (!call.thought_signature) continue
const index = call.index ?? result[0].latestToolIndex
const id =
call.id ?? (index === undefined ? undefined : (result[0].tools[index]?.id ?? result[0].pendingTools[index]?.id))
if (id) tools[id] = call.thought_signature
}
return [
{
shared: result[0],
pending: pending ? text.slice(-pending) : "",
reasoning: state.reasoning + scalar,
encrypted: state.encrypted || marker >= 0,
signature: delta?.thought_signature ?? state.signature,
tools,
},
result[1],
] as const
})
const onHalt = Effect.fn("VeniceChat.onHalt")(function* (state: State) {
const events = yield* OpenAIChat.finishEvents(state.shared)
return events.flatMap((event): LLMEvent[] => {
if (
event.type !== "reasoning-end" &&
event.type !== "text-end" &&
event.type !== "tool-call" &&
event.type !== "tool-input-end"
)
return [event]
const signature = event.type === "tool-call" || event.type === "tool-input-end" ? state.tools[event.id] : undefined
const providerMetadata = {
...event.providerMetadata,
venice: {
...event.providerMetadata?.venice,
...(state.signature ? { messageThoughtSignature: state.signature } : {}),
...(signature ? { thoughtSignature: signature } : {}),
...(event.type === "reasoning-end" && state.encrypted ? { encryptedReasoningContent: state.reasoning } : {}),
},
}
if (event.type === "reasoning-end" && state.pending)
return [LLMEvent.reasoningDelta({ id: event.id, text: state.pending }), { ...event, providerMetadata }]
return [{ ...event, providerMetadata }]
})
})
export const compatibility = {
maxTokensField: "max_completion_tokens",
supportsStore: false,
supportsPromptCacheKey: true,
} satisfies LanguageModelCompatibility
export const protocol = Protocol.make({
id: "venice-chat",
body: { schema: Body, from: fromRequest },
stream: {
event: Schema.Union([Schema.Literal("[DONE]"), Protocol.jsonEvent(Event)]),
initial: (request): State => ({
shared: OpenAIChat.protocol.stream.initial(request),
pending: "",
reasoning: "",
encrypted: false,
tools: {},
}),
step: (state: State, event) => (event === "[DONE]" ? Effect.succeed([state, []] as const) : step(state, event)),
terminal: (event) => event === "[DONE]",
onHalt,
},
})
export * as VeniceChat from "./venice-chat.js"
@@ -54,7 +54,6 @@ const route = Route.make({
),
},
stream: AnthropicMessages.protocol.stream,
supportsEffortUpdates: AnthropicMessages.protocol.supportsEffortUpdates,
}),
endpoint: Endpoint.path(({ request }) => `/${request.model.id}:streamRawPredict`),
auth: Auth.none,
-1
View File
@@ -40,7 +40,6 @@ export * as Stability from "./stability.js"
export * as TogetherAI from "./togetherai.js"
export * as TypeSafeAI from "./typesafe-ai.js"
export * as VercelAIGateway from "./vercel-ai-gateway.js"
export * as Venice from "./venice.js"
export * as XAI from "./xai.js"
export * as ZAI from "./zai.js"
export * as ZAICodingPlan from "./zai-coding-plan.js"
-62
View File
@@ -1,62 +0,0 @@
import type { ProviderPackage } from "../provider-package.js"
import { VeniceChat } from "../protocols/venice-chat.js"
import { OpenAIChat } from "../protocols/openai-chat.js"
import { AuthOptions, type ProviderAuthOption } from "../route/auth-options.js"
import { Route, type RouteDefaultsInput } from "../route/client.js"
import { Endpoint } from "../route/endpoint.js"
import { ProviderID, type ModelID } from "../schema/index.js"
export const id = ProviderID.make("venice")
export type ChatOptionsInput = VeniceChat.OptionsInput
export type Config = Omit<RouteDefaultsInput, "providerOptions"> &
ProviderAuthOption<"optional"> & {
readonly baseURL?: string
readonly queryParams?: Readonly<Record<string, string>>
readonly providerOptions?: ChatOptionsInput
}
export type Settings = ProviderPackage.Settings &
ChatOptionsInput & {
readonly apiKey?: string
readonly baseURL?: string
readonly queryParams?: Readonly<Record<string, string>>
}
const route = Route.make({
id: "venice-chat",
provider: id,
providerMetadataKey: "venice",
protocol: VeniceChat.protocol,
endpoint: Endpoint.path("/chat/completions", { baseURL: "https://api.venice.ai/api/v1" }),
framing: OpenAIChat.framing,
})
export const routes = [route]
export const configure = (input: Config = {}) => {
const { apiKey: _apiKey, auth: _auth, baseURL, queryParams, ...rest } = input
const chat = (modelID: string | ModelID) =>
route
.with({
...rest,
endpoint: { baseURL: baseURL ?? route.endpoint.baseURL, query: queryParams },
auth: AuthOptions.bearer(input, "VENICE_API_KEY"),
})
.model<ChatOptionsInput>({ id: modelID, compatibility: VeniceChat.compatibility })
return { id, model: chat, chat, configure }
}
export const provider = configure()
export const chat = provider.chat
export const model: ProviderPackage.Definition<Settings, ChatOptionsInput>["model"] = (
modelID,
{ apiKey, baseURL, queryParams, body, headers, ...providerOptions },
) =>
configure({
apiKey,
baseURL,
queryParams,
headers,
http: body === undefined ? undefined : { body: { ...body } },
providerOptions,
}).model(modelID)
export * as Venice from "./venice.js"
+7 -42
View File
@@ -171,33 +171,6 @@ describe("Anthropic Messages effort updates", () => {
}),
)
it.effect("releases a held system update next to an effort marker as one valid section", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
LLM.request({
model: opus5,
messages: [
Message.user("Fix it."),
Message.assistant("Done."),
lowFromHigh,
Message.system("Update."),
Message.user("Next."),
],
providerOptions: { effort: "low" },
cache: "none",
}),
)
expect(prepared.body.messages).toEqual([
{ role: "user", content: [{ type: "text", text: "Fix it." }] },
{ role: "assistant", content: [{ type: "text", text: "Done." }] },
{ role: "system", content: [], output_config: { effort: "low" } },
{ role: "user", content: [{ type: "text", text: "Next." }] },
{ role: "system", content: [{ type: "text", text: "Update.", cache_control: undefined }] },
])
}),
)
it.effect("falls back to a plain top-level effort when history drifted from the current effort", () =>
Effect.gen(function* () {
const drifted = yield* compileRequest(
@@ -293,28 +266,20 @@ describe("Anthropic Messages effort updates", () => {
}),
)
it.effect("lowers markers on the Vertex Anthropic route for models that support them", () =>
it.effect("strips markers on the Vertex Anthropic route, whose protocol wrapper does not forward support", () =>
Effect.gen(function* () {
const vertex = GoogleVertexMessages.configure({ accessToken: "test", location: "global", project: "test" })
const opus5 = yield* compileRequest(
const prepared = yield* compileRequest(
LLM.request({
model: vertex.model("claude-opus-5"),
messages: conversation,
providerOptions: { effort: "low" },
}),
)
const opus48 = yield* compileRequest(
LLM.request({
model: vertex.model("claude-opus-4-8"),
model: GoogleVertexMessages.configure({ accessToken: "test", location: "global", project: "test" }).model(
"claude-opus-5",
),
messages: conversation,
providerOptions: { effort: "low" },
}),
)
expect(systemMessages(opus5.body)).toEqual([{ role: "system", content: [], output_config: { effort: "low" } }])
expect(opus5.body.output_config).toEqual({ effort: "high" })
expect(systemMessages(opus48.body)).toHaveLength(0)
expect(opus48.body.output_config).toEqual({ effort: "low" })
expect(systemMessages(prepared.body)).toHaveLength(0)
expect(prepared.body.output_config).toEqual({ effort: "low" })
}),
)
})
@@ -1,75 +0,0 @@
{
"version": 1,
"metadata": {
"model": "claude-opus-4-8",
"tags": [
"prefix:venice-chat",
"provider:venice",
"protocol:venice-chat",
"reasoning",
"tool",
"tool-loop",
"continuation",
"usage",
"signed"
],
"name": "venice-chat/claude-opus-4-8-preserves-reasoning-through-a-tool-loop-and-follow-up",
"recordedAt": "2026-10-05T03:00:37.127Z"
},
"interactions": [
{
"transport": "http",
"request": {
"method": "POST",
"url": "https://api.venice.ai/api/v1/chat/completions",
"headers": {
"content-type": "application/json"
},
"body": "{\"model\":\"claude-opus-4-8\",\"messages\":[{\"role\":\"system\",\"content\":\"Use the provided tool for private data. After receiving the result, give only the requested number; do not call the tool again.\"},{\"role\":\"user\",\"content\":\"Look up conversion rate for code ZEBRA. Then multiply that rate by 17 and add 9. You must use lookup_rate before answering.\"}],\"tools\":[{\"type\":\"function\",\"function\":{\"name\":\"lookup_rate\",\"description\":\"Retrieve the private conversion rate for a code. Only this tool knows the rate.\",\"parameters\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\",\"enum\":[\"ZEBRA\",\"MOOSE\"]}},\"required\":[\"code\"],\"additionalProperties\":false},\"strict\":false}}],\"stream\":true,\"stream_options\":{\"include_usage\":true},\"max_completion_tokens\":4096,\"reasoning\":{\"effort\":\"high\"},\"venice_parameters\":{\"include_venice_system_prompt\":false}}"
},
"response": {
"status": 200,
"headers": {
"content-type": "text/event-stream; charset=utf-8"
},
"body": "data: {\"id\":\"msg_011CfiLtM3Rj4dVUWFJAEjoy\",\"object\":\"chat.completion.chunk\",\"created\":1791169233,\"model\":\"claude-opus-4-8\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\"},\"finish_reason\":null}]}\n\n: processing\n\ndata: {\"id\":\"msg_011CfiLtM3Rj4dVUWFJAEjoy\",\"object\":\"chat.completion.chunk\",\"created\":1791169234,\"model\":\"claude-opus-4-8\",\"choices\":[{\"index\":0,\"delta\":{\"reasoning_content\":\"I need to find the rate for\",\"reasoning_details\":[{\"type\":\"reasoning.text\",\"format\":\"anthropic-claude-v1\",\"index\":0,\"text\":\"I need to find the rate for\"}],\"content\":\"\"},\"finish_reason\":null}]}\n\ndata: {\"id\":\"msg_011CfiLtM3Rj4dVUWFJAEjoy\",\"object\":\"chat.completion.chunk\",\"created\":1791169234,\"model\":\"claude-opus-4-8\",\"choices\":[{\"index\":0,\"delta\":{\"reasoning_content\":\" ZEBRA.\",\"reasoning_details\":[{\"type\":\"reasoning.text\",\"format\":\"anthropic-claude-v1\",\"index\":0,\"text\":\" ZEBRA.\"}],\"content\":\"\"},\"finish_reason\":null}]}\n\ndata: {\"id\":\"msg_011CfiLtM3Rj4dVUWFJAEjoy\",\"object\":\"chat.completion.chunk\",\"created\":1791169234,\"model\":\"claude-opus-4-8\",\"choices\":[{\"index\":0,\"delta\":{\"reasoning_content\":\"\\n\\n\",\"reasoning_details\":[{\"type\":\"reasoning.text\",\"format\":\"anthropic-claude-v1\",\"index\":0,\"text\":\"\\n\\n\"}],\"content\":\"\"},\"finish_reason\":null}]}\n\ndata: {\"id\":\"msg_011CfiLtM3Rj4dVUWFJAEjoy\",\"object\":\"chat.completion.chunk\",\"created\":1791169234,\"model\":\"claude-opus-4-8\",\"choices\":[{\"index\":0,\"delta\":{\"reasoning_details\":[{\"type\":\"reasoning.text\",\"format\":\"anthropic-claude-v1\",\"index\":0,\"text\":\"\",\"signature\":\"EsECCrwBCBIYAipASM082xA/ckQU1q1MLSuAGRD/F94kvqoBlyXoEKdxvH2f2kn+ESR0AZfmYtv8VB8ux7LQ+qq4DnODYQ2iBHemajIPY2xhdWRlLW9wdXMtNC04OABCCHRoaW5raW5nWiQ1YzJjNzI5YS0wMzVjLTQ5ZDYtODNiOC03NGNhZTAyMzU2NWRyEOtICOPsjcfgmcoVEjPetDWIAQGaAREKD2NsYXVkZS1vcHVzLTQtOKgB0qWM1gawAQISDFJE1AThUPJ/0OxIBxoMak46jCiQRsOzj1nIIjBV1UQ4WoUBtZX8F6kyFtKyRPkLdWp7WQKQYd+f4VT3GvmEqenUY2ANNJImtDj/BQAqMhz7JVFNqrlemYzJNRbF6ntHDPc2enCrd/tYXBG6nms1qbcfKWc7CYc2ZGogOqiItKNDGAE=\",\"id\":\"toolu_01KYDseHbTDZXSYg9bNwiwxU\"}],\"tool_calls\":[{\"index\":0,\"id\":\"toolu_01KYDseHbTDZXSYg9bNwiwxU\",\"type\":\"function\",\"function\":{\"name\":\"lookup_rate\",\"arguments\":\"\"}}],\"content\":\"\"},\"finish_reason\":null}]}\n\ndata: {\"id\":\"msg_011CfiLtM3Rj4dVUWFJAEjoy\",\"object\":\"chat.completion.chunk\",\"created\":1791169234,\"model\":\"claude-opus-4-8\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"{\\\"code\\\"\"}}]},\"finish_reason\":null}]}\n\ndata: {\"id\":\"msg_011CfiLtM3Rj4dVUWFJAEjoy\",\"object\":\"chat.completion.chunk\",\"created\":1791169234,\"model\":\"claude-opus-4-8\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\": \\\"ZEBR\"}}]},\"finish_reason\":null}]}\n\ndata: {\"id\":\"msg_011CfiLtM3Rj4dVUWFJAEjoy\",\"object\":\"chat.completion.chunk\",\"created\":1791169234,\"model\":\"claude-opus-4-8\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"A\\\"}\"}}]},\"finish_reason\":null}]}\n\ndata: {\"id\":\"msg_011CfiLtM3Rj4dVUWFJAEjoy\",\"object\":\"chat.completion.chunk\",\"created\":1791169234,\"model\":\"claude-opus-4-8\",\"choices\":[{\"index\":0,\"delta\":{},\"finish_reason\":\"tool_calls\"}]}\n\ndata: {\"id\":\"msg_011CfiLtM3Rj4dVUWFJAEjoy\",\"object\":\"chat.completion.chunk\",\"created\":1791169234,\"model\":\"claude-opus-4-8\",\"choices\":[],\"usage\":{\"prompt_tokens\":499,\"completion_tokens\":74,\"total_tokens\":573,\"prompt_tokens_details\":{\"cached_tokens\":0}},\"cost\":{\"usd\":0.005214,\"diem\":0}}\n\ndata: [DONE]\n\n"
}
},
{
"transport": "http",
"request": {
"method": "POST",
"url": "https://api.venice.ai/api/v1/chat/completions",
"headers": {
"content-type": "application/json"
},
"body": "{\"model\":\"claude-opus-4-8\",\"messages\":[{\"role\":\"system\",\"content\":\"Use the provided tool for private data. After receiving the result, give only the requested number; do not call the tool again.\"},{\"role\":\"user\",\"content\":\"Look up conversion rate for code ZEBRA. Then multiply that rate by 17 and add 9. You must use lookup_rate before answering.\"},{\"role\":\"assistant\",\"content\":null,\"tool_calls\":[{\"id\":\"toolu_01KYDseHbTDZXSYg9bNwiwxU\",\"type\":\"function\",\"function\":{\"name\":\"lookup_rate\",\"arguments\":\"{\\\"code\\\":\\\"ZEBRA\\\"}\"}}],\"reasoning_details\":[{\"type\":\"reasoning.text\",\"text\":\"I need to find the rate for ZEBRA.\\n\\n\",\"id\":\"toolu_01KYDseHbTDZXSYg9bNwiwxU\",\"format\":\"anthropic-claude-v1\",\"index\":0,\"signature\":\"EsECCrwBCBIYAipASM082xA/ckQU1q1MLSuAGRD/F94kvqoBlyXoEKdxvH2f2kn+ESR0AZfmYtv8VB8ux7LQ+qq4DnODYQ2iBHemajIPY2xhdWRlLW9wdXMtNC04OABCCHRoaW5raW5nWiQ1YzJjNzI5YS0wMzVjLTQ5ZDYtODNiOC03NGNhZTAyMzU2NWRyEOtICOPsjcfgmcoVEjPetDWIAQGaAREKD2NsYXVkZS1vcHVzLTQtOKgB0qWM1gawAQISDFJE1AThUPJ/0OxIBxoMak46jCiQRsOzj1nIIjBV1UQ4WoUBtZX8F6kyFtKyRPkLdWp7WQKQYd+f4VT3GvmEqenUY2ANNJImtDj/BQAqMhz7JVFNqrlemYzJNRbF6ntHDPc2enCrd/tYXBG6nms1qbcfKWc7CYc2ZGogOqiItKNDGAE=\"}],\"reasoning_content\":\"I need to find the rate for ZEBRA.\\n\\n\"},{\"role\":\"tool\",\"tool_call_id\":\"toolu_01KYDseHbTDZXSYg9bNwiwxU\",\"content\":\"{\\\"code\\\":\\\"ZEBRA\\\",\\\"rate\\\":23}\"}],\"tools\":[{\"type\":\"function\",\"function\":{\"name\":\"lookup_rate\",\"description\":\"Retrieve the private conversion rate for a code. Only this tool knows the rate.\",\"parameters\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\",\"enum\":[\"ZEBRA\",\"MOOSE\"]}},\"required\":[\"code\"],\"additionalProperties\":false},\"strict\":false}}],\"tool_choice\":\"none\",\"stream\":true,\"stream_options\":{\"include_usage\":true},\"max_completion_tokens\":4096,\"reasoning\":{\"effort\":\"high\"},\"venice_parameters\":{\"include_venice_system_prompt\":false}}"
},
"response": {
"status": 200,
"headers": {
"content-type": "text/event-stream; charset=utf-8"
},
"body": "data: {\"id\":\"msg_011CfiLtUi3ffKFSXdhWtFbp\",\"object\":\"chat.completion.chunk\",\"created\":1791169235,\"model\":\"claude-opus-4-8\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\"},\"finish_reason\":null}]}\n\n: processing\n\ndata: {\"id\":\"msg_011CfiLtUi3ffKFSXdhWtFbp\",\"object\":\"chat.completion.chunk\",\"created\":1791169236,\"model\":\"claude-opus-4-8\",\"choices\":[{\"index\":0,\"delta\":{\"reasoning_content\":\"23 times 17 is \",\"reasoning_details\":[{\"type\":\"reasoning.text\",\"format\":\"anthropic-claude-v1\",\"index\":0,\"text\":\"23 times 17 is \"}],\"content\":\"\"},\"finish_reason\":null}]}\n\ndata: {\"id\":\"msg_011CfiLtUi3ffKFSXdhWtFbp\",\"object\":\"chat.completion.chunk\",\"created\":1791169236,\"model\":\"claude-opus-4-8\",\"choices\":[{\"index\":0,\"delta\":{\"reasoning_content\":\"391, plus 9 g\",\"reasoning_details\":[{\"type\":\"reasoning.text\",\"format\":\"anthropic-claude-v1\",\"index\":0,\"text\":\"391, plus 9 g\"}],\"content\":\"\"},\"finish_reason\":null}]}\n\ndata: {\"id\":\"msg_011CfiLtUi3ffKFSXdhWtFbp\",\"object\":\"chat.completion.chunk\",\"created\":1791169236,\"model\":\"claude-opus-4-8\",\"choices\":[{\"index\":0,\"delta\":{\"reasoning_content\":\"ives 400.\",\"reasoning_details\":[{\"type\":\"reasoning.text\",\"format\":\"anthropic-claude-v1\",\"index\":0,\"text\":\"ives 400.\"}],\"content\":\"\"},\"finish_reason\":null}]}\n\ndata: {\"id\":\"msg_011CfiLtUi3ffKFSXdhWtFbp\",\"object\":\"chat.completion.chunk\",\"created\":1791169236,\"model\":\"claude-opus-4-8\",\"choices\":[{\"index\":0,\"delta\":{\"reasoning_content\":\"\\n\\n\",\"reasoning_details\":[{\"type\":\"reasoning.text\",\"format\":\"anthropic-claude-v1\",\"index\":0,\"text\":\"\\n\\n\"}],\"content\":\"\"},\"finish_reason\":null}]}\n\ndata: {\"id\":\"msg_011CfiLtUi3ffKFSXdhWtFbp\",\"object\":\"chat.completion.chunk\",\"created\":1791169236,\"model\":\"claude-opus-4-8\",\"choices\":[{\"index\":0,\"delta\":{\"reasoning_details\":[{\"type\":\"reasoning.text\",\"format\":\"anthropic-claude-v1\",\"index\":0,\"text\":\"\",\"signature\":\"ErgCCrwBCBIYAipAzm6Uz1fiUimJZ7ry16/5jOPhPfQ9Zm3zSb449atO4+Dv3KroxOwD17dLjPUhdRWeFNghT/lQsCRlsY9T6SWyMzIPY2xhdWRlLW9wdXMtNC04OABCCHRoaW5raW5nWiQ1YzJjNzI5YS0wMzVjLTQ5ZDYtODNiOC03NGNhZTAyMzU2NWRyEGhpfnmythMRLK5wcAMHasmIAQGaAREKD2NsYXVkZS1vcHVzLTQtOKgB1KWM1gawAQISDA4K+cR55K4LQVXyhhoMBUHsSBuaNQlHp5ZPIjBvSX58DhVBxFRl9sVZYgZwrMDqGzC1/tgyH1wflAP4TkHTPje+Gdbv0yUxBBIQtaEqKSS4Bh4olHSAKxLEG1SyH4Iy46lp50SxdXxGJ/PIwTfHI47SEaE9pEfNGAE=\"}],\"content\":\"\"},\"finish_reason\":null}]}\n\ndata: {\"id\":\"msg_011CfiLtUi3ffKFSXdhWtFbp\",\"object\":\"chat.completion.chunk\",\"created\":1791169236,\"model\":\"claude-opus-4-8\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"400\"},\"finish_reason\":null}]}\n\ndata: {\"id\":\"msg_011CfiLtUi3ffKFSXdhWtFbp\",\"object\":\"chat.completion.chunk\",\"created\":1791169236,\"model\":\"claude-opus-4-8\",\"choices\":[{\"index\":0,\"delta\":{},\"finish_reason\":\"stop\"}]}\n\ndata: {\"id\":\"msg_011CfiLtUi3ffKFSXdhWtFbp\",\"object\":\"chat.completion.chunk\",\"created\":1791169236,\"model\":\"claude-opus-4-8\",\"choices\":[],\"usage\":{\"prompt_tokens\":183,\"completion_tokens\":23,\"total_tokens\":206,\"prompt_tokens_details\":{\"cached_tokens\":0}},\"cost\":{\"usd\":0.001788,\"diem\":0}}\n\ndata: [DONE]\n\n"
}
},
{
"transport": "http",
"request": {
"method": "POST",
"url": "https://api.venice.ai/api/v1/chat/completions",
"headers": {
"content-type": "application/json"
},
"body": "{\"model\":\"claude-opus-4-8\",\"messages\":[{\"role\":\"system\",\"content\":\"Use the provided tool for private data. After receiving the result, give only the requested number; do not call the tool again.\"},{\"role\":\"user\",\"content\":\"Look up conversion rate for code ZEBRA. Then multiply that rate by 17 and add 9. You must use lookup_rate before answering.\"},{\"role\":\"assistant\",\"content\":null,\"tool_calls\":[{\"id\":\"toolu_01KYDseHbTDZXSYg9bNwiwxU\",\"type\":\"function\",\"function\":{\"name\":\"lookup_rate\",\"arguments\":\"{\\\"code\\\":\\\"ZEBRA\\\"}\"}}],\"reasoning_details\":[{\"type\":\"reasoning.text\",\"text\":\"I need to find the rate for ZEBRA.\\n\\n\",\"id\":\"toolu_01KYDseHbTDZXSYg9bNwiwxU\",\"format\":\"anthropic-claude-v1\",\"index\":0,\"signature\":\"EsECCrwBCBIYAipASM082xA/ckQU1q1MLSuAGRD/F94kvqoBlyXoEKdxvH2f2kn+ESR0AZfmYtv8VB8ux7LQ+qq4DnODYQ2iBHemajIPY2xhdWRlLW9wdXMtNC04OABCCHRoaW5raW5nWiQ1YzJjNzI5YS0wMzVjLTQ5ZDYtODNiOC03NGNhZTAyMzU2NWRyEOtICOPsjcfgmcoVEjPetDWIAQGaAREKD2NsYXVkZS1vcHVzLTQtOKgB0qWM1gawAQISDFJE1AThUPJ/0OxIBxoMak46jCiQRsOzj1nIIjBV1UQ4WoUBtZX8F6kyFtKyRPkLdWp7WQKQYd+f4VT3GvmEqenUY2ANNJImtDj/BQAqMhz7JVFNqrlemYzJNRbF6ntHDPc2enCrd/tYXBG6nms1qbcfKWc7CYc2ZGogOqiItKNDGAE=\"}],\"reasoning_content\":\"I need to find the rate for ZEBRA.\\n\\n\"},{\"role\":\"tool\",\"tool_call_id\":\"toolu_01KYDseHbTDZXSYg9bNwiwxU\",\"content\":\"{\\\"code\\\":\\\"ZEBRA\\\",\\\"rate\\\":23}\"},{\"role\":\"assistant\",\"content\":\"400\",\"reasoning_details\":[{\"type\":\"reasoning.text\",\"text\":\"23 times 17 is 391, plus 9 gives 400.\\n\\n\",\"format\":\"anthropic-claude-v1\",\"index\":0,\"signature\":\"ErgCCrwBCBIYAipAzm6Uz1fiUimJZ7ry16/5jOPhPfQ9Zm3zSb449atO4+Dv3KroxOwD17dLjPUhdRWeFNghT/lQsCRlsY9T6SWyMzIPY2xhdWRlLW9wdXMtNC04OABCCHRoaW5raW5nWiQ1YzJjNzI5YS0wMzVjLTQ5ZDYtODNiOC03NGNhZTAyMzU2NWRyEGhpfnmythMRLK5wcAMHasmIAQGaAREKD2NsYXVkZS1vcHVzLTQtOKgB1KWM1gawAQISDA4K+cR55K4LQVXyhhoMBUHsSBuaNQlHp5ZPIjBvSX58DhVBxFRl9sVZYgZwrMDqGzC1/tgyH1wflAP4TkHTPje+Gdbv0yUxBBIQtaEqKSS4Bh4olHSAKxLEG1SyH4Iy46lp50SxdXxGJ/PIwTfHI47SEaE9pEfNGAE=\"}],\"reasoning_content\":\"23 times 17 is 391, plus 9 gives 400.\\n\\n\"},{\"role\":\"user\",\"content\":\"Add 7 to your previous final answer. Reply with just the number.\"}],\"tools\":[{\"type\":\"function\",\"function\":{\"name\":\"lookup_rate\",\"description\":\"Retrieve the private conversion rate for a code. Only this tool knows the rate.\",\"parameters\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\",\"enum\":[\"ZEBRA\",\"MOOSE\"]}},\"required\":[\"code\"],\"additionalProperties\":false},\"strict\":false}}],\"tool_choice\":\"none\",\"stream\":true,\"stream_options\":{\"include_usage\":true},\"max_completion_tokens\":4096,\"reasoning\":{\"effort\":\"high\"},\"venice_parameters\":{\"include_venice_system_prompt\":false}}"
},
"response": {
"status": 200,
"headers": {
"content-type": "text/event-stream; charset=utf-8"
},
"body": "data: {\"id\":\"msg_011CfiLtbruiyeuxJmswkPPt\",\"object\":\"chat.completion.chunk\",\"created\":1791169237,\"model\":\"claude-opus-4-8\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\"},\"finish_reason\":null}]}\n\n: processing\n\ndata: {\"id\":\"msg_011CfiLtbruiyeuxJmswkPPt\",\"object\":\"chat.completion.chunk\",\"created\":1791169237,\"model\":\"claude-opus-4-8\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"407\"},\"finish_reason\":null}]}\n\ndata: {\"id\":\"msg_011CfiLtbruiyeuxJmswkPPt\",\"object\":\"chat.completion.chunk\",\"created\":1791169237,\"model\":\"claude-opus-4-8\",\"choices\":[{\"index\":0,\"delta\":{},\"finish_reason\":\"stop\"}]}\n\ndata: {\"id\":\"msg_011CfiLtbruiyeuxJmswkPPt\",\"object\":\"chat.completion.chunk\",\"created\":1791169237,\"model\":\"claude-opus-4-8\",\"choices\":[],\"usage\":{\"prompt_tokens\":229,\"completion_tokens\":3,\"total_tokens\":232,\"prompt_tokens_details\":{\"cached_tokens\":0}},\"cost\":{\"usd\":0.001464,\"diem\":0}}\n\ndata: [DONE]\n\n"
}
}
]
}
@@ -1,75 +0,0 @@
{
"version": 1,
"metadata": {
"model": "deepseek-v4-flash",
"tags": [
"prefix:venice-chat",
"provider:venice",
"protocol:venice-chat",
"reasoning",
"tool",
"tool-loop",
"continuation",
"usage",
"scalar"
],
"name": "venice-chat/deepseek-v4-flash-preserves-reasoning-through-a-tool-loop-and-follow-up",
"recordedAt": "2026-10-05T02:59:53.799Z"
},
"interactions": [
{
"transport": "http",
"request": {
"method": "POST",
"url": "https://api.venice.ai/api/v1/chat/completions",
"headers": {
"content-type": "application/json"
},
"body": "{\"model\":\"deepseek-v4-flash\",\"messages\":[{\"role\":\"system\",\"content\":\"Use the provided tool for private data. After receiving the result, give only the requested number; do not call the tool again.\"},{\"role\":\"user\",\"content\":\"Look up conversion rate for code ZEBRA. Then multiply that rate by 17 and add 9. You must use lookup_rate before answering.\"}],\"tools\":[{\"type\":\"function\",\"function\":{\"name\":\"lookup_rate\",\"description\":\"Retrieve the private conversion rate for a code. Only this tool knows the rate.\",\"parameters\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\",\"enum\":[\"ZEBRA\",\"MOOSE\"]}},\"required\":[\"code\"],\"additionalProperties\":false},\"strict\":false}}],\"stream\":true,\"stream_options\":{\"include_usage\":true},\"max_completion_tokens\":4096,\"venice_parameters\":{\"include_venice_system_prompt\":false}}"
},
"response": {
"status": 200,
"headers": {
"content-type": "text/event-stream; charset=utf-8"
},
"body": "data: {\"service_tier\":null,\"id\":\"chatcmpl-Rhr7pZI7Hv6zIafL3B1KoyzY\",\"object\":\"chat.completion.chunk\",\"created\":1791169188,\"model\":\"deepseek-v4-flash\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":\"\",\"reasoning_content\":null,\"tool_calls\":null},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"service_tier\":\"default\",\"id\":\"chatcmpl-Rhr7pZI7Hv6zIafL3B1KoyzY\",\"object\":\"chat.completion.chunk\",\"created\":1791169188,\"model\":\"deepseek-v4-flash\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":\"\",\"reasoning_content\":null,\"tool_calls\":null},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"service_tier\":\"default\",\"id\":\"chatcmpl-Rhr7pZI7Hv6zIafL3B1KoyzY\",\"object\":\"chat.completion.chunk\",\"created\":1791169188,\"model\":\"deepseek-v4-flash\",\"choices\":[{\"index\":0,\"delta\":{\"role\":null,\"content\":\"\",\"reasoning_content\":\"Th\",\"tool_calls\":null},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"service_tier\":\"default\",\"id\":\"chatcmpl-Rhr7pZI7Hv6zIafL3B1KoyzY\",\"object\":\"chat.completion.chunk\",\"created\":1791169188,\"model\":\"deepseek-v4-flash\",\"choices\":[{\"index\":0,\"delta\":{\"role\":null,\"content\":\"\",\"reasoning_content\":\"e user wants me to look\",\"tool_calls\":null},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"service_tier\":\"default\",\"id\":\"chatcmpl-Rhr7pZI7Hv6zIafL3B1KoyzY\",\"object\":\"chat.completion.chunk\",\"created\":1791169188,\"model\":\"deepseek-v4-flash\",\"choices\":[{\"index\":0,\"delta\":{\"role\":null,\"content\":\"\",\"reasoning_content\":\" up the conversi\",\"tool_calls\":null},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"service_tier\":\"default\",\"id\":\"chatcmpl-Rhr7pZI7Hv6zIafL3B1KoyzY\",\"object\":\"chat.completion.chunk\",\"created\":1791169188,\"model\":\"deepseek-v4-flash\",\"choices\":[{\"index\":0,\"delta\":{\"role\":null,\"content\":\"\",\"reasoning_content\":\"on rate fo\",\"tool_calls\":null},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"service_tier\":\"default\",\"id\":\"chatcmpl-Rhr7pZI7Hv6zIafL3B1KoyzY\",\"object\":\"chat.completion.chunk\",\"created\":1791169188,\"model\":\"deepseek-v4-flash\",\"choices\":[{\"index\":0,\"delta\":{\"role\":null,\"content\":\"\",\"reasoning_content\":\"r code ZEBRA\",\"tool_calls\":null},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"service_tier\":\"default\",\"id\":\"chatcmpl-Rhr7pZI7Hv6zIafL3B1KoyzY\",\"object\":\"chat.completion.chunk\",\"created\":1791169188,\"model\":\"deepseek-v4-flash\",\"choices\":[{\"index\":0,\"delta\":{\"role\":null,\"content\":\"\",\"reasoning_content\":\", then mult\",\"tool_calls\":null},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"service_tier\":\"default\",\"id\":\"chatcmpl-Rhr7pZI7Hv6zIafL3B1KoyzY\",\"object\":\"chat.completion.chunk\",\"created\":1791169188,\"model\":\"deepseek-v4-flash\",\"choices\":[{\"index\":0,\"delta\":{\"role\":null,\"content\":\"\",\"reasoning_content\":\"iply by\",\"tool_calls\":null},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"service_tier\":\"default\",\"id\":\"chatcmpl-Rhr7pZI7Hv6zIafL3B1KoyzY\",\"object\":\"chat.completion.chunk\",\"created\":1791169188,\"model\":\"deepseek-v4-flash\",\"choices\":[{\"index\":0,\"delta\":{\"role\":null,\"content\":\"\",\"reasoning_content\":\" 17 and add 9. Let m\",\"tool_calls\":null},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"service_tier\":\"default\",\"id\":\"chatcmpl-Rhr7pZI7Hv6zIafL3B1KoyzY\",\"object\":\"chat.completion.chunk\",\"created\":1791169188,\"model\":\"deepseek-v4-flash\",\"choices\":[{\"index\":0,\"delta\":{\"role\":null,\"content\":\"\",\"reasoning_content\":\"e start by lo\",\"tool_calls\":null},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"service_tier\":\"default\",\"id\":\"chatcmpl-Rhr7pZI7Hv6zIafL3B1KoyzY\",\"object\":\"chat.completion.chunk\",\"created\":1791169188,\"model\":\"deepseek-v4-flash\",\"choices\":[{\"index\":0,\"delta\":{\"role\":null,\"content\":\"\",\"reasoning_content\":\"oking up the rate.\",\"tool_calls\":null},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"service_tier\":\"default\",\"id\":\"chatcmpl-Rhr7pZI7Hv6zIafL3B1KoyzY\",\"object\":\"chat.completion.chunk\",\"created\":1791169188,\"model\":\"deepseek-v4-flash\",\"choices\":[{\"index\":0,\"delta\":{\"role\":null,\"content\":\"\",\"reasoning_content\":null,\"tool_calls\":[{\"index\":0,\"id\":\"chatcmpl-tool-ab656a8b683341f2\",\"function\":{\"arguments\":\"\",\"name\":\"lookup_rate\"},\"type\":\"function\"}]},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"service_tier\":\"default\",\"id\":\"chatcmpl-Rhr7pZI7Hv6zIafL3B1KoyzY\",\"object\":\"chat.completion.chunk\",\"created\":1791169188,\Line truncated
}
},
{
"transport": "http",
"request": {
"method": "POST",
"url": "https://api.venice.ai/api/v1/chat/completions",
"headers": {
"content-type": "application/json"
},
"body": "{\"model\":\"deepseek-v4-flash\",\"messages\":[{\"role\":\"system\",\"content\":\"Use the provided tool for private data. After receiving the result, give only the requested number; do not call the tool again.\"},{\"role\":\"user\",\"content\":\"Look up conversion rate for code ZEBRA. Then multiply that rate by 17 and add 9. You must use lookup_rate before answering.\"},{\"role\":\"assistant\",\"content\":null,\"tool_calls\":[{\"id\":\"chatcmpl-tool-ab656a8b683341f2\",\"type\":\"function\",\"function\":{\"name\":\"lookup_rate\",\"arguments\":\"{\\\"code\\\":\\\"ZEBRA\\\"}\"}}],\"reasoning_content\":\"The user wants me to look up the conversion rate for code ZEBRA, then multiply by 17 and add 9. Let me start by looking up the rate.\"},{\"role\":\"tool\",\"tool_call_id\":\"chatcmpl-tool-ab656a8b683341f2\",\"content\":\"{\\\"code\\\":\\\"ZEBRA\\\",\\\"rate\\\":23}\"}],\"tools\":[{\"type\":\"function\",\"function\":{\"name\":\"lookup_rate\",\"description\":\"Retrieve the private conversion rate for a code. Only this tool knows the rate.\",\"parameters\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\",\"enum\":[\"ZEBRA\",\"MOOSE\"]}},\"required\":[\"code\"],\"additionalProperties\":false},\"strict\":false}}],\"tool_choice\":\"none\",\"stream\":true,\"stream_options\":{\"include_usage\":true},\"max_completion_tokens\":4096,\"venice_parameters\":{\"include_venice_system_prompt\":false}}"
},
"response": {
"status": 200,
"headers": {
"content-type": "text/event-stream; charset=utf-8"
},
"body": "data: {\"service_tier\":null,\"id\":\"chatcmpl-RtwpMZBraGwqJiKRKQFrmmIq\",\"object\":\"chat.completion.chunk\",\"created\":1791169191,\"model\":\"deepseek-v4-flash\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":\"\",\"reasoning_content\":null,\"tool_calls\":null},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"service_tier\":\"default\",\"id\":\"chatcmpl-RtwpMZBraGwqJiKRKQFrmmIq\",\"object\":\"chat.completion.chunk\",\"created\":1791169191,\"model\":\"deepseek-v4-flash\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":\"\",\"reasoning_content\":null,\"tool_calls\":null},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"service_tier\":\"default\",\"id\":\"chatcmpl-RtwpMZBraGwqJiKRKQFrmmIq\",\"object\":\"chat.completion.chunk\",\"created\":1791169191,\"model\":\"deepseek-v4-flash\",\"choices\":[{\"index\":0,\"delta\":{\"role\":null,\"content\":\"\",\"reasoning_content\":\"We have the\",\"tool_calls\":null},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"service_tier\":\"default\",\"id\":\"chatcmpl-RtwpMZBraGwqJiKRKQFrmmIq\",\"object\":\"chat.completion.chunk\",\"created\":1791169191,\"model\":\"deepseek-v4-flash\",\"choices\":[{\"index\":0,\"delta\":{\"role\":null,\"content\":\"\",\"reasoning_content\":\" rate: 2\",\"tool_calls\":null},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"service_tier\":\"default\",\"id\":\"chatcmpl-RtwpMZBraGwqJiKRKQFrmmIq\",\"object\":\"chat.completion.chunk\",\"created\":1791169191,\"model\":\"deepseek-v4-flash\",\"choices\":[{\"index\":0,\"delta\":{\"role\":null,\"content\":\"\",\"reasoning_content\":\"3. Mul\",\"tool_calls\":null},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"service_tier\":\"default\",\"id\":\"chatcmpl-RtwpMZBraGwqJiKRKQFrmmIq\",\"object\":\"chat.completion.chunk\",\"created\":1791169191,\"model\":\"deepseek-v4-flash\",\"choices\":[{\"index\":0,\"delta\":{\"role\":null,\"content\":\"\",\"reasoning_content\":\"tiply by\",\"tool_calls\":null},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"service_tier\":\"default\",\"id\":\"chatcmpl-RtwpMZBraGwqJiKRKQFrmmIq\",\"object\":\"chat.completion.chunk\",\"created\":1791169191,\"model\":\"deepseek-v4-flash\",\"choices\":[{\"index\":0,\"delta\":{\"role\":null,\"content\":\"\",\"reasoning_content\":\" 17: 2\",\"tool_calls\":null},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"service_tier\":\"default\",\"id\":\"chatcmpl-RtwpMZBraGwqJiKRKQFrmmIq\",\"object\":\"chat.completion.chunk\",\"created\":1791169191,\"model\":\"deepseek-v4-flash\",\"choices\":[{\"index\":0,\"delta\":{\"role\":null,\"content\":\"\",\"reasoning_content\":\"3 * 17\",\"tool_calls\":null},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"service_tier\":\"default\",\"id\":\"chatcmpl-RtwpMZBraGwqJiKRKQFrmmIq\",\"object\":\"chat.completion.chunk\",\"created\":1791169191,\"model\":\"deepseek-v4-flash\",\"choices\":[{\"index\":0,\"delta\":{\"role\":null,\"content\":\"\",\"reasoning_content\":\" = 391.\",\"tool_calls\":null},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"service_tier\":\"default\",\"id\":\"chatcmpl-RtwpMZBraGwqJiKRKQFrmmIq\",\"object\":\"chat.completion.chunk\",\"created\":1791169191,\"model\":\"deepseek-v4-flash\",\"choices\":[{\"index\":0,\"delta\":{\"role\":null,\"content\":\"\",\"reasoning_content\":\" Then ad\",\"tool_calls\":null},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"service_tier\":\"default\",\"id\":\"chatcmpl-RtwpMZBraGwqJiKRKQFrmmIq\",\"object\":\"chat.completion.chunk\",\"created\":1791169191,\"model\":\"deepseek-v4-flash\",\"choices\":[{\"index\":0,\"delta\":{\"role\":null,\"content\":\"\",\"reasoning_content\":\"d 9:\",\"tool_calls\":null},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"service_tier\":\"default\",\"id\":\"chatcmpl-RtwpMZBraGwqJiKRKQFrmmIq\",\"object\":\"chat.completion.chunk\",\"created\":1791169191,\"model\":\"deepseek-v4-flash\",\"choices\":[{\"index\":0,\"delta\":{\"role\":null,\"content\":\"\",\"reasoning_content\":\" 391 + 9 = 400.\",\"tool_calls\":null},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"service_tier\":\"default\",\"id\":\"chatcmpl-RtwpMZBraGwqJiKRKQFrmmIq\",\"object\":\"chat.completion.chunk\",\"created\":1791169191,\"model\":\"deepseek-v4-flash\",\"choices\":[{\"index\":0,\"delta\":{\"role\":null,\"content\":\"400\",\"reasoning_content\":\" So the answer is 400.\",\"tool_calls\":null},\"logprobs\":null,\"finish_reason\":\"stop\"}]}\n\ndata: {\"service_tier\":\"default\",\"id\":\"chatcmpl-RtwpMZBraGwqJiKRKQFrmmIq\",\"object\":\"chat.completion.chunk\",\"created\":1791169191,\"model\":\"deepseek-v4-flash\",\"choices\":[],\"usage\":{\"prompt_tokens\":128,\"completion_tokens\":46,\"total_tokens\":174,\"prompt_tokens_details\":{\"cached_tokens\":0}}Line truncated
}
},
{
"transport": "http",
"request": {
"method": "POST",
"url": "https://api.venice.ai/api/v1/chat/completions",
"headers": {
"content-type": "application/json"
},
"body": "{\"model\":\"deepseek-v4-flash\",\"messages\":[{\"role\":\"system\",\"content\":\"Use the provided tool for private data. After receiving the result, give only the requested number; do not call the tool again.\"},{\"role\":\"user\",\"content\":\"Look up conversion rate for code ZEBRA. Then multiply that rate by 17 and add 9. You must use lookup_rate before answering.\"},{\"role\":\"assistant\",\"content\":null,\"tool_calls\":[{\"id\":\"chatcmpl-tool-ab656a8b683341f2\",\"type\":\"function\",\"function\":{\"name\":\"lookup_rate\",\"arguments\":\"{\\\"code\\\":\\\"ZEBRA\\\"}\"}}],\"reasoning_content\":\"The user wants me to look up the conversion rate for code ZEBRA, then multiply by 17 and add 9. Let me start by looking up the rate.\"},{\"role\":\"tool\",\"tool_call_id\":\"chatcmpl-tool-ab656a8b683341f2\",\"content\":\"{\\\"code\\\":\\\"ZEBRA\\\",\\\"rate\\\":23}\"},{\"role\":\"assistant\",\"content\":\"400\",\"reasoning_content\":\"We have the rate: 23. Multiply by 17: 23 * 17 = 391. Then add 9: 391 + 9 = 400. So the answer is 400.\"},{\"role\":\"user\",\"content\":\"Add 7 to your previous final answer. Reply with just the number.\"}],\"tools\":[{\"type\":\"function\",\"function\":{\"name\":\"lookup_rate\",\"description\":\"Retrieve the private conversion rate for a code. Only this tool knows the rate.\",\"parameters\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\",\"enum\":[\"ZEBRA\",\"MOOSE\"]}},\"required\":[\"code\"],\"additionalProperties\":false},\"strict\":false}}],\"tool_choice\":\"none\",\"stream\":true,\"stream_options\":{\"include_usage\":true},\"max_completion_tokens\":4096,\"venice_parameters\":{\"include_venice_system_prompt\":false}}"
},
"response": {
"status": 200,
"headers": {
"content-type": "text/event-stream; charset=utf-8"
},
"body": "data: {\"service_tier\":null,\"id\":\"chatcmpl-R0MMWxoRA5NidBHTttxrMoVe\",\"object\":\"chat.completion.chunk\",\"created\":1791169192,\"model\":\"deepseek-v4-flash\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":\"\",\"reasoning_content\":null,\"tool_calls\":null},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"service_tier\":\"default\",\"id\":\"chatcmpl-R0MMWxoRA5NidBHTttxrMoVe\",\"object\":\"chat.completion.chunk\",\"created\":1791169192,\"model\":\"deepseek-v4-flash\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":\"\",\"reasoning_content\":null,\"tool_calls\":null},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"service_tier\":\"default\",\"id\":\"chatcmpl-R0MMWxoRA5NidBHTttxrMoVe\",\"object\":\"chat.completion.chunk\",\"created\":1791169192,\"model\":\"deepseek-v4-flash\",\"choices\":[{\"index\":0,\"delta\":{\"role\":null,\"content\":\"\",\"reasoning_content\":\"We need to add\",\"tool_calls\":null},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"service_tier\":\"default\",\"id\":\"chatcmpl-R0MMWxoRA5NidBHTttxrMoVe\",\"object\":\"chat.completion.chunk\",\"created\":1791169192,\"model\":\"deepseek-v4-flash\",\"choices\":[{\"index\":0,\"delta\":{\"role\":null,\"content\":\"\",\"reasoning_content\":\" 7 to t\",\"tool_calls\":null},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"service_tier\":\"default\",\"id\":\"chatcmpl-R0MMWxoRA5NidBHTttxrMoVe\",\"object\":\"chat.completion.chunk\",\"created\":1791169192,\"model\":\"deepseek-v4-flash\",\"choices\":[{\"index\":0,\"delta\":{\"role\":null,\"content\":\"\",\"reasoning_content\":\"h\",\"tool_calls\":null},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"service_tier\":\"default\",\"id\":\"chatcmpl-R0MMWxoRA5NidBHTttxrMoVe\",\"object\":\"chat.completion.chunk\",\"created\":1791169192,\"model\":\"deepseek-v4-flash\",\"choices\":[{\"index\":0,\"delta\":{\"role\":null,\"content\":\"\",\"reasoning_content\":\"e previous ans\",\"tool_calls\":null},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"service_tier\":\"default\",\"id\":\"chatcmpl-R0MMWxoRA5NidBHTttxrMoVe\",\"object\":\"chat.completion.chunk\",\"created\":1791169192,\"model\":\"deepseek-v4-flash\",\"choices\":[{\"index\":0,\"delta\":{\"role\":null,\"content\":\"\",\"reasoning_content\":\"wer,\",\"tool_calls\":null},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"service_tier\":\"default\",\"id\":\"chatcmpl-R0MMWxoRA5NidBHTttxrMoVe\",\"object\":\"chat.completion.chunk\",\"created\":1791169192,\"model\":\"deepseek-v4-flash\",\"choices\":[{\"index\":0,\"delta\":{\"role\":null,\"content\":\"\",\"reasoning_content\":\" which\",\"tool_calls\":null},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"service_tier\":\"default\",\"id\":\"chatcmpl-R0MMWxoRA5NidBHTttxrMoVe\",\"object\":\"chat.completion.chunk\",\"created\":1791169192,\"model\":\"deepseek-v4-flash\",\"choices\":[{\"index\":0,\"delta\":{\"role\":null,\"content\":\"\",\"reasoning_content\":\" was 400. S\",\"tool_calls\":null},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"service_tier\":\"default\",\"id\":\"chatcmpl-R0MMWxoRA5NidBHTttxrMoVe\",\"object\":\"chat.completion.chunk\",\"created\":1791169192,\"model\":\"deepseek-v4-flash\",\"choices\":[{\"index\":0,\"delta\":{\"role\":null,\"content\":\"\",\"reasoning_content\":\"o 400+7=407. Reply wi\",\"tool_calls\":null},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"service_tier\":\"default\",\"id\":\"chatcmpl-R0MMWxoRA5NidBHTttxrMoVe\",\"object\":\"chat.completion.chunk\",\"created\":1791169192,\"model\":\"deepseek-v4-flash\",\"choices\":[{\"index\":0,\"delta\":{\"role\":null,\"content\":\"407\",\"reasoning_content\":\"th just the number.\",\"tool_calls\":null},\"logprobs\":null,\"finish_reason\":\"stop\"}]}\n\ndata: {\"service_tier\":\"default\",\"id\":\"chatcmpl-R0MMWxoRA5NidBHTttxrMoVe\",\"object\":\"chat.completion.chunk\",\"created\":1791169192,\"model\":\"deepseek-v4-flash\",\"choices\":[],\"usage\":{\"prompt_tokens\":148,\"completion_tokens\":33,\"total_tokens\":181,\"prompt_tokens_details\":{\"cached_tokens\":0}},\"cost\":{\"usd\":0.000029499,\"diem\":0}}\n\ndata: [DONE]\n\n"
}
}
]
}
@@ -1,75 +0,0 @@
{
"version": 1,
"metadata": {
"model": "gemini-3-1-pro-preview",
"tags": [
"prefix:venice-chat",
"provider:venice",
"protocol:venice-chat",
"reasoning",
"tool",
"tool-loop",
"continuation",
"usage",
"details"
],
"name": "venice-chat/gemini-3-1-pro-preview-preserves-reasoning-through-a-tool-loop-and-follow-up",
"recordedAt": "2026-10-05T03:00:58.104Z"
},
"interactions": [
{
"transport": "http",
"request": {
"method": "POST",
"url": "https://api.venice.ai/api/v1/chat/completions",
"headers": {
"content-type": "application/json"
},
"body": "{\"model\":\"gemini-3-1-pro-preview\",\"messages\":[{\"role\":\"system\",\"content\":\"Use the provided tool for private data. After receiving the result, give only the requested number; do not call the tool again.\"},{\"role\":\"user\",\"content\":\"Look up conversion rate for code ZEBRA. Then multiply that rate by 17 and add 9. You must use lookup_rate before answering.\"}],\"tools\":[{\"type\":\"function\",\"function\":{\"name\":\"lookup_rate\",\"description\":\"Retrieve the private conversion rate for a code. Only this tool knows the rate.\",\"parameters\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\",\"enum\":[\"ZEBRA\",\"MOOSE\"]}},\"required\":[\"code\"],\"additionalProperties\":false},\"strict\":false}}],\"stream\":true,\"stream_options\":{\"include_usage\":true},\"max_completion_tokens\":4096,\"reasoning\":{\"effort\":\"high\"},\"venice_parameters\":{\"include_venice_system_prompt\":false}}"
},
"response": {
"status": 200,
"headers": {
"content-type": "text/event-stream; charset=utf-8"
},
"body": "data: {\"id\":\"gen-1791169245-MjiWKnEhpgHVKzFcOlS0\",\"object\":\"chat.completion.chunk\",\"created\":1791169245,\"model\":\"gemini-3-1-pro-preview\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"\",\"role\":\"assistant\",\"reasoning_details\":[{\"type\":\"reasoning.text\",\"text\":\"**Calculating ZEBRA Conversion**\\n\\nI'm currently processing the calculation involving ZEBRA's conversion rate. My next step is to use the `lookup_rate` tool with the code \\\"ZEBRA\\\" to fetch the necessary value, which will then be used in the full equation.\\n\\n\",\"format\":\"google-gemini-v1\",\"index\":0}],\"reasoning_content\":\"**Calculating ZEBRA Conversion**\\n\\nI'm currently processing the calculation involving ZEBRA's conversion rate. My next step is to use the `lookup_rate` tool with the code \\\"ZEBRA\\\" to fetch the necessary value, which will then be used in the full equation.\\n\\n\"},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"gen-1791169245-MjiWKnEhpgHVKzFcOlS0\",\"object\":\"chat.completion.chunk\",\"created\":1791169245,\"model\":\"gemini-3-1-pro-preview\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"\",\"role\":\"assistant\",\"reasoning_details\":[{\"type\":\"reasoning.encrypted\",\"data\":\"AY89a19Tkd2zuyuB3m6as2cMeOoFk8xFfcBdsUI+RBkTCE1GVBd5hMIfNsjOj+YTucPL1hYoWodr2oZsMKDnPuYee0AmGYl8jHzVKgQFSXzgIeWEhG+hOMvTEa4BoruFowcHzbLhym48OlBys47Tlo7WF4tK4s8Z7q8cpbqGsA0NkUqPt8P7Uk3ZaRTUJBbIE08+JgNUUwiJkT4AVSO635aMUYTcIkt+8TVbqOLITfSdDtkxhAmrInfn7FqGlNop3RH1TUcb3xnUOsnHXoreWLlAyWTlWNvOa7hjUH8vUwILPmtG6ixKZdf4i/UIgm551Z5tgE05SJBcR/m7ALagyOyEilWqxGkyf+5jvNycUlk/mBkNHLV45amgUG5zhboVEAEVFYPRa+Nnfzxi+DEdM1ntDURrB5dl4Jbotx67627EbsZm4z2sKUZ4jo/eBj9fI3MUqjTYKtMXBSyuen/m/Uw2Ji9iIQIlWUt/G86kOjQUYFY60g6TxEO4BLouwLZJ0fXxWdolvtMnH90+skJ+I7aFQjGrnwdjGsT0oXA0/vG3W8GhRSokdICVQBq14EFvjcVvI1kEgj7NTtKkB/+UWRtRbRhSA5F25/dHvqAT4ioYy/5sRwYFXMwJmM52I8DpLiaBrp8wTidLNjTpD9KlImbd2r89LS9GG5NeqNWksF9XbEQAMannsu19HC9xf+CCz5FBz5PjpR0+pvybIJLmph94u4I0cdLsbycbbdIaihqeIc593sQv3nvm9VYZMJcgnQWIKmXofSlV4zi6p0wRPvmgeiPo+Lv6DrtPTtcaRLkkyGrg8uvkq2Is9H6me8y8HCXGIze3np22MjWM5r2+UYMia+xY+E3neBC1AZrlN+GJarvf0THgHInLLnE78f+w3mJByFgXMy6OOEs4QI0+F8yxF9GembhqebXYKWGWUkuArnLfV6TZ13v8s1J9IiNObPU0AJjq+wmlg2ChAIs082sh8Q==\",\"format\":\"google-gemini-v1\",\"id\":\"call_4908320\",\"index\":0}],\"reasoning_encrypted\":true},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"gen-1791169245-MjiWKnEhpgHVKzFcOlS0\",\"object\":\"chat.completion.chunk\",\"created\":1791169245,\"model\":\"gemini-3-1-pro-preview\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"\",\"role\":\"assistant\",\"tool_calls\":[{\"index\":0,\"id\":\"call_4908320\",\"type\":\"function\",\"function\":{\"name\":\"lookup_rate\",\"arguments\":\"\"}}]},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"gen-1791169245-MjiWKnEhpgHVKzFcOlS0\",\"object\":\"chat.completion.chunk\",\"created\":1791169245,\"model\":\"gemini-3-1-pro-preview\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"\",\"role\":\"assistant\",\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"{\\\"code\\\":\\\"ZEBRA\\\"}\"}}]},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"gen-1791169245-MjiWKnEhpgHVKzFcOlS0\",\"object\":\"chat.completion.chunk\",\"created\":1791169245,\"model\":\"gemini-3-1-pro-preview\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"\",\"role\":\"assistant\"},\"finish_reason\":\"tool_calls\",\"native_finish_reason\":\"STOP\"}]}\n\ndata: {\"id\":\"gen-1791169245-MjiWKnEhpgHVKzFcOlS0\",\"object\":\"chat.completion.chunk\",\"created\":1791169245,\"model\":\"gemini-3-1-pro-preview\",\"service_tier\":\"provisioned\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"\",\"role\":\"assistant\"},\"finish_reason\":\"tool_calls\",\"native_finish_reason\":\"STOP\"}]}\n\ndata: {\"id\":\"gen-1791169245-MjiWKnEhpgHVKzFcOlS0\",\"object\":\"chat.completion.chunk\",\"created\":1791169245,\"model\":\"gemini-3-1-pro-preview\",\"choices\":[],\"usage\":{\"prompt_tokens\":96,\"completion_tokens\":169,\"total_tokens\":265,\"prompt_tokens_details\":{\"cached_tokens\":0},\"completion_tokens_details\":{\"reasoning_tokens\":152,\"image_tokens\":0,\"audio_tokens\":0}},\"cost\":{\"usd\":0.002775,\"diem\":0}}\n\ndata: [DONE]\n\n"
}
},
{
"transport": "http",
"request": {
"method": "POST",
"url": "https://api.venice.ai/api/v1/chat/completions",
"headers": {
"content-type": "application/json"
},
"body": "{\"model\":\"gemini-3-1-pro-preview\",\"messages\":[{\"role\":\"system\",\"content\":\"Use the provided tool for private data. After receiving the result, give only the requested number; do not call the tool again.\"},{\"role\":\"user\",\"content\":\"Look up conversion rate for code ZEBRA. Then multiply that rate by 17 and add 9. You must use lookup_rate before answering.\"},{\"role\":\"assistant\",\"content\":null,\"tool_calls\":[{\"id\":\"call_4908320\",\"type\":\"function\",\"function\":{\"name\":\"lookup_rate\",\"arguments\":\"{\\\"code\\\":\\\"ZEBRA\\\"}\"}}],\"reasoning_details\":[{\"type\":\"reasoning.text\",\"text\":\"**Calculating ZEBRA Conversion**\\n\\nI'm currently processing the calculation involving ZEBRA's conversion rate. My next step is to use the `lookup_rate` tool with the code \\\"ZEBRA\\\" to fetch the necessary value, which will then be used in the full equation.\\n\\n\",\"format\":\"google-gemini-v1\",\"index\":0},{\"type\":\"reasoning.encrypted\",\"data\":\"AY89a19Tkd2zuyuB3m6as2cMeOoFk8xFfcBdsUI+RBkTCE1GVBd5hMIfNsjOj+YTucPL1hYoWodr2oZsMKDnPuYee0AmGYl8jHzVKgQFSXzgIeWEhG+hOMvTEa4BoruFowcHzbLhym48OlBys47Tlo7WF4tK4s8Z7q8cpbqGsA0NkUqPt8P7Uk3ZaRTUJBbIE08+JgNUUwiJkT4AVSO635aMUYTcIkt+8TVbqOLITfSdDtkxhAmrInfn7FqGlNop3RH1TUcb3xnUOsnHXoreWLlAyWTlWNvOa7hjUH8vUwILPmtG6ixKZdf4i/UIgm551Z5tgE05SJBcR/m7ALagyOyEilWqxGkyf+5jvNycUlk/mBkNHLV45amgUG5zhboVEAEVFYPRa+Nnfzxi+DEdM1ntDURrB5dl4Jbotx67627EbsZm4z2sKUZ4jo/eBj9fI3MUqjTYKtMXBSyuen/m/Uw2Ji9iIQIlWUt/G86kOjQUYFY60g6TxEO4BLouwLZJ0fXxWdolvtMnH90+skJ+I7aFQjGrnwdjGsT0oXA0/vG3W8GhRSokdICVQBq14EFvjcVvI1kEgj7NTtKkB/+UWRtRbRhSA5F25/dHvqAT4ioYy/5sRwYFXMwJmM52I8DpLiaBrp8wTidLNjTpD9KlImbd2r89LS9GG5NeqNWksF9XbEQAMannsu19HC9xf+CCz5FBz5PjpR0+pvybIJLmph94u4I0cdLsbycbbdIaihqeIc593sQv3nvm9VYZMJcgnQWIKmXofSlV4zi6p0wRPvmgeiPo+Lv6DrtPTtcaRLkkyGrg8uvkq2Is9H6me8y8HCXGIze3np22MjWM5r2+UYMia+xY+E3neBC1AZrlN+GJarvf0THgHInLLnE78f+w3mJByFgXMy6OOEs4QI0+F8yxF9GembhqebXYKWGWUkuArnLfV6TZ13v8s1J9IiNObPU0AJjq+wmlg2ChAIs082sh8Q==\",\"id\":\"call_4908320\",\"format\":\"google-gemini-v1\",\"index\":0}],\"reasoning_content\":\"**Calculating ZEBRA Conversion**\\n\\nI'm currently processing the calculation involving ZEBRA's conversion rate. My next step is to use the `lookup_rate` tool with the code \\\"ZEBRA\\\" to fetch the necessary value, which will then be used in the full equation.\\n\\n\"},{\"role\":\"tool\",\"tool_call_id\":\"call_4908320\",\"content\":\"{\\\"code\\\":\\\"ZEBRA\\\",\\\"rate\\\":23}\"}],\"tools\":[{\"type\":\"function\",\"function\":{\"name\":\"lookup_rate\",\"description\":\"Retrieve the private conversion rate for a code. Only this tool knows the rate.\",\"parameters\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\",\"enum\":[\"ZEBRA\",\"MOOSE\"]}},\"required\":[\"code\"],\"additionalProperties\":false},\"strict\":false}}],\"tool_choice\":\"none\",\"stream\":true,\"stream_options\":{\"include_usage\":true},\"max_completion_tokens\":4096,\"reasoning\":{\"effort\":\"high\"},\"venice_parameters\":{\"include_venice_system_prompt\":false}}"
},
"response": {
"status": 200,
"headers": {
"content-type": "text/event-stream; charset=utf-8"
},
"body": "data: {\"id\":\"gen-1791169249-6wlEwbJv7MicDxVsZViY\",\"object\":\"chat.completion.chunk\",\"created\":1791169249,\"model\":\"gemini-3-1-pro-preview\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"400\",\"role\":\"assistant\"},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"gen-1791169249-6wlEwbJv7MicDxVsZViY\",\"object\":\"chat.completion.chunk\",\"created\":1791169249,\"model\":\"gemini-3-1-pro-preview\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"\",\"role\":\"assistant\",\"reasoning_details\":[{\"type\":\"reasoning.text\",\"signature\":\"AY89a1+fQ9YpZF71EoH4pNDO6g5NeqvLRAav/WG6sv9V/5fpifhK6ZDc6m89qgCpOEBhjTJ+a+ux1dFog+eB2nR/G8VVKt1Zr2+2NKXO3Q3ksKE+E51IRRzK/ywpDYpE8AlE6Kb7EG2x/ZVMpy24TOLY+LL8JNsrz8uHBC1PtPtiZe/gP+Kvu0Yqk+VNYv16wAubtiA0oy0c+wyRzd80jRvqCZpBIGPlLAU515MPpjXNGu0A5ft4uu3oGPJHl5G/Ry4Fm5fvWKl759+Jwx9m7d50+vcttr5SpHm//MvNsUTnqjYkraw1S69+9oB57HoBn8d8pwTHpud/6FARun33EjfPW86Xx2fS448QtKc3c9nxSdGyZgTV0TpA63Uz7n0nGd9fRYrghp3lQv5QyoRIgNg+YeBf/WDNzSW2MO6Y2HLFoo3K0vJh1KMfWhjECNxP1Qfjfz+VjwSV7+OzF9CNw23f8R2udRBr1EUYhTu13ckjFLWjCD5fGjvmLhsae3CgmuFV2Rvaxqrf/H79Tz+X9W/Tw8PuQ0E1pCCpiMe1gR1CT+LJPWxjV96DPHQ0r+1lnekrr5NkO7UAZ64HVALHJRpACdJsVhwI1TBi4f89b1by1s7OaWlQTw7thX7CLRV2W3HoMUccJaofszVEfbD8+SaeZtZgyw9mkYvxgYtu3Odxx2YGHgs=\",\"format\":\"google-gemini-v1\",\"index\":0}]},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"gen-1791169249-6wlEwbJv7MicDxVsZViY\",\"object\":\"chat.completion.chunk\",\"created\":1791169249,\"model\":\"gemini-3-1-pro-preview\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"\",\"role\":\"assistant\"},\"finish_reason\":\"stop\",\"native_finish_reason\":\"STOP\"}]}\n\ndata: {\"id\":\"gen-1791169249-6wlEwbJv7MicDxVsZViY\",\"object\":\"chat.completion.chunk\",\"created\":1791169249,\"model\":\"gemini-3-1-pro-preview\",\"service_tier\":\"provisioned\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"\",\"role\":\"assistant\"},\"finish_reason\":\"stop\",\"native_finish_reason\":\"STOP\"}]}\n\ndata: {\"id\":\"gen-1791169249-6wlEwbJv7MicDxVsZViY\",\"object\":\"chat.completion.chunk\",\"created\":1791169249,\"model\":\"gemini-3-1-pro-preview\",\"choices\":[],\"usage\":{\"prompt_tokens\":233,\"completion_tokens\":129,\"total_tokens\":362,\"prompt_tokens_details\":{\"cached_tokens\":0},\"completion_tokens_details\":{\"reasoning_tokens\":126,\"image_tokens\":0,\"audio_tokens\":0}},\"cost\":{\"usd\":0.0025175,\"diem\":0}}\n\ndata: [DONE]\n\n"
}
},
{
"transport": "http",
"request": {
"method": "POST",
"url": "https://api.venice.ai/api/v1/chat/completions",
"headers": {
"content-type": "application/json"
},
"body": "{\"model\":\"gemini-3-1-pro-preview\",\"messages\":[{\"role\":\"system\",\"content\":\"Use the provided tool for private data. After receiving the result, give only the requested number; do not call the tool again.\"},{\"role\":\"user\",\"content\":\"Look up conversion rate for code ZEBRA. Then multiply that rate by 17 and add 9. You must use lookup_rate before answering.\"},{\"role\":\"assistant\",\"content\":null,\"tool_calls\":[{\"id\":\"call_4908320\",\"type\":\"function\",\"function\":{\"name\":\"lookup_rate\",\"arguments\":\"{\\\"code\\\":\\\"ZEBRA\\\"}\"}}],\"reasoning_details\":[{\"type\":\"reasoning.text\",\"text\":\"**Calculating ZEBRA Conversion**\\n\\nI'm currently processing the calculation involving ZEBRA's conversion rate. My next step is to use the `lookup_rate` tool with the code \\\"ZEBRA\\\" to fetch the necessary value, which will then be used in the full equation.\\n\\n\",\"format\":\"google-gemini-v1\",\"index\":0},{\"type\":\"reasoning.encrypted\",\"data\":\"AY89a19Tkd2zuyuB3m6as2cMeOoFk8xFfcBdsUI+RBkTCE1GVBd5hMIfNsjOj+YTucPL1hYoWodr2oZsMKDnPuYee0AmGYl8jHzVKgQFSXzgIeWEhG+hOMvTEa4BoruFowcHzbLhym48OlBys47Tlo7WF4tK4s8Z7q8cpbqGsA0NkUqPt8P7Uk3ZaRTUJBbIE08+JgNUUwiJkT4AVSO635aMUYTcIkt+8TVbqOLITfSdDtkxhAmrInfn7FqGlNop3RH1TUcb3xnUOsnHXoreWLlAyWTlWNvOa7hjUH8vUwILPmtG6ixKZdf4i/UIgm551Z5tgE05SJBcR/m7ALagyOyEilWqxGkyf+5jvNycUlk/mBkNHLV45amgUG5zhboVEAEVFYPRa+Nnfzxi+DEdM1ntDURrB5dl4Jbotx67627EbsZm4z2sKUZ4jo/eBj9fI3MUqjTYKtMXBSyuen/m/Uw2Ji9iIQIlWUt/G86kOjQUYFY60g6TxEO4BLouwLZJ0fXxWdolvtMnH90+skJ+I7aFQjGrnwdjGsT0oXA0/vG3W8GhRSokdICVQBq14EFvjcVvI1kEgj7NTtKkB/+UWRtRbRhSA5F25/dHvqAT4ioYy/5sRwYFXMwJmM52I8DpLiaBrp8wTidLNjTpD9KlImbd2r89LS9GG5NeqNWksF9XbEQAMannsu19HC9xf+CCz5FBz5PjpR0+pvybIJLmph94u4I0cdLsbycbbdIaihqeIc593sQv3nvm9VYZMJcgnQWIKmXofSlV4zi6p0wRPvmgeiPo+Lv6DrtPTtcaRLkkyGrg8uvkq2Is9H6me8y8HCXGIze3np22MjWM5r2+UYMia+xY+E3neBC1AZrlN+GJarvf0THgHInLLnE78f+w3mJByFgXMy6OOEs4QI0+F8yxF9GembhqebXYKWGWUkuArnLfV6TZ13v8s1J9IiNObPU0AJjq+wmlg2ChAIs082sh8Q==\",\"id\":\"call_4908320\",\"format\":\"google-gemini-v1\",\"index\":0}],\"reasoning_content\":\"**Calculating ZEBRA Conversion**\\n\\nI'm currently processing the calculation involving ZEBRA's conversion rate. My next step is to use the `lookup_rate` tool with the code \\\"ZEBRA\\\" to fetch the necessary value, which will then be used in the full equation.\\n\\n\"},{\"role\":\"tool\",\"tool_call_id\":\"call_4908320\",\"content\":\"{\\\"code\\\":\\\"ZEBRA\\\",\\\"rate\\\":23}\"},{\"role\":\"assistant\",\"content\":\"400\",\"reasoning_details\":[{\"type\":\"reasoning.text\",\"format\":\"google-gemini-v1\",\"index\":0,\"signature\":\"AY89a1+fQ9YpZF71EoH4pNDO6g5NeqvLRAav/WG6sv9V/5fpifhK6ZDc6m89qgCpOEBhjTJ+a+ux1dFog+eB2nR/G8VVKt1Zr2+2NKXO3Q3ksKE+E51IRRzK/ywpDYpE8AlE6Kb7EG2x/ZVMpy24TOLY+LL8JNsrz8uHBC1PtPtiZe/gP+Kvu0Yqk+VNYv16wAubtiA0oy0c+wyRzd80jRvqCZpBIGPlLAU515MPpjXNGu0A5ft4uu3oGPJHl5G/Ry4Fm5fvWKl759+Jwx9m7d50+vcttr5SpHm//MvNsUTnqjYkraw1S69+9oB57HoBn8d8pwTHpud/6FARun33EjfPW86Xx2fS448QtKc3c9nxSdGyZgTV0TpA63Uz7n0nGd9fRYrghp3lQv5QyoRIgNg+YeBf/WDNzSW2MO6Y2HLFoo3K0vJh1KMfWhjECNxP1Qfjfz+VjwSV7+OzF9CNw23f8R2udRBr1EUYhTu13ckjFLWjCD5fGjvmLhsae3CgmuFV2Rvaxqrf/H79Tz+X9W/Tw8PuQ0E1pCCpiMe1gR1CT+LJPWxjV96DPHQ0r+1lnekrr5NkO7UAZ64HVALHJRpACdJsVhwI1TBi4f89b1by1s7OaWlQTw7thX7CLRV2W3HoMUccJaofszVEfbD8+SaeZtZgyw9mkYvxgYtu3Odxx2YGHgs=\"}]},{\"role\":\"user\",\"content\":\"Add 7 to your previous final answer. Reply with just the number.\"}],\"tools\":[{\"type\":\"function\",\"function\":{\"name\":\"lookup_rate\",\"description\":\"Retrieve the private conversion rate for a code. Only this tool knows the rate.\",\"parameters\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\",\"enum\":[\"ZEBRA\",\"MOOSE\"]}},\"required\":[\"code\"],\"additionalProperties\":false},\"strict\":false}}],\"tool_choice\":\"none\",\"stream\":true,\"stream_options\":{\"include_usage\":true},\"max_completion_tokens\":4096,\"reasoning\":{\"effort\":\"high\"},\"venice_parameters\":{\"include_venice_system_prompt\":false}}"
},
"response": {
"status": 200,
"headers": {
"content-type": "text/event-stream; charset=utf-8"
},
"body": "data: {\"id\":\"gen-1791169254-bvEsScICXWimu8xzJFF1\",\"object\":\"chat.completion.chunk\",\"created\":1791169254,\"model\":\"gemini-3-1-pro-preview\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"407\",\"role\":\"assistant\"},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"gen-1791169254-bvEsScICXWimu8xzJFF1\",\"object\":\"chat.completion.chunk\",\"created\":1791169254,\"model\":\"gemini-3-1-pro-preview\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"\",\"role\":\"assistant\",\"reasoning_details\":[{\"type\":\"reasoning.text\",\"signature\":\"AY89a1/t2vneT6Dy8hJmYd66cSC0Fk3bKHK7hJI5vTtBcKe38EQ6JXMwTlxi2K47pfmvruV0B6Ks0CZCF9N7rEOT+fXTleU1WPEAHHJPX7KkncEJK4KNIeJsTzyEjSPn5t8kb/vNruIV2KE2/l0kylycTNOubuO8DGlkKOp478zuwZppB0vFaoLp1byEUtyT/MHdN/KckvvKY99ZB2ksWrc51/5Z7jx7GeXGpTl3jHEKzdLWrfsYYyjPOv4h0ZHN8ANmYMTWpGUm8jqxB0pHQFBVS0U5YhF+fuU6Nk3lUNuLnEydq6UH6VydtTxDjUDALIfHhkasmTRXnL/7X3/z3rLhRXRGhga9lHnSnEhmjdLj9iOn/73wjA4QxnT14P+8F45z4ePO19ZGKr/j5WBvBMem92Yqv9HweDo9IreBa8EbqPwlsnjbXvJB7MuBp2TJd4szb7azQ+zaPU7pf5B3fjKC3K8p26qF0C+TkGd72PezZw662/FMkkC/b88sgDgMfgSK7dMqVvv7swafRzFL1IZzTFoJJ3Ouk/VCKGWCHOx+c2EPtvv+IzGhQ1k9dn44bq953wBamkzDjQWcP46+VKp2PPz5yV9AgGCmdi1SRYNU361FiUGlU40cImBLK1MoIqa/YnYaaXMtK9L7/35uOjyg/hae3REIw5N4Y1fiSx9UCKi7eYviBwrY3kZJYdy4UdsN63ahHb2SO1gfZKAg1G36Gn5TIiYGO4dNWE9c\",\"format\":\"google-gemini-v1\",\"index\":0}]},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"gen-1791169254-bvEsScICXWimu8xzJFF1\",\"object\":\"chat.completion.chunk\",\"created\":1791169254,\"model\":\"gemini-3-1-pro-preview\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"\",\"role\":\"assistant\"},\"finish_reason\":\"stop\",\"native_finish_reason\":\"STOP\"}]}\n\ndata: {\"id\":\"gen-1791169254-bvEsScICXWimu8xzJFF1\",\"object\":\"chat.completion.chunk\",\"created\":1791169254,\"model\":\"gemini-3-1-pro-preview\",\"service_tier\":\"provisioned\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"\",\"role\":\"assistant\"},\"finish_reason\":\"stop\",\"native_finish_reason\":\"STOP\"}]}\n\ndata: {\"id\":\"gen-1791169254-bvEsScICXWimu8xzJFF1\",\"object\":\"chat.completion.chunk\",\"created\":1791169254,\"model\":\"gemini-3-1-pro-preview\",\"choices\":[],\"usage\":{\"prompt_tokens\":99,\"completion_tokens\":119,\"total_tokens\":218,\"prompt_tokens_details\":{\"cached_tokens\":0},\"completion_tokens_details\":{\"reasoning_tokens\":116,\"image_tokens\":0,\"audio_tokens\":0}},\"cost\":{\"usd\":0.0020325,\"diem\":0}}\n\ndata: [DONE]\n\n"
}
}
]
}
@@ -1,71 +0,0 @@
{
"version": 1,
"metadata": {
"tags": [
"prefix:venice-chat",
"provider:venice",
"protocol:venice-chat",
"tool",
"parallel",
"signature"
],
"name": "venice-chat/gemini-preserves-per-call-signatures-through-parallel-tools",
"recordedAt": "2026-10-05T03:21:54.383Z"
},
"interactions": [
{
"transport": "http",
"request": {
"method": "POST",
"url": "https://api.venice.ai/api/v1/chat/completions",
"headers": {
"content-type": "application/json"
},
"body": "{\"model\":\"gemini-3-8-flash\",\"messages\":[{\"role\":\"user\",\"content\":\"Look up private rates for both ZEBRA and MOOSE using two calls to lookup_rate. Then add the rates and multiply by 7. Use parallel calls if possible.\"}],\"tools\":[{\"type\":\"function\",\"function\":{\"name\":\"lookup_rate\",\"description\":\"Retrieve the private conversion rate for a code. Only this tool knows the rate.\",\"parameters\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\",\"enum\":[\"ZEBRA\",\"MOOSE\"]}},\"required\":[\"code\"],\"additionalProperties\":false},\"strict\":false}}],\"stream\":true,\"stream_options\":{\"include_usage\":true},\"max_completion_tokens\":4096,\"reasoning\":{\"effort\":\"high\"},\"venice_parameters\":{\"include_venice_system_prompt\":false},\"parallel_tool_calls\":true}"
},
"response": {
"status": 200,
"headers": {
"content-type": "text/event-stream; charset=utf-8"
},
"body": "data: {\"id\":\"gcp-1791170510482-itouwq\",\"object\":\"chat.completion.chunk\",\"created\":1791170510,\"model\":\"gemini-3-8-flash\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"tool_calls\":[{\"index\":0,\"id\":\"call_0\",\"type\":\"function\",\"thought_signature\":\"AY89a1/bp+ihzOXpD71E6UhQv6xnBjHTAdPeNHvLEAevtqznKg46D/3qO3e9Qg7wGmccL6IpLV56PwWIT+6pXoeU6ix8JCCk7nlml/Sz2huX/rNFoSRHpB+YAZK07lHP9cbotXy7FPNMaRhsl18sPSJZLlPK0iTGhJa7MmH72r4v/G3/izffMKUPRos6i5qQ52La10FUWSunPigqNnglEvWEC3Yb+d+PnEq5iOPaVCD0awXjhsIk8L0n4+T6jM4epO+whUSSgvH9PGwgmsP197hb7l5YSjQncNX0oF1G4ch3xrFzXyS/noFFy6SlBfzfN0NJmAyP//4oVqjffUJngigTAtboI4HdDwH1fPHuI5FTfmVcUGVVIEjhAEDAAHtS77uW/XdLH4bsSigjYeCzgRTYmHP8bqFb1HjRjl2HK2Pr1y6ch0JSIgtAKv8Kj4R0PM8rIYxxf7LgrsaVWogt4rq2M4HvcUcBi6IhQ/500JUCoqdhlTGMqCFUHkN6P/qAoPGZXmo50OvtNChQ4vXVGYxWp89IRwdcv8vqjIFk75/R6oZG9BsrUaC1JnQN8i3BbYc1bz/lzFCQ1IC/g1Sm+wPhsaPmoVN77101ZAdNMiP4B5Ow7K/UycMunFKWy4xdpaT7Pb+LLNy6GYy9q2TvoRXTq1lJugLH4Q8TQZj5neWZmiDxqzsj0Xvwew7zLwGSMfkpBB4rgx9XWMx5CwqO068qjhmo+0gUkxMug2RHblEGnPO7i0i9fYj9ltDfUpFxN2adDpyk4dwuIeVSlu4N4oHOgjGms5jWLp0MqkyoKFo=\",\"function\":{\"name\":\"lookup_rate\",\"arguments\":\"{\\\"code\\\":\\\"ZEBRA\\\"}\"}}]}}]}\n\ndata: {\"id\":\"gcp-1791170510482-itouwq\",\"object\":\"chat.completion.chunk\",\"created\":1791170510,\"model\":\"gemini-3-8-flash\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":1,\"id\":\"call_1\",\"type\":\"function\",\"function\":{\"name\":\"lookup_rate\",\"arguments\":\"{\\\"code\\\":\\\"MOOSE\\\"}\"}}]}}]}\n\ndata: {\"id\":\"gcp-1791170510482-itouwq\",\"object\":\"chat.completion.chunk\",\"created\":1791170510,\"model\":\"gemini-3-8-flash\",\"choices\":[{\"index\":0,\"delta\":{},\"finish_reason\":\"tool_calls\"}]}\n\ndata: {\"id\":\"gcp-1791170510482-itouwq\",\"object\":\"chat.completion.chunk\",\"created\":1791170510,\"model\":\"gemini-3-8-flash\",\"choices\":[],\"usage\":{\"prompt_tokens\":74,\"completion_tokens\":156,\"total_tokens\":230},\"cost\":{\"usd\":0.000800625,\"diem\":0}}\n\ndata: [DONE]\n\n"
}
},
{
"transport": "http",
"request": {
"method": "POST",
"url": "https://api.venice.ai/api/v1/chat/completions",
"headers": {
"content-type": "application/json"
},
"body": "{\"model\":\"gemini-3-8-flash\",\"messages\":[{\"role\":\"user\",\"content\":\"Look up private rates for both ZEBRA and MOOSE using two calls to lookup_rate. Then add the rates and multiply by 7. Use parallel calls if possible.\"},{\"role\":\"assistant\",\"content\":null,\"tool_calls\":[{\"id\":\"call_0\",\"type\":\"function\",\"function\":{\"name\":\"lookup_rate\",\"arguments\":\"{\\\"code\\\":\\\"ZEBRA\\\"}\"},\"thought_signature\":\"AY89a1/bp+ihzOXpD71E6UhQv6xnBjHTAdPeNHvLEAevtqznKg46D/3qO3e9Qg7wGmccL6IpLV56PwWIT+6pXoeU6ix8JCCk7nlml/Sz2huX/rNFoSRHpB+YAZK07lHP9cbotXy7FPNMaRhsl18sPSJZLlPK0iTGhJa7MmH72r4v/G3/izffMKUPRos6i5qQ52La10FUWSunPigqNnglEvWEC3Yb+d+PnEq5iOPaVCD0awXjhsIk8L0n4+T6jM4epO+whUSSgvH9PGwgmsP197hb7l5YSjQncNX0oF1G4ch3xrFzXyS/noFFy6SlBfzfN0NJmAyP//4oVqjffUJngigTAtboI4HdDwH1fPHuI5FTfmVcUGVVIEjhAEDAAHtS77uW/XdLH4bsSigjYeCzgRTYmHP8bqFb1HjRjl2HK2Pr1y6ch0JSIgtAKv8Kj4R0PM8rIYxxf7LgrsaVWogt4rq2M4HvcUcBi6IhQ/500JUCoqdhlTGMqCFUHkN6P/qAoPGZXmo50OvtNChQ4vXVGYxWp89IRwdcv8vqjIFk75/R6oZG9BsrUaC1JnQN8i3BbYc1bz/lzFCQ1IC/g1Sm+wPhsaPmoVN77101ZAdNMiP4B5Ow7K/UycMunFKWy4xdpaT7Pb+LLNy6GYy9q2TvoRXTq1lJugLH4Q8TQZj5neWZmiDxqzsj0Xvwew7zLwGSMfkpBB4rgx9XWMx5CwqO068qjhmo+0gUkxMug2RHblEGnPO7i0i9fYj9ltDfUpFxN2adDpyk4dwuIeVSlu4N4oHOgjGms5jWLp0MqkyoKFo=\"},{\"id\":\"call_1\",\"type\":\"function\",\"function\":{\"name\":\"lookup_rate\",\"arguments\":\"{\\\"code\\\":\\\"MOOSE\\\"}\"}}]},{\"role\":\"tool\",\"tool_call_id\":\"call_0\",\"content\":\"{\\\"rate\\\":23}\"},{\"role\":\"tool\",\"tool_call_id\":\"call_1\",\"content\":\"{\\\"rate\\\":31}\"}],\"tools\":[{\"type\":\"function\",\"function\":{\"name\":\"lookup_rate\",\"description\":\"Retrieve the private conversion rate for a code. Only this tool knows the rate.\",\"parameters\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\",\"enum\":[\"ZEBRA\",\"MOOSE\"]}},\"required\":[\"code\"],\"additionalProperties\":false},\"strict\":false}}],\"tool_choice\":\"none\",\"stream\":true,\"stream_options\":{\"include_usage\":true},\"max_completion_tokens\":4096,\"reasoning\":{\"effort\":\"high\"},\"venice_parameters\":{\"include_venice_system_prompt\":false},\"parallel_tool_calls\":true}"
},
"response": {
"status": 200,
"headers": {
"content-type": "text/event-stream; charset=utf-8"
},
"body": "data: {\"id\":\"gcp-1791170512529-w6lmsz\",\"object\":\"chat.completion.chunk\",\"created\":1791170512,\"model\":\"gemini-3-8-flash\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":\"The retrieved rates are:\\n- **\"}}]}\n\ndata: {\"id\":\"gcp-1791170512529-w6lmsz\",\"object\":\"chat.completion.chunk\",\"created\":1791170512,\"model\":\"gemini-3-8-flash\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"ZEBRA:** 23\\n- **MOOSE:** 31\\n\\n### Calculation:\\n1. **Add\"}}]}\n\ndata: {\"id\":\"gcp-1791170512529-w6lmsz\",\"object\":\"chat.completion.chunk\",\"created\":1791170512,\"model\":\"gemini-3-8-flash\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" the rates:** \\n $$23 + 31 = 54$$\\n\\n2. **Multiply by 7:** \"}}]}\n\ndata: {\"id\":\"gcp-1791170512529-w6lmsz\",\"object\":\"chat.completion.chunk\",\"created\":1791170512,\"model\":\"gemini-3-8-flash\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"\\n $$54 \\\\times 7 = 378$$\\n\\nThe final result is **378\"}}]}\n\ndata: {\"id\":\"gcp-1791170512529-w6lmsz\",\"object\":\"chat.completion.chunk\",\"created\":1791170512,\"model\":\"gemini-3-8-flash\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"**.\"}}]}\n\ndata: {\"id\":\"gcp-1791170512529-w6lmsz\",\"object\":\"chat.completion.chunk\",\"created\":1791170512,\"model\":\"gemini-3-8-flash\",\"choices\":[{\"index\":0,\"delta\":{\"thought_signature\":\"AY89a18wXUWI7m5/5GGqxfoJDqE4MU2X2SR/ZQMCX+JQ/clHQynyEjplK4a8w9tXAi8uuxYPVxYj1ULcb83zUQBsr77tSkJN5R0J2Jgc2HzYtmTW7OYHoFQqmMESnDjvDiO51y3xpXGbVkqRlXwfmap+Tpivfx45hiXlns/hObEQo1gE70NzD7d8CQOseYVz2N7MPMmGDqpAy+5SAHXGAcQ9K2ufHNUdfWM/uUi+sGUcDhGnjPXPlzIuYk7IjUQXNAH9FFqo19dBQPhE106C3aI69QAuMvyZZ/no4ou9XSOUXrstjzQwUM+TOJXsab80F1+C6DghvYm0UZSb827xCKPtj7oCQwtwg+f4sWELhflp42IL3URo2UxpZaDNpt7PYXmmcCvKAN7/PTirCijNWMtlDL0LDbiKj+DfOFEuS2VxVq+yQgWRbKJZF7bl8BCSe7+Qab0bAsu0ZJXKBlxs46ulaXn5cc933y/Yv7O0XGHbVULMzeEpZndzkHqZYs5D+ve+CJFfjWsVk1NKrTdXqt11Ov470+MCpSlHTXdjug7oUd0A0b5AH+bF4yAZAGj4snJ7hi04EQ4Ch7rASB3OB+YB+8SnStAGf0XuimNgY2wzR68qaQ==\"}}]}\n\ndata: {\"id\":\"gcp-1791170512529-w6lmsz\",\"object\":\"chat.completion.chunk\",\"created\":1791170512,\"model\":\"gemini-3-8-flash\",\"choices\":[{\"index\":0,\"delta\":{},\"finish_reason\":\"stop\"}]}\n\ndata: {\"id\":\"gcp-1791170512529-w6lmsz\",\"object\":\"chat.completion.chunk\",\"created\":1791170512,\"model\":\"gemini-3-8-flash\",\"choices\":[],\"usage\":{\"prompt_tokens\":206,\"completion_tokens\":208,\"total_tokens\":414},\"cost\":{\"usd\":0.001168125,\"diem\":0}}\n\ndata: [DONE]\n\n"
}
},
{
"transport": "http",
"request": {
"method": "POST",
"url": "https://api.venice.ai/api/v1/chat/completions",
"headers": {
"content-type": "application/json"
},
"body": "{\"model\":\"gemini-3-8-flash\",\"messages\":[{\"role\":\"user\",\"content\":\"Look up private rates for both ZEBRA and MOOSE using two calls to lookup_rate. Then add the rates and multiply by 7. Use parallel calls if possible.\"},{\"role\":\"assistant\",\"content\":null,\"tool_calls\":[{\"id\":\"call_0\",\"type\":\"function\",\"function\":{\"name\":\"lookup_rate\",\"arguments\":\"{\\\"code\\\":\\\"ZEBRA\\\"}\"},\"thought_signature\":\"AY89a1/bp+ihzOXpD71E6UhQv6xnBjHTAdPeNHvLEAevtqznKg46D/3qO3e9Qg7wGmccL6IpLV56PwWIT+6pXoeU6ix8JCCk7nlml/Sz2huX/rNFoSRHpB+YAZK07lHP9cbotXy7FPNMaRhsl18sPSJZLlPK0iTGhJa7MmH72r4v/G3/izffMKUPRos6i5qQ52La10FUWSunPigqNnglEvWEC3Yb+d+PnEq5iOPaVCD0awXjhsIk8L0n4+T6jM4epO+whUSSgvH9PGwgmsP197hb7l5YSjQncNX0oF1G4ch3xrFzXyS/noFFy6SlBfzfN0NJmAyP//4oVqjffUJngigTAtboI4HdDwH1fPHuI5FTfmVcUGVVIEjhAEDAAHtS77uW/XdLH4bsSigjYeCzgRTYmHP8bqFb1HjRjl2HK2Pr1y6ch0JSIgtAKv8Kj4R0PM8rIYxxf7LgrsaVWogt4rq2M4HvcUcBi6IhQ/500JUCoqdhlTGMqCFUHkN6P/qAoPGZXmo50OvtNChQ4vXVGYxWp89IRwdcv8vqjIFk75/R6oZG9BsrUaC1JnQN8i3BbYc1bz/lzFCQ1IC/g1Sm+wPhsaPmoVN77101ZAdNMiP4B5Ow7K/UycMunFKWy4xdpaT7Pb+LLNy6GYy9q2TvoRXTq1lJugLH4Q8TQZj5neWZmiDxqzsj0Xvwew7zLwGSMfkpBB4rgx9XWMx5CwqO068qjhmo+0gUkxMug2RHblEGnPO7i0i9fYj9ltDfUpFxN2adDpyk4dwuIeVSlu4N4oHOgjGms5jWLp0MqkyoKFo=\"},{\"id\":\"call_1\",\"type\":\"function\",\"function\":{\"name\":\"lookup_rate\",\"arguments\":\"{\\\"code\\\":\\\"MOOSE\\\"}\"}}]},{\"role\":\"tool\",\"tool_call_id\":\"call_0\",\"content\":\"{\\\"rate\\\":23}\"},{\"role\":\"tool\",\"tool_call_id\":\"call_1\",\"content\":\"{\\\"rate\\\":31}\"},{\"role\":\"assistant\",\"content\":\"The retrieved rates are:\\n- **ZEBRA:** 23\\n- **MOOSE:** 31\\n\\n### Calculation:\\n1. **Add the rates:** \\n $$23 + 31 = 54$$\\n\\n2. **Multiply by 7:** \\n $$54 \\\\times 7 = 378$$\\n\\nThe final result is **378**.\",\"thought_signature\":\"AY89a18wXUWI7m5/5GGqxfoJDqE4MU2X2SR/ZQMCX+JQ/clHQynyEjplK4a8w9tXAi8uuxYPVxYj1ULcb83zUQBsr77tSkJN5R0J2Jgc2HzYtmTW7OYHoFQqmMESnDjvDiO51y3xpXGbVkqRlXwfmap+Tpivfx45hiXlns/hObEQo1gE70NzD7d8CQOseYVz2N7MPMmGDqpAy+5SAHXGAcQ9K2ufHNUdfWM/uUi+sGUcDhGnjPXPlzIuYk7IjUQXNAH9FFqo19dBQPhE106C3aI69QAuMvyZZ/no4ou9XSOUXrstjzQwUM+TOJXsab80F1+C6DghvYm0UZSb827xCKPtj7oCQwtwg+f4sWELhflp42IL3URo2UxpZaDNpt7PYXmmcCvKAN7/PTirCijNWMtlDL0LDbiKj+DfOFEuS2VxVq+yQgWRbKJZF7bl8BCSe7+Qab0bAsu0ZJXKBlxs46ulaXn5cc933y/Yv7O0XGHbVULMzeEpZndzkHqZYs5D+ve+CJFfjWsVk1NKrTdXqt11Ov470+MCpSlHTXdjug7oUd0A0b5AH+bF4yAZAGj4snJ7hi04EQ4Ch7rASB3OB+YB+8SnStAGf0XuimNgY2wzR68qaQ==\"},{\"role\":\"user\",\"content\":\"Add 7 to your previous final answer. Reply with just the number.\"}],\"tools\":[{\"type\":\"function\",\"function\":{\"name\":\"lookup_rate\",\"description\":\"Retrieve the private conversion rate for a code. Only this tool knows the rate.\",\"parameters\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\",\"enum\":[\"ZEBRA\",\"MOOSE\"]}},\"required\":[\"code\"],\"additionalProperties\":false},\"strict\":false}}],\"tool_choice\":\"none\",\"stream\":true,\"stream_options\":{\"include_usage\":true},\"max_completion_tokens\":4096,\"reasoning\":{\"effort\":\"high\"},\"venice_parameters\":{\"include_venice_system_prompt\":false},\"parallel_tool_calls\":true}"
},
"response": {
"status": 200,
"headers": {
"content-type": "text/event-stream; charset=utf-8"
},
"body": "data: {\"id\":\"gcp-1791170514341-j5gh1h\",\"object\":\"chat.completion.chunk\",\"created\":1791170514,\"model\":\"gemini-3-8-flash\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":\"385\"}}]}\n\ndata: {\"id\":\"gcp-1791170514341-j5gh1h\",\"object\":\"chat.completion.chunk\",\"created\":1791170514,\"model\":\"gemini-3-8-flash\",\"choices\":[{\"index\":0,\"delta\":{\"thought_signature\":\"AY89a1/DMIB/2qixC5QVQQH1tA2QiQFpEsXnIDd7wlCgwuYQXBGWgSsP1bpTZJUJM/yqKdQh96+/c6c28VMbW5iovCDxxeK56KfmSBSBIPtNW8BUHHLxQw9vdsxQvQyHcGW6rLb4W3gElqx6JFmNkTMvijUS9QDgkPlPipirbv7pqLejFyQ040GD4IVhP8ovyR49EJm61W0JvJOPFT30/gVvaKA6RnRgTKGiHrZWVqLxhKVkb0gC7jI9CNNASOYH67uelFQ2trbcf5Qbhx4q93vAErFsBzvzliSuaELDrxZAF9VCNIiWz4AUtEWI1FLCMYxkgNry/K+iNpwcXu8BxABETbNkEb722wjpt1A2EZK/5qhqKQvUOq+WgSCPCLoI1zfUiGN6UV9D/QGtyS2Iyt3AYlvKjZVVzP217ItWSDacGzigLd4t+4MMW7Dvbs7ffBvBV+EzMCP1dnsLkKCxicfvT3e2EgY+liDSaSZG3sE7Lm4WKCYbI4rrBLMupERSaFfAHWdTlea5HeM=\"}}]}\n\ndata: {\"id\":\"gcp-1791170514341-j5gh1h\",\"object\":\"chat.completion.chunk\",\"created\":1791170514,\"model\":\"gemini-3-8-flash\",\"choices\":[{\"index\":0,\"delta\":{},\"finish_reason\":\"stop\"}]}\n\ndata: {\"id\":\"gcp-1791170514341-j5gh1h\",\"object\":\"chat.completion.chunk\",\"created\":1791170514,\"model\":\"gemini-3-8-flash\",\"choices\":[],\"usage\":{\"prompt_tokens\":429,\"completion_tokens\":83,\"total_tokens\":512},\"cost\":{\"usd\":0.00079125,\"diem\":0}}\n\ndata: [DONE]\n\n"
}
}
]
}
@@ -1,75 +0,0 @@
{
"version": 1,
"metadata": {
"model": "openai-gpt-54-mini",
"tags": [
"prefix:venice-chat",
"provider:venice",
"protocol:venice-chat",
"reasoning",
"tool",
"tool-loop",
"continuation",
"usage",
"encrypted"
],
"name": "venice-chat/openai-gpt-54-mini-preserves-reasoning-through-a-tool-loop-and-follow-up",
"recordedAt": "2026-10-05T03:01:10.096Z"
},
"interactions": [
{
"transport": "http",
"request": {
"method": "POST",
"url": "https://api.venice.ai/api/v1/chat/completions",
"headers": {
"content-type": "application/json"
},
"body": "{\"model\":\"openai-gpt-54-mini\",\"messages\":[{\"role\":\"system\",\"content\":\"Use the provided tool for private data. After receiving the result, give only the requested number; do not call the tool again.\"},{\"role\":\"user\",\"content\":\"Look up conversion rate for code ZEBRA. Then multiply that rate by 17 and add 9. You must use lookup_rate before answering.\"}],\"tools\":[{\"type\":\"function\",\"function\":{\"name\":\"lookup_rate\",\"description\":\"Retrieve the private conversion rate for a code. Only this tool knows the rate.\",\"parameters\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\",\"enum\":[\"ZEBRA\",\"MOOSE\"]}},\"required\":[\"code\"],\"additionalProperties\":false},\"strict\":false}}],\"stream\":true,\"stream_options\":{\"include_usage\":true},\"max_completion_tokens\":4096,\"venice_parameters\":{\"include_venice_system_prompt\":false}}"
},
"response": {
"status": 200,
"headers": {
"content-type": "text/event-stream; charset=utf-8"
},
"body": "data: {\"id\":\"resp_014406f0f45e19c6016ac312efd07487d28fc51d6572174c85\",\"object\":\"chat.completion.chunk\",\"created\":1791169264,\"model\":\"openai-gpt-54-mini\",\"choices\":[{\"index\":0,\"delta\":{\"reasoning_content\":\"**Calculating private data**\\n\\nI\"},\"finish_reason\":null}]}\n\ndata: {\"id\":\"resp_014406f0f45e19c6016ac312efd07487d28fc51d6572174c85\",\"object\":\"chat.completion.chunk\",\"created\":1791169264,\"model\":\"openai-gpt-54-mini\",\"choices\":[{\"index\":0,\"delta\":{\"reasoning_content\":\" need\"},\"finish_reason\":null}]}\n\ndata: {\"id\":\"resp_014406f0f45e19c6016ac312efd07487d28fc51d6572174c85\",\"object\":\"chat.completion.chunk\",\"created\":1791169264,\"model\":\"openai-gpt-54-mini\",\"choices\":[{\"index\":0,\"delta\":{\"reasoning_content\":\" to\"},\"finish_reason\":null}]}\n\ndata: {\"id\":\"resp_014406f0f45e19c6016ac312efd07487d28fc51d6572174c85\",\"object\":\"chat.completion.chunk\",\"created\":1791169264,\"model\":\"openai-gpt-54-mini\",\"choices\":[{\"index\":0,\"delta\":{\"reasoning_content\":\" follow\"},\"finish_reason\":null}]}\n\ndata: {\"id\":\"resp_014406f0f45e19c6016ac312efd07487d28fc51d6572174c85\",\"object\":\"chat.completion.chunk\",\"created\":1791169264,\"model\":\"openai-gpt-54-mini\",\"choices\":[{\"index\":0,\"delta\":{\"reasoning_content\":\" the\"},\"finish_reason\":null}]}\n\ndata: {\"id\":\"resp_014406f0f45e19c6016ac312efd07487d28fc51d6572174c85\",\"object\":\"chat.completion.chunk\",\"created\":1791169264,\"model\":\"openai-gpt-54-mini\",\"choices\":[{\"index\":0,\"delta\":{\"reasoning_content\":\" developer\"},\"finish_reason\":null}]}\n\ndata: {\"id\":\"resp_014406f0f45e19c6016ac312efd07487d28fc51d6572174c85\",\"object\":\"chat.completion.chunk\",\"created\":1791169264,\"model\":\"openai-gpt-54-mini\",\"choices\":[{\"index\":0,\"delta\":{\"reasoning_content\":\"'s\"},\"finish_reason\":null}]}\n\ndata: {\"id\":\"resp_014406f0f45e19c6016ac312efd07487d28fc51d6572174c85\",\"object\":\"chat.completion.chunk\",\"created\":1791169264,\"model\":\"openai-gpt-54-mini\",\"choices\":[{\"index\":0,\"delta\":{\"reasoning_content\":\" instructions\"},\"finish_reason\":null}]}\n\ndata: {\"id\":\"resp_014406f0f45e19c6016ac312efd07487d28fc51d6572174c85\",\"object\":\"chat.completion.chunk\",\"created\":1791169264,\"model\":\"openai-gpt-54-mini\",\"choices\":[{\"index\":0,\"delta\":{\"reasoning_content\":\" and\"},\"finish_reason\":null}]}\n\ndata: {\"id\":\"resp_014406f0f45e19c6016ac312efd07487d28fc51d6572174c85\",\"object\":\"chat.completion.chunk\",\"created\":1791169264,\"model\":\"openai-gpt-54-mini\",\"choices\":[{\"index\":0,\"delta\":{\"reasoning_content\":\" use\"},\"finish_reason\":null}]}\n\ndata: {\"id\":\"resp_014406f0f45e19c6016ac312efd07487d28fc51d6572174c85\",\"object\":\"chat.completion.chunk\",\"created\":1791169264,\"model\":\"openai-gpt-54-mini\",\"choices\":[{\"index\":0,\"delta\":{\"reasoning_content\":\" the\"},\"finish_reason\":null}]}\n\ndata: {\"id\":\"resp_014406f0f45e19c6016ac312efd07487d28fc51d6572174c85\",\"object\":\"chat.completion.chunk\",\"created\":1791169264,\"model\":\"openai-gpt-54-mini\",\"choices\":[{\"index\":0,\"delta\":{\"reasoning_content\":\" provided\"},\"finish_reason\":null}]}\n\ndata: {\"id\":\"resp_014406f0f45e19c6016ac312efd07487d28fc51d6572174c85\",\"object\":\"chat.completion.chunk\",\"created\":1791169264,\"model\":\"openai-gpt-54-mini\",\"choices\":[{\"index\":0,\"delta\":{\"reasoning_content\":\" tool\"},\"finish_reason\":null}]}\n\ndata: {\"id\":\"resp_014406f0f45e19c6016ac312efd07487d28fc51d6572174c85\",\"object\":\"chat.completion.chunk\",\"created\":1791169264,\"model\":\"openai-gpt-54-mini\",\"choices\":[{\"index\":0,\"delta\":{\"reasoning_content\":\" to\"},\"finish_reason\":null}]}\n\ndata: {\"id\":\"resp_014406f0f45e19c6016ac312efd07487d28fc51d6572174c85\",\"object\":\"chat.completion.chunk\",\"created\":1791169264,\"model\":\"openai-gpt-54-mini\",\"choices\":[{\"index\":0,\"delta\":{\"reasoning_content\":\" handle\"},\"finish_reason\":null}]}\n\ndata: {\"id\":\"resp_014406f0f45e19c6016ac312efd07487d28fc51d6572174c85\",\"object\":\"chat.completion.chunk\",\"created\":1791169264,\"model\":\"openai-gpt-54-mini\",\"choices\":[{\"index\":0,\"delta\":{\"reasoning_content\":\" private\"},\"finish_reason\":null}]}\n\ndata: {\"id\":\"resp_014406f0f45e19c6016ac312efd07487d28fc51d6572174c85\",\"object\":\"chat.completion.chunk\",\"created\":1791169264,\"model\":\"openai-gpt-54-mini\",\"choices\":[{\"index\":0,\"delta\":{\"reasoning_content\":\" data\"},\"finish_reason\":null}]}\n\ndata: {\"id\":\"resp_014406f0f45e19c6016ac312efd07487d28fc51d6572174c85\",\"object\":\"chat.completion.chunk\",\"created\":1791169264,\"model\":\"openai-gpt-54-mini\",\"choices\":[{\"index\":0,\"delta\":{\"reasoning_content\":\".\"},\"finish_reason\":null}]}\n\ndata: {\"id\":\"resp_014406f0f45e19c6016ac312efd07487d28fc51d6572174c85\",\"object\":\"chat.completion.chunk\",\"created\":1791169264,\"model\":\"openai-gpLine truncated
}
},
{
"transport": "http",
"request": {
"method": "POST",
"url": "https://api.venice.ai/api/v1/chat/completions",
"headers": {
"content-type": "application/json"
},
"body": "{\"model\":\"openai-gpt-54-mini\",\"messages\":[{\"role\":\"system\",\"content\":\"Use the provided tool for private data. After receiving the result, give only the requested number; do not call the tool again.\"},{\"role\":\"user\",\"content\":\"Look up conversion rate for code ZEBRA. Then multiply that rate by 17 and add 9. You must use lookup_rate before answering.\"},{\"role\":\"assistant\",\"content\":null,\"tool_calls\":[{\"id\":\"call_onp7EArhOgXu0hGRF6Y5WHLu\",\"type\":\"function\",\"function\":{\"name\":\"lookup_rate\",\"arguments\":\"{\\\"code\\\":\\\"ZEBRA\\\"}\"}}],\"reasoning_details\":[{\"type\":\"reasoning.encrypted\",\"data\":\"gAAAAABqwxLxEN4vtmI2WcZ_aDDttdmPwAHgnV2gLZBo_f_mmTdGm4oZzE2JVzcj72AeXHyABmmo2VQN4nXaZK9GSSUZRlYL8mECMxKTj9AMgL8zlnuN5WDexxU-e9SduGP36RN9nH56-go4dz8eu5DEMvMf_TMoTnxz5AnC_UWbty63Vp8jEgkD-DoRjd0cg9LjthP_FLyMvP2Wj9qMJxEuO7Mw2_Pu4I3YeChnygLvmRfRR2QkamL5vjb9yHkl0Lb502vEGB2DW_czNEpHxisifvwRBjA7vk0mbn2dlhiR80aNHO71hC-QqrtUfPXx2HIbVLfbPdJf0saI0wD7ZuCJSQCBfB7fMCnj2ZLQeXa67lgXwsZDLBg4HmSJFvsMinx1IHIjUk7iebsKvmYx40cjqpXL3wj7htZWk2hO9OyGGUwlRsn5THdhTsgwg2SPbuHxb-sLPQnEFZ7bRp5lsVm31X2dlHDa1uZ7KLJZyy9g2CLIKsBwFFoCK7jY3HBXfL21ac5TAkOzVmz-XKl6OMPSl-PSdByOO15uHHpyOcJvJvuaea5Tj1nihtuDlhUP5EQPi7MiWpJMFo9AhDCafAbaQOEF5W8AQPeb_GXvPTzhySRAC8PxrhraEcZ4fjpjLqkv6mi_A1YHwbHEHHWjcK5VEKc3dadlIM_s6djclFBocVXrvnQQElVhWBDM-RtK9sgR07BFj5LVk6fYvg0b6gQnbDuQH18jhtPoj0SX0lN921XEQ1ebMCWGLANND2ew_wyNShYouJohefLULicWG57xh_yV1KEG1ufBedq2CPvCHA9Hm5UssbeJx5yBriyUA2Yh4D7j2UBT_KDWEHydDLQ4mWEyLTfQn0ZB3ZOqkU79z3HpK2w_VHUnkl5nEYEU2bmF5lsG33B4gtbQIBtZT76-fizXsvVZx__taOWXg96VMi3CD3DY0TS76B4U92KFmAFJ4aeiM8bkPuzf1Qd202EWE39CotUDoxAM6mzQu0NObTDnGQE776P6kzUoIKUmP8sDwYG5xot8ZTuXcBYysvj4-pFC_fvqMhsdfgonbke78SIjHtmePsOkfHBnCjSSUFKEbaM0nm8jJ6nYVwiYFuXBKD31QD462TAC7ORVIn32sWSgV2t3Sd7vq4ws2Yy7nUoILRAt_NYQUEXt7_R8J5YWj1L6Ruagk7Vtt-CAWxT6FhVyU4JhMb6-tJrYHgdmtunYrjpHBiOVdQR25aOmLbc_WlDvpO4azcRazpgkASAjPaaDycYceKcheiyUDBgFrvcSYrS1fJb6YZrTzOKw1Ztd0YsklS4nc3jffLNLrBQVvnkajj0OHjGcW8wkP93TCMJyt8tfIH0Rd5hIZLY5EqUWj9yZgXO7CuDKQZIEI0oZXef3mjfQqH8Q9rZsSgS3DI3mCF3CN003R_ZKKLGWkG8VzwlvdocNxaV_Z-ujp_O0YMyOcmEoTJI-j3JP3nefNboyP7EWND6VWDjJbqg64dvu0PK0EW5L0lnAu0l-WVb2qmXLYao_7bOlD8JLFZOyUgHAWmetmnm0NuxzevJEVBkNUu1jQatPzf2Jzf9SGKM_3bM_z0TM5McCZ2Pikzwp96QlaiEEWV_j2g02PT5Y3td-lfpoKxoFD5tltGNY9TlPzK-4a9Iz0hloiCprxWMLGKw2JzctrPF1xr42PKGNgPE84xAQ3sQ4mQ==\",\"id\":\"rs_014406f0f45e19c6016ac312f01d2487d2a5f2bdb7fbc42f76\",\"format\":\"openai-responses-v1\",\"index\":0}],\"reasoning_content\":\"**Calculating private data**\\n\\nI need to follow the developer's instructions and use the provided tool to handle private data. First, I should look up the rate for \\\"ZEBRA\\\" before I answer. After that, I’ll multiply the rate by 17 and then add 9 to get the required number. It’s important to give only the requested number and avoid making another tool call. I’ll likely use the commentary channel for this tool call.\\n\\n__ENCRYPTED_REASONING__id=rs_014406f0f45e19c6016ac312f01d2487d2a5f2bdb7fbc42f76\\ngAAAAABqwxLxEN4vtmI2WcZ_aDDttdmPwAHgnV2gLZBo_f_mmTdGm4oZzE2JVzcj72AeXHyABmmo2VQN4nXaZK9GSSUZRlYL8mECMxKTj9AMgL8zlnuN5WDexxU-e9SduGP36RN9nH56-go4dz8eu5DEMvMf_TMoTnxz5AnC_UWbty63Vp8jEgkD-DoRjd0cg9LjthP_FLyMvP2Wj9qMJxEuO7Mw2_Pu4I3YeChnygLvmRfRR2QkamL5vjb9yHkl0Lb502vEGB2DW_czNEpHxisifvwRBjA7vk0mbn2dlhiR80aNHO71hC-QqrtUfPXx2HIbVLfbPdJf0saI0wD7ZuCJSQCBfB7fMCnj2ZLQeXa67lgXwsZDLBg4HmSJFvsMinx1IHIjUk7iebsKvmYx40cjqpXL3wj7htZWk2hO9OyGGUwlRsn5THdhTsgwg2SPbuHxb-sLPQnEFZ7bRp5lsVm31X2dlHDa1uZ7KLJZyy9g2CLIKsBwFFoCK7jY3HBXfL21ac5TAkOzVmz-XKl6OMPSl-PSdByOO15uHHpyOcJvJvuaea5Tj1nihtuDlhUP5EQPi7MiWpJMFo9AhDCafAbaQOEF5W8AQPeb_GXvPTzhySRAC8PxrhraEcZ4fjpjLqkv6mi_A1YHwbHEHHWjcK5VEKc3dadlIM_s6djclFBocVXrvnQQElVhWBDM-RtK9sgR07BFj5LVk6fYvg0b6gQnbDuQH18jhtPoj0SX0lN921XEQ1ebMCWGLANND2ew_wyNShYouJohefLULicWG57xh_yV1KEG1ufBedq2CPvCHA9Hm5UssbeJx5yBriyUA2Yh4D7j2UBT_KDWEHydDLQ4mWEyLTfQn0ZB3ZOqkU79z3HpK2w_VHUnkl5nEYEU2bmF5lsG33B4gtbQIBtZT76-fizXsvVZx__taOWXg96VMi3CD3DY0TS76B4U92KFmAFJ4aeiM8bkPuzf1Qd202EWE39CotUDoxAM6mzQu0NObTDnGQE776P6kzUoIKUmP8sDwYG5xot8ZTuXcBYysvj4-pFC_fvqMhsdfgonbke78SIjHtmePsOkfHBnCjSSUFKEbaM0nm8jJ6nYVwiYFuXBKD31QD462TAC7ORVIn32sWSgV2t3Sd7vq4ws2Yy7nUoILRAt_NYQUEXt7_R8J5YWj1L6Ruagk7Vtt-CAWxT6FhVyU4JhMb6-tJrYHgdmtunYrjpHBiOVdQR25aOmLbc_WlDvpO4azcRazpgkASAjPaaDycYceKcheiyUDBgFrvcSYrS1fJb6YZrTzOKw1Ztd0YsklS4nc3jffLNLrBQVvnkajj0OHjGcW8wkP93TCMJyt8tfIH0Rd5hIZLY5EqUWj9yZgXO7CuDKQZIEI0oZXef3mjfQqH8Q9rZsSgS3DI3mCF3CN003R_ZKKLGWkG8VzwlvdocNxaV_Z-ujp_O0YMyOcmEoTJI-j3JP3nefNboyP7EWND6VWDjJbqg64dvu0PK0EW5L0lnAu0l-WVb2qmXLYao_7bOlD8JLFZOyUgHAWmetmnm0NuxzevJEVBkNUu1jQatPzf2Jzf9SGKM_3bM_z0TM5McCZ2Pikzwp96QlaiEEWV_j2g02PT5Y3td-lfpoKxoFD5tltGNY9TlPzK-4a9Iz0hloiCprxWMLGKw2JzctrPF1xr42PKGNgPE84xAQ3sQ4mQ==\"},{\"role\":\"tool\",\"tool_call_id\":\"call_onp7EArhOgXu0hGRF6Y5WHLu\",\"content\":\"{\\\"code\\\":\\\"ZEBRA\\\",\\\"rate\\\":23}\"}],\"tools\":[{\"type\":\"function\",\"function\":{\"name\":\"lookup_rate\",\"description\":\"Retrieve the private conversion rate for aLine truncated
},
"response": {
"status": 200,
"headers": {
"content-type": "text/event-stream; charset=utf-8"
},
"body": "data: {\"id\":\"resp_014406f0f45e19c6016ac312f2137487d2aa3a1f180cbf07eb\",\"object\":\"chat.completion.chunk\",\"created\":1791169266,\"model\":\"openai-gpt-54-mini\",\"choices\":[{\"index\":0,\"delta\":{\"reasoning_content\":\"**Calculating a simple math problem**\\n\\nI'm\"},\"finish_reason\":null}]}\n\ndata: {\"id\":\"resp_014406f0f45e19c6016ac312f2137487d2aa3a1f180cbf07eb\",\"object\":\"chat.completion.chunk\",\"created\":1791169266,\"model\":\"openai-gpt-54-mini\",\"choices\":[{\"index\":0,\"delta\":{\"reasoning_content\":\" working\"},\"finish_reason\":null}]}\n\ndata: {\"id\":\"resp_014406f0f45e19c6016ac312f2137487d2aa3a1f180cbf07eb\",\"object\":\"chat.completion.chunk\",\"created\":1791169266,\"model\":\"openai-gpt-54-mini\",\"choices\":[{\"index\":0,\"delta\":{\"reasoning_content\":\" through\"},\"finish_reason\":null}]}\n\ndata: {\"id\":\"resp_014406f0f45e19c6016ac312f2137487d2aa3a1f180cbf07eb\",\"object\":\"chat.completion.chunk\",\"created\":1791169266,\"model\":\"openai-gpt-54-mini\",\"choices\":[{\"index\":0,\"delta\":{\"reasoning_content\":\" the\"},\"finish_reason\":null}]}\n\ndata: {\"id\":\"resp_014406f0f45e19c6016ac312f2137487d2aa3a1f180cbf07eb\",\"object\":\"chat.completion.chunk\",\"created\":1791169266,\"model\":\"openai-gpt-54-mini\",\"choices\":[{\"index\":0,\"delta\":{\"reasoning_content\":\" math\"},\"finish_reason\":null}]}\n\ndata: {\"id\":\"resp_014406f0f45e19c6016ac312f2137487d2aa3a1f180cbf07eb\",\"object\":\"chat.completion.chunk\",\"created\":1791169266,\"model\":\"openai-gpt-54-mini\",\"choices\":[{\"index\":0,\"delta\":{\"reasoning_content\":\":\"},\"finish_reason\":null}]}\n\ndata: {\"id\":\"resp_014406f0f45e19c6016ac312f2137487d2aa3a1f180cbf07eb\",\"object\":\"chat.completion.chunk\",\"created\":1791169266,\"model\":\"openai-gpt-54-mini\",\"choices\":[{\"index\":0,\"delta\":{\"reasoning_content\":\" first\"},\"finish_reason\":null}]}\n\ndata: {\"id\":\"resp_014406f0f45e19c6016ac312f2137487d2aa3a1f180cbf07eb\",\"object\":\"chat.completion.chunk\",\"created\":1791169266,\"model\":\"openai-gpt-54-mini\",\"choices\":[{\"index\":0,\"delta\":{\"reasoning_content\":\",\"},\"finish_reason\":null}]}\n\ndata: {\"id\":\"resp_014406f0f45e19c6016ac312f2137487d2aa3a1f180cbf07eb\",\"object\":\"chat.completion.chunk\",\"created\":1791169266,\"model\":\"openai-gpt-54-mini\",\"choices\":[{\"index\":0,\"delta\":{\"reasoning_content\":\" I\"},\"finish_reason\":null}]}\n\ndata: {\"id\":\"resp_014406f0f45e19c6016ac312f2137487d2aa3a1f180cbf07eb\",\"object\":\"chat.completion.chunk\",\"created\":1791169266,\"model\":\"openai-gpt-54-mini\",\"choices\":[{\"index\":0,\"delta\":{\"reasoning_content\":\" multiply\"},\"finish_reason\":null}]}\n\ndata: {\"id\":\"resp_014406f0f45e19c6016ac312f2137487d2aa3a1f180cbf07eb\",\"object\":\"chat.completion.chunk\",\"created\":1791169267,\"model\":\"openai-gpt-54-mini\",\"choices\":[{\"index\":0,\"delta\":{\"reasoning_content\":\" 23\"},\"finish_reason\":null}]}\n\ndata: {\"id\":\"resp_014406f0f45e19c6016ac312f2137487d2aa3a1f180cbf07eb\",\"object\":\"chat.completion.chunk\",\"created\":1791169267,\"model\":\"openai-gpt-54-mini\",\"choices\":[{\"index\":0,\"delta\":{\"reasoning_content\":\" by\"},\"finish_reason\":null}]}\n\ndata: {\"id\":\"resp_014406f0f45e19c6016ac312f2137487d2aa3a1f180cbf07eb\",\"object\":\"chat.completion.chunk\",\"created\":1791169267,\"model\":\"openai-gpt-54-mini\",\"choices\":[{\"index\":0,\"delta\":{\"reasoning_content\":\" 17\"},\"finish_reason\":null}]}\n\ndata: {\"id\":\"resp_014406f0f45e19c6016ac312f2137487d2aa3a1f180cbf07eb\",\"object\":\"chat.completion.chunk\",\"created\":1791169267,\"model\":\"openai-gpt-54-mini\",\"choices\":[{\"index\":0,\"delta\":{\"reasoning_content\":\",\"},\"finish_reason\":null}]}\n\ndata: {\"id\":\"resp_014406f0f45e19c6016ac312f2137487d2aa3a1f180cbf07eb\",\"object\":\"chat.completion.chunk\",\"created\":1791169267,\"model\":\"openai-gpt-54-mini\",\"choices\":[{\"index\":0,\"delta\":{\"reasoning_content\":\" and\"},\"finish_reason\":null}]}\n\ndata: {\"id\":\"resp_014406f0f45e19c6016ac312f2137487d2aa3a1f180cbf07eb\",\"object\":\"chat.completion.chunk\",\"created\":1791169267,\"model\":\"openai-gpt-54-mini\",\"choices\":[{\"index\":0,\"delta\":{\"reasoning_content\":\" that\"},\"finish_reason\":null}]}\n\ndata: {\"id\":\"resp_014406f0f45e19c6016ac312f2137487d2aa3a1f180cbf07eb\",\"object\":\"chat.completion.chunk\",\"created\":1791169267,\"model\":\"openai-gpt-54-mini\",\"choices\":[{\"index\":0,\"delta\":{\"reasoning_content\":\" gives\"},\"finish_reason\":null}]}\n\ndata: {\"id\":\"resp_014406f0f45e19c6016ac312f2137487d2aa3a1f180cbf07eb\",\"object\":\"chat.completion.chunk\",\"created\":1791169267,\"model\":\"openai-gpt-54-mini\",\"choices\":[{\"index\":0,\"delta\":{\"reasoning_content\":\" me\"},\"finish_reason\":null}]}\n\ndata: {\"id\":\"resp_014406f0f45e19c6016ac312f2137487d2aa3a1f180cbf07eb\",\"object\":\"chat.completion.chunk\",\"created\":1791169267,\"model\":\"openai-gpt-54-mini\"Line truncated
}
},
{
"transport": "http",
"request": {
"method": "POST",
"url": "https://api.venice.ai/api/v1/chat/completions",
"headers": {
"content-type": "application/json"
},
"body": "{\"model\":\"openai-gpt-54-mini\",\"messages\":[{\"role\":\"system\",\"content\":\"Use the provided tool for private data. After receiving the result, give only the requested number; do not call the tool again.\"},{\"role\":\"user\",\"content\":\"Look up conversion rate for code ZEBRA. Then multiply that rate by 17 and add 9. You must use lookup_rate before answering.\"},{\"role\":\"assistant\",\"content\":null,\"tool_calls\":[{\"id\":\"call_onp7EArhOgXu0hGRF6Y5WHLu\",\"type\":\"function\",\"function\":{\"name\":\"lookup_rate\",\"arguments\":\"{\\\"code\\\":\\\"ZEBRA\\\"}\"}}],\"reasoning_details\":[{\"type\":\"reasoning.encrypted\",\"data\":\"gAAAAABqwxLxEN4vtmI2WcZ_aDDttdmPwAHgnV2gLZBo_f_mmTdGm4oZzE2JVzcj72AeXHyABmmo2VQN4nXaZK9GSSUZRlYL8mECMxKTj9AMgL8zlnuN5WDexxU-e9SduGP36RN9nH56-go4dz8eu5DEMvMf_TMoTnxz5AnC_UWbty63Vp8jEgkD-DoRjd0cg9LjthP_FLyMvP2Wj9qMJxEuO7Mw2_Pu4I3YeChnygLvmRfRR2QkamL5vjb9yHkl0Lb502vEGB2DW_czNEpHxisifvwRBjA7vk0mbn2dlhiR80aNHO71hC-QqrtUfPXx2HIbVLfbPdJf0saI0wD7ZuCJSQCBfB7fMCnj2ZLQeXa67lgXwsZDLBg4HmSJFvsMinx1IHIjUk7iebsKvmYx40cjqpXL3wj7htZWk2hO9OyGGUwlRsn5THdhTsgwg2SPbuHxb-sLPQnEFZ7bRp5lsVm31X2dlHDa1uZ7KLJZyy9g2CLIKsBwFFoCK7jY3HBXfL21ac5TAkOzVmz-XKl6OMPSl-PSdByOO15uHHpyOcJvJvuaea5Tj1nihtuDlhUP5EQPi7MiWpJMFo9AhDCafAbaQOEF5W8AQPeb_GXvPTzhySRAC8PxrhraEcZ4fjpjLqkv6mi_A1YHwbHEHHWjcK5VEKc3dadlIM_s6djclFBocVXrvnQQElVhWBDM-RtK9sgR07BFj5LVk6fYvg0b6gQnbDuQH18jhtPoj0SX0lN921XEQ1ebMCWGLANND2ew_wyNShYouJohefLULicWG57xh_yV1KEG1ufBedq2CPvCHA9Hm5UssbeJx5yBriyUA2Yh4D7j2UBT_KDWEHydDLQ4mWEyLTfQn0ZB3ZOqkU79z3HpK2w_VHUnkl5nEYEU2bmF5lsG33B4gtbQIBtZT76-fizXsvVZx__taOWXg96VMi3CD3DY0TS76B4U92KFmAFJ4aeiM8bkPuzf1Qd202EWE39CotUDoxAM6mzQu0NObTDnGQE776P6kzUoIKUmP8sDwYG5xot8ZTuXcBYysvj4-pFC_fvqMhsdfgonbke78SIjHtmePsOkfHBnCjSSUFKEbaM0nm8jJ6nYVwiYFuXBKD31QD462TAC7ORVIn32sWSgV2t3Sd7vq4ws2Yy7nUoILRAt_NYQUEXt7_R8J5YWj1L6Ruagk7Vtt-CAWxT6FhVyU4JhMb6-tJrYHgdmtunYrjpHBiOVdQR25aOmLbc_WlDvpO4azcRazpgkASAjPaaDycYceKcheiyUDBgFrvcSYrS1fJb6YZrTzOKw1Ztd0YsklS4nc3jffLNLrBQVvnkajj0OHjGcW8wkP93TCMJyt8tfIH0Rd5hIZLY5EqUWj9yZgXO7CuDKQZIEI0oZXef3mjfQqH8Q9rZsSgS3DI3mCF3CN003R_ZKKLGWkG8VzwlvdocNxaV_Z-ujp_O0YMyOcmEoTJI-j3JP3nefNboyP7EWND6VWDjJbqg64dvu0PK0EW5L0lnAu0l-WVb2qmXLYao_7bOlD8JLFZOyUgHAWmetmnm0NuxzevJEVBkNUu1jQatPzf2Jzf9SGKM_3bM_z0TM5McCZ2Pikzwp96QlaiEEWV_j2g02PT5Y3td-lfpoKxoFD5tltGNY9TlPzK-4a9Iz0hloiCprxWMLGKw2JzctrPF1xr42PKGNgPE84xAQ3sQ4mQ==\",\"id\":\"rs_014406f0f45e19c6016ac312f01d2487d2a5f2bdb7fbc42f76\",\"format\":\"openai-responses-v1\",\"index\":0}],\"reasoning_content\":\"**Calculating private data**\\n\\nI need to follow the developer's instructions and use the provided tool to handle private data. First, I should look up the rate for \\\"ZEBRA\\\" before I answer. After that, I’ll multiply the rate by 17 and then add 9 to get the required number. It’s important to give only the requested number and avoid making another tool call. I’ll likely use the commentary channel for this tool call.\\n\\n__ENCRYPTED_REASONING__id=rs_014406f0f45e19c6016ac312f01d2487d2a5f2bdb7fbc42f76\\ngAAAAABqwxLxEN4vtmI2WcZ_aDDttdmPwAHgnV2gLZBo_f_mmTdGm4oZzE2JVzcj72AeXHyABmmo2VQN4nXaZK9GSSUZRlYL8mECMxKTj9AMgL8zlnuN5WDexxU-e9SduGP36RN9nH56-go4dz8eu5DEMvMf_TMoTnxz5AnC_UWbty63Vp8jEgkD-DoRjd0cg9LjthP_FLyMvP2Wj9qMJxEuO7Mw2_Pu4I3YeChnygLvmRfRR2QkamL5vjb9yHkl0Lb502vEGB2DW_czNEpHxisifvwRBjA7vk0mbn2dlhiR80aNHO71hC-QqrtUfPXx2HIbVLfbPdJf0saI0wD7ZuCJSQCBfB7fMCnj2ZLQeXa67lgXwsZDLBg4HmSJFvsMinx1IHIjUk7iebsKvmYx40cjqpXL3wj7htZWk2hO9OyGGUwlRsn5THdhTsgwg2SPbuHxb-sLPQnEFZ7bRp5lsVm31X2dlHDa1uZ7KLJZyy9g2CLIKsBwFFoCK7jY3HBXfL21ac5TAkOzVmz-XKl6OMPSl-PSdByOO15uHHpyOcJvJvuaea5Tj1nihtuDlhUP5EQPi7MiWpJMFo9AhDCafAbaQOEF5W8AQPeb_GXvPTzhySRAC8PxrhraEcZ4fjpjLqkv6mi_A1YHwbHEHHWjcK5VEKc3dadlIM_s6djclFBocVXrvnQQElVhWBDM-RtK9sgR07BFj5LVk6fYvg0b6gQnbDuQH18jhtPoj0SX0lN921XEQ1ebMCWGLANND2ew_wyNShYouJohefLULicWG57xh_yV1KEG1ufBedq2CPvCHA9Hm5UssbeJx5yBriyUA2Yh4D7j2UBT_KDWEHydDLQ4mWEyLTfQn0ZB3ZOqkU79z3HpK2w_VHUnkl5nEYEU2bmF5lsG33B4gtbQIBtZT76-fizXsvVZx__taOWXg96VMi3CD3DY0TS76B4U92KFmAFJ4aeiM8bkPuzf1Qd202EWE39CotUDoxAM6mzQu0NObTDnGQE776P6kzUoIKUmP8sDwYG5xot8ZTuXcBYysvj4-pFC_fvqMhsdfgonbke78SIjHtmePsOkfHBnCjSSUFKEbaM0nm8jJ6nYVwiYFuXBKD31QD462TAC7ORVIn32sWSgV2t3Sd7vq4ws2Yy7nUoILRAt_NYQUEXt7_R8J5YWj1L6Ruagk7Vtt-CAWxT6FhVyU4JhMb6-tJrYHgdmtunYrjpHBiOVdQR25aOmLbc_WlDvpO4azcRazpgkASAjPaaDycYceKcheiyUDBgFrvcSYrS1fJb6YZrTzOKw1Ztd0YsklS4nc3jffLNLrBQVvnkajj0OHjGcW8wkP93TCMJyt8tfIH0Rd5hIZLY5EqUWj9yZgXO7CuDKQZIEI0oZXef3mjfQqH8Q9rZsSgS3DI3mCF3CN003R_ZKKLGWkG8VzwlvdocNxaV_Z-ujp_O0YMyOcmEoTJI-j3JP3nefNboyP7EWND6VWDjJbqg64dvu0PK0EW5L0lnAu0l-WVb2qmXLYao_7bOlD8JLFZOyUgHAWmetmnm0NuxzevJEVBkNUu1jQatPzf2Jzf9SGKM_3bM_z0TM5McCZ2Pikzwp96QlaiEEWV_j2g02PT5Y3td-lfpoKxoFD5tltGNY9TlPzK-4a9Iz0hloiCprxWMLGKw2JzctrPF1xr42PKGNgPE84xAQ3sQ4mQ==\"},{\"role\":\"tool\",\"tool_call_id\":\"call_onp7EArhOgXu0hGRF6Y5WHLu\",\"content\":\"{\\\"code\\\":\\\"ZEBRA\\\",\\\"rate\\\":23}\"},{\"role\":\"assistant\",\"content\":\"400\",\"reasoning_details\":[{\"type\":\"reasoning.encrypted\",\"data\":\"gAAAAABqwxL0Nl6Jl5EZuOLine truncated
},
"response": {
"status": 200,
"headers": {
"content-type": "text/event-stream; charset=utf-8"
},
"body": "data: {\"id\":\"resp_014406f0f45e19c6016ac312f49b9c87d2afc60121b8910612\",\"object\":\"chat.completion.chunk\",\"created\":1791169269,\"model\":\"openai-gpt-54-mini\",\"choices\":[{\"index\":0,\"delta\":{\"reasoning_content\":\"**Calculating a simple addition**\\n\\nThe\"},\"finish_reason\":null}]}\n\ndata: {\"id\":\"resp_014406f0f45e19c6016ac312f49b9c87d2afc60121b8910612\",\"object\":\"chat.completion.chunk\",\"created\":1791169269,\"model\":\"openai-gpt-54-mini\",\"choices\":[{\"index\":0,\"delta\":{\"reasoning_content\":\" user\"},\"finish_reason\":null}]}\n\ndata: {\"id\":\"resp_014406f0f45e19c6016ac312f49b9c87d2afc60121b8910612\",\"object\":\"chat.completion.chunk\",\"created\":1791169269,\"model\":\"openai-gpt-54-mini\",\"choices\":[{\"index\":0,\"delta\":{\"reasoning_content\":\" wants\"},\"finish_reason\":null}]}\n\ndata: {\"id\":\"resp_014406f0f45e19c6016ac312f49b9c87d2afc60121b8910612\",\"object\":\"chat.completion.chunk\",\"created\":1791169269,\"model\":\"openai-gpt-54-mini\",\"choices\":[{\"index\":0,\"delta\":{\"reasoning_content\":\" me\"},\"finish_reason\":null}]}\n\ndata: {\"id\":\"resp_014406f0f45e19c6016ac312f49b9c87d2afc60121b8910612\",\"object\":\"chat.completion.chunk\",\"created\":1791169269,\"model\":\"openai-gpt-54-mini\",\"choices\":[{\"index\":0,\"delta\":{\"reasoning_content\":\" to\"},\"finish_reason\":null}]}\n\ndata: {\"id\":\"resp_014406f0f45e19c6016ac312f49b9c87d2afc60121b8910612\",\"object\":\"chat.completion.chunk\",\"created\":1791169269,\"model\":\"openai-gpt-54-mini\",\"choices\":[{\"index\":0,\"delta\":{\"reasoning_content\":\" add\"},\"finish_reason\":null}]}\n\ndata: {\"id\":\"resp_014406f0f45e19c6016ac312f49b9c87d2afc60121b8910612\",\"object\":\"chat.completion.chunk\",\"created\":1791169269,\"model\":\"openai-gpt-54-mini\",\"choices\":[{\"index\":0,\"delta\":{\"reasoning_content\":\" 7\"},\"finish_reason\":null}]}\n\ndata: {\"id\":\"resp_014406f0f45e19c6016ac312f49b9c87d2afc60121b8910612\",\"object\":\"chat.completion.chunk\",\"created\":1791169269,\"model\":\"openai-gpt-54-mini\",\"choices\":[{\"index\":0,\"delta\":{\"reasoning_content\":\" to\"},\"finish_reason\":null}]}\n\ndata: {\"id\":\"resp_014406f0f45e19c6016ac312f49b9c87d2afc60121b8910612\",\"object\":\"chat.completion.chunk\",\"created\":1791169269,\"model\":\"openai-gpt-54-mini\",\"choices\":[{\"index\":0,\"delta\":{\"reasoning_content\":\" the\"},\"finish_reason\":null}]}\n\ndata: {\"id\":\"resp_014406f0f45e19c6016ac312f49b9c87d2afc60121b8910612\",\"object\":\"chat.completion.chunk\",\"created\":1791169269,\"model\":\"openai-gpt-54-mini\",\"choices\":[{\"index\":0,\"delta\":{\"reasoning_content\":\" previous\"},\"finish_reason\":null}]}\n\ndata: {\"id\":\"resp_014406f0f45e19c6016ac312f49b9c87d2afc60121b8910612\",\"object\":\"chat.completion.chunk\",\"created\":1791169269,\"model\":\"openai-gpt-54-mini\",\"choices\":[{\"index\":0,\"delta\":{\"reasoning_content\":\" final\"},\"finish_reason\":null}]}\n\ndata: {\"id\":\"resp_014406f0f45e19c6016ac312f49b9c87d2afc60121b8910612\",\"object\":\"chat.completion.chunk\",\"created\":1791169269,\"model\":\"openai-gpt-54-mini\",\"choices\":[{\"index\":0,\"delta\":{\"reasoning_content\":\" answer\"},\"finish_reason\":null}]}\n\ndata: {\"id\":\"resp_014406f0f45e19c6016ac312f49b9c87d2afc60121b8910612\",\"object\":\"chat.completion.chunk\",\"created\":1791169269,\"model\":\"openai-gpt-54-mini\",\"choices\":[{\"index\":0,\"delta\":{\"reasoning_content\":\",\"},\"finish_reason\":null}]}\n\ndata: {\"id\":\"resp_014406f0f45e19c6016ac312f49b9c87d2afc60121b8910612\",\"object\":\"chat.completion.chunk\",\"created\":1791169269,\"model\":\"openai-gpt-54-mini\",\"choices\":[{\"index\":0,\"delta\":{\"reasoning_content\":\" which\"},\"finish_reason\":null}]}\n\ndata: {\"id\":\"resp_014406f0f45e19c6016ac312f49b9c87d2afc60121b8910612\",\"object\":\"chat.completion.chunk\",\"created\":1791169269,\"model\":\"openai-gpt-54-mini\",\"choices\":[{\"index\":0,\"delta\":{\"reasoning_content\":\" was\"},\"finish_reason\":null}]}\n\ndata: {\"id\":\"resp_014406f0f45e19c6016ac312f49b9c87d2afc60121b8910612\",\"object\":\"chat.completion.chunk\",\"created\":1791169269,\"model\":\"openai-gpt-54-mini\",\"choices\":[{\"index\":0,\"delta\":{\"reasoning_content\":\" 400\"},\"finish_reason\":null}]}\n\ndata: {\"id\":\"resp_014406f0f45e19c6016ac312f49b9c87d2afc60121b8910612\",\"object\":\"chat.completion.chunk\",\"created\":1791169269,\"model\":\"openai-gpt-54-mini\",\"choices\":[{\"index\":0,\"delta\":{\"reasoning_content\":\".\"},\"finish_reason\":null}]}\n\ndata: {\"id\":\"resp_014406f0f45e19c6016ac312f49b9c87d2afc60121b8910612\",\"object\":\"chat.completion.chunk\",\"created\":1791169269,\"model\":\"openai-gpt-54-mini\",\"choices\":[{\"index\":0,\"delta\":{\"reasoning_content\":\" This\"},\"finish_reason\":null}]}\n\ndata: {\"id\":\"resp_014406f0f45e19c6016ac312f49b9c87d2afc60121b8910612\",\"object\":\"chat.completion.chunk\",\"created\":1791169269,\"model\":\"openai-gpt-54-mini\",\"Line truncated
}
}
]
}
@@ -1,35 +0,0 @@
{
"version": 1,
"metadata": {
"tags": [
"prefix:venice-chat",
"provider:venice",
"protocol:venice-chat",
"text",
"reasoning",
"toggle"
],
"name": "venice-chat/qwen-disables-reasoning",
"recordedAt": "2026-10-05T02:59:04.614Z"
},
"interactions": [
{
"transport": "http",
"request": {
"method": "POST",
"url": "https://api.venice.ai/api/v1/chat/completions",
"headers": {
"content-type": "application/json"
},
"body": "{\"model\":\"qwen3-6-27b\",\"messages\":[{\"role\":\"user\",\"content\":\"What is 23 multiplied by 17 plus 9? Reply with just the number.\"}],\"stream\":true,\"stream_options\":{\"include_usage\":true},\"max_completion_tokens\":4096,\"reasoning\":{\"enabled\":false},\"venice_parameters\":{\"include_venice_system_prompt\":false}}"
},
"response": {
"status": 200,
"headers": {
"content-type": "text/event-stream; charset=utf-8"
},
"body": "data: {\"service_tier\":null,\"id\":\"chatcmpl-RxYUpStKYw7YgEZcXjsb2dOt\",\"object\":\"chat.completion.chunk\",\"created\":1791169144,\"model\":\"qwen3-6-27b\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":\"\",\"reasoning_content\":null,\"tool_calls\":null},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"service_tier\":\"default\",\"id\":\"chatcmpl-RxYUpStKYw7YgEZcXjsb2dOt\",\"object\":\"chat.completion.chunk\",\"created\":1791169144,\"model\":\"qwen3-6-27b\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":\"\",\"reasoning_content\":null,\"tool_calls\":null},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"service_tier\":\"default\",\"id\":\"chatcmpl-RxYUpStKYw7YgEZcXjsb2dOt\",\"object\":\"chat.completion.chunk\",\"created\":1791169144,\"model\":\"qwen3-6-27b\",\"choices\":[{\"index\":0,\"delta\":{\"role\":null,\"content\":\"4\",\"reasoning_content\":null,\"tool_calls\":null},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"service_tier\":\"default\",\"id\":\"chatcmpl-RxYUpStKYw7YgEZcXjsb2dOt\",\"object\":\"chat.completion.chunk\",\"created\":1791169144,\"model\":\"qwen3-6-27b\",\"choices\":[{\"index\":0,\"delta\":{\"role\":null,\"content\":\"0\",\"reasoning_content\":null,\"tool_calls\":null},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"service_tier\":\"default\",\"id\":\"chatcmpl-RxYUpStKYw7YgEZcXjsb2dOt\",\"object\":\"chat.completion.chunk\",\"created\":1791169144,\"model\":\"qwen3-6-27b\",\"choices\":[{\"index\":0,\"delta\":{\"role\":null,\"content\":\"0\",\"reasoning_content\":null,\"tool_calls\":null},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"service_tier\":\"default\",\"id\":\"chatcmpl-RxYUpStKYw7YgEZcXjsb2dOt\",\"object\":\"chat.completion.chunk\",\"created\":1791169144,\"model\":\"qwen3-6-27b\",\"choices\":[{\"index\":0,\"delta\":{\"role\":null,\"content\":\"\",\"reasoning_content\":null,\"tool_calls\":null},\"logprobs\":null,\"finish_reason\":\"stop\"}]}\n\ndata: {\"service_tier\":\"default\",\"id\":\"chatcmpl-RxYUpStKYw7YgEZcXjsb2dOt\",\"object\":\"chat.completion.chunk\",\"created\":1791169144,\"model\":\"qwen3-6-27b\",\"choices\":[],\"usage\":{\"prompt_tokens\":32,\"completion_tokens\":4,\"total_tokens\":36},\"cost\":{\"usd\":0.0000234,\"diem\":0}}\n\ndata: [DONE]\n\n"
}
}
]
}
@@ -1,33 +0,0 @@
{
"version": 1,
"metadata": {
"tags": [
"prefix:venice-chat",
"provider:venice",
"protocol:venice-chat",
"error"
],
"name": "venice-chat/surfaces-venice-model-errors",
"recordedAt": "2026-10-05T02:59:11.768Z"
},
"interactions": [
{
"transport": "http",
"request": {
"method": "POST",
"url": "https://api.venice.ai/api/v1/chat/completions",
"headers": {
"content-type": "application/json"
},
"body": "{\"model\":\"no-such-model-xyz\",\"messages\":[{\"role\":\"user\",\"content\":\"Hello\"}],\"stream\":true,\"stream_options\":{\"include_usage\":true},\"venice_parameters\":{\"include_venice_system_prompt\":false}}"
},
"response": {
"status": 404,
"headers": {
"content-type": "application/json; charset=utf-8"
},
"body": "{\"error\":\"Specified model not found: no-such-model-xyz. Did you mean: z-ai-glm-5-3, z-ai-glm-5-3-flash, z-ai-glm-5-turbo?\"}"
}
}
]
}
@@ -1,26 +0,0 @@
import { LLM } from "../../src/index.js"
import { Venice } from "../../src/providers.js"
LLM.request({
model: Venice.chat("qwen3-6-27b"),
providerOptions: { reasoningEffort: "high", reasoning: { summary: "concise" }, veniceParameters: { includeVeniceSystemPrompt: false } },
})
LLM.request({
model: Venice.configure({ providerOptions: { reasoningEffort: "future-effort" } }).model("future-model"),
providerOptions: { reasoning: { enabled: false }, promptCacheRetention: "future-retention", parallelToolCalls: false },
})
LLM.request({
model: Venice.chat("qwen3-6-27b"),
// @ts-expect-error Thinking toggles are boolean.
providerOptions: { reasoning: { enabled: "false" } },
})
LLM.request({
model: Venice.chat("qwen3-6-27b"),
// @ts-expect-error Venice uses nested reasoning, not Anthropic thinking controls.
providerOptions: { thinking: { type: "disabled" } },
})
LLM.request({
model: Venice.chat("qwen3-6-27b"),
// @ts-expect-error Venice's system-prompt toggle is boolean.
providerOptions: { veniceParameters: { includeVeniceSystemPrompt: "false" } },
})
@@ -431,115 +431,42 @@ describe("Anthropic Messages route", () => {
(yield* compileRequest(
LLM.request({
model: opus48,
messages: [
Message.user("Start."),
Message.assistant("One."),
Message.system("Update."),
Message.assistant("Two."),
],
messages: [Message.user("Before."), Message.system("One."), Message.system("Two.")],
cache: "none",
}),
)).body.messages,
).toEqual([
{ role: "user", content: [{ type: "text", text: "Start." }] },
{ role: "assistant", content: [{ type: "text", text: "One." }] },
{ role: "user", content: [{ type: "text", text: "<system-update>\nUpdate.\n</system-update>" }] },
{ role: "assistant", content: [{ type: "text", text: "Two." }] },
{
role: "user",
content: [
{ type: "text", text: "Before." },
{ type: "text", text: "<system-update>\nOne.\n</system-update>" },
{ type: "text", text: "<system-update>\nTwo.\n</system-update>" },
],
},
])
}),
)
it.effect("moves system updates to the next assistant turn and sends consecutive updates together", () =>
Effect.gen(function* () {
const lower = (messages: ReadonlyArray<Message>) =>
compileRequest(LLM.request({ model: opus48, messages: [...messages], cache: "none" })).pipe(
Effect.map((prepared) => prepared.body.messages),
)
const system = (text: string) => ({ role: "system", content: [{ type: "text", text, cache_control: undefined }] })
const user = (text: string) => ({ role: "user", content: [{ type: "text", text }] })
const assistant = (text: string) => ({ role: "assistant", content: [{ type: "text", text }] })
expect(
yield* lower([
Message.user("Fix it."),
Message.assistant("Done."),
Message.system("Update."),
Message.user("Next."),
]),
).toEqual([user("Fix it."), assistant("Done."), user("Next."), system("Update.")])
expect(yield* lower([Message.user("Before."), Message.system("One."), Message.system("Two.")])).toEqual([
user("Before."),
system("One."),
system("Two."),
])
expect(
yield* lower([
Message.user("Fix it."),
Message.assistant("Done."),
Message.system("One."),
Message.user("Next."),
Message.system("Two."),
Message.assistant("After."),
]),
).toEqual([
user("Fix it."),
assistant("Done."),
user("Next."),
system("One."),
system("Two."),
assistant("After."),
])
expect(
yield* lower([
Message.user("Use the tool."),
Message.assistant([ToolCallPart.make({ id: "call_1", name: "lookup", input: {} })]),
Message.tool({ id: "call_1", name: "lookup", result: "Done." }),
Message.system("Update."),
Message.user("Also check tests."),
]),
).toEqual([
user("Use the tool."),
{ role: "assistant", content: [{ type: "tool_use", id: "call_1", name: "lookup", input: {} }] },
{ role: "user", content: [{ type: "tool_result", tool_use_id: "call_1", content: '"Done."' }] },
user("Also check tests."),
system("Update."),
])
}),
)
it.effect("keeps wrapped system updates in place for models without native system updates", () =>
it.effect("keeps a terminal Vertex system update in the tool-result turn", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
LLM.request({
model,
model: vertexOpus48,
messages: [
Message.user("Fix it."),
Message.assistant("Done."),
Message.system("Update."),
Message.user("Next."),
Message.assistant([ToolCallPart.make({ id: "call_1", name: "lookup", input: {} })]),
Message.tool({ id: "call_1", name: "lookup", result: "Done." }),
Message.system("Operator update."),
],
cache: "none",
}),
)
expect(prepared.body.messages).toEqual([
{ role: "user", content: [{ type: "text", text: "Fix it." }] },
{ role: "assistant", content: [{ type: "text", text: "Done." }] },
{ role: "user", content: [{ type: "text", text: "<system-update>\nUpdate.\n</system-update>" }] },
{ role: "user", content: [{ type: "text", text: "Next." }] },
])
}),
)
it.effect("sends Vertex system updates after local tool results as native system messages", () =>
Effect.gen(function* () {
const toolTurn = [
Message.assistant([ToolCallPart.make({ id: "call_1", name: "lookup", input: {} })]),
Message.tool({ id: "call_1", name: "lookup", result: "Done." }),
Message.system("Operator update."),
]
const lowered = [
{ role: "assistant", content: [{ type: "tool_use", id: "call_1", name: "lookup", input: {} }] },
{
role: "assistant",
content: [{ type: "tool_use", id: "call_1", name: "lookup", input: {} }],
},
{
role: "user",
content: [
@@ -550,23 +477,55 @@ describe("Anthropic Messages route", () => {
is_error: undefined,
cache_control: undefined,
},
{
type: "text",
text: "<system-update>\nOperator update.\n</system-update>",
cache_control: undefined,
},
],
},
{ role: "system", content: [{ type: "text", text: "Operator update.", cache_control: undefined }] },
]
])
}),
)
const terminal = yield* compileRequest(LLM.request({ model: vertexOpus48, messages: toolTurn, cache: "none" }))
const history = yield* compileRequest(
it.effect("preserves folded tool-result system updates across multi-turn Vertex history", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
LLM.request({
model: vertexOpus48,
messages: [...toolTurn, Message.assistant("Acknowledged."), Message.user("Next step.")],
messages: [
Message.assistant([ToolCallPart.make({ id: "call_1", name: "lookup", input: {} })]),
Message.tool({ id: "call_1", name: "lookup", result: "Done." }),
Message.system("Operator update."),
Message.assistant("Acknowledged."),
Message.user("Next step."),
],
cache: "none",
}),
)
expect(terminal.body.messages).toEqual(lowered)
expect(history.body.messages).toEqual([
...lowered,
expect(prepared.body.messages).toEqual([
{
role: "assistant",
content: [{ type: "tool_use", id: "call_1", name: "lookup", input: {} }],
},
{
role: "user",
content: [
{
type: "tool_result",
tool_use_id: "call_1",
content: '"Done."',
is_error: undefined,
cache_control: undefined,
},
{
type: "text",
text: "<system-update>\nOperator update.\n</system-update>",
cache_control: undefined,
},
],
},
{ role: "assistant", content: [{ type: "text", text: "Acknowledged." }] },
{ role: "user", content: [{ type: "text", text: "Next step." }] },
])
@@ -1,233 +0,0 @@
import { expect } from "bun:test"
import { Effect, Schema } from "effect"
import { LLM, LLMEvent, LLMRequest, Message, ToolChoice, ToolDefinition } from "../../src/index.js"
import { Venice } from "../../src/providers.js"
import { LLMClient } from "../../src/route.js"
import { compileRequest } from "../../src/route/client.js"
import { recordedTests } from "../recorded-test.js"
const apiKey = process.env.VENICE_API_KEY ?? "fixture"
const recorded = recordedTests({
prefix: "venice-chat",
provider: "venice",
protocol: "venice-chat",
requires: ["VENICE_API_KEY"],
})
const lookup = ToolDefinition.make({
name: "lookup_rate",
description: "Retrieve the private conversion rate for a code. Only this tool knows the rate.",
inputSchema: {
type: "object",
properties: { code: { type: "string", enum: ["ZEBRA", "MOOSE"] } },
required: ["code"],
additionalProperties: false,
},
})
for (const item of [
{ model: "deepseek-v4-flash", reasoning: "scalar" },
{ model: "claude-opus-4-8", reasoning: "signed", effort: "high" },
{ model: "gemini-3-1-pro-preview", reasoning: "details", effort: "high" },
{ model: "openai-gpt-54-mini", reasoning: "encrypted" },
] as const) {
recorded.effect.with(
`${item.model} preserves reasoning through a tool loop and follow-up`,
{
tags: ["reasoning", "tool", "tool-loop", "continuation", "usage", item.reasoning],
metadata: { model: item.model },
},
() =>
Effect.gen(function* () {
const request = LLM.request({
model: Venice.configure({ apiKey }).chat(item.model),
system:
"Use the provided tool for private data. After receiving the result, give only the requested number; do not call the tool again.",
prompt:
"Look up conversion rate for code ZEBRA. Then multiply that rate by 17 and add 9. You must use lookup_rate before answering.",
tools: [lookup],
generation: { maxTokens: 4096 },
providerOptions: "effort" in item ? { reasoningEffort: item.effort } : undefined,
})
const first = yield* LLMClient.generate(request)
expect(first.toolCalls).toMatchObject([{ name: "lookup_rate", input: { code: "ZEBRA" } }])
expect(first.finishReason.normalized).toBe("tool-calls")
expect(first.events.filter(LLMEvent.is.finish)).toHaveLength(1)
const continuation = LLMRequest.update(request, {
toolChoice: ToolChoice.make("none"),
messages: [
...request.messages,
first.message,
...first.toolCalls.map((call) =>
Message.tool({ id: call.id, name: call.name, result: { code: "ZEBRA", rate: 23 } }),
),
],
})
const compiled = yield* compileRequest(continuation)
const assistant = compiled.body.messages.find((message) => message.role === "assistant")
expect(assistant).toBeDefined()
// Some models call a tool immediately and only reason after its result.
if (item.reasoning === "scalar" && first.reasoning.length > 0) {
expect(assistant).toMatchObject({ reasoning_content: first.reasoning })
}
if (item.reasoning === "signed") {
const details = first.message.content
.filter((part) => part.type === "reasoning")
.flatMap((part) => part.providerMetadata?.venice?.reasoningDetails ?? [])
expect(details).toEqual(
expect.arrayContaining([
expect.objectContaining({ type: "reasoning.text", signature: expect.any(String) }),
]),
)
expect(assistant).toMatchObject({ reasoning_details: details })
}
if (item.reasoning === "details")
expect(assistant).toMatchObject({
reasoning_details: expect.arrayContaining([
expect.objectContaining({ type: "reasoning.encrypted", data: expect.any(String) }),
]),
})
if (item.reasoning === "encrypted") {
expect(first.reasoning).not.toContain("__ENCRYPTED_REASONING__")
const details = first.message.content
.filter((part) => part.type === "reasoning")
.flatMap((part) => part.providerMetadata?.venice?.reasoningDetails ?? [])
if (details.length > 0) expect(assistant).toMatchObject({ reasoning_details: details })
}
const second = yield* LLMClient.generate(continuation)
expect(second.text).toContain("400")
expect(second.reasoning).not.toContain("__ENCRYPTED_REASONING__")
expect(second.toolCalls).toHaveLength(0)
expect(second.finishReason.normalized).toBe("stop")
expect(second.usage?.inputTokens).toBeGreaterThan(0)
expect(second.usage?.outputTokens).toBeGreaterThan(0)
const followUp = LLMRequest.update(continuation, {
messages: [
...continuation.messages,
second.message,
Message.user("Add 7 to your previous final answer. Reply with just the number."),
],
})
const third = yield* LLMClient.generate(followUp)
expect(third.text).toContain("407")
expect(third.finishReason.normalized).toBe("stop")
if (item.reasoning === "encrypted") {
expect(
[first, second, third].flatMap((response) =>
response.message.content
.filter((part) => part.type === "reasoning")
.flatMap((part) => part.providerMetadata?.venice?.reasoningDetails ?? []),
),
).toEqual(
expect.arrayContaining([
expect.objectContaining({ type: "reasoning.encrypted", data: expect.any(String) }),
]),
)
}
}),
180_000,
)
}
recorded.effect.with(
"Gemini preserves per-call signatures through parallel tools",
{ tags: ["tool", "parallel", "signature"] },
() =>
Effect.gen(function* () {
const request = LLM.request({
model: Venice.configure({ apiKey }).chat("gemini-3-8-flash"),
prompt:
"Look up private rates for both ZEBRA and MOOSE using two calls to lookup_rate. Then add the rates and multiply by 7. Use parallel calls if possible.",
tools: [lookup],
providerOptions: { reasoningEffort: "high", parallelToolCalls: true },
generation: { maxTokens: 4096 },
})
const first = yield* LLMClient.generate(request)
expect(first.toolCalls).toHaveLength(2)
const continuation = LLMRequest.update(request, {
toolChoice: ToolChoice.make("none"),
messages: [
...request.messages,
first.message,
...first.toolCalls.map((call) =>
Message.tool({
id: call.id,
name: call.name,
result: {
rate:
Schema.decodeUnknownSync(Schema.Struct({ code: Schema.String }))(call.input).code === "ZEBRA"
? 23
: 31,
},
}),
),
],
})
const compiled = yield* compileRequest(continuation)
expect(compiled.body.messages).toEqual(
expect.arrayContaining([
expect.objectContaining({
tool_calls: first.toolCalls.map((call) =>
expect.objectContaining({
id: call.id,
...(call.providerMetadata?.venice?.thoughtSignature === undefined
? {}
: { thought_signature: call.providerMetadata.venice.thoughtSignature }),
}),
),
}),
]),
)
const second = yield* LLMClient.generate(continuation)
expect(second.text).toContain("378")
const signature = second.message.content
.filter((part) => part.type === "text")
.map((part) => part.providerMetadata?.venice?.messageThoughtSignature)
.find((value) => value !== undefined)
expect(signature).toEqual(expect.any(String))
const followUp = LLMRequest.update(continuation, {
messages: [
...continuation.messages,
second.message,
Message.user("Add 7 to your previous final answer. Reply with just the number."),
],
})
expect((yield* compileRequest(followUp)).body.messages).toEqual(
expect.arrayContaining([expect.objectContaining({ role: "assistant", thought_signature: signature })]),
)
expect((yield* LLMClient.generate(followUp)).text).toContain("385")
}),
180_000,
)
recorded.effect.with(
"Qwen disables reasoning",
{ tags: ["text", "reasoning", "toggle"] },
() =>
Effect.gen(function* () {
const response = yield* LLMClient.generate(
LLM.request({
model: Venice.configure({ apiKey }).chat("qwen3-6-27b"),
prompt: "What is 23 multiplied by 17 plus 9? Reply with just the number.",
providerOptions: { reasoning: { enabled: false } },
generation: { maxTokens: 4096 },
}),
)
expect(response.text).toContain("400")
expect(response.reasoning).toBe("")
}),
120_000,
)
recorded.effect.with("surfaces Venice model errors", { tags: ["error"] }, () =>
Effect.gen(function* () {
const result = yield* LLMClient.generate(
LLM.request({ model: Venice.configure({ apiKey }).chat("no-such-model-xyz"), prompt: "Hello" }),
).pipe(Effect.result)
expect(result._tag).toBe("Failure")
if (result._tag === "Failure") {
expect(result.failure.reason._tag).toBe("InvalidRequest")
expect(result.failure.message).toContain("Specified model not found")
expect(result.failure.reason.http?.status).toBe(404)
}
}),
)
-370
View File
@@ -1,370 +0,0 @@
import { expect } from "bun:test"
import { ConfigProvider, Effect } from "effect"
import { Headers } from "effect/unstable/http"
import { Auth, LLM, LLMClient, Message } from "../../src/index.js"
import { OpenAI, Venice } 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 { fixedResponse } from "../lib/http.js"
import { sseEvents } from "../lib/sse.js"
const model = Venice.configure({ apiKey: "fixture" }).chat("fixture-model")
const frame = (delta: Record<string, unknown>, finish_reason: string | null = null) => ({
choices: [{ delta, finish_reason }],
})
it.effect("Venice owns its endpoint, environment auth, aliases, and SDK-compatible prompt default", () =>
Effect.gen(function* () {
expect(Venice.provider.model).toBe(Venice.provider.chat)
const selected = Venice.chat("qwen3-6-27b")
const request = LLM.request({ model: selected, prompt: "Hello", promptCacheKey: "session-123" })
const compiled = yield* compileRequest(request)
const url = Endpoint.render(selected.route.endpoint, { request, body: compiled.body }).toString()
expect(url).toBe("https://api.venice.ai/api/v1/chat/completions")
expect(selected.provider).toBe("venice")
expect(compiled.body.venice_parameters).toEqual({ include_venice_system_prompt: false })
expect(compiled.body.prompt_cache_key).toBe("session-123")
expect(compiled.body.store).toBeUndefined()
expect(compiled.body.stream_options).toEqual({ include_usage: true })
const headers = yield* selected.route.auth.apply({
request,
method: "POST",
url,
body: "{}",
headers: Headers.empty,
})
expect(headers.authorization).toBe("Bearer fixture")
const custom = Venice.configure({
baseURL: "https://gateway.example/v1",
queryParams: { deployment: "private" },
auth: Auth.header("x-key", "fixture"),
}).chat("qwen3-6-27b")
expect(Endpoint.render(custom.route.endpoint, { request, body: compiled.body }).toString()).toBe(
"https://gateway.example/v1/chat/completions?deployment=private",
)
}).pipe(Effect.provide(ConfigProvider.layer(ConfigProvider.fromEnv({ env: { VENICE_API_KEY: "fixture" } })))),
)
it.effect("Venice lowers typed options, model defaults, and explicit reasoning overrides", () =>
Effect.gen(function* () {
const compiled = yield* compileRequest(
LLM.request({
model: Venice.configure({ apiKey: "fixture", providerOptions: { reasoningEffort: "low" } }).model(
"qwen3-6-27b",
),
generation: { maxTokens: 200, topK: 20 },
providerOptions: {
reasoningEffort: "future-effort",
reasoning: { effort: "high", enabled: true, summary: "concise" },
veniceParameters: { includeVeniceSystemPrompt: true, enableWebSearch: "auto", stripThinkingResponse: false },
parallelToolCalls: false,
maxCompletionTokens: 123,
minP: 0.1,
repetitionPenalty: 1.1,
stopTokenIds: [12],
promptCacheRetention: "24h",
logprobs: true,
topLogprobs: 2,
minTemp: 0.1,
maxTemp: 1.5,
responseFormat: { type: "json_object" },
user: "user-123",
},
}),
)
expect(compiled.body).toMatchObject({
max_completion_tokens: 123,
top_k: 20,
min_p: 0.1,
repetition_penalty: 1.1,
reasoning: { effort: "future-effort", enabled: true, summary: "concise" },
venice_parameters: {
include_venice_system_prompt: true,
enable_web_search: "auto",
strip_thinking_response: false,
},
parallel_tool_calls: false,
stop_token_ids: [12],
prompt_cache_retention: "24h",
logprobs: true,
top_logprobs: 2,
min_temp: 0.1,
max_temp: 1.5,
response_format: { type: "json_object" },
user: "user-123",
})
expect(compiled.body.reasoning_effort).toBeUndefined()
expect(compiled.body.max_tokens).toBeUndefined()
expect(
(yield* compileRequest(LLM.request({ model, providerOptions: { reasoning: { enabled: false } } }))).body
.reasoning,
).toEqual({ enabled: false })
expect(
(yield* compileRequest(LLM.request({ model, cache: "none", promptCacheKey: "session-123" }))).body
.prompt_cache_key,
).toBeUndefined()
}),
)
it.effect("Venice hides encrypted trailers across every marker split while preserving exact replay", () =>
Effect.gen(function* () {
const marker = "__ENCRYPTED_REASONING__"
for (const split of Array.from({ length: marker.length + 1 }, (_, index) => index)) {
const raw = `Visible summary\n${marker}id=rs_123\nopaque-payload`
const response = yield* LLMClient.generate(LLM.request({ model, prompt: "Hello" })).pipe(
Effect.provide(
fixedResponse(
sseEvents(
frame({ reasoning_content: `Visible summary\n${marker.slice(0, split)}` }),
frame({
reasoning_content: `${marker.slice(split)}id=rs_123\nopaque-payload`,
reasoning_details: [
{
type: "reasoning.encrypted",
data: "opaque-payload",
format: "openai-responses-v1",
id: "rs_123",
index: 0,
},
],
}),
frame({ content: "42" }, "stop"),
),
),
),
)
expect(response.reasoning).toBe("Visible summary\n")
expect(
response.events
.filter((event) => event.type === "reasoning-delta")
.map((event) => event.text)
.join(""),
).toBe("Visible summary\n")
const replay = yield* compileRequest(LLM.request({ model, messages: [Message.user("Hello"), response.message] }))
expect(replay.body.messages).toEqual(
expect.arrayContaining([
expect.objectContaining({
role: "assistant",
reasoning_content: raw,
reasoning_details: [
{
type: "reasoning.encrypted",
data: "opaque-payload",
format: "openai-responses-v1",
id: "rs_123",
index: 0,
},
],
}),
]),
)
}
}),
)
it.effect("Venice preserves opaque-only reasoning and does not eat an incomplete marker prefix", () =>
Effect.gen(function* () {
for (const scalar of ["__ENCRYPTED_REASONING__opaque", "normal _", "normal __ENCRYPTED_"]) {
const response = yield* LLMClient.generate(LLM.request({ model, prompt: "Hello" })).pipe(
Effect.provide(
fixedResponse(sseEvents(frame({ reasoning_content: scalar }), frame({ content: "42" }, "stop"))),
),
)
expect(response.reasoning).toBe(scalar.startsWith("__ENCRYPTED_REASONING__") ? "" : scalar)
const body = (yield* compileRequest(LLM.request({ model, messages: [response.message] }))).body
expect(body.messages).toEqual(expect.arrayContaining([expect.objectContaining({ reasoning_content: scalar })]))
}
}),
)
it.effect("Venice encrypted trailers do not change the base OpenAI Chat protocol", () =>
LLMClient.generate(
LLM.request({ model: OpenAI.configure({ apiKey: "fixture" }).chat("gpt-4o"), prompt: "Hello" }),
).pipe(
Effect.provide(
fixedResponse(
sseEvents(
frame({ reasoning_content: "text __ENCRYPTED_REASONING__payload" }),
frame({ content: "42" }, "stop"),
),
),
),
Effect.tap((response) => Effect.sync(() => expect(response.reasoning).toBe("text __ENCRYPTED_REASONING__payload"))),
),
)
it.effect("Venice keeps late Gemini signatures attached to the correct parallel call", () =>
Effect.gen(function* () {
const response = yield* LLMClient.generate(LLM.request({ model, prompt: "Lookup" })).pipe(
Effect.provide(
fixedResponse(
sseEvents(
frame({
tool_calls: [
{ index: 0, id: "call_0", function: { name: "lookup", arguments: '{"key":"A"}' } },
{ index: 1, id: "call_1", function: { name: "lookup", arguments: '{"key":"B"}' } },
],
}),
frame({ tool_calls: [{ index: 0, thought_signature: "signed-first-call" }] }),
frame({}, "tool_calls"),
),
),
),
)
expect(response.toolCalls).toHaveLength(2)
expect(response.toolCalls[0].providerMetadata?.venice?.thoughtSignature).toBe("signed-first-call")
expect(response.toolCalls[1].providerMetadata?.venice?.thoughtSignature).toBeUndefined()
const replay = yield* compileRequest(LLM.request({ model, messages: [Message.user("Lookup"), response.message] }))
expect(replay.body.messages).toEqual(
expect.arrayContaining([
expect.objectContaining({
tool_calls: [
expect.objectContaining({ id: "call_0", thought_signature: "signed-first-call" }),
expect.not.objectContaining({ thought_signature: expect.anything() }),
],
}),
]),
)
}),
)
it.effect("Venice preserves message signatures even for identical answers and empty intervening messages", () =>
Effect.gen(function* () {
const response = yield* LLMClient.generate(LLM.request({ model, prompt: "Hello" })).pipe(
Effect.provide(
fixedResponse(
sseEvents(frame({ content: "42" }), frame({ thought_signature: "second-signature" }), frame({}, "stop")),
),
),
)
const request = LLM.request({
model,
messages: [
Message.assistant([
{ type: "text", text: "42", providerMetadata: { venice: { messageThoughtSignature: "first-signature" } } },
]),
Message.assistant(""),
response.message,
],
})
expect((yield* compileRequest(request)).body.messages).toEqual([
{ role: "assistant", content: "42", thought_signature: "first-signature" },
{ role: "assistant", content: "42", thought_signature: "second-signature" },
])
}),
)
it.effect("Venice merges signed Claude reasoning without losing its final signature", () =>
Effect.gen(function* () {
const response = yield* LLMClient.generate(LLM.request({ model, prompt: "Hello" })).pipe(
Effect.provide(
fixedResponse(
sseEvents(
frame({
reasoning_content: "Think ",
reasoning_details: [{ type: "reasoning.text", text: "Think ", format: "anthropic-claude-v1", index: 0 }],
}),
frame({
reasoning_content: "carefully",
reasoning_details: [
{ type: "reasoning.text", text: "carefully", format: "anthropic-claude-v1", index: 0 },
],
}),
frame({
reasoning_details: [
{
type: "reasoning.text",
text: "",
signature: "claude-signature",
format: "anthropic-claude-v1",
index: 0,
},
],
}),
frame({ content: "42" }, "stop"),
),
),
),
)
expect(response.reasoning).toBe("Think carefully")
expect((yield* compileRequest(LLM.request({ model, messages: [response.message] }))).body.messages).toEqual(
expect.arrayContaining([
expect.objectContaining({
reasoning_details: [
{
type: "reasoning.text",
text: "Think carefully",
signature: "claude-signature",
format: "anthropic-claude-v1",
index: 0,
},
],
}),
]),
)
}),
)
it.effect("Venice maps cache-write usage and retains the raw wire spelling", () =>
LLMClient.generate(LLM.request({ model, prompt: "Hello" })).pipe(
Effect.provide(
fixedResponse(
sseEvents(frame({ content: "42" }, "stop"), {
choices: [],
usage: {
prompt_tokens: 100,
completion_tokens: 20,
total_tokens: 120,
prompt_tokens_details: { cached_tokens: 40, cache_creation_input_tokens: 30 },
completion_tokens_details: { reasoning_tokens: 10 },
},
}),
),
),
Effect.tap((response) =>
Effect.sync(() => {
expect(response.usage).toMatchObject({
inputTokens: 100,
outputTokens: 20,
nonCachedInputTokens: 30,
cacheReadInputTokens: 40,
cacheWriteInputTokens: 30,
reasoningTokens: 10,
})
expect(response.usage?.providerMetadata?.venice).toMatchObject({
prompt_tokens_details: { cache_creation_input_tokens: 30 },
})
}),
),
),
)
it.effect("Venice surfaces HTTP string errors and streaming validation details without losing the payload", () =>
Effect.gen(function* () {
const http = yield* LLMClient.generate(LLM.request({ model, prompt: "Hello" })).pipe(
Effect.provide(
fixedResponse('{"error":"Specified model not found"}', {
status: 404,
headers: { "content-type": "application/json" },
}),
),
Effect.result,
)
expect(http._tag).toBe("Failure")
if (http._tag === "Failure") expect(http.failure.message).toContain("Specified model not found")
const payload = {
error: "Invalid request parameters",
issues: [{ message: "Unsupported value", path: ["reasoning", "effort"] }],
}
const streamed = yield* LLMClient.generate(LLM.request({ model, prompt: "Hello" })).pipe(
Effect.provide(fixedResponse(sseEvents(payload))),
Effect.result,
)
expect(streamed._tag).toBe("Failure")
if (streamed._tag === "Failure") {
expect(streamed.failure.message).toContain("reasoning.effort: Unsupported value")
expect(JSON.parse(streamed.failure.reason.body ?? "{}")).toEqual(payload)
}
}),
)
+11 -171
View File
@@ -5,149 +5,11 @@ import { expectSessionTitle } from "../utils/waits"
test.use({ viewport: { width: 1440, height: 900 } })
test("keeps separate one-shot /btw answers and restores them after reload", async ({ page }, testInfo) => {
const prompts: unknown[] = []
const generations: { sessionID: string; prompt: string }[] = []
const { editor } = await openSession(page, {
name: "BtwQuestions",
onPrompt: (input) => prompts.push(input),
generate: (input) => {
generations.push(input)
return {
text: input.prompt.includes("second question")
? "Second answer"
: `First answer\n\n${"A detailed explanation of the first question.\n\n".repeat(100)}End of first answer`,
}
},
})
const panel = page.locator('[data-slot="session-btw-panel"]')
await editor.fill("/btw first question")
await editor.press("Enter")
await expect(panel.getByText("First answer", { exact: true })).toBeVisible()
await expect(panel.getByRole("textbox")).toHaveCount(0)
await expect(panel.getByRole("button", { name: "Send", exact: true })).toHaveCount(0)
await panel.getByText("End of first answer", { exact: true }).scrollIntoViewIfNeeded()
await expect(panel.getByText("End of first answer", { exact: true })).toBeInViewport()
await editor.fill("/btw second question")
await editor.press("Enter")
await expect(panel.getByText("Second answer", { exact: true })).toBeVisible()
const first = page.getByRole("tab", { name: "first question", exact: true })
const second = page.getByRole("tab", { name: "second question", exact: true })
await expect(first).toBeVisible()
await expect(second).toHaveAttribute("data-selected", "")
await page.screenshot({ path: testInfo.outputPath("separate-tabs.png") })
expect(generations).toHaveLength(2)
expect(generations[1]?.prompt).not.toContain("first question")
expect(generations[1]?.prompt).not.toContain("First answer")
await page.reload()
await expect(first).toBeVisible()
await expect(second).toHaveAttribute("data-selected", "")
await expect(panel.getByText("Second answer", { exact: true })).toBeVisible()
await first.click()
await expect(panel.getByText("First answer", { exact: true })).toBeVisible()
await expect(panel.getByText("Second answer", { exact: true })).toHaveCount(0)
await expect(panel.getByRole("textbox")).toHaveCount(0)
await page.screenshot({ path: testInfo.outputPath("restored-answer.png") })
// Closing a tab forgets its question; the other tab and its answer survive a reload.
await first.click({ button: "middle" })
await expect(first).toHaveCount(0)
await expect(second).toHaveAttribute("data-selected", "")
await expect(panel.getByText("Second answer", { exact: true })).toBeVisible()
await page.reload()
await expect(second).toHaveAttribute("data-selected", "")
await expect(panel.getByText("Second answer", { exact: true })).toBeVisible()
await expect(first).toHaveCount(0)
expect(generations).toHaveLength(2)
expect(prompts).toEqual([])
})
test("isolates concurrent side questions and makes an interrupted reload retryable", async ({ page }) => {
const held = Promise.withResolvers<void>()
const attempts: string[] = []
const prompts: unknown[] = []
const { editor } = await openSession(page, {
name: "BtwConcurrent",
onPrompt: (input) => prompts.push(input),
generate: async (input) => {
attempts.push(input.prompt)
if (input.prompt.includes("slow question") && attempts.length === 1) {
await held.promise
return { text: "Abandoned answer" }
}
return { text: input.prompt.includes("fast question") ? "Fast answer" : "Retried answer" }
},
})
const panel = page.locator('[data-slot="session-btw-panel"]')
await editor.fill("/btw slow question")
await editor.press("Enter")
await expect(panel.getByRole("status")).toContainText("Working")
await editor.fill("/btw fast question")
await editor.press("Enter")
await expect(panel.getByText("Fast answer", { exact: true })).toBeVisible()
await page.getByRole("tab", { name: "slow question", exact: true }).click()
await expect(panel.getByRole("status")).toContainText("Working")
await expect(panel.getByText("Fast answer", { exact: true })).toHaveCount(0)
await page.reload()
await expect(panel.getByText("Couldn’t answer that question", { exact: true })).toBeVisible()
await expect(panel.getByRole("status")).toHaveCount(0)
expect(attempts).toHaveLength(2)
await panel.getByRole("button", { name: "Retry", exact: true }).click()
await expect(panel.getByText("Retried answer", { exact: true })).toBeVisible()
expect(attempts).toHaveLength(3)
expect(attempts[2]).toContain("slow question")
expect(attempts[2]).not.toContain("fast question")
held.resolve()
await page.getByRole("tab", { name: "fast question", exact: true }).click()
await expect(panel.getByText("Fast answer", { exact: true })).toBeVisible()
await page.getByRole("tab", { name: "slow question", exact: true }).click()
await expect(panel.getByText("Retried answer", { exact: true })).toBeVisible()
await expect(panel.getByText("Abandoned answer", { exact: true })).toHaveCount(0)
await expect(panel.getByRole("textbox")).toHaveCount(0)
expect(prompts).toEqual([])
})
test("keeps many long-titled side tabs usable at a narrow desktop width", async ({ page }, testInfo) => {
await page.setViewportSize({ width: 900, height: 700 })
const { editor } = await openSession(page, {
name: "BtwOverflow",
generate: () => ({ text: "Saved overflow answer" }),
})
const panel = page.locator('[data-slot="session-btw-panel"]')
const questions = Array.from({ length: 12 }, (_, index) => `Side ${index + 1}: ${"long-title-".repeat(30)}`)
for (const question of questions) {
await editor.fill(`/btw ${question}`)
await editor.press("Enter")
await expect(page.getByRole("tab", { name: question, exact: true })).toHaveAttribute("data-selected", "")
await expect(panel.getByText("Saved overflow answer", { exact: true })).toBeVisible()
}
await expect(page.getByRole("tab", { name: /^Side \d+:/ })).toHaveCount(12)
await expect(panel.getByRole("textbox")).toHaveCount(0)
await page.reload()
await expect(page.getByRole("tab", { name: /^Side \d+:/ })).toHaveCount(12)
await expect(panel.getByText("Saved overflow answer", { exact: true })).toBeVisible()
await page.screenshot({ path: testInfo.outputPath("narrow-many-tabs.png") })
})
test("answers /btw in the side panel without admitting a prompt", async ({ page }) => {
const generations: { sessionID: string; prompt: string }[] = []
const prompts: unknown[] = []
const generated = Promise.withResolvers<void>()
const leftBehind = Promise.withResolvers<void>()
const abandoned = Promise.withResolvers<void>()
const main = { id: "ses_btw_sidebar", title: "Side question session" }
const other = { id: "ses_btw_sidebar_other", title: "Other side question session" }
const ownerWarnings: string[] = []
@@ -166,7 +28,7 @@ test("answers /btw in the side panel without admitting a prompt", async ({ page
if (input.sessionID === other.id) return { text: "This answer belongs to the **other session**." }
if (input.prompt.includes("left behind")) {
await leftBehind.promise
await abandoned.promise
return { text: "This answer arrived after the user left." }
}
@@ -185,6 +47,7 @@ test("answers /btw in the side panel without admitting a prompt", async ({ page
await suggestion.click()
await expect(editor).toHaveText("/btw ")
await editor.press("Enter")
const panel = page.locator('[data-slot="session-btw-panel"]')
await expect(panel).toBeHidden()
await expect(page.getByText("Add a question after /btw", { exact: true })).toBeVisible()
@@ -196,10 +59,7 @@ test("answers /btw in the side panel without admitting a prompt", async ({ page
await expect(panel).toBeVisible()
await expect(panel.getByRole("textbox")).toHaveCount(0)
await expect(panel.getByRole("status")).toContainText("Working")
await expect(page.getByRole("tab", { name: "how does the retry loop work?", exact: true })).toHaveAttribute(
"data-selected",
"",
)
await expect(page.getByRole("tab", { name: "/btw" })).toHaveAttribute("data-selected", "")
generated.resolve()
await expect(panel.getByText("how does the retry loop work?", { exact: true })).toBeVisible()
await expect(panel.getByText("exponential backoff", { exact: false })).toBeVisible()
@@ -215,11 +75,13 @@ test("answers /btw in the side panel without admitting a prompt", async ({ page
await editor.fill("/btw what belongs here?")
await editor.press("Enter")
await expect(panel.getByText("other session", { exact: false })).toBeVisible()
await page.locator(`[data-titlebar-tab-link][href="${sessionHref(main.id)}"]`).click()
await expectSessionTitle(page, main.title)
await expect(panel.getByText("exponential backoff", { exact: false })).toBeVisible()
await expect(panel.getByText("other session", { exact: false })).toHaveCount(0)
// Leaving a session abandons its in-flight question, so it reads as failed on return.
await editor.fill("/btw is this question left behind?")
await editor.press("Enter")
await expect(panel.getByRole("status")).toContainText("Working")
@@ -227,35 +89,13 @@ test("answers /btw in the side panel without admitting a prompt", async ({ page
await expectSessionTitle(page, other.title)
await page.locator(`[data-titlebar-tab-link][href="${sessionHref(main.id)}"]`).click()
await expectSessionTitle(page, main.title)
// Leaving the session does not abandon its question: it is still working, and its answer lands once it arrives.
await expect(panel.getByRole("status")).toContainText("Working")
await page.getByRole("button", { name: "Home", exact: true }).click()
await expect(page).toHaveURL(/\/$/)
leftBehind.resolve()
await page.locator(`[data-titlebar-tab-link][href="${sessionHref(main.id)}"]`).click()
await expectSessionTitle(page, main.title)
await expect(panel.getByText("This answer arrived after the user left.", { exact: true })).toBeVisible()
await expect(panel.getByText("Couldn’t answer that question", { exact: true })).toHaveCount(0)
expect(generations.filter((item) => item.prompt.includes("left behind"))).toHaveLength(1)
await expect(panel.getByText("Couldn’t answer that question", { exact: true })).toBeVisible()
await expect(panel.getByRole("button", { name: "Retry", exact: true })).toBeVisible()
abandoned.resolve()
await page.reload()
await expectSessionTitle(page, main.title)
await expect(page.getByRole("tab", { name: "is this question left behind?", exact: true })).toHaveAttribute(
"data-selected",
"",
)
await expect(panel.getByText("This answer arrived after the user left.", { exact: true })).toBeVisible()
await expect(panel.getByRole("status")).toHaveCount(0)
await page.getByRole("tab", { name: "how does the retry loop work?", exact: true }).click()
await expect(panel.getByText("exponential backoff", { exact: false })).toBeVisible()
await page.getByRole("button", { name: "Home", exact: true }).click()
await expect(page).toHaveURL(/\/$/)
await page.locator(`[data-titlebar-tab-link][href="${sessionHref(main.id)}"]`).click()
await expectSessionTitle(page, main.title)
await expect(page.getByRole("tab", { name: "how does the retry loop work?", exact: true })).toHaveAttribute(
"data-selected",
"",
)
await expect(panel.getByText("exponential backoff", { exact: false })).toBeVisible()
await expect(page.getByRole("tab", { name: "/btw" })).toHaveCount(0)
await expect(panel).toHaveCount(0)
expect(ownerWarnings).toEqual([])
})
@@ -30,11 +30,9 @@ for (const position of ["top", "bottom"] as const) {
const navigation = page.locator('[data-slot="session-mobile-view-navigation"]')
const more = navigation.getByRole("button", { name: "More options", exact: true })
const picker = tabs.getByRole("tab", { selected: true })
const message = page.locator(
`[data-timeline-row="UserMessage"][data-message-id="${fixture.expected.targetMessageIDs.at(-1)}"]`,
)
const composer = page.getByRole("textbox", { name: "Prompt", exact: true })
await expect(picker).toHaveText("Session")
await expect(message).toBeVisible()
@@ -42,17 +40,9 @@ for (const position of ["top", "bottom"] as const) {
await expect(tabs.getByRole("tab")).toHaveText(["Session", "Changes", "Files", "Terminal"])
await expect(tabs).toHaveCSS("padding-left", "0px")
await expect(tabs).toHaveCSS("padding-right", "0px")
if (position === "top") {
const titlebar = page.locator('[data-slot="titlebar-v2"]')
await expect(titlebar).toHaveCSS("padding-top", "16px")
await expect(titlebar).toHaveCSS("height", "44px")
}
await expect
.poll(async () => {
const bounds = await navigation.boundingBox()
return !!bounds && bounds.x >= 8 && bounds.x <= 9 && bounds.width >= 372 && bounds.width <= 374
})
.toBe(true)
@@ -61,7 +51,6 @@ for (const position of ["top", "bottom"] as const) {
const bar = await tabs.boundingBox()
const input = await composer.boundingBox()
const panel = await page.locator('[data-slot="session-chat-panel"]').boundingBox()
return !!bar && !!input && !!panel && Math.abs(bar.y - panel.y) <= 1 && bar.y + bar.height <= input.y
})
.toBe(true)
@@ -160,19 +149,16 @@ for (const position of ["top", "bottom"] as const) {
// The view resets to Session whenever the routed session changes, including through Home.
const trigger = page.locator('[data-slot="mobile-tabs-trigger"]')
const openTabs = async () => {
await trigger.click()
await expect(drawer).not.toHaveAttribute("data-transitioning")
}
const openTab = async (title: string) => {
await openTabs()
await drawer.locator('[data-slot="tab-link"]').filter({ hasText: title }).click()
await expect(drawer).toBeHidden()
await expect(trigger).toContainText(title)
}
await more.click()
await page.getByRole("menuitem", { name: "Usage", exact: true }).click()
await expect(page.getByText("Total Cost", { exact: true })).toBeVisible()
@@ -352,7 +338,6 @@ test.describe("touch", () => {
.poll(async () => {
const navigation = await settings.getByRole("button", { name: "Models", exact: true }).boundingBox()
const content = await panel.boundingBox()
return !!navigation && !!content && navigation.y + navigation.height <= content.y
})
.toBe(true)
@@ -650,43 +650,6 @@ test.describe("Working", () => {
})
}
test("keeps Working while a new read joins a standalone read group", async ({ page }) => {
const timeline = await setupTimeline(page, {
messages: [
userMessage(),
assistantMessage(
["one", "two", "three"].map((name) =>
toolPart(`prt_read_${name}`, "read", "completed", { path: `src/${name}.ts` }),
),
{ completed: false },
),
],
settings: { timelineDetail: timelinePresets[0].value },
})
const working = page.locator('[data-component="session-working"]')
const group = page.locator('[data-component="read-tool-group"]')
const shimmer = group.locator('[data-component="text-shimmer"]')
await expect(group).toHaveAttribute("data-timeline-part-ids", "prt_read_one,prt_read_two,prt_read_three")
await expect(working).toBeVisible()
await timeline.send(partUpdated(toolPart("prt_read_four", "read", "streaming", { path: "src/four.ts" })))
await expect(group).toHaveAttribute(
"data-timeline-part-ids",
"prt_read_one,prt_read_two,prt_read_three,prt_read_four",
)
await expect(shimmer).toHaveAttribute("data-active", "true")
await expect(working).toBeVisible()
await timeline.send(partUpdated(toolPart("prt_read_four", "read", "running", { path: "src/four.ts" })))
await expect(group.locator('[data-slot="basic-tool-tool-subtitle"]')).toHaveText("one.ts, two.ts, three.ts, four.ts")
await expect(working).toBeVisible()
await timeline.send(partUpdated(toolPart("prt_read_four", "read", "completed", { path: "src/four.ts" })))
await expect(shimmer).toHaveAttribute("data-active", "false")
await expect(working).toBeVisible()
})
for (const grouped of [false, true]) {
test(`uses ${grouped ? "grouped" : "standalone"} background shell presentation`, async ({ page }) => {
await setupTimeline(page, {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode/app",
"version": "2.0.23",
"version": "2.0.22",
"description": "",
"type": "module",
"exports": {
+2 -3
View File
@@ -145,7 +145,7 @@ function session(input: {
}
describe("Composer submission", () => {
test("applies the selection and runs a client argument command without admitting it to the session", async () => {
test("runs a client argument command without admitting it to the session", async () => {
const state = createMemoryComposerState().capture()
const image = {
@@ -169,8 +169,7 @@ describe("Composer submission", () => {
}
}).submit(new Event("submit"))
// Client commands such as /btw generate with the session's model, so the composer's selection commits first.
expect(calls).toEqual(["switch-agent", "switch-model", "btw"])
expect(calls).toEqual(["btw"])
expect(history).toEqual([])
expect(state.current()).toEqual([{ type: "text", content: "", start: 0, end: 0 }, image])
expect(state.context.items()).toHaveLength(1)
+14 -31
View File
@@ -66,16 +66,6 @@ export function createComposerSubmit(input: ComposerSubmitInput) {
submitting.add(input.adapter.state)
try {
// Client commands such as /btw answer with the session's model, so apply the composer's selection first,
// following the same steer rule as server commands.
const selection = currentSelection(input)
if (input.adapter.kind === "active-session" && selection && (input.delivery?.(false) ?? "steer") === "steer")
await applySelection(
input.adapter.session(),
selection,
input.adapter.controls().model.selection.trackSessionCommit,
)
clearClientCommand(input, prompt)
await clientCommand()
} catch (error) {
@@ -282,22 +272,25 @@ function readSubmission(
if (!text.trim() && !prompt.some(isAttachment) && comments === 0) return
const selection = currentSelection(input)
const controls = input.adapter.controls()
const model = controls.model.selection.current()
const agent = controls.agents.current
if (!selection) {
if (!model || !agent) {
input.notify.missingSelection()
return
}
const variant = controls.model.selection.variant.current()
const retry = input.adapter.state.retry.current()
const retryID =
retry &&
retry.agent === selection.agent &&
retry.providerID === selection.model.providerID &&
retry.modelID === selection.model.modelID &&
(retry.variant ?? "default") === (selection.variant ?? "default")
retry.agent === agent &&
retry.providerID === model.provider.id &&
retry.modelID === model.id &&
(retry.variant ?? "default") === (variant ?? "default")
? retry.id
: undefined
@@ -308,25 +301,15 @@ function readSubmission(
context,
text,
images,
selection,
selection: {
agent,
model: { modelID: model.id, providerID: model.provider.id },
variant,
},
delivery: input.delivery?.(alternate) ?? "steer",
}
}
function currentSelection(input: ComposerSubmitInput): ComposerSelection | undefined {
const controls = input.adapter.controls()
const model = controls.model.selection.current()
const agent = controls.agents.current
if (!model || !agent) return
return {
agent,
model: { modelID: model.id, providerID: model.provider.id },
variant: controls.model.selection.variant.current(),
}
}
function clearSubmission(input: ComposerSubmitInput, submission: ReturnType<typeof createComposerSubmission>) {
submission.clear()
submission.target().mode.set("normal")
@@ -5,7 +5,6 @@ import { IconButton } from "@opencode/ui/icon-button"
import { Menu } from "@opencode/ui/menu"
import { TextShimmer } from "@opencode/ui/text-shimmer"
import { createMemo, For, Show } from "solid-js"
import { createStore } from "solid-js/store"
import { useLanguage } from "@/runtime/i18n/language"
import { useServerSDK } from "@/runtime/server/client"
import { useServer } from "@/runtime/server/current"
@@ -36,7 +35,6 @@ export function SessionRunningMenu(props: {
const openRoute = useOpenSessionRoute()
const server = useServer()
const sdk = useServerSDK()
const [menu, setMenu] = createStore({ open: false })
const sessionAgent = (id: string | undefined) => (id ? server.ctx.data.session.get(id)?.agent : undefined)
// Foreground shells stay out: the timeline already shows them at the bottom.
@@ -107,7 +105,7 @@ export function SessionRunningMenu(props: {
return (
<Show when={items().length > 0}>
<Menu gutter={6} placement="bottom-start" open={menu.open} onOpenChange={(open) => setMenu("open", open)}>
<Menu gutter={6} placement="bottom-start">
<Menu.Trigger
as="button"
type="button"
@@ -125,12 +123,7 @@ export function SessionRunningMenu(props: {
class="group/running-item"
classList={{ "!bg-v2-overlay-simple-overlay-hover": viewing(item) }}
aria-current={viewing(item) ? "page" : undefined}
onSelect={() => {
// Close before navigating: the router's transition would hold the close until this
// timeline detaches, and the menu would flash at the viewport origin.
setMenu("open", false)
open(item)
}}
onSelect={() => open(item)}
onKeyDown={(event) => {
if ((event.key !== "Delete" && event.key !== "Backspace") || !stoppable(item)) return
@@ -374,9 +374,8 @@ function MessageTimelineView(
return message?.type === "compaction" && message.status === "running"
}
// Used and read groups keep the fallback, so each new read does not swap Working out for its short call.
if (!Predicate.isTagged(row, "AssistantPart") || row.group.type === "context" || row.group.type === "read")
return false
// Used groups keep the fallback regardless of disclosure state.
if (!Predicate.isTagged(row, "AssistantPart") || row.group.type === "context") return false
return (row.group.type === "part" ? [row.group.ref] : row.group.refs).some((ref) => {
const content = Timeline.resolveContent(messageByID().get(ref.messageID), ref.partID)
+3 -12
View File
@@ -59,7 +59,6 @@ export function Titlebar(props: { verticalTabs?: { mount?: HTMLElement } }) {
const location = useLocation()
const mobile = createMediaQuery("(max-width: 767px)")
const bottom = createMemo(() => mobile() && settings.general.mobileTitlebarPosition() === "bottom")
const mobileTop = createMemo(() => platform.platform === "web" && mobile() && !bottom())
const mac = createMemo(() => platform.platform === "desktop" && platform.os === "macos")
const windows = createMemo(() => platform.platform === "desktop" && platform.os === "windows")
@@ -151,17 +150,9 @@ export function Titlebar(props: { verticalTabs?: { mount?: HTMLElement } }) {
platform.platform === "web"
? bottom()
? "calc(28px + max(8px, var(--safe-area-inset-bottom, env(safe-area-inset-bottom, 0px))))"
: mobileTop()
? "calc(28px + 16px + env(safe-area-inset-top, 0px))"
: "calc(28px + max(8px, env(safe-area-inset-top, 0px)))"
: "calc(28px + max(8px, env(safe-area-inset-top, 0px)))"
: undefined,
// iOS blurs page content just below the status bar in Home Screen web apps, so keep the phone titlebar row
// 16px clear of the safe area, as ChatGPT's phone header does.
"padding-top": bottom()
? "0px"
: mobileTop()
? "calc(16px + env(safe-area-inset-top, 0px))"
: "env(safe-area-inset-top, 0px)",
"padding-top": bottom() ? "0px" : "env(safe-area-inset-top, 0px)",
"padding-bottom": bottom() ? "var(--safe-area-inset-bottom, env(safe-area-inset-bottom, 0px))" : "0px",
"min-height": minHeight(),
// Keep native macOS traffic lights clear even when the desktop window is narrow.
@@ -521,7 +512,7 @@ export function Titlebar(props: { verticalTabs?: { mount?: HTMLElement } }) {
<div
class="h-full flex-1 overflow-hidden flex flex-row items-center gap-1.5 px-2 md:pe-3"
classList={{
"pt-[max(0px,calc(8px-env(safe-area-inset-top,0px)))]": !bottom() && !windows() && !mobileTop(),
"pt-[max(0px,calc(8px-env(safe-area-inset-top,0px)))]": !bottom() && !windows(),
"pb-[max(0px,calc(8px-var(--safe-area-inset-bottom,env(safe-area-inset-bottom,0px))))]": bottom(),
"pl-4": macTrafficLights(),
// Center the 20px app icon over the sidebar's 16px icon column.
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/cli",
"version": "2.0.23",
"version": "2.0.22",
"type": "module",
"license": "MIT",
"bin": {
+31 -1
View File
@@ -3,6 +3,7 @@
import { $ } from "bun"
import { mkdir, rm } from "fs/promises"
import path from "path"
import { discoverEffectSpecifiers } from "@opencode/plugin/runtime-modules.bun"
import { Script } from "@opencode/script"
import { createSolidTransformPlugin } from "@opentui/solid/bun-plugin"
import type { BunPlugin } from "bun"
@@ -78,6 +79,28 @@ const appAssetsPlugin: BunPlugin = {
}))
},
}
const pluginRuntimeSpecifiers = [
"@opencode/plugin",
"@opencode/plugin/effect",
"@opencode/plugin/effect/plugin",
"@opencode/plugin/effect/tool",
"@opencode/plugin/promise/plugin",
"@opencode/plugin/promise/tool",
"@opencode/plugin/rpc",
...discoverEffectSpecifiers().map(([specifier]) => specifier),
]
const pluginRuntimeModulesSource = `const modules = {\n${pluginRuntimeSpecifiers
.map((specifier) => ` ${JSON.stringify(specifier)}: () => require(${JSON.stringify(specifier)}),`)
.join("\n")}\n}\nexport function pluginRuntimeModules() {\n return modules\n}\n`
const pluginRuntimeModulesPlugin: BunPlugin = {
name: "opencode-plugin-runtime-modules",
setup(build) {
build.onLoad({ filter: /plugin[/\\]src[/\\]runtime-modules\.bun\.ts$/ }, () => ({
contents: pluginRuntimeModulesSource,
loader: "js",
}))
},
}
for (const item of targets) {
const opencodePty = await resolveOpencodePty({
@@ -124,7 +147,14 @@ export default { path: file, version: ${JSON.stringify(opencodePty.version)}, sh
const result = await Bun.build({
entrypoints: ["./src/index.ts"],
tsconfig: "./tsconfig.json",
plugins: [appAssetsPlugin, solidPlugin, parcelWatcherPlugin, opencodePtyPlugin, simulationGraphPlugin],
plugins: [
appAssetsPlugin,
solidPlugin,
parcelWatcherPlugin,
opencodePtyPlugin,
pluginRuntimeModulesPlugin,
simulationGraphPlugin,
],
external: ["node-gyp"],
format: "esm",
minify: true,
+30 -28
View File
@@ -1,13 +1,13 @@
export * as ServerProcess from "./server-process"
import { NodeServices } from "@effect/platform-node"
import { Service } from "@opencode/client/effect/service"
import { Service, type DiscoverOptions } from "@opencode/client/effect/service"
import { LayerNode } from "@opencode/util/effect/layer-node"
import { Global } from "@opencode/util/global"
import { OPENCODE_ARTIFACT, OPENCODE_CHANNEL, OPENCODE_VERSION } from "./version"
import { AppProcess } from "@opencode/util/process"
import { randomBytes, randomUUID } from "node:crypto"
import { Effect, Option, Redacted, Schema } from "effect"
import { Effect, Option, Redacted, Schedule, Schema } from "effect"
import { PersistentPty } from "@opencode/schema/persistent-pty"
import { HttpServer } from "effect/unstable/http"
import { Env } from "./env"
@@ -64,11 +64,11 @@ const processEffect = Effect.fnUntraced(function* (options: Options) {
const config = options.mode === "service" ? yield* ServiceConfig.read() : {}
const hostname = options.hostname ?? config.hostname ?? "127.0.0.1"
const port = options.port ?? config.port ?? (options.mode === "service" ? ServiceConfig.defaultPort() : undefined)
const findIncumbent =
const incumbent =
serviceOptions !== undefined && port !== undefined
? Service.incumbent({ ...serviceOptions, url: serviceURL(hostname, port) })
? yield* Service.incumbent({ ...serviceOptions, url: serviceURL(hostname, port) })
: undefined
if (findIncumbent !== undefined && (yield* findIncumbent) !== undefined) return
if (incumbent !== undefined) return
// Keep a package-manager or curl install replaceable while the service runs; Desktop updates its own copy.
if (options.mode === "service" && process.platform === "win32" && RetainedImage.installed(global.home))
yield* RetainedImage.retain(global.cache, "service")
@@ -88,7 +88,7 @@ const processEffect = Effect.fnUntraced(function* (options: Options) {
if (!password) return yield* Effect.fail(new Error("Missing server password"))
const instanceID = randomUUID()
const transform = yield* WebUi.handler()
const launch = start(
const server = yield* start(
{
app: {
name: process.env.OPENCODE_CLIENT ?? OPENCODE_ARTIFACT,
@@ -144,29 +144,22 @@ const processEffect = Effect.fnUntraced(function* (options: Options) {
}),
},
transform,
)
const server = yield* launch.pipe(
).pipe(
Effect.catch((error) => {
if (findIncumbent === undefined || !addressInUse(error)) return Effect.fail(error)
return Effect.gen(function* () {
const deadline = Date.now() + 15_000
while (Date.now() < deadline) {
const found = yield* findIncumbent.pipe(Effect.timeoutOption(deadline - Date.now()))
if (Option.isSome(found) && found.value !== undefined) return
yield* Effect.sleep("100 millis")
if (Date.now() >= deadline) break
// Failed binds close their scope; a successful bind may take longer than this window to boot.
const server = yield* launch.pipe(Effect.catchIf(addressInUse, () => Effect.void))
if (server !== undefined) return server
}
return yield* Effect.fail(
new Error(
`Managed service port ${port} on ${hostname} is already in use by another process. ` +
"Configure another port with `opencode service set port <port>` and start the service again.",
{ cause: error },
),
)
})
if (serviceOptions === undefined || port === undefined || !addressInUse(error)) return Effect.fail(error)
return recognizeIncumbent(serviceOptions, hostname, port).pipe(
Effect.flatMap((found) =>
found
? Effect.void
: Effect.fail(
new Error(
`Managed service port ${port} on ${hostname} is already in use by another process. ` +
"Configure another port with `opencode service set port <port>` and start the service again.",
{ cause: error },
),
),
),
)
}),
)
if (server === undefined) return
@@ -182,6 +175,15 @@ const processEffect = Effect.fnUntraced(function* (options: Options) {
)
})
const recognizeIncumbent = Effect.fnUntraced(function* (options: DiscoverOptions, hostname: string, port: number) {
const found = yield* Service.incumbent({ ...options, url: serviceURL(hostname, port) }).pipe(
Effect.filterOrFail((value) => value !== undefined),
Effect.retry(Schedule.spaced("100 millis")),
Effect.timeoutOption("15 seconds"),
)
return Option.isSome(found)
})
function serviceURL(hostname: string, port: number) {
return `http://${hostname.includes(":") ? `[${hostname}]` : hostname}:${port}`
}
+1 -3
View File
@@ -58,10 +58,8 @@ function isRouteNotFound(error: unknown) {
return error instanceof HttpServerError.HttpServerError && error.reason._tag === "RouteNotFound"
}
// qr-scanner decodes in a worker it creates from a blob: URL whenever the browser has no BarcodeDetector (Safari, desktop
// Chrome on Windows and Linux). Without worker-src that worker falls under script-src 'self' and never starts.
function csp(hash = "") {
return `default-src 'self'; script-src 'self' 'wasm-unsafe-eval'${hash ? ` 'sha256-${hash}'` : ""}; style-src 'self' 'unsafe-inline'; img-src 'self' data: https: blob:; font-src 'self' data:; media-src 'self' data:; connect-src * data: blob:; worker-src 'self' blob:`
return `default-src 'self'; script-src 'self' 'wasm-unsafe-eval'${hash ? ` 'sha256-${hash}'` : ""}; style-src 'self' 'unsafe-inline'; img-src 'self' data: https: blob:; font-src 'self' data:; media-src 'self' data:; connect-src * data: blob:`
}
function cspForHtml(body: string) {
+1 -72
View File
@@ -3,7 +3,7 @@ import { Service, type Info } from "@opencode/client/effect/service"
import { Global } from "@opencode/util/global"
import { OPENCODE_VERSION } from "../src/version"
import { expect, test } from "bun:test"
import { Effect, FileSystem, Schedule, Schema } from "effect"
import { Effect, FileSystem, Schema } from "effect"
import fs from "node:fs/promises"
import os from "node:os"
import path from "node:path"
@@ -403,77 +403,6 @@ test("unrelated managed port occupancy reports an actionable conflict", async ()
}
}, 30_000)
test("the original managed service contender binds when the occupied port is released", async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-service-bind-retry-"))
const recognizing = Promise.withResolvers<void>()
const requests: string[] = []
using listener = Bun.serve({
hostname: "127.0.0.1",
port: 0,
fetch(request) {
requests.push(new URL(request.url).pathname)
if (requests.length === 2) recognizing.resolve()
return Response.json({ unrelated: true })
},
})
const port = listener.port
if (port === undefined) throw new Error("Server did not bind a port")
const registration = path.join(root, "state", "opencode", "service-local.json")
await fs.mkdir(path.join(root, "config"), { recursive: true })
await fs.mkdir(path.dirname(registration), { recursive: true })
await fs.writeFile(path.join(root, "config", "service-local.json"), JSON.stringify({ port }))
await fs.writeFile(
registration,
JSON.stringify({
id: "stale",
version: OPENCODE_VERSION,
url: "http://127.0.0.1:1",
pid: 2_147_483_647,
password: "stale",
}),
)
const contender = Bun.spawn([process.execPath, path.join(import.meta.dir, "../src/index.ts"), "serve", "--service"], {
env: isolatedEnv(root),
stderr: "pipe",
stdout: "ignore",
})
const stderr = new Response(contender.stderr).text()
try {
// The first probe is preflight; the second only happens after start() fails to bind.
expect(await Promise.race([recognizing.promise.then(() => true), Bun.sleep(20_000).then(() => false)])).toBe(true)
expect(requests).toEqual(["/api/info", "/api/info"])
await listener.stop(true)
const info = await Promise.race([
waitForInfo(registration, (info) => info.pid === contender.pid),
contender.exited.then(() => undefined),
])
expect(info?.pid, contender.exitCode === null ? undefined : await stderr).toBe(contender.pid)
const endpoint = await Effect.runPromise(
Service.discover({ file: registration }).pipe(
Effect.filterOrFail((value) => value !== undefined),
Effect.retry({ times: 400, schedule: Schedule.spaced("50 millis") }),
Effect.provide(NodeFileSystem.layer),
),
)
expect(new URL(endpoint.url).port).toBe(String(port))
expect(
await fetch(new URL("/api/info", endpoint.url), { headers: Service.headers(endpoint) }).then((response) =>
response.json(),
),
).toMatchObject({ pid: contender.pid, version: OPENCODE_VERSION, urls: [endpoint.url] })
expect(contender.exitCode).toBe(null)
await Effect.runPromise(Service.stop({ file: registration }).pipe(Effect.provide(NodeFileSystem.layer)))
expect(await waitForExit(contender)).toBe(true)
expect(await Bun.file(registration).exists()).toBe(false)
await expectPortAvailable(port)
} finally {
contender.kill("SIGTERM")
await contender.exited
await fs.rm(root, { recursive: true, force: true })
}
}, 45_000)
test("unresponsive managed port occupancy reports a bounded conflict", async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-service-unresponsive-conflict-"))
const recognizing = Promise.withResolvers<void>()
-1
View File
@@ -171,7 +171,6 @@ describe("web UI", () => {
expect(yield* Effect.promise(() => fallback.text())).toContain("embedded")
expect(fallback.headers.get("content-security-policy")).toContain("default-src 'self'")
expect(fallback.headers.get("content-security-policy")).toContain("connect-src * data: blob:")
expect(fallback.headers.get("content-security-policy")).toContain("worker-src 'self' blob:")
const dotted = yield* Effect.promise(() => fetch(`${origin}/workspace/example.js`))
expect(dotted.status).toBe(200)
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/client",
"version": "2.0.23",
"version": "2.0.22",
"type": "module",
"license": "MIT",
"repository": {
+106 -31
View File
@@ -1,5 +1,7 @@
import { Effect, FileSystem, Option, Schedule, Schema } from "effect"
import type { DiscoverOptions, EnsureOptions, StopOptions } from "../service.js"
import { homedir } from "node:os"
import { join } from "node:path"
import type { DiscoverOptions, Endpoint, EnsureOptions, StopOptions } from "../service.js"
import {
contenderFailure,
contenderFinished,
@@ -9,10 +11,8 @@ import {
import { defaultEnsureTiming, ensureTiming, type EnsureTiming } from "../service-timing.js"
import { matchesVersion } from "../service-version.js"
import { PtyHandoff } from "../pty-handoff.js"
import { fallback, headers, type LocalService, probeResult, same } from "../service-probe.js"
export * from "../service.js"
export { headers }
/** Contents of the local service registration file. */
export type Info = import("../service.js").Info
@@ -40,9 +40,9 @@ export const incumbent = Effect.fn("service.incumbent")(function* (
options: DiscoverOptions & { readonly url: string },
) {
const info = yield* read(options.file)
if (info === undefined) return undefined
const found = (yield* Effect.promise(() => probeResult({ ...info, url: options.url }))).service
if (!found?.compatible) return undefined
const found = info === undefined ? undefined : yield* probe({ ...info, url: options.url })
if (found === undefined) return undefined
if (!found.compatible) return undefined
if (!matchesVersion(found.version, options)) return undefined
return { endpoint: found.endpoint, state: found.state }
})
@@ -58,7 +58,6 @@ export const ensure = Effect.fn("service.ensure")(function* (options: EnsureOpti
let announced = false
let lastSpawn = 0
let spawnDelay = timing.spawnDelay
let failure: Error | undefined
const announce = (reason: "missing" | "version-mismatch", previousVersion?: string) =>
Effect.sync(() => {
if (announced) return
@@ -90,13 +89,6 @@ export const ensure = Effect.fn("service.ensure")(function* (options: EnsureOpti
yield* Effect.logWarning("Background service is unresponsive; recovery cannot preserve persistent terminals")
yield* Effect.tryPromise(() => PtyHandoff.clear(options.file ?? fallback()))
yield* terminate(info, options, timing)
for (const item of contenders) {
if (item.child.pid === info.pid || contenderFinished(item)) {
item.release()
contenders.delete(item)
}
}
failure = undefined
timeouts = undefined
lastSpawn = Date.now() - spawnDelay
}
@@ -125,27 +117,19 @@ export const ensure = Effect.fn("service.ensure")(function* (options: EnsureOpti
file: options.file,
pty: service.state === "ready" ? "handoff" : "clear",
}).pipe(Effect.ignore)
for (const item of contenders) {
if (item.child.pid === service.info.pid || contenderFinished(item)) {
item.release()
contenders.delete(item)
}
}
failure = undefined
lastSpawn = 0
return Option.none<LocalService>()
} else if (lastSpawn === 0 && info !== undefined) lastSpawn = Date.now()
const finished = [...contenders].filter(contenderFinished)
failure ??= finished.map(contenderFailure).find((error): error is Error => error !== undefined)
const failure = finished.map(contenderFailure).find((error): error is Error => error !== undefined)
if (finished.some((item) => item.child.exitCode === 0)) {
spawnDelay = Math.min(spawnDelay * 2, timing.maxSpawnDelay)
}
finished.forEach((item) => contenders.delete(item))
if (failure !== undefined && contenders.size === 0) return yield* Effect.fail(failure)
// Keep one candidate plus one lock probe for pre-lock stalls. After a failure, let the
// survivors finish without recruiting replacements that could hide the error indefinitely.
if (failure === undefined && contenders.size < 2 && Date.now() - lastSpawn >= spawnDelay) {
// Keep one candidate plus one lock probe so a pre-lock stall cannot block recovery.
if (contenders.size < 2 && Date.now() - lastSpawn >= spawnDelay) {
yield* announce("missing")
contenders.add(yield* spawnContender)
lastSpawn = Date.now()
@@ -161,7 +145,7 @@ export const ensure = Effect.fn("service.ensure")(function* (options: EnsureOpti
Effect.ensuring(Effect.sync(() => contenders.forEach((contender) => contender.release()))),
)
if (Option.isNone(found))
return yield* Effect.fail(failure ?? new Error("Timed out waiting for the background service to start"))
return yield* Effect.fail(new Error("Timed out waiting for the background service to start"))
return found.value.endpoint
})
@@ -173,10 +157,23 @@ export const stop = Effect.fn("service.stop")(function* (options: StopOptions =
options.pty === "handoff" && info !== undefined
? PtyHandoff.prepare(options.file ?? fallback(), info, defaultEnsureTiming.requestTimeout)
: PtyHandoff.clear(options.file ?? fallback()),
).pipe(Effect.catch((cause) => Effect.logWarning("Failed to prepare persistent terminals for replacement", cause)))
if (info !== undefined) yield* terminate(info, options, ensureTiming(options))
).pipe(
Effect.catch((cause) => Effect.logWarning("Failed to prepare persistent terminals for replacement", cause)),
)
if (info !== undefined) yield* terminate(info, options, defaultEnsureTiming)
})
function fallback() {
const state = process.env["XDG_STATE_HOME"] ?? join(homedir(), ".local", "state")
return join(state, "opencode", "service.json")
}
/** Create HTTP authentication headers for a service endpoint. */
export function headers(endpoint: Endpoint) {
if (endpoint.auth === undefined) return undefined
return { authorization: "Basic " + btoa(endpoint.auth.username + ":" + endpoint.auth.password) }
}
/** Schema for the local service registration file. */
export const Info = Schema.Struct({
id: Schema.optional(Schema.String),
@@ -187,6 +184,13 @@ export const Info = Schema.Struct({
})
const decode = Schema.decodeUnknownEffect(Schema.fromJsonString(Info))
const decodeInfo = Schema.decodeUnknownOption(
Schema.Struct({
version: Schema.String,
pid: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
}),
)
// A missing or corrupt file means no valid info; callers treat both
// the same (the registering server self-evicts, clients rediscover).
const read = Effect.fnUntraced(function* (file?: string) {
@@ -196,10 +200,77 @@ const read = Effect.fnUntraced(function* (file?: string) {
return yield* decode(text.value).pipe(Effect.option, Effect.map(Option.getOrUndefined))
})
type LocalService = {
readonly info: Info
readonly endpoint: Endpoint
readonly version?: string
readonly state: "ready" | "waiting" | "failed"
readonly compatible: boolean
}
const probe = Effect.fnUntraced(function* (info: Info) {
return (yield* probeResult(info)).service
})
const probeResult = Effect.fnUntraced(function* (info: Info, timeout = defaultEnsureTiming.requestTimeout) {
const endpoint = {
url: info.url,
auth:
info.password === undefined
? undefined
: { type: "basic" as const, username: "opencode", password: info.password },
} satisfies Endpoint
const signal = AbortSignal.timeout(timeout)
const result = yield* Effect.promise(() =>
fetch(new URL("/api/info", info.url), { headers: headers(endpoint), signal })
.then(async (response) => ({
response,
body: response.status === 404 ? undefined : ((await response.json()) as unknown),
}))
.then(
(value) => ({ value }),
(cause: unknown) => ({ cause }),
),
)
if ("cause" in result) return { service: undefined, timedOut: signal.aborted }
const response = result.value.response
// A missing health endpoint identifies protocol incompatibility, not an older
// version. Only an unmet version requirement lets ensure replace this owner.
if (response.status === 404)
return {
service: {
info,
endpoint,
version: info.version,
state: "ready" as const,
compatible: false,
} satisfies LocalService,
timedOut: false,
}
const body = result.value.body
const serverInfo = decodeInfo(body)
if (Option.isSome(serverInfo)) {
if (serverInfo.value.pid !== info.pid) return { service: undefined, timedOut: false }
if (info.version !== undefined && serverInfo.value.version !== info.version)
return { service: undefined, timedOut: false }
return {
service: {
info,
endpoint,
version: serverInfo.value.version,
state: response.ok ? "ready" : response.status === 500 ? "failed" : "waiting",
compatible: true,
} satisfies LocalService,
timedOut: false,
}
}
return { service: undefined, timedOut: false }
})
const registered = Effect.fnUntraced(function* (file?: string, timeout?: number) {
const info = yield* read(file)
if (info === undefined) return { info: undefined, service: undefined, timedOut: false }
return { info, ...(yield* Effect.promise(() => probeResult(info, timeout))) }
return { info, ...(yield* probeResult(info, timeout)) }
})
// 50ms cadence bounded at ~5s, shared by stop escalation and each ensure
@@ -218,14 +289,18 @@ const stopped = Effect.fnUntraced(function* (pid: number) {
return yield* Effect.fail(new Error(`Server process ${pid} is still running`))
})
function same(left: Info, right: Info) {
return left.id === right.id && left.version === right.version && left.url === right.url && left.pid === right.pid
}
const terminate = Effect.fnUntraced(function* (info: Info, options: { readonly file?: string }, timing: EnsureTiming) {
const current = yield* read(options.file)
if (current === undefined || !same(current, info)) return
yield* signal(info.pid, "SIGTERM")
const done = yield* stopped(info.pid).pipe(Effect.retry(poll(timing)), Effect.option)
// The registration can disappear or change hands before this process exits. Only the PID we
// signalled can tell us whether it has stopped, so escalate based on that process.
if (Option.isNone(done)) {
const latest = yield* read(options.file)
if (latest === undefined || !same(latest, info)) return
yield* signal(info.pid, "SIGKILL")
yield* stopped(info.pid).pipe(Effect.retry(poll(timing)))
}
+92 -25
View File
@@ -1,4 +1,6 @@
import { readFile, rm } from "node:fs/promises"
import { homedir } from "node:os"
import { join } from "node:path"
import type { DiscoverOptions, Endpoint, Info, EnsureOptions, StopOptions } from "../service.js"
import {
contenderFailure,
@@ -9,10 +11,8 @@ import {
import { defaultEnsureTiming, ensureTiming, type EnsureTiming } from "../service-timing.js"
import { matchesVersion } from "../service-version.js"
import { PtyHandoff } from "../pty-handoff.js"
import { fallback, headers, probeResult, same } from "../service-probe.js"
export * from "../service.js"
export { headers }
// Find, start, and stop the local opencode background service.
//
@@ -38,7 +38,6 @@ export async function ensure(options: EnsureOptions = {}): Promise<Endpoint> {
let announced = false
let lastSpawn = 0
let spawnDelay = timing.spawnDelay
let failure: Error | undefined
const announce = (reason: "missing" | "version-mismatch", previousVersion?: string) => {
if (announced) return
@@ -57,7 +56,7 @@ export async function ensure(options: EnsureOptions = {}): Promise<Endpoint> {
try {
while (true) {
if (Date.now() >= deadline) throw failure ?? new Error("Timed out waiting for the background service to start")
if (Date.now() >= deadline) throw new Error("Timed out waiting for the background service to start")
const registration = await registered(options.file, timing.requestTimeout)
if (registration.timedOut && registration.info !== undefined) {
timeouts = {
@@ -69,13 +68,6 @@ export async function ensure(options: EnsureOptions = {}): Promise<Endpoint> {
console.warn("Background service is unresponsive; recovery cannot preserve persistent terminals")
await PtyHandoff.clear(options.file ?? fallback())
await terminate(registration.info, options, timing)
for (const item of contenders) {
if (item.child.pid === registration.info.pid || contenderFinished(item)) {
item.release()
contenders.delete(item)
}
}
failure = undefined
timeouts = undefined
lastSpawn = Date.now() - spawnDelay
}
@@ -103,27 +95,19 @@ export async function ensure(options: EnsureOptions = {}): Promise<Endpoint> {
file: options.file,
pty: service.state === "ready" ? "handoff" : "clear",
}).catch(() => undefined)
for (const item of contenders) {
if (item.child.pid === service.info.pid || contenderFinished(item)) {
item.release()
contenders.delete(item)
}
}
failure = undefined
lastSpawn = 0
}
} else {
if (lastSpawn === 0 && registration.info !== undefined) lastSpawn = Date.now()
const finished = [...contenders].filter(contenderFinished)
failure ??= finished.map(contenderFailure).find((error) => error !== undefined)
const failure = finished.map(contenderFailure).find((error) => error !== undefined)
if (finished.some((item) => item.child.exitCode === 0)) {
spawnDelay = Math.min(spawnDelay * 2, timing.maxSpawnDelay)
}
finished.forEach((item) => contenders.delete(item))
if (failure !== undefined && contenders.size === 0) throw failure
// Keep one candidate plus one lock probe for pre-lock stalls. After a failure, let the
// survivors finish without recruiting replacements that could hide the error indefinitely.
if (failure === undefined && contenders.size < 2 && Date.now() - lastSpawn >= spawnDelay) {
// Keep one candidate plus one lock probe so a pre-lock stall cannot block recovery.
if (contenders.size < 2 && Date.now() - lastSpawn >= spawnDelay) {
announce("missing")
contenders.add(await spawnContender())
lastSpawn = Date.now()
@@ -145,7 +129,19 @@ export async function stop(options: StopOptions = {}) {
? PtyHandoff.prepare(options.file ?? fallback(), info, defaultEnsureTiming.requestTimeout)
: PtyHandoff.clear(options.file ?? fallback())
).catch((cause: unknown) => console.warn("Failed to prepare persistent terminals for replacement", cause))
if (info !== undefined) await terminate(info, options, ensureTiming(options))
if (info !== undefined) await terminate(info, options, defaultEnsureTiming)
}
function fallback() {
return join(process.env["XDG_STATE_HOME"] ?? join(homedir(), ".local", "state"), "opencode", "service.json")
}
/** Create HTTP authentication headers for a service endpoint. */
export function headers(endpoint: Endpoint) {
if (endpoint.auth === undefined) return undefined
return {
authorization: "Basic " + Buffer.from(endpoint.auth.username + ":" + endpoint.auth.password).toString("base64"),
}
}
async function read(file?: string) {
@@ -158,6 +154,73 @@ async function read(file?: string) {
}
}
type LocalService = {
readonly info: Info
readonly endpoint: Endpoint
readonly version?: string
readonly state: "ready" | "waiting" | "failed"
readonly compatible: boolean
}
async function probeResult(info: Info, timeout = defaultEnsureTiming.requestTimeout) {
const endpoint = {
url: info.url,
auth:
info.password === undefined
? undefined
: { type: "basic" as const, username: "opencode", password: info.password },
} satisfies Endpoint
const signal = AbortSignal.timeout(timeout)
const result = await fetch(new URL("/api/info", info.url), { headers: headers(endpoint), signal })
.then(async (response) => ({
response,
body: response.status === 404 ? undefined : ((await response.json()) as unknown),
}))
.then(
(value) => ({ value }),
(cause: unknown) => ({ cause }),
)
if ("cause" in result) return { service: undefined, timedOut: signal.aborted }
const response = result.value.response
// A missing health endpoint identifies protocol incompatibility, not an older
// version. Only an unmet version requirement lets ensure replace this owner.
if (response.status === 404)
return {
service: {
info,
endpoint,
version: info.version,
state: "ready" as const,
compatible: false,
} satisfies LocalService,
timedOut: false,
}
const serverInfo = decodeInfo(result.value.body)
if (serverInfo !== undefined) {
if (serverInfo.pid !== info.pid) return { service: undefined, timedOut: false }
if (info.version !== undefined && serverInfo.version !== info.version)
return { service: undefined, timedOut: false }
return {
service: {
info,
endpoint,
version: serverInfo.version,
state: response.ok ? "ready" : response.status === 500 ? "failed" : "waiting",
compatible: true,
} satisfies LocalService,
timedOut: false,
}
}
return { service: undefined, timedOut: false }
}
function decodeInfo(input: unknown) {
if (typeof input !== "object" || input === null) return
if (!("version" in input) || typeof input.version !== "string") return
if (!("pid" in input) || typeof input.pid !== "number" || !Number.isInteger(input.pid) || input.pid < 0) return
return { version: input.version, pid: input.pid }
}
async function registered(file?: string, timeout?: number) {
const info = await read(file)
if (info === undefined) return { info: undefined, service: undefined, timedOut: false }
@@ -187,13 +250,17 @@ async function waitUntilStopped(pid: number, timing: EnsureTiming) {
return false
}
function same(left: Info, right: Info) {
return left.id === right.id && left.version === right.version && left.url === right.url && left.pid === right.pid
}
async function terminate(info: Info, options: { readonly file?: string }, timing: EnsureTiming) {
const current = await read(options.file)
if (current === undefined || !same(current, info)) return
signal(info.pid, "SIGTERM")
// The registration can disappear or change hands before this process exits. Only the PID we
// signalled can tell us whether it has stopped, so escalate based on that process.
if (!(await waitUntilStopped(info.pid, timing))) {
const latest = await read(options.file)
if (latest === undefined || !same(latest, info)) return
signal(info.pid, "SIGKILL")
if (!(await waitUntilStopped(info.pid, timing))) throw new Error(`Server process ${info.pid} is still running`)
}
-84
View File
@@ -1,84 +0,0 @@
import { homedir } from "node:os"
import { join } from "node:path"
import type { Endpoint, Info } from "./service.js"
import { defaultEnsureTiming } from "./service-timing.js"
// Shared by the Effect and Promise clients. Keep this module free of Effect so the
// Promise client never loads the Effect runtime.
export type LocalService = {
readonly info: Info
readonly endpoint: Endpoint
readonly version?: string
readonly state: "ready" | "waiting" | "failed"
readonly compatible: boolean
}
/** The default registration file. */
export function fallback() {
return join(process.env["XDG_STATE_HOME"] ?? join(homedir(), ".local", "state"), "opencode", "service.json")
}
/** Whether two registrations describe the same service instance. */
export function same(left: Info, right: Info) {
return left.id === right.id && left.version === right.version && left.url === right.url && left.pid === right.pid
}
/** Ask a registered owner for its health and classify the answer. */
export async function probeResult(info: Info, timeout = defaultEnsureTiming.requestTimeout) {
const endpoint = {
url: info.url,
auth: info.password === undefined ? undefined : { type: "basic", username: "opencode", password: info.password },
} satisfies Endpoint
const signal = AbortSignal.timeout(timeout)
const result = await fetch(new URL("/api/info", info.url), { headers: headers(endpoint), signal })
.then(async (response) => ({
response,
body: response.status === 404 ? undefined : ((await response.json()) as unknown),
}))
.catch(() => undefined)
if (result === undefined) return { service: undefined, timedOut: signal.aborted }
const response = result.response
// A missing health endpoint identifies protocol incompatibility, not an older
// version. Only an unmet version requirement lets ensure replace this owner.
if (response.status === 404)
return {
service: {
info,
endpoint,
version: info.version,
state: "ready",
compatible: false,
} satisfies LocalService,
timedOut: false,
}
const serverInfo = decodeInfo(result.body)
if (serverInfo === undefined) return { service: undefined, timedOut: false }
if (serverInfo.pid !== info.pid) return { service: undefined, timedOut: false }
if (info.version !== undefined && serverInfo.version !== info.version) return { service: undefined, timedOut: false }
return {
service: {
info,
endpoint,
version: serverInfo.version,
state: response.ok ? "ready" : response.status === 500 ? "failed" : "waiting",
compatible: true,
} satisfies LocalService,
timedOut: false,
}
}
/** Create HTTP authentication headers for a service endpoint. */
export function headers(endpoint: Endpoint) {
if (endpoint.auth === undefined) return undefined
return {
authorization: "Basic " + Buffer.from(endpoint.auth.username + ":" + endpoint.auth.password).toString("base64"),
}
}
function decodeInfo(input: unknown) {
if (typeof input !== "object" || input === null) return
if (!("version" in input) || typeof input.version !== "string") return
if (!("pid" in input) || typeof input.pid !== "number" || !Number.isInteger(input.pid) || input.pid < 0) return
return { version: input.version, pid: input.pid }
}
@@ -1,4 +1,4 @@
import { mkdtemp, rename, rm } from "node:fs/promises"
import { mkdtemp, rm } from "node:fs/promises"
import { tmpdir } from "node:os"
import { join } from "node:path"
import { waitForExit } from "./service-timing"
@@ -8,16 +8,6 @@ export async function serviceFixture() {
const registration = join(directory, "service.json")
const processes: Bun.Subprocess[] = []
const pids = new Set<number>()
const starts = async () =>
(
await Bun.file(registration + ".starts")
.text()
.catch(() => "")
)
.trim()
.split("\n")
.filter(Boolean)
.map(Number)
const command = (mode: string, ...args: string[]) => [
process.execPath,
join(import.meta.dir, "service.ts"),
@@ -30,36 +20,11 @@ export async function serviceFixture() {
directory,
registration,
command,
starts,
async waitForStarts(count: number) {
for (let attempt = 0; attempt < 600; attempt++) {
const result = await starts()
if (result.length >= count) return result
await Bun.sleep(5)
}
throw new Error(`Timed out waiting for ${count} contenders`)
},
async release(pid: number, action: "fail" | "hang" | "ready") {
const file = registration + `.release-${pid}`
await Bun.write(file + ".tmp", action)
await rename(file + ".tmp", file)
if (action === "fail") await waitForExit(pid)
},
spawn(mode: string, ...args: string[]) {
const subprocess = Bun.spawn(command(mode, ...args), { stdout: "ignore", stderr: "inherit" })
processes.push(subprocess)
return subprocess
},
// The service's parent execs `sleep`, which never reaps it, so SIGKILL leaves a zombie that
// still answers `kill(pid, 0)`. Terminating the returned parent lets init reap the service.
spawnUnreaped(mode: string, ...args: string[]) {
const subprocess = Bun.spawn(["sh", "-c", '"$@" & exec sleep 60', "sh", ...command(mode, ...args)], {
stdout: "ignore",
stderr: "inherit",
})
processes.push(subprocess)
return subprocess
},
// Service.ensure detaches contenders; track the elected process before asserting.
track(pid: number) {
pids.add(pid)
@@ -72,8 +37,6 @@ export async function serviceFixture() {
throw new Error(`Timed out waiting for ${file}`)
},
async [Symbol.asyncDispose]() {
// Include detached contenders that never registered, even when an assertion fails.
for (const pid of await starts()) pids.add(pid)
await Promise.all([
...processes.map(async (subprocess) => {
subprocess.kill("SIGTERM")
@@ -91,8 +54,3 @@ export async function serviceFixture() {
},
}
}
export async function expectPortAvailable(url: string) {
const server = Bun.serve({ port: Number(new URL(url).port), fetch: () => new Response() })
await server.stop(true)
}
+4 -26
View File
@@ -1,4 +1,4 @@
import { appendFile, rename, rm, writeFile } from "node:fs/promises"
import { appendFile, rename, writeFile } from "node:fs/promises"
const [registration, mode, delay] = process.argv.slice(2)
if (registration === undefined || mode === undefined) throw new Error("Missing service fixture arguments")
@@ -17,18 +17,6 @@ if (mode === "environment") {
}
if (mode === "signal") process.kill(process.pid, process.platform === "win32" ? "SIGTERM" : "SIGKILL")
let controlled = ""
if (mode === "controlled") {
await appendFile(registration + ".starts", process.pid + "\n")
const release = registration + `.release-${process.pid}`
while (!(await Bun.file(release).exists())) await Bun.sleep(5)
controlled = await Bun.file(release).text()
if (controlled === "fail") {
process.stderr.write("actionable startup failure: storage initialization denied\n")
process.exit(23)
}
}
if (mode === "delayed" || mode === "delayed-failed" || mode === "coordinated" || mode === "coordinated-failed-loser") {
await appendFile(registration + ".starts", process.pid + "\n")
const owner = await writeFile(registration + ".owner", String(process.pid), { flag: "wx" })
@@ -75,7 +63,7 @@ const server = Bun.serve({
return new Response(null, { status: 404 })
requests += 1
if (mode === "starting") await writeFile(registration + ".status-request", "")
if (mode === "hanging" || controlled === "hang") {
if (mode === "hanging") {
await appendFile(registration + ".requests", process.pid + "\n")
return new Promise<Response>(() => {})
}
@@ -103,12 +91,6 @@ const server = Bun.serve({
},
})
// Install handlers before publishing: a test may signal as soon as the registration appears.
if (controlled !== "hang") {
process.on("SIGTERM", () => void shutdown("SIGTERM"))
process.on("SIGINT", () => void shutdown("SIGINT"))
}
await writeFile(
registration + ".tmp",
JSON.stringify({
@@ -124,12 +106,8 @@ await rename(registration + ".tmp", registration)
async function shutdown(signal?: NodeJS.Signals) {
if (signal !== undefined) await writeFile(registration + ".signal", signal)
// A lingering server unregisters on SIGTERM but keeps running, and holds its port, until killed.
if (mode === "lingering") {
await rm(registration, { force: true })
await writeFile(registration + ".unregistered", "")
await Bun.sleep(Number(delay))
}
server.stop(true)
process.exit()
}
process.on("SIGTERM", () => void shutdown("SIGTERM"))
process.on("SIGINT", () => void shutdown("SIGINT"))
+1 -135
View File
@@ -1,10 +1,9 @@
import { expect, test } from "bun:test"
import { Service, type EnsureReason } from "../src/promise/service"
import { expectPortAvailable, serviceFixture } from "./fixture/service-fixture"
import { serviceFixture } from "./fixture/service-fixture"
import { accelerate } from "./fixture/service-timing"
const ensure = accelerate(Service.ensure)
const stop = accelerate(Service.stop)
test("discovers a registered service", async () => {
await using fixture = await serviceFixture()
@@ -112,76 +111,6 @@ test("reports a failed registered service", async () => {
)
})
test("reports overlapping contender failures without recruiting replacements", async () => {
await using fixture = await serviceFixture()
const started = Date.now()
const pending = ensure({ file: fixture.registration, version: "test", command: fixture.command("controlled") }).catch(
(error: unknown) => error,
)
const [first, second] = await fixture.waitForStarts(2)
await fixture.release(first, "fail")
// Let discovery observe the exit across two accelerated spawn windows before the survivor exits.
await Bun.sleep(450)
await fixture.release(second, "fail")
const error = await pending
expect(Date.now() - started).toBeLessThan(3_000)
expect(error).toBeInstanceOf(Error)
if (!(error instanceof Error)) throw error
expect(error.message).toContain("Server process exited with code 23")
expect(error.message).toContain("storage initialization denied")
expect(await fixture.starts()).toHaveLength(2)
})
test("retains a contender failure until the deadline while its survivor stalls", async () => {
await using fixture = await serviceFixture()
const started = Date.now()
const pending = ensure({ file: fixture.registration, version: "test", command: fixture.command("controlled") }).catch(
(error: unknown) => error,
)
const [first, second] = await fixture.waitForStarts(2)
await fixture.release(first, "fail")
const error = await pending
expect(Date.now() - started).toBeGreaterThanOrEqual(3_000)
expect(error).toBeInstanceOf(Error)
if (!(error instanceof Error)) throw error
expect(error.message).toContain("Server process exited with code 23")
expect(error.message).toContain("storage initialization denied")
expect(await fixture.starts()).toHaveLength(2)
expect(() => process.kill(second, 0)).not.toThrow()
})
test("accepts a surviving contender after a failure without recruiting replacements", async () => {
await using fixture = await serviceFixture()
const pending = ensure({ file: fixture.registration, version: "test", command: fixture.command("controlled") })
const [first, second] = await fixture.waitForStarts(2)
await fixture.release(first, "fail")
await Bun.sleep(450)
await fixture.release(second, "ready")
const endpoint = await pending
expect((await Service.discover({ file: fixture.registration, version: "test" }))?.url).toBe(endpoint.url)
expect((await Bun.file(fixture.registration).json()).pid).toBe(second)
expect(await fixture.starts()).toHaveLength(2)
})
test("recovers when an unresponsive contender is evicted after a prior failure", async () => {
await using fixture = await serviceFixture()
const pending = ensure({ file: fixture.registration, version: "test", command: fixture.command("controlled") })
const [first, second] = await fixture.waitForStarts(2)
await fixture.release(second, "hang")
await fixture.waitForFile()
await fixture.release(first, "fail")
const [, , third] = await fixture.waitForStarts(3)
await fixture.release(third, "ready")
const endpoint = await pending
fixture.track(third)
expect((await Service.discover({ file: fixture.registration, version: "test" }))?.url).toBe(endpoint.url)
expect((await Bun.file(fixture.registration).json()).pid).toBe(third)
})
test("reports a bounded contender stderr tail with native promises", async () => {
await using fixture = await serviceFixture()
const registration = fixture.registration
@@ -242,66 +171,3 @@ test("signals the registered service process", async () => {
expect(await Bun.file(registration + ".signal").text()).toBe("SIGTERM")
expect(await Bun.file(registration).exists()).toBe(false)
})
test("stop escalates when the registration disappears before the process exits", async () => {
await using fixture = await serviceFixture()
const registration = fixture.registration
const existing = fixture.spawn("lingering", "5000")
await fixture.waitForFile()
const original = await Bun.file(registration).json()
await stop({ file: registration })
expect(await Bun.file(registration + ".signal").text()).toBe("SIGTERM")
expect(() => process.kill(original.pid, 0)).toThrow()
await expectPortAvailable(original.url)
expect(await Bun.file(registration).exists()).toBe(false)
await existing.exited
}, 15_000)
test.skipIf(process.platform === "win32")(
"stop fails when the process survives SIGKILL",
async () => {
await using fixture = await serviceFixture()
const registration = fixture.registration
fixture.spawnUnreaped("lingering", "60000")
await fixture.waitForFile()
const original = await Bun.file(registration).json()
await expect(stop({ file: registration })).rejects.toThrow(`Server process ${original.pid} is still running`)
expect(await Bun.file(registration + ".signal").text()).toBe("SIGTERM")
},
15_000,
)
test("stop waits for the original process while preserving a newly registered successor", async () => {
await using fixture = await serviceFixture()
const registration = fixture.registration
const existing = fixture.spawn("lingering", "15000")
await fixture.waitForFile()
const original = await Bun.file(registration).json()
// Default timing keeps the grace period open long enough for the successor to register first.
const stopping = Service.stop({ file: registration })
try {
await fixture.waitForFile(registration + ".unregistered")
const successor = fixture.spawn("graceful")
await fixture.waitForFile()
const replacement = await Bun.file(registration).json()
expect(existing.exitCode).toBe(null)
expect(replacement.pid).toBe(successor.pid)
await stopping
expect(() => process.kill(original.pid, 0)).toThrow()
await expectPortAvailable(original.url)
expect(await Bun.file(registration).json()).toEqual(replacement)
expect(await fetch(new URL("/api/info", replacement.url)).then((response) => response.json())).toMatchObject({
pid: successor.pid,
})
expect(successor.exitCode).toBe(null)
} finally {
existing.kill("SIGKILL")
await stopping
}
}, 20_000)
+1 -133
View File
@@ -3,11 +3,10 @@ import { expect, test } from "bun:test"
import { Effect, FileSystem } from "effect"
import { writeFile } from "node:fs/promises"
import { Service, type EnsureReason } from "../src/effect/service"
import { expectPortAvailable, serviceFixture } from "./fixture/service-fixture"
import { serviceFixture } from "./fixture/service-fixture"
import { accelerate } from "./fixture/service-timing"
const ensure = accelerate(Service.ensure)
const stop = accelerate(Service.stop)
test("a concurrent same-version start cannot invalidate a resolved endpoint", async () => {
await using fixture = await serviceFixture()
@@ -162,67 +161,6 @@ test("signals an unresponsive registered service process", async () => {
expect(await Bun.file(registration).exists()).toBe(false)
})
test("stop escalates when the registration disappears before the process exits", async () => {
await using fixture = await serviceFixture()
const registration = fixture.registration
const existing = fixture.spawn("lingering", "5000")
await fixture.waitForFile()
const original = await Bun.file(registration).json()
await run(stop({ file: registration }))
expect(await Bun.file(registration + ".signal").text()).toBe("SIGTERM")
expect(() => process.kill(original.pid, 0)).toThrow()
await expectPortAvailable(original.url)
expect(await Bun.file(registration).exists()).toBe(false)
await existing.exited
}, 15_000)
test.skipIf(process.platform === "win32")(
"stop fails when the process survives SIGKILL",
async () => {
await using fixture = await serviceFixture()
const registration = fixture.registration
fixture.spawnUnreaped("lingering", "60000")
await fixture.waitForFile()
const original = await Bun.file(registration).json()
await expect(run(stop({ file: registration }))).rejects.toThrow(`Server process ${original.pid} is still running`)
expect(await Bun.file(registration + ".signal").text()).toBe("SIGTERM")
},
15_000,
)
test("stop waits for the original process while preserving a newly registered successor", async () => {
await using fixture = await serviceFixture()
const registration = fixture.registration
const existing = fixture.spawn("lingering", "15000")
await fixture.waitForFile()
const original = await Bun.file(registration).json()
// Default timing keeps the grace period open long enough for the successor to register first.
const stopping = run(Service.stop({ file: registration }))
try {
await fixture.waitForFile(registration + ".unregistered")
const successor = fixture.spawn("graceful")
await fixture.waitForFile()
const replacement = await Bun.file(registration).json()
expect(existing.exitCode).toBe(null)
expect(replacement.pid).toBe(successor.pid)
await stopping
expect(() => process.kill(original.pid, 0)).toThrow()
await expectPortAvailable(original.url)
expect(await Bun.file(registration).json()).toEqual(replacement)
expect(await status(replacement.url)).toMatchObject({ pid: successor.pid })
expect(successor.exitCode).toBe(null)
} finally {
existing.kill("SIGKILL")
await stopping
}
}, 20_000)
test("signals an incompatible service before starting its replacement", async () => {
await using fixture = await serviceFixture()
const registration = fixture.registration
@@ -290,76 +228,6 @@ test("reports a contender that fails to start", async () => {
).rejects.toThrow("Server process exited with code 1")
})
test("reports overlapping contender failures without recruiting replacements", async () => {
await using fixture = await serviceFixture()
const started = Date.now()
const pending = run(
ensure({ file: fixture.registration, version: "test", command: fixture.command("controlled") }),
).catch((error: unknown) => error)
const [first, second] = await fixture.waitForStarts(2)
await fixture.release(first, "fail")
// Let discovery observe the exit across two accelerated spawn windows before the survivor exits.
await Bun.sleep(450)
await fixture.release(second, "fail")
const error = await pending
expect(Date.now() - started).toBeLessThan(3_000)
expect(error).toBeInstanceOf(Error)
if (!(error instanceof Error)) throw error
expect(error.message).toContain("Server process exited with code 23")
expect(error.message).toContain("storage initialization denied")
expect(await fixture.starts()).toHaveLength(2)
})
test("retains a contender failure until the deadline while its survivor stalls", async () => {
await using fixture = await serviceFixture()
const started = Date.now()
const pending = run(
ensure({ file: fixture.registration, version: "test", command: fixture.command("controlled") }),
).catch((error: unknown) => error)
const [first, second] = await fixture.waitForStarts(2)
await fixture.release(first, "fail")
const error = await pending
expect(Date.now() - started).toBeGreaterThanOrEqual(3_000)
expect(error).toBeInstanceOf(Error)
if (!(error instanceof Error)) throw error
expect(error.message).toContain("Server process exited with code 23")
expect(error.message).toContain("storage initialization denied")
expect(await fixture.starts()).toHaveLength(2)
expect(() => process.kill(second, 0)).not.toThrow()
})
test("accepts a surviving contender after a failure without recruiting replacements", async () => {
await using fixture = await serviceFixture()
const pending = run(ensure({ file: fixture.registration, version: "test", command: fixture.command("controlled") }))
const [first, second] = await fixture.waitForStarts(2)
await fixture.release(first, "fail")
await Bun.sleep(450)
await fixture.release(second, "ready")
const endpoint = await pending
expect((await run(Service.discover({ file: fixture.registration, version: "test" })))?.url).toBe(endpoint.url)
expect((await Bun.file(fixture.registration).json()).pid).toBe(second)
expect(await fixture.starts()).toHaveLength(2)
})
test("recovers when an unresponsive contender is evicted after a prior failure", async () => {
await using fixture = await serviceFixture()
const pending = run(ensure({ file: fixture.registration, version: "test", command: fixture.command("controlled") }))
const [first, second] = await fixture.waitForStarts(2)
await fixture.release(second, "hang")
await fixture.waitForFile()
await fixture.release(first, "fail")
const [, , third] = await fixture.waitForStarts(3)
await fixture.release(third, "ready")
const endpoint = await pending
fixture.track(third)
expect((await run(Service.discover({ file: fixture.registration, version: "test" })))?.url).toBe(endpoint.url)
expect((await Bun.file(fixture.registration).json()).pid).toBe(third)
})
test("reports a bounded contender stderr tail", async () => {
await using fixture = await serviceFixture()
const registration = fixture.registration
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/codemode",
"version": "2.0.23",
"version": "2.0.22",
"description": "Effect-native confined code execution over schema-described tools",
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode/console-app",
"version": "2.0.23",
"version": "2.0.22",
"type": "module",
"license": "MIT",
"scripts": {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/console-core",
"version": "2.0.23",
"version": "2.0.22",
"private": true,
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode/console-function",
"version": "2.0.23",
"version": "2.0.22",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode/console-mail",
"version": "2.0.23",
"version": "2.0.22",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode/console-support",
"version": "2.0.23",
"version": "2.0.22",
"type": "module",
"license": "MIT",
"scripts": {
+2 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "2.0.23",
"version": "2.0.22",
"name": "@opencode/core",
"type": "module",
"license": "MIT",
@@ -134,6 +134,7 @@
"mime-types": "3.0.2",
"tree-sitter-bash": "0.25.0",
"tree-sitter-powershell": "0.25.10",
"venice-ai-sdk-provider": "2.1.1",
"web-tree-sitter": "0.25.10",
"which": "6.0.1",
"zod": "catalog:"
-1
View File
@@ -70,7 +70,6 @@ const PACKAGES: Readonly<Record<string, string>> = {
"@ai-sdk/xai": "@opencode/ai/providers/xai",
"@openrouter/ai-sdk-provider": "@opencode/ai/providers/openrouter",
"ai-gateway-provider": "@opencode/ai/providers/cloudflare-ai-gateway",
"venice-ai-sdk-provider": "@opencode/ai/providers/venice",
}
const protocols = (name: string) => ({
+11 -3
View File
@@ -61,9 +61,17 @@ const layer = Layer.effect(
release: holdUnsafe(),
})
})
const exit = yield* Effect.suspend(() =>
plugin.effect({ ...host, storage: PluginHost.storage(kv, plugin.id) }),
).pipe(
const exit = yield* Effect.suspend(() => {
const effect = plugin.effect({ ...host, storage: PluginHost.storage(kv, plugin.id) })
if (!Effect.isEffect(effect) || effect.pipe !== Effect.void.pipe) {
return Effect.die(
new Error(
`Plugin "${plugin.id}" returned an Effect from a bundled copy of effect. Declare "effect" as a peerDependency and do not bundle it.`,
),
)
}
return effect
}).pipe(
grouped,
inherit,
Effect.updateContext((context: Context.Context<never>) =>
+12 -1
View File
@@ -128,6 +128,17 @@ const load = Effect.fn("PluginModule.load")(function* (
target: operation.target,
...(installed?.version ? { version: installed.version } : {}),
},
effect: (host) => plugin.effect({ ...host, options: operation.options }),
effect: (host) =>
Effect.suspend(() => {
const effect = plugin.effect({ ...host, options: operation.options })
if (!Effect.isEffect(effect) || effect.pipe !== Effect.void.pipe) {
return Effect.die(
new Error(
`Plugin "${plugin.id}" returned an Effect from a bundled copy of effect. Declare "effect" as a peerDependency and do not bundle it.`,
),
)
}
return effect
}),
} satisfies Generation
})
+2
View File
@@ -26,6 +26,7 @@ import { PoePlugin } from "./provider/poe.js"
import { PromptCacheKeyPlugin } from "./provider/prompt-cache-key.js"
import { SapAICorePlugin } from "./provider/sap-ai-core.js"
import { VercelPlugin } from "./provider/vercel.js"
import { VenicePlugin } from "./provider/venice.js"
import { VLLMPlugin } from "./provider/vllm.js"
import { XAIPlugin } from "./provider/xai.js"
import { ZenmuxPlugin } from "./provider/zenmux.js"
@@ -59,6 +60,7 @@ export const ProviderPlugins: PluginInternal.InternalPlugin[] = [
PromptCacheKeyPlugin,
SapAICorePlugin,
VercelPlugin,
VenicePlugin,
VLLMPlugin,
XAIPlugin,
ZenmuxPlugin,
@@ -0,0 +1,10 @@
import { createProviderPlugin } from "./factory.js"
export const VenicePlugin = createProviderPlugin({
id: "opencode.provider.venice",
package: "venice-ai-sdk-provider",
load: async (options) => {
const { createVenice } = await import("venice-ai-sdk-provider")
return createVenice(options)
},
})
-1
View File
@@ -97,7 +97,6 @@ const builtins = new Map<string, () => Promise<unknown>>([
["@opencode/ai/providers/openai-compatible", () => import("@opencode/ai/providers/openai-compatible")],
["@opencode/ai/providers/openrouter", () => import("@opencode/ai/providers/openrouter")],
["@opencode/ai/providers/togetherai", () => import("@opencode/ai/providers/togetherai")],
["@opencode/ai/providers/venice", () => import("@opencode/ai/providers/venice")],
["@opencode/ai/providers/xai", () => import("@opencode/ai/providers/xai")],
["@opencode/ai/providers/zai/chat", () => import("@opencode/ai/providers/zai/chat")],
["@opencode/ai/providers/zai-coding-plan/chat", () => import("@opencode/ai/providers/zai-coding-plan/chat")],
+10 -25
View File
@@ -19,11 +19,9 @@ import type { SessionCompactionResult } from "@opencode/plugin/effect/session"
import type { SessionError } from "@opencode/schema/session-error"
import { makeLocationNode } from "@opencode/util/effect/app-node"
import { Context, Effect, Layer, Result, Stream } from "effect"
import { Agent } from "../agent.js"
import { Bus } from "../bus.js"
import { Database } from "../database/database.js"
import { llmClient } from "../effect/app-node-platform.js"
import { Model } from "../model.js"
import { State } from "../state.js"
import { Token } from "../util/token.js"
import type { SessionContext } from "./context.js"
@@ -32,7 +30,6 @@ import { SessionHistory } from "./history.js"
import type { SessionMessage } from "./message.js"
import { SessionModelRequest } from "./model-request.js"
import { SessionProviderContext } from "./provider-context.js"
import { SessionRunnerModel } from "./runner/model.js"
import { SessionRunnerRetry } from "./runner/retry.js"
import { toLLMMessages } from "./runner/to-llm-message.js"
import { contentFilterError, toSessionError } from "./to-session-error.js"
@@ -184,11 +181,8 @@ const LEGACY_HEADING = "## Additional Context"
export const layer = Layer.effect(
Service,
Effect.gen(function* () {
const agents = yield* Agent.Service
const bus = yield* Bus.Service
const llm = yield* LLMClient.Service
const catalog = yield* Model.Service
const models = yield* SessionRunnerModel.Service
const db = (yield* Database.Service).db
const requests = yield* SessionModelRequest.Service
@@ -208,33 +202,24 @@ export const layer = Layer.effect(
// Only the user compacts when automatic compaction is off, overflow included.
if (trigger.reason !== "manual" && !settings.auto) return { status: "skipped" }
if (trigger.reason === "auto" && !due(context, calculateCeiling(context.model.limit, settings.buffer)))
return { status: "skipped" }
const native = context.model.compaction?.type === "native"
const agent = native ? undefined : yield* agents.get(Agent.ID.make("compaction"))
const model =
(agent?.model &&
(yield* models
.resolve({ ...context.session, model: agent.model }, catalog.available)
.pipe(Effect.orElseSucceed(() => undefined)))) ??
context.model
const ceiling = calculateCeiling(model.limit, settings.buffer)
const ceiling = calculateCeiling(context.model.limit, settings.buffer)
if (trigger.reason === "auto" && !due(context, ceiling)) return { status: "skipped" }
// An unknown window never triggers auto compaction, but the compaction request still needs a size to aim for.
const cap = Number.isFinite(ceiling)
? ceiling
: calculateCeiling({ ...model.limit, context: UNKNOWN_WINDOW }, settings.buffer)
: calculateCeiling({ ...context.model.limit, context: UNKNOWN_WINDOW }, settings.buffer)
// The provider just rejected this context, so the estimate ran low; the first attempt already aims below it.
const budget =
trigger.reason === "overflow" ? Math.min(cap, Math.floor(estimateContext(context) * SHRINK_STEPS[0])) : cap
const selected = model === context.model ? trigger : { ...trigger, context: { ...context, model } }
const compaction = native
? compactNatively(selected, budget, settings.keep)
: summarize(selected, budget, settings.keep)
const compaction =
context.model.compaction?.type === "native"
? compactNatively(trigger, budget, settings.keep)
: summarize(trigger, budget, settings.keep)
return yield* compaction.pipe(
Effect.matchEffect({
onSuccess: (result) => publish(selected, result),
onFailure: (failure) => publish(selected, failure),
onSuccess: (result) => publish(trigger, result),
onFailure: (failure) => publish(trigger, failure),
}),
)
})
@@ -677,7 +662,7 @@ export const layer = Layer.effect(
export const node = makeLocationNode({
service: Service,
layer,
deps: [Agent.node, Bus.node, Database.node, llmClient, Model.node, SessionModelRequest.node, SessionRunnerModel.node],
deps: [Bus.node, Database.node, llmClient, SessionModelRequest.node],
})
/** History loads from the latest completed compaction, so a previous one is always the first message. */
+1 -10
View File
@@ -220,14 +220,6 @@ const openaiCompatible: Protocol = (model, support) => {
return openaiChat(model, support)
}
const veniceChat: Protocol = (_, support) => {
if (support.type === "effort")
return efforts(support.values ?? EFFORTS, (effort) => ({ settings: { reasoningEffort: effort } }))
if (support.type === "toggle")
return toggle({ settings: { reasoning: { enabled: false } } }, { settings: { reasoning: { enabled: true } } })
return []
}
const moonshotChat: Protocol = (model, support) => {
const id = modelID(model).toLowerCase()
const toggleable = id.includes("k2.5") || id.includes("k2-5") || id.includes("k2.6") || id.includes("k2-6")
@@ -591,7 +583,6 @@ const PROTOCOLS: Readonly<Record<string, Protocol>> = {
"@opencode/ai/providers/mistral": openaiChat,
"@opencode/ai/providers/moonshot/chat": moonshotChat,
"@opencode/ai/providers/togetherai": openaiChat,
"@opencode/ai/providers/venice": veniceChat,
"@opencode/ai/providers/xai": xaiResponses,
"@opencode/ai/providers/zai/chat": zaiChat,
"@opencode/ai/providers/zai-coding-plan/chat": zaiChat,
@@ -611,7 +602,7 @@ const PROTOCOLS: Readonly<Record<string, Protocol>> = {
"@opencode/ai/providers/cohere": cohere,
"@opencode/ai/providers/openrouter": openrouter,
[Provider.aisdk("venice-ai-sdk-provider")]: veniceChat,
[Provider.aisdk("venice-ai-sdk-provider")]: openaiChat,
"@opencode/ai/providers/cloudflare-ai-gateway": cloudflareAIGateway,
[Provider.aisdk("@ai-sdk/gateway")]: vercelGateway,
[Provider.aisdk("@jerome-benoit/sap-ai-provider-v2")]: sapAICore,
@@ -46,7 +46,6 @@ const it = testEffect(
}),
),
Config.node.replace(config),
Location.node.replace(Location.boundNode({ directory: AbsolutePath.make("/tmp") })),
]),
),
)
+261 -2
View File
@@ -1,17 +1,27 @@
import { expect } from "bun:test"
import { cp, mkdir, readFile, writeFile } from "node:fs/promises"
import path from "node:path"
import { Deferred, Effect, Exit, Fiber, Layer, Schedule, Scope, Stream } from "effect"
import { Brand, Deferred, Effect, Exit, Fiber, Layer, Option, Schedule, Schema, Scope, Stream } from "effect"
import { Agent } from "@opencode/schema/agent"
import { Session } from "@opencode/schema/session"
import { SessionMessage } from "@opencode/schema/session-message"
import { AppNodeBuilder } from "@opencode/core/effect/app-node-builder"
import { Watcher } from "@opencode/core/filesystem/watcher"
import { Plugin } from "@opencode/core/plugin"
import { PluginModule } from "@opencode/core/plugin/module"
import { Rpc } from "@opencode/core/rpc"
import { Tool } from "@opencode/core/tool"
import { execute } from "@opencode/core/tool/runtime"
import { Global } from "@opencode/util/global"
import { Npm } from "@opencode/util/npm"
import { tempGlobalLayer } from "../fixture/global"
import { tmpdirScoped } from "../fixture/tmpdir"
import { testEffect } from "../lib/effect"
import { PluginTestLayer } from "./fixture"
const it = testEffect(
Layer.merge(
Layer.mergeAll(
PluginTestLayer,
AppNodeBuilder.build(Npm.node, [Global.node.replace(tempGlobalLayer)]),
Watcher.layer().pipe(Layer.provide(Watcher.nativeLayer)),
),
@@ -90,3 +100,252 @@ it.live("interrupts pending watcher setup when the loader scope closes during mo
yield* Effect.promise(() => Bun.sleep(50))
}),
)
it.live("loads plugins and their transitive dependencies against the host's Effect instance", () =>
Effect.gen(function* () {
const directory = yield* tmpdirScoped()
const pluginDir = path.join(directory.path, "plugin")
const pluginEffectDir = path.join(pluginDir, "node_modules/effect")
const hostEffectDir = path.dirname(Bun.resolveSync("effect/package.json", import.meta.dir))
yield* Effect.promise(async () => {
await mkdir(path.join(pluginDir, "node_modules"), { recursive: true })
await cp(hostEffectDir, pluginEffectDir, { recursive: true })
const pkgPath = path.join(pluginEffectDir, "package.json")
const pkg = { ...(await Bun.file(pkgPath).json()), version: "4.0.0-rc.111" }
await writeFile(pkgPath, JSON.stringify(pkg, null, 2))
// Reproduce the version-skew failure modes on the plugin's own Effect copy so loading it would crash:
// 1. Effect.log reading an incompatible fiber log-level property (crashing host logger with logLevel.toUpperCase)
// 2. Effect.runPromise calling fiber.succeedWith on a host fiber
// 3. Schema.withDecodingDefault / Schema.Int / Schema.isPattern / Schema.Trim using foreign parser sentinels
const internalEffectPath = path.join(pluginEffectDir, "dist/internal/effect.js")
const internalEffect = (await readFile(internalEffectPath, "utf8"))
.replace(
"const logLevel = level ?? fiber.currentLogLevel;\n if (isLogLevelGreaterThan(fiber.minimumLogLevel, logLevel)) {",
"const logLevel = level ?? fiber.cache?.logLevel;\n if (isLogLevelGreaterThan(fiber.cache?.minimumLogLevel, logLevel)) {",
)
.replace(
"export const runPromiseWith = context => {",
"export const runPromiseWith = context => {\n return (effect) => Promise.resolve().then(() => { const fiber = {}; return fiber.succeedWith(effect); });",
)
await writeFile(internalEffectPath, internalEffect)
const depDir = path.join(pluginDir, "node_modules/transitive-dep")
await mkdir(depDir, { recursive: true })
await writeFile(
path.join(depDir, "package.json"),
JSON.stringify({ name: "transitive-dep", type: "module", exports: { ".": "./index.js" } }),
)
await writeFile(
path.join(depDir, "index.js"),
`import { Effect as DepEffect, Schema as DepSchema } from "effect"
import { some as depSome } from "effect/Option"
export const depToolInput = DepSchema.Struct({
mode: DepSchema.String.pipe(DepSchema.withDecodingDefault(DepEffect.succeed("from-dep"))),
})
export { DepEffect, DepSchema, depSome }`,
)
const foreignPluginPkgDir = path.join(pluginDir, "node_modules/@opencode/plugin")
await mkdir(foreignPluginPkgDir, { recursive: true })
await writeFile(
path.join(foreignPluginPkgDir, "package.json"),
JSON.stringify({
name: "@opencode/plugin",
type: "module",
exports: { "./effect": "./effect.js", "./rpc": "./rpc.js" },
}),
)
await writeFile(
path.join(foreignPluginPkgDir, "effect.js"),
"export const Plugin = { define: (p) => p }",
)
await writeFile(path.join(foreignPluginPkgDir, "rpc.js"), "export const Rpc = { define: (d) => d }")
await writeFile(
path.join(pluginDir, "index.ts"),
`import { Plugin } from "@opencode/plugin/effect"
import { Rpc } from "@opencode/plugin/rpc"
import { Effect as PluginEffect, Schema as PluginSchema } from "effect"
import { some as pluginSome } from "effect/Option"
import { nominal as pluginNominal } from "effect/Brand"
import { DepEffect, DepSchema, depSome, depToolInput } from "transitive-dep"
export const captured = {
PluginEffect,
PluginSchema,
pluginSome,
pluginNominal,
DepEffect,
DepSchema,
depSome,
pluginCount: -1,
}
const Contract = Rpc.define({
id: "host-effect-rpc",
methods: {
check: {
input: PluginSchema.Struct({
count: PluginSchema.Int.pipe(PluginSchema.withDecodingDefault(PluginEffect.succeed(5))),
tag: PluginSchema.Trim.check(PluginSchema.isPattern(/^v[0-9]+$/)),
}),
output: PluginSchema.Struct({
value: PluginSchema.String,
}),
},
},
events: {},
})
export default Plugin.define({
id: "host-effect-fixture",
effect: (ctx) =>
PluginEffect.gen(function* () {
yield* PluginEffect.log("setup log from plugin")
const listed = yield* PluginEffect.promise(() =>
PluginEffect.runPromise(ctx.plugin.list().pipe(PluginEffect.orDie)),
)
captured.pluginCount = listed.data.length
yield* ctx.tool.transform((editor) => {
editor.add({
name: "default_tool",
description: "Tool with decoding default from transitive dependency",
input: depToolInput,
output: PluginSchema.Struct({ mode: PluginSchema.String }),
execute: ({ mode }) =>
PluginEffect.log("executing default_tool").pipe(
PluginEffect.as({ output: { mode }, content: mode }),
),
})
editor.add({
name: "check_tool",
description: "Tool with Int, Trim, and isPattern checks",
input: PluginSchema.Struct({
count: PluginSchema.Int,
code: PluginSchema.Trim.check(PluginSchema.isPattern(/^v[0-9]+$/)),
}),
output: PluginSchema.Struct({ formatted: PluginSchema.String }),
execute: ({ count, code }) =>
PluginEffect.succeed({
output: { formatted: \`\${code}:\${count}\` },
content: \`\${code}:\${count}\`,
}),
})
})
yield* ctx.rpc.register(Contract, {
check: ({ count, tag }) =>
PluginEffect.log("executing rpc check").pipe(
PluginEffect.as({ value: \`\${tag}#\${count}\` }),
),
}).pipe(PluginEffect.orDie)
}),
})`,
)
})
const modules = yield* PluginModule.make()
const plugins = yield* Plugin.Service
const tools = yield* Tool.Service
const rpc = yield* Rpc.Service
const definition = yield* modules.load({ type: "add", target: pluginDir, options: {} })
if ("pending" in definition) return yield* Effect.die(new Error("Local plugin was not loaded"))
yield* plugins.activate([definition])
yield* plugins.awaitActivation
expect(yield* plugins.list()).toMatchObject([{ id: "host-effect-fixture", state: { status: "active" } }])
const imported = (yield* Effect.promise(() => import(path.join(pluginDir, "index.ts")))) as {
captured: {
PluginEffect: unknown
PluginSchema: unknown
pluginSome: unknown
pluginNominal: unknown
DepEffect: unknown
DepSchema: unknown
depSome: unknown
pluginCount: number
}
}
expect(imported.captured.PluginEffect).toBe(Effect)
expect(imported.captured.PluginSchema).toBe(Schema)
expect(imported.captured.pluginSome).toBe(Option.some)
expect(imported.captured.pluginNominal).toBe(Brand.nominal)
expect(imported.captured.DepEffect).toBe(Effect)
expect(imported.captured.DepSchema).toBe(Schema)
expect(imported.captured.depSome).toBe(Option.some)
expect(imported.captured.pluginCount).toBe(0)
const registeredTools = yield* tools.list()
const defaultTool = registeredTools.find((tool) => tool.id === "default_tool")
const checkTool = registeredTools.find((tool) => tool.id === "check_tool")
expect(defaultTool).toBeDefined()
expect(checkTool).toBeDefined()
if (!defaultTool || !checkTool) return
const context = {
sessionID: Session.ID.make("ses_host_effect"),
agent: Agent.ID.make("build"),
messageID: SessionMessage.ID.make("msg_host_effect"),
id: Tool.CallID.make("call_host_effect"),
progress: () => Effect.void,
}
expect(yield* execute(defaultTool, {}, context)).toEqual({
output: { mode: "from-dep" },
content: [{ type: "text", text: "from-dep" }],
})
expect(yield* execute(checkTool, { count: 3, code: " v42 " }, context)).toEqual({
output: { formatted: "v42:3" },
content: [{ type: "text", text: "v42:3" }],
})
expect(yield* rpc.call("host-effect-rpc", "check", { tag: " v9 " })).toEqual({ value: "v9#5" })
}),
)
it.live("fails activation when a plugin returns an Effect from a bundled copy of effect", () =>
Effect.gen(function* () {
const directory = yield* tmpdirScoped()
const pluginDir = path.join(directory.path, "bundled-plugin")
const bundledEffectDir = path.join(pluginDir, "bundled-effect")
const hostEffectDir = path.dirname(Bun.resolveSync("effect/package.json", import.meta.dir))
yield* Effect.promise(async () => {
await mkdir(pluginDir, { recursive: true })
await cp(hostEffectDir, bundledEffectDir, { recursive: true })
await writeFile(
path.join(pluginDir, "index.ts"),
`import { Effect as BundledEffect } from "./bundled-effect/dist/index.js"
export default {
id: "bundled-effect-plugin",
effect: () => BundledEffect.void,
}`,
)
})
const modules = yield* PluginModule.make()
const plugins = yield* Plugin.Service
const definition = yield* modules.load({ type: "add", target: pluginDir, options: {} })
if ("pending" in definition) return yield* Effect.die(new Error("Local plugin was not loaded"))
yield* plugins.activate([definition])
yield* plugins.awaitActivation
expect(yield* plugins.list()).toMatchObject([
{
id: "bundled-effect-plugin",
state: {
status: "failed",
error: expect.stringContaining(
'Plugin "bundled-effect-plugin" returned an Effect from a bundled copy of effect. Declare "effect" as a peerDependency and do not bundle it.',
),
},
},
])
}),
)
@@ -7,6 +7,7 @@ import { PluginHost } from "@opencode/core/plugin/host"
import { CoherePlugin } from "@opencode/core/plugin/provider/cohere"
import { GatewayPlugin } from "@opencode/core/plugin/provider/gateway"
import { PerplexityPlugin } from "@opencode/core/plugin/provider/perplexity"
import { VenicePlugin } from "@opencode/core/plugin/provider/venice"
import { Provider } from "@opencode/core/provider"
import { testEffect } from "../lib/effect"
import { PluginTestLayer } from "./fixture"
@@ -17,6 +18,7 @@ const providers = [
{ id: "cohere", plugin: CoherePlugin, package: "@ai-sdk/cohere", provider: "cohere.chat" },
{ id: "gateway", plugin: GatewayPlugin, package: "@ai-sdk/gateway", provider: "gateway" },
{ id: "perplexity", plugin: PerplexityPlugin, package: "@ai-sdk/perplexity", provider: "perplexity" },
{ id: "venice", plugin: VenicePlugin, package: "venice-ai-sdk-provider", provider: "custom-provider.chat" },
] as const
const it = testEffect(PluginTestLayer)
@@ -18,7 +18,6 @@ import { SessionRunnerModel } from "@opencode/core/session/runner/model"
import { SessionTable } from "@opencode/core/session/sql"
import { SessionStore } from "@opencode/core/session/store"
import { Session } from "@opencode/core/session"
import { Location } from "@opencode/core/location"
import { Project } from "@opencode/core/project"
import { ProjectTable } from "@opencode/core/project/sql"
import { App } from "@opencode/core/app"
@@ -88,11 +87,7 @@ const it = testEffect(
SessionModelRequest.node,
PluginHooks.node,
]),
[
Bus.node.replace(Bus.configured({ persist: true })),
llmClient.replace(client),
Location.node.replace(Location.boundNode({ directory: AbsolutePath.make("/project") })),
],
[Bus.node.replace(Bus.configured({ persist: true })), llmClient.replace(client)],
),
)
@@ -8,7 +8,6 @@ import { AppNodeBuilder } from "@opencode/core/effect/app-node-builder"
import { llmClient } from "@opencode/core/effect/app-node-platform"
import { Instructions } from "@opencode/core/instructions/index"
import { PluginHooks } from "@opencode/core/plugin/hooks"
import { Location } from "@opencode/core/location"
import { Project } from "@opencode/core/project"
import { ProjectTable } from "@opencode/core/project/sql"
import { AbsolutePath } from "@opencode/core/schema"
@@ -41,10 +40,7 @@ const it = testEffect(
PluginHooks.node,
llmClient,
]),
[
Bus.node.replace(Bus.configured({ persist: true })),
Location.node.replace(Location.boundNode({ directory: AbsolutePath.make("/project") })),
],
[Bus.node.replace(Bus.configured({ persist: true }))],
),
)
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@opencode/desktop",
"private": true,
"version": "2.0.23",
"version": "2.0.22",
"type": "module",
"license": "MIT",
"homepage": "https://opencode.ai",
+1 -32
View File
@@ -4,26 +4,20 @@ import { Script } from "@opencode/script"
import { UpdateArtifact } from "../../../script/update-artifact"
const dryRun = process.argv.includes("--dry-run")
if (!Script.release) {
console.log("skipped desktop publication without a release")
process.exit(0)
}
const directory = process.env.OPENCODE_DESKTOP_DIST
if (!directory) throw new Error("OPENCODE_DESKTOP_DIST is required")
const files = (
await Array.fromAsync(
new Bun.Glob("*.{exe,blockmap,dmg,zip,AppImage,deb,rpm,app.tar.gz}").scan({ cwd: directory, absolute: true }),
)
).sort()
if (!files.length) throw new Error("No desktop release files found")
const uploaded = await UpdateArtifact.upload({ version: Script.version, files, dryRun })
const artifact = {
channel: Script.channel,
name: "desktop",
@@ -31,9 +25,7 @@ const artifact = {
version: Script.version,
metadata: { files: uploaded, ...(await metadata(Script.version, uploaded)) },
}
if (dryRun) console.log(`dry-run artifact: ${JSON.stringify(artifact)}`)
if (!dryRun) await UpdateArtifact.publish(artifact)
type DesktopFile = {
@@ -45,9 +37,7 @@ type DesktopFile = {
async function metadata(version: string, files: Record<string, { url: string }>) {
const directory = process.env.LATEST_YML_DIR
if (!directory) throw new Error("LATEST_YML_DIR is required")
const entries = await Promise.all(
[
{
@@ -74,75 +64,54 @@ async function metadata(version: string, files: Record<string, { url: string }>)
await Promise.all(
item.sources.map(async ([subdirectory, source]) => {
const file = Bun.file(`${directory}/${subdirectory}/${source}`)
if (!(await file.exists())) return undefined
return parse(await file.text(), version, files)
}),
)
).filter((manifest) => manifest !== undefined)
if (manifests.length !== item.sources.length) return undefined
return [
item.name,
{ files: manifests.flatMap((manifest) => manifest.files), releaseDate: manifests[0]!.releaseDate },
] as const
}),
)
if (entries.some((entry) => entry === undefined)) throw new Error("Desktop update metadata is incomplete")
const manifests = Object.fromEntries(entries.filter((entry) => entry !== undefined))
return { manifests }
}
function parse(content: string, version: string, uploaded: Record<string, { url: string }>) {
const lines = content.split("\n")
const found = lines
.find((line) => line.startsWith("version:"))
?.slice("version:".length)
.trim()
if (found !== version) throw new Error(`Desktop metadata version mismatch: expected ${version}, got ${found}`)
const releaseDate = lines
.find((line) => line.startsWith("releaseDate:"))
?.slice("releaseDate:".length)
.trim()
.replace(/^['"]|['"]$/g, "")
if (!releaseDate) throw new Error("Desktop metadata did not include a release date")
const files: DesktopFile[] = []
lines.forEach((line) => {
const value = line.trim()
if (value.startsWith("- url:")) {
const name = value.slice("- url:".length).trim()
const filename = name.startsWith("http") ? decodeURIComponent(new URL(name).pathname.split("/").pop()!) : name
const url = uploaded[filename]?.url
if (!url) throw new Error(`Desktop update file was not uploaded: ${filename}`)
files.push({ url, sha512: "", size: 0 })
return
}
const current = files.at(-1)
// electron-builder repeats the first file's sha512 as a top-level legacy field after the list.
if (!current || !line.startsWith(" ")) return
if (!current) return
if (value.startsWith("sha512:")) current.sha512 = value.slice("sha512:".length).trim()
if (value.startsWith("size:")) current.size = Number(value.slice("size:".length).trim())
if (value.startsWith("blockMapSize:")) current.blockMapSize = Number(value.slice("blockMapSize:".length).trim())
})
if (!files.length || files.some((file) => !file.sha512 || !file.size))
throw new Error("Desktop metadata contained an incomplete file")
return { files, releaseDate }
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode/enterprise",
"version": "2.0.23",
"version": "2.0.22",
"private": true,
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode/function",
"version": "2.0.23",
"version": "2.0.22",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/gui-extensions",
"version": "2.0.23",
"version": "2.0.22",
"private": true,
"type": "module",
"license": "MIT",
+1 -12
View File
@@ -1,18 +1,7 @@
import { Schema } from "effect"
import { Extension, Store } from "../sdk"
import { Extension } from "../sdk"
import en from "./i18n/en"
const Chats = Schema.Struct({
chats: Schema.Array(
Schema.Struct({ id: Schema.String, question: Schema.String, answer: Schema.optional(Schema.String) }),
),
})
export default Extension.define({
id: "btw",
stores: {
// Each open /btw tab's chat (its question and answer), kept until its tab closes.
chats: Store.session(Chats, { chats: [] }),
},
i18n: { en },
})
+65 -76
View File
@@ -1,7 +1,7 @@
import { batch, onCleanup } from "solid-js"
import { createStore } from "solid-js/store"
import { showToast } from "@opencode/ui/toast"
import type { MountedSession, SetupContext } from "../sdk"
import { createKeyed, type MountedSession, type SetupContext } from "../sdk"
import type Btw from "./index"
const instructions = [
@@ -10,56 +10,38 @@ const instructions = [
"Do not call any tools and do not take any actions.",
].join(" ")
/** One-shot side questions per session, one tab each, stored until the tab closes. */
const empty = {
question: "",
answer: "",
error: false,
pending: false,
}
/** Side questions per session. Window-local: a reload drops them, and with them the tab. */
export function createBtw(ctx: SetupContext<typeof Btw>) {
const sessions = ctx.sessions
const layout = ctx.layout
const [states, setStates] = createStore<Record<string, typeof empty>>({})
const requests = new Map<string, number>()
const controllers = new Map<string, AbortController>()
const [requests, setRequests] = createStore<{ pending: string[] }>({ pending: [] })
const saved = (session: MountedSession) => ctx.stores.chats(session)
const stop = (id: string) => {
controllers.get(id)?.abort()
controllers.delete(id)
setRequests("pending", (list) => list.filter((item) => item !== id))
const stop = (key: string) => {
const controller = controllers.get(key)
if (!controller) return
controller.abort()
controllers.delete(key)
if (states[key]?.pending) setStates(key, { pending: false, error: true })
}
// In-flight questions keep running when the user leaves their session and answer into its store; only a reload,
// closing the question's tab or disposing the extension abandons one, which leaves it retryable.
// Leaving a session abandons its in-flight question; the same session moving to another directory does not.
createKeyed(
() => sessions.current()?.key,
(key) => onCleanup(() => stop(key)),
)
onCleanup(() => Array.from(controllers.keys()).forEach(stop))
const entry = (session: MountedSession, id: string) => saved(session).value?.chats.find((item) => item.id === id)
const pending = (id: string) => requests.pending.includes(id)
// Takes the question rather than reading it back, because a new chat's write may still wait for the store to load.
const generate = (session: MountedSession, id: string, question: string) => {
if (pending(id)) return
const store = saved(session)
const controller = new AbortController()
controllers.set(id, controller)
setRequests("pending", (list) => [...list, id])
return (
session.server.client.session
.generate(
{ sessionID: session.id, prompt: [instructions, question].join("\n\n") },
{ signal: controller.signal },
)
.then((result) => {
if (ctx.signal.aborted || controller.signal.aborted) return
store.update((draft) => {
const chat = draft.chats.find((item) => item.id === id)
if (chat) chat.answer = result.text.trim()
})
})
// A missing answer is the retry state; no transient error flag is stored.
.catch(() => undefined)
.finally(() => {
if (controllers.get(id) === controller) stop(id)
})
)
}
const ask = (value?: string) => {
const question = value?.trim()
@@ -69,47 +51,54 @@ export function createBtw(ctx: SetupContext<typeof Btw>) {
return
}
const session = ctx.sessions.current()
const session = sessions.current()
if (!session?.id) return
const id = crypto.randomUUID()
// Desktop storage loads asynchronously. A write before load queues and lands in the same batch that marks the
// store ready, and the panel keeps a tab hidden until then, so the chat and its tab need not wait. The session's
// store lives while its tab stays open, so leaving the session first still records the chat.
const key = session.key
const request = (requests.get(key) ?? 0) + 1
requests.set(key, request)
controllers.get(key)?.abort()
const controller = new AbortController()
controllers.set(key, controller)
// The tab lists only while its session has a question, and the host drops unlisted transient keys, so
// record the question in the same batch that opens the tab.
batch(() => {
saved(session).update((draft) => {
draft.chats.push({ id, question })
})
ctx.layout.open(`${ctx.id}:${id}`, session, { tab: "select" })
setStates(key, { question, answer: "", error: false, pending: true })
layout.open(`${ctx.id}:main`, session)
})
void generate(session, id, question)
return session.server.client.session
.generate(
{
sessionID: session.id,
prompt: [instructions, question].join("\n\n"),
},
{ signal: controller.signal },
)
.then((result) => {
if (controller.signal.aborted || requests.get(key) !== request) return
setStates(key, { answer: result.text.trim(), pending: false })
})
.catch(() => {
if (controller.signal.aborted || requests.get(key) !== request) return
setStates(key, { error: true, pending: false })
})
.finally(() => {
if (controllers.get(key) === controller) controllers.delete(key)
})
}
const state = (session: MountedSession) => states[session.key]
return {
ask,
saved,
pending,
question: (session: MountedSession, id: string) => entry(session, id)?.question,
answer: (session: MountedSession, id: string) => entry(session, id)?.answer,
error: (session: MountedSession, id: string) => {
const item = entry(session, id)
return !!item && item.answer === undefined && !pending(id)
},
retry: (session: MountedSession, id: string) => {
const question = entry(session, id)?.question
if (question) void generate(session, id, question)
},
/** Closing a tab forgets its question and answer. */
remove: (session: MountedSession, id: string) => {
stop(id)
saved(session).update((draft) => {
draft.chats = draft.chats.filter((item) => item.id !== id)
})
},
has: (session: MountedSession) => !!state(session),
answer: (session: MountedSession) => (state(session) ?? empty).answer,
error: (session: MountedSession) => (state(session) ?? empty).error,
pending: (session: MountedSession) => (state(session) ?? empty).pending,
question: (session: MountedSession) => (state(session) ?? empty).question,
retry: (session: MountedSession) => ask((state(session) ?? empty).question),
}
}
+11 -13
View File
@@ -10,10 +10,10 @@ import { Markdown } from "@opencode/session-ui/markdown"
import { useExtension, type MountedSession } from "../sdk"
import type { BtwModel } from "./model"
export default function SessionBtwPanel(props: { btw: BtwModel; session: MountedSession; id: string }) {
export default function SessionBtwPanel(props: { btw: BtwModel; session: MountedSession }) {
const ctx = useExtension()
const system = ctx.system
const answer = () => props.btw.answer(props.session, props.id)
const answer = () => props.btw.answer(props.session)
// A new token for each answer, so the copied mark clears when the answer changes.
const shown = createMemo(on(answer, () => ({})))
const [copiedAnswer, setCopiedAnswer] = createSignal<object>()
@@ -32,7 +32,7 @@ export default function SessionBtwPanel(props: { btw: BtwModel; session: Mounted
return (
<div class="flex h-full min-h-0 flex-col bg-v2-background-bg-base" data-slot="session-btw-panel">
<div class="flex shrink-0 items-start justify-between gap-3 border-b border-v2-border-border-base px-5 py-4">
<div class="min-w-0 text-13-regular text-text-weak">{props.btw.question(props.session, props.id)}</div>
<div class="min-w-0 text-13-regular text-text-weak">{props.btw.question(props.session)}</div>
<Show when={answer()}>
<Tooltip value={copied() ? ctx.t("common.copied") : ctx.t("copy")}>
<IconButton
@@ -48,7 +48,7 @@ export default function SessionBtwPanel(props: { btw: BtwModel; session: Mounted
<div class="relative min-h-0 flex-1">
<Switch>
<Match when={props.btw.pending(props.id)}>
<Match when={props.btw.pending(props.session)}>
<div
data-component="session-working"
role="status"
@@ -57,22 +57,20 @@ export default function SessionBtwPanel(props: { btw: BtwModel; session: Mounted
<TextShimmer text={ctx.t("session.timeline.working")} active />
</div>
</Match>
<Match when={props.btw.error(props.session, props.id)}>
<Match when={props.btw.error(props.session)}>
<div class="flex h-full flex-col items-center justify-center gap-3 px-8 pb-24 text-center">
<div class="text-13-regular text-text-weak">{ctx.t("error")}</div>
<Button size="small" variant="outline" onClick={() => props.btw.retry(props.session, props.id)}>
<Button size="small" variant="outline" onClick={() => props.btw.retry(props.session)}>
{ctx.t("retry")}
</Button>
</div>
</Match>
<Match when={answer()}>
{(text) => (
<ScrollView class="absolute inset-0">
<div class="px-5 py-4 pb-8">
<Markdown text={text()} class="text-14-regular" />
</div>
</ScrollView>
)}
<ScrollView class="absolute inset-0">
<div class="px-5 py-4 pb-8">
<Markdown text={answer()} class="text-14-regular" />
</div>
</ScrollView>
</Match>
</Switch>
</div>
+15 -55
View File
@@ -1,6 +1,6 @@
import { createMemo, lazy, onCleanup, Suspense } from "solid-js"
import { Icon } from "@opencode/ui/icon"
import { Command, createKeyed, onIdle, Panel, type PanelTab, type Setup } from "../sdk"
import { Command, onIdle, Panel, type PanelTab, type Setup } from "../sdk"
import type Btw from "./index"
import { createBtw } from "./model"
@@ -13,19 +13,18 @@ const setup: Setup<typeof Btw> = (ctx) => {
// Changes when a session mounts or unmounts, not on every switch between sessions.
const mounted = createMemo(() => !!sessions.current())
// Tab objects per session, reused so strip updates and session switches do not rebuild a trigger.
const tabs = new Map<string, Map<string, PanelTab>>()
// Drops the tab objects of sessions whose shell tab closed.
createKeyed(
() => [...sessions.list().map((session) => session.key), sessions.current()?.key].join("\u0000"),
() => {
const keep = new Set([...sessions.list().map((session) => session.key), sessions.current()?.key])
tabs.forEach((_cache, key) => {
if (!keep.has(key)) tabs.delete(key)
})
const tab: PanelTab = {
id: "main",
get title() {
return ctx.t("tab.title")
},
)
label: () => (
<div class="flex items-center gap-1.5">
<Icon name="bubble-5" size="small" />
<span>{ctx.t("tab.title")}</span>
</div>
),
}
ctx.add(
Command,
@@ -49,52 +48,13 @@ const setup: Setup<typeof Btw> = (ctx) => {
transient: true,
// Layouts saved before extensions store the tab as "btw"; as a panel key it leaves like any unlisted transient tab.
legacy: { btw: "main" },
// One tab per stored question. Until the session's store loads, its tabs stay listed but hidden so restore keeps
// them; afterwards a tab without a stored question leaves the strip.
list: (input) => {
if (input.open.length === 0) return []
const saved = btw.saved(input.session)
const cache = tabs.get(input.session.key) ?? new Map<string, PanelTab>()
tabs.set(input.session.key, cache)
return input.open.flatMap((id) => {
if (saved.ready() && !saved.value?.chats.some((item) => item.id === id)) return []
const existing = cache.get(id)
if (existing) return [existing]
const tab: PanelTab = {
id,
get title() {
return saved.value?.chats.find((item) => item.id === id)?.question ?? ctx.t("tab.title")
},
get hidden() {
return !saved.ready()
},
label: (state) => (
<div class="flex min-w-0 items-center gap-1.5">
<Icon name="bubble-5" size="small" />
<span class="truncate">{btw.question(state.session, id) ?? ctx.t("tab.title")}</span>
</div>
),
}
cache.set(id, tab)
return [tab]
})
},
// The answer lives only in this window's memory, so the tab lists while its session has one.
list: (input) => (input.open.includes("main") && btw.has(input.session) ? [tab] : []),
render: (props) => (
<Suspense>
<SessionBtwPanel btw={btw} session={props.session} id={props.tab.id} />
<SessionBtwPanel btw={btw} session={props.session} />
</Suspense>
),
close: (input) => {
btw.remove(input.session, input.tab.id)
tabs.get(input.session.key)?.delete(input.tab.id)
},
})
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "2.0.23",
"version": "2.0.22",
"name": "@opencode/http-recorder",
"description": "Record and replay Effect HTTP and WebSocket traffic with deterministic cassettes",
"type": "module",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/httpapi-codegen",
"version": "2.0.23",
"version": "2.0.22",
"private": true,
"type": "module",
"exports": {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/latex",
"version": "2.0.23",
"version": "2.0.22",
"private": true,
"type": "module",
"exports": {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/merman",
"version": "2.0.23",
"version": "2.0.22",
"private": true,
"type": "module",
"exports": {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/plugin-browser",
"version": "2.0.23",
"version": "2.0.22",
"description": "OpenCode's desktop browser plugin",
"type": "module",
"license": "MIT",
+16
View File
@@ -0,0 +1,16 @@
# `@opencode/plugin`
Authoring interfaces and runtime loader support for OpenCode V2 plugins:
- `@opencode/plugin` — [Promise plugin API](./src/README.md)
- `@opencode/plugin/effect` — [Effect plugin API](./src/effect/README.md)
- `@opencode/plugin/rpc` — portable RPC contract definitions
- `@opencode/plugin/tui` — terminal UI plugin API
## Packaging And Runtime `effect`
OpenCode resolves imports of `effect`, every exported `effect/*` subpath, and `@opencode/plugin` entrypoints (including transitive imports from dependencies inside a plugin's `node_modules`) to the host's runtime module instances.
- Declare `effect` as a `peerDependency` (and `devDependency` for local type-checking/testing) rather than bundling it.
- Do not bundle `effect` into published plugin files; if you build with a bundler, keep `effect` and `effect/*` external. OpenCode detects and rejects Effect plugins whose returned `Effect` comes from a bundled copy.
- At runtime, plugins execute against the host OpenCode release's `effect` version, so plugins must use `effect` APIs and module paths compatible with the OpenCode release they target.
+7 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/plugin",
"version": "2.0.23",
"version": "2.0.22",
"type": "module",
"license": "MIT",
"scripts": {
@@ -17,6 +17,12 @@
"./*": "./src/*.ts"
},
"imports": {
"#plugin-runtime": {
"workerd": "./src/runtime.workerd.ts",
"bun": "./src/runtime.bun.ts",
"node": "./src/runtime.node.ts",
"default": "./src/runtime.node.ts"
},
"#plugin-source": {
"bun": "./src/source.bun.ts",
"node": "./src/source.node.ts",
+2
View File
@@ -140,6 +140,8 @@ await ctx.tool.transform((tools) => {
})
```
When using `effect` schemas in a plugin, declare `effect` as a `peerDependency` and do not bundle it; OpenCode resolves `effect` and `effect/*` imports to the host's runtime instance.
## Reloading A Domain
When data captured by a transform changes, reload the affected domain:
+8
View File
@@ -29,6 +29,14 @@ Configuration supplied for the plugin is available as `ctx.options`.
Registrations are owned by the plugin scope. Closing the scope removes them automatically; a registration may also be removed early through `dispose`.
## Packaging And Runtime `effect`
OpenCode redirects imports of `effect`, `effect/*` subpaths, and `@opencode/plugin` (including transitive imports from packages in the plugin's `node_modules`) to the host's runtime instances so fibers, loggers, and `Schema` parsers share one runtime copy.
- Declare `effect` in `peerDependencies` (and `devDependencies` for local development) rather than bundling it.
- Do not bundle `effect` into the published plugin artifact; if using a bundler, mark `effect` and `effect/*` external. OpenCode rejects Effect plugins whose returned `Effect` comes from a bundled copy.
- Plugins always execute against the host OpenCode release's `effect` instance, so use `effect` APIs and module paths compatible with the OpenCode release you target.
## Transform Hooks
Transform hooks contribute to stateful domains. Their editor callbacks are
+1
View File
@@ -14,6 +14,7 @@ export { Provider } from "@opencode/schema/provider"
export { Reference } from "@opencode/schema/reference"
export { Rpc } from "@opencode/schema/rpc"
export { Skill } from "@opencode/schema/skill"
export { Tool } from "@opencode/schema/tool"
export { Vcs } from "@opencode/schema/vcs"
export { WebSearch } from "@opencode/schema/websearch"
export { Worktree } from "@opencode/schema/worktree"
+3
View File
@@ -1,3 +1,6 @@
export { CallID, Error, Tool } from "@opencode/schema/tool"
export type { Metadata, Options, Result } from "@opencode/schema/tool"
import { Tool } from "@opencode/schema/tool"
import type { Agent } from "@opencode/schema/agent"
import type { Session } from "@opencode/schema/session"
+3 -1
View File
@@ -2,6 +2,7 @@ export * as Host from "./host.js"
import path from "node:path"
import { importModule, resolveModule } from "@opencode/util/runtime-import"
import { ensurePluginRuntime } from "#plugin-runtime"
export interface Target {
readonly directory: string
@@ -43,6 +44,7 @@ export function resolve(target: Target): Entrypoints {
return { server: entry(["server", ""]), tui: entry(["tui"]), rpc: entry(["rpc"]) }
}
export function load(entrypoint: string): Promise<unknown> {
export async function load(entrypoint: string): Promise<unknown> {
await ensurePluginRuntime(entrypoint)
return importModule(entrypoint)
}
+1
View File
@@ -15,6 +15,7 @@ export { Provider } from "@opencode/schema/provider"
export { Reference } from "@opencode/schema/reference"
export { Rpc } from "@opencode/schema/rpc"
export { Skill } from "@opencode/schema/skill"
export { Tool } from "@opencode/schema/tool"
export { Vcs } from "@opencode/schema/vcs"
export { WebSearch } from "@opencode/schema/websearch"
export { Worktree } from "@opencode/schema/worktree"
@@ -0,0 +1,46 @@
import path from "node:path"
import { pathToFileURL } from "node:url"
export type RuntimeModuleLoader = () => Record<string, unknown> | Promise<Record<string, unknown>>
export type RuntimeModuleEntry = Record<string, unknown> | RuntimeModuleLoader
export function discoverEffectSpecifiers(from = import.meta.dir): ReadonlyArray<readonly [string, string]> {
const pkg = Bun.resolveSync("effect/package.json", from)
const dir = path.dirname(pkg)
const dist = path.join(dir, "dist")
const entries: Array<readonly [string, string]> = [["effect", Bun.resolveSync("effect", dir)]]
for (const file of new Bun.Glob("**/*.js").scanSync({ cwd: dist })) {
const normalized = file.replaceAll("\\", "/")
if (normalized === "index.js" || normalized.startsWith("internal/") || normalized.includes("/internal/")) continue
const subpath = normalized.endsWith("/index.js")
? normalized.slice(0, -"/index.js".length)
: normalized.slice(0, -".js".length)
const specifier = `effect/${subpath}`
try {
entries.push([specifier, Bun.resolveSync(specifier, dir)])
} catch {}
}
return entries.toSorted(([left], [right]) => left.localeCompare(right))
}
let cached: Readonly<Record<string, RuntimeModuleLoader>> | undefined
export function pluginRuntimeModules(): Readonly<Record<string, RuntimeModuleLoader>> {
if (cached) return cached
cached = {
"@opencode/plugin": () => import("./promise/index.js"),
"@opencode/plugin/effect": () => import("./effect/index.js"),
"@opencode/plugin/effect/plugin": () => import("./effect/plugin.js"),
"@opencode/plugin/effect/tool": () => import("./effect/tool.js"),
"@opencode/plugin/promise/plugin": () => import("./promise/plugin.js"),
"@opencode/plugin/promise/tool": () => import("./promise/tool.js"),
"@opencode/plugin/rpc": () => import("./rpc.js"),
...Object.fromEntries(
discoverEffectSpecifiers().map(([specifier, resolved]) => [
specifier,
() => import(pathToFileURL(resolved).href) as Promise<Record<string, unknown>>,
]),
),
}
return cached
}
+306
View File
@@ -0,0 +1,306 @@
import { existsSync, readFileSync, realpathSync } from "node:fs"
import { dirname, isAbsolute, join } from "node:path"
import { fileURLToPath } from "node:url"
import { plugin, type PluginBuilder } from "bun"
import { pluginRuntimeModules, type RuntimeModuleEntry } from "./runtime-modules.bun.js"
export { pluginRuntimeModules } from "./runtime-modules.bun.js"
const RUNTIME_MODULE_PREFIX = "opentui:runtime-module:"
const opentuiInstalledKey = Symbol.for("opentui.solid.runtime-plugin-support")
const solidTransformKey = Symbol.for("opentui.solid.transform")
const pluginRuntimeStateKey = Symbol.for("opencode.plugin.runtime-plugin-support")
type RuntimeState = {
readonly prepare: (entrypoint?: string) => void
}
type GlobalState = typeof globalThis & {
[opentuiInstalledKey]?: { readonly specifiers?: ReadonlySet<string> }
[solidTransformKey]?: { readonly installed?: boolean }
[pluginRuntimeStateKey]?: RuntimeState
}
const escapeRegExp = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
const runtimeModuleIdForSpecifier = (specifier: string) => `${RUNTIME_MODULE_PREFIX}${encodeURIComponent(specifier)}`
const sourcePath = (value: string) => {
const search = value.indexOf("?")
const hash = value.indexOf("#")
const end = [search, hash].filter((index) => index >= 0).sort((a, b) => a - b)[0]
return end === undefined ? value : value.slice(0, end)
}
const normalizedPaths = new Map<string, string>()
const normalizeSourcePath = (value: string) => {
const clean = sourcePath(value)
const cached = normalizedPaths.get(clean)
if (cached !== undefined) return cached
const normalized = (() => {
try {
return realpathSync(clean)
} catch {
return clean
}
})()
normalizedPaths.set(clean, normalized)
return normalized
}
const exactPathFilter = (paths: readonly string[]) => {
const candidates = [...new Set(paths.map(sourcePath))]
return new RegExp(`^(?:${candidates.map(escapeRegExp).join("|")})(?:[?#].*)?$`)
}
const isNodeModulesPath = (value: string) => /(?:^|[/\\])node_modules(?:[/\\])/.test(value)
const runtimeLoaderForPath = (value: string) => {
const clean = sourcePath(value)
if (clean.endsWith(".tsx")) return "tsx" as const
if (clean.endsWith(".jsx")) return "jsx" as const
if (clean.endsWith(".ts") || clean.endsWith(".mts") || clean.endsWith(".cts")) return "ts" as const
if (clean.endsWith(".js") || clean.endsWith(".mjs") || clean.endsWith(".cjs")) return "js" as const
return null
}
const resolveImportSpecifierPatterns = [
/(\bfrom\s*["'])([^"']+)(["'])/g,
/(\bimport\s*["'])([^"']+)(["'])/g,
/(\bimport\s*\(\s*["'])([^"']+)(["']\s*\))/g,
/(\brequire\s*\(\s*["'])([^"']+)(["']\s*\))/g,
]
const isImportLikeMatchInsideQuotes = (code: string, offset: number) => {
const previous = code[offset - 1]
return previous === '"' || previous === "'" || previous === "`"
}
const collectImportSpecifiers = (code: string) => {
const specifiers = new Set<string>()
for (const pattern of resolveImportSpecifierPatterns) {
code.replace(pattern, (full, _prefix, specifier: string, _suffix, offset: number) => {
if (!isImportLikeMatchInsideQuotes(code, offset)) specifiers.add(specifier)
return full
})
}
return [...specifiers]
}
const resolveSourcePathFromSpecifier = (specifier: string, importer?: string) => {
if (
specifier.startsWith("node:") ||
specifier.startsWith("bun:") ||
specifier.startsWith("http:") ||
specifier.startsWith("https:") ||
specifier.startsWith("data:") ||
specifier.startsWith(RUNTIME_MODULE_PREFIX)
)
return null
const candidate = specifier.startsWith("file:") ? sourcePath(fileURLToPath(specifier)) : sourcePath(specifier)
if (isAbsolute(candidate) && existsSync(candidate) && runtimeLoaderForPath(candidate)) return candidate
const baseDir = importer
? dirname(importer.startsWith("file:") ? fileURLToPath(importer) : sourcePath(importer))
: process.cwd()
try {
const resolved = Bun.resolveSync(candidate, baseDir)
if (resolved.startsWith("node:") || resolved.startsWith("bun:")) return null
return sourcePath(resolved)
} catch {
return null
}
}
export function ensurePluginRuntime(entrypoint?: string) {
const state = globalThis as GlobalState
if (state[opentuiInstalledKey]) return
if (!state[pluginRuntimeStateKey]) {
state[pluginRuntimeStateKey] = install(state)
}
state[pluginRuntimeStateKey].prepare(entrypoint)
}
function install(state: GlobalState): RuntimeState {
const modules: Readonly<Record<string, RuntimeModuleEntry>> = pluginRuntimeModules()
const runtimeModuleIds = new Map<string, string>()
for (const specifier of Object.keys(modules)) {
runtimeModuleIds.set(specifier, runtimeModuleIdForSpecifier(specifier))
}
const resolveRuntimeId = (specifier: string) => {
const known = runtimeModuleIds.get(specifier)
if (known) return known
if (
specifier === "@opentui/core" ||
specifier === "@opentui/core/testing" ||
state[opentuiInstalledKey]?.specifiers?.has(specifier)
)
return runtimeModuleIdForSpecifier(specifier)
return undefined
}
const isSolidTransformActiveFor = (file: string) =>
Boolean(state[solidTransformKey]?.installed) && /\.[cm]?[jt]sx$/.test(sourcePath(file))
const installedLoaders = new Set<string>()
const packageTypes = new Map<string, "module" | "commonjs">()
const nodeModulesAnalysis = new Map<
string,
{ readonly importSpecifiers: readonly string[]; readonly needsRewrite: boolean; readonly isEsm: boolean }
>()
const nodeModulesRewritePaths = new Map<string, readonly string[]>()
let activeBuild: PluginBuilder | undefined
const packageTypeForPath = (file: string) => {
let current = dirname(file)
while (true) {
const pkgJson = join(current, "package.json")
if (existsSync(pkgJson)) {
const cached = packageTypes.get(pkgJson)
if (cached) return cached
const type = (() => {
try {
return JSON.parse(readFileSync(pkgJson, "utf8")).type === "module"
? ("module" as const)
: ("commonjs" as const)
} catch {
return "commonjs" as const
}
})()
packageTypes.set(pkgJson, type)
return type
}
const parent = dirname(current)
if (parent === current) return "commonjs"
current = parent
}
}
const analyzeFile = (file: string, cacheable: boolean) => {
const normalized = normalizeSourcePath(file)
if (cacheable) {
const cached = nodeModulesAnalysis.get(normalized)
if (cached) return cached
}
const contents = (() => {
try {
return readFileSync(normalized, "utf8")
} catch {
return undefined
}
})()
if (contents === undefined) return { importSpecifiers: [], needsRewrite: false, isEsm: false }
const importSpecifiers = collectImportSpecifiers(contents)
const analysis = {
importSpecifiers,
needsRewrite: importSpecifiers.some((specifier) => resolveRuntimeId(specifier) !== undefined),
isEsm: /\b(?:import|export)\b/.test(contents),
}
if (cacheable) nodeModulesAnalysis.set(normalized, analysis)
return analysis
}
const isNodeModulesEsm = (file: string) => {
const normalized = normalizeSourcePath(file)
if (!isNodeModulesPath(normalized)) return false
if (/\.(?:mjs|mts|ts|tsx|jsx)$/.test(normalized)) return true
if (/\.(?:cjs|cts)$/.test(normalized) || !normalized.endsWith(".js")) return false
return packageTypeForPath(normalized) === "module" || analyzeFile(normalized, true).isEsm
}
const collectNodeModulesRewrites = (file: string, visiting = new Set<string>()): readonly string[] => {
const normalized = normalizeSourcePath(file)
if (!isNodeModulesEsm(normalized)) return []
const cached = nodeModulesRewritePaths.get(normalized)
if (cached) return cached
if (visiting.has(normalized)) return []
visiting.add(normalized)
const rewrites = new Set<string>()
const analysis = analyzeFile(normalized, true)
if (analysis.needsRewrite) rewrites.add(normalized)
for (const specifier of analysis.importSpecifiers) {
if (resolveRuntimeId(specifier) !== undefined) continue
const resolved = resolveSourcePathFromSpecifier(specifier, normalized)
if (!resolved || !isNodeModulesEsm(resolved)) continue
for (const nested of collectNodeModulesRewrites(resolved, visiting)) rewrites.add(nested)
}
visiting.delete(normalized)
const result = [...rewrites]
nodeModulesRewritePaths.set(normalized, result)
return result
}
const installRewriteLoader = (file: string) => {
if (!activeBuild) return
const resolvedTarget = sourcePath(file)
if (!isNodeModulesPath(resolvedTarget) && isSolidTransformActiveFor(resolvedTarget)) return
const canonicalTarget = normalizeSourcePath(resolvedTarget)
if (installedLoaders.has(canonicalTarget)) return
installedLoaders.add(canonicalTarget)
const build = activeBuild
build.onLoad({ filter: exactPathFilter([resolvedTarget, canonicalTarget]) }, (args) => {
const loaded = normalizeSourcePath(args.path)
if (loaded !== canonicalTarget) return undefined as never
const loader = runtimeLoaderForPath(args.path)
if (!loader) throw new Error(`Unable to determine runtime loader for path: ${args.path}`)
let code = readFileSync(loaded, "utf8")
for (const pattern of resolveImportSpecifierPatterns) {
code = code.replace(pattern, (full, prefix: string, specifier: string, suffix: string, offset: number) => {
if (isImportLikeMatchInsideQuotes(code, offset)) return full
const replacement = resolveRuntimeId(specifier)
return replacement ? `${prefix}${replacement}${suffix}` : full
})
}
return { contents: code, loader }
})
}
const prescanGraph = (entryFile: string, visiting = new Set<string>()) => {
const normalized = normalizeSourcePath(entryFile)
if (visiting.has(normalized)) return
visiting.add(normalized)
const analysis = analyzeFile(normalized, false)
if (analysis.needsRewrite) installRewriteLoader(entryFile)
for (const specifier of analysis.importSpecifiers) {
if (resolveRuntimeId(specifier) !== undefined) continue
const resolved = resolveSourcePathFromSpecifier(specifier, normalized)
if (!resolved || !runtimeLoaderForPath(resolved)) continue
if (isNodeModulesPath(resolved)) {
for (const rewrite of collectNodeModulesRewrites(resolved)) installRewriteLoader(rewrite)
continue
}
prescanGraph(resolved, visiting)
}
}
plugin({
name: "opencode-plugin-runtime-modules",
setup(build) {
activeBuild = build
if (!state[opentuiInstalledKey]) {
for (const [specifier, entry] of Object.entries(modules)) {
const moduleId = runtimeModuleIds.get(specifier)
if (!moduleId) continue
build.module(moduleId, async () => ({
exports: typeof entry === "function" ? await entry() : entry,
loader: "object",
}))
}
}
build.onResolve({ filter: /^(?:effect(?:\/|$)|@opencode\/plugin(?:\/|$))/ }, (args) => {
const direct = resolveRuntimeId(args.path)
return direct ? { path: direct } : undefined
})
},
})
return {
prepare(entrypoint?: string) {
if (!entrypoint) return
const resolved = resolveSourcePathFromSpecifier(entrypoint)
if (!resolved || !runtimeLoaderForPath(resolved)) return
prescanGraph(resolved)
},
}
}
+99
View File
@@ -0,0 +1,99 @@
import { createRequire, registerHooks } from "node:module"
import path from "node:path"
import { pathToFileURL } from "node:url"
const PREFIX = "opencode:runtime-module:"
const modulesKey = Symbol.for("opencode.plugin.runtime.modules")
type GlobalState = typeof globalThis & {
[modulesKey]?: Map<string, Record<string, unknown>>
}
let pending: Promise<void> | undefined
export function ensurePluginRuntime(_entrypoint?: string): Promise<void> {
if (pending) return pending
pending = setup()
return pending
}
async function setup() {
const state = globalThis as GlobalState
if (state[modulesKey]) return
const [effect, promiseIndex, promisePlugin, promiseTool, effectIndex, effectPlugin, effectTool, rpc] =
await Promise.all([
import("effect") as Promise<Record<string, unknown>>,
import("./promise/index.js") as Promise<Record<string, unknown>>,
import("./promise/plugin.js") as Promise<Record<string, unknown>>,
import("./promise/tool.js") as Promise<Record<string, unknown>>,
import("./effect/index.js") as Promise<Record<string, unknown>>,
import("./effect/plugin.js") as Promise<Record<string, unknown>>,
import("./effect/tool.js") as Promise<Record<string, unknown>>,
import("./rpc.js") as Promise<Record<string, unknown>>,
])
const modules = new Map<string, Record<string, unknown>>([
["effect", effect],
["@opencode/plugin", promiseIndex],
["@opencode/plugin/promise/plugin", promisePlugin],
["@opencode/plugin/promise/tool", promiseTool],
["@opencode/plugin/effect", effectIndex],
["@opencode/plugin/effect/plugin", effectPlugin],
["@opencode/plugin/effect/tool", effectTool],
["@opencode/plugin/rpc", rpc],
])
for (const [key, value] of Object.entries(effect)) {
if (typeof value === "object" && value !== null) {
modules.set(`effect/${key}`, value as Record<string, unknown>)
}
}
state[modulesKey] = modules
const loadEffectSubpath = (specifier: string) => {
const existing = modules.get(specifier)
if (existing) return existing
for (const base of [import.meta.url, pathToFileURL(path.join(process.cwd(), "package.json")).href]) {
try {
const loaded = createRequire(base)(specifier) as Record<string, unknown>
if (typeof loaded === "object" && loaded !== null) {
modules.set(specifier, loaded)
return loaded
}
} catch {}
}
return undefined
}
registerHooks({
resolve(specifier, context, nextResolve) {
if (modules.has(specifier) || (specifier.startsWith("effect/") && loadEffectSubpath(specifier) !== undefined)) {
return { url: `${PREFIX}${encodeURIComponent(specifier)}`, shortCircuit: true }
}
return nextResolve(specifier, context)
},
load(url, context, nextLoad) {
if (url.startsWith(PREFIX)) {
const specifier = decodeURIComponent(url.slice(PREFIX.length))
const exports = modules.get(specifier)
if (exports) {
return { format: "module", source: createModuleSource(specifier, exports), shortCircuit: true }
}
}
return nextLoad(url, context)
},
})
}
function createModuleSource(specifier: string, exports: Record<string, unknown>) {
const keys = Object.keys(exports).filter((key) => key !== "default" && /^[A-Za-z_$][\w$]*$/.test(key))
const lines = [
`const mod = globalThis[Symbol.for("opencode.plugin.runtime.modules")].get(${JSON.stringify(specifier)})`,
...keys.map((key, index) => `const e${index} = mod[${JSON.stringify(key)}]`),
...(keys.length > 0 ? [`export { ${keys.map((key, index) => `e${index} as ${key}`).join(", ")} }`] : []),
]
if ("default" in exports) lines.push("export default mod.default")
return lines.join("\n")
}
+1
View File
@@ -0,0 +1 @@
export function ensurePluginRuntime(_entrypoint?: string) {}
+2
View File
@@ -3,12 +3,14 @@ import { readFileSync } from "node:fs"
import path from "node:path"
import { fileURLToPath, pathToFileURL } from "node:url"
import { Host } from "./host.js"
import { ensurePluginRuntime } from "./runtime.bun.js"
import { localSource } from "./source.js"
import { missingPackageTarget } from "./source.package.js"
let generation = Date.now()
export async function prepareSource(entrypoint: string, track: (file: string, directory?: boolean) => void) {
ensurePluginRuntime(entrypoint)
const root = fileURLToPath(entrypoint)
const files = new Set<string>()
const visit = (file: string, search = "") => {
+2
View File
@@ -4,10 +4,12 @@ import { fileURLToPath } from "node:url"
import { localSource } from "./source.js"
import { missingPackageTarget } from "./source.package.js"
import { Host } from "./host.js"
import { ensurePluginRuntime } from "./runtime.node.js"
let generation = Date.now()
export async function prepareSource(entrypoint: string, track: (file: string, directory?: boolean) => void) {
await ensurePluginRuntime()
const version = String(++generation)
const fresh = (specifier: string) => {
const url = new URL(specifier)
+172 -2
View File
@@ -1,9 +1,14 @@
import assert from "node:assert/strict"
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"
import { cp, mkdir, mkdtemp, realpath, rm, writeFile } from "node:fs/promises"
import { tmpdir } from "node:os"
import path from "node:path"
import { describe, it } from "node:test"
import { pathToFileURL } from "node:url"
import { Effect, Option, Schema } from "effect"
import { Brand } from "effect"
import { HttpClient } from "effect/unstable/http"
import { Plugin, Tool } from "../src/effect/index.js"
import { Rpc } from "../src/rpc.js"
import { Host } from "../src/host.js"
// Every entrypoint throws if evaluated: resolution must never execute plugins.
@@ -11,7 +16,7 @@ const source = 'throw new Error("Plugin code must not run during resolution")'
const name = "@fixture/plugin"
async function fixture(files: Record<string, string>, installed = false) {
const root = await mkdtemp(path.join(tmpdir(), "opencode-host-"))
const root = await realpath(await mkdtemp(path.join(tmpdir(), "opencode-host-")))
const directory = installed ? path.join(root, "node_modules", name) : root
await Promise.all(
Object.entries(files).map(async ([file, content]) => {
@@ -154,3 +159,168 @@ describe("Host.resolve", () => {
assert.deepEqual(Host.resolve(plugin.target), { server: undefined, tui: undefined, rpc: undefined })
})
})
describe("Host.load", () => {
it("resolves effect, effect/* subpaths, and @opencode/plugin/* to the host across plugin node_modules", async () => {
const root = await realpath(await mkdtemp(path.join(tmpdir(), "opencode-host-load-")))
try {
const hostEffectDir = path.dirname(Bun.resolveSync("effect/package.json", import.meta.dir))
const pluginEffectDir = path.join(root, "node_modules/effect")
await mkdir(path.join(root, "node_modules"), { recursive: true })
await cp(hostEffectDir, pluginEffectDir, { recursive: true })
const manifest = { ...(await Bun.file(path.join(pluginEffectDir, "package.json")).json()), version: "4.0.0-rc.111" }
await writeFile(path.join(pluginEffectDir, "package.json"), JSON.stringify(manifest, null, 2))
const depDir = path.join(root, "node_modules/transitive-dep")
await mkdir(depDir, { recursive: true })
await writeFile(
path.join(depDir, "package.json"),
JSON.stringify({ name: "transitive-dep", type: "module", exports: { ".": "./index.js" } }),
)
await writeFile(
path.join(depDir, "index.js"),
`import { Effect as DepEffect, Schema as DepSchema } from "effect"
import { some as depSome } from "effect/Option"
export { DepEffect, DepSchema, depSome }`,
)
const foreignPluginPkgDir = path.join(root, "node_modules/@opencode/plugin")
await mkdir(foreignPluginPkgDir, { recursive: true })
await writeFile(
path.join(foreignPluginPkgDir, "package.json"),
JSON.stringify({
name: "@opencode/plugin",
type: "module",
exports: { "./effect": "./effect.js", "./rpc": "./rpc.js" },
}),
)
await writeFile(
path.join(foreignPluginPkgDir, "effect.js"),
"export const Plugin = { define: (p) => p }; export const Tool = {}",
)
await writeFile(path.join(foreignPluginPkgDir, "rpc.js"), "export const Rpc = { define: (d) => d }")
await writeFile(
path.join(root, "helper.ts"),
`import { nominal } from "effect/Brand"
import { HttpClient as HelperHttpClient } from "effect/unstable/http"
import { DepEffect, DepSchema, depSome } from "transitive-dep"
export { nominal, HelperHttpClient, DepEffect, DepSchema, depSome }`,
)
const entry = path.join(root, "index.ts")
await writeFile(
entry,
`import { Effect as PluginEffect, Schema as PluginSchema } from "effect"
import { some as pluginSome } from "effect/Option"
import { Plugin as PluginDef, Tool as PluginTool } from "@opencode/plugin/effect"
import { Rpc as PluginRpc } from "@opencode/plugin/rpc"
import { nominal, HelperHttpClient, DepEffect, DepSchema, depSome } from "./helper"
export { PluginEffect, PluginSchema, pluginSome, PluginDef, PluginTool, PluginRpc, nominal, HelperHttpClient, DepEffect, DepSchema, depSome }`,
)
const loaded = (await Host.load(pathToFileURL(entry).href)) as Record<string, unknown>
assert.equal(loaded.PluginEffect, Effect)
assert.equal(loaded.PluginSchema, Schema)
assert.equal(loaded.pluginSome, Option.some)
assert.equal(loaded.nominal, Brand.nominal)
assert.equal(loaded.HelperHttpClient, HttpClient)
assert.equal(loaded.PluginDef, Plugin)
assert.equal(loaded.PluginTool, Tool)
assert.equal(loaded.PluginRpc, Rpc)
assert.equal(loaded.DepEffect, Effect)
assert.equal(loaded.DepSchema, Schema)
assert.equal(loaded.depSome, Option.some)
} finally {
await rm(root, { recursive: true, force: true })
}
})
it("resolves effect, effect/* subpaths, and @opencode/plugin/effect to the host under Node", async () => {
const root = await realpath(await mkdtemp(path.join(tmpdir(), "opencode-host-node-")))
try {
const pluginDir = path.join(root, "plugin")
await mkdir(path.join(pluginDir, "node_modules/effect"), { recursive: true })
await writeFile(
path.join(pluginDir, "node_modules/effect/package.json"),
JSON.stringify({
name: "effect",
version: "4.0.0-rc.111",
type: "module",
exports: { ".": "./index.js", "./Option": "./Option.js" },
}),
)
await writeFile(path.join(pluginDir, "node_modules/effect/index.js"), "export const Effect = { foreign: true }")
await writeFile(path.join(pluginDir, "node_modules/effect/Option.js"), "export const some = () => null")
await mkdir(path.join(pluginDir, "node_modules/transitive-dep"), { recursive: true })
await writeFile(
path.join(pluginDir, "node_modules/transitive-dep/package.json"),
JSON.stringify({ name: "transitive-dep", type: "module", exports: { ".": "./index.js" } }),
)
await writeFile(
path.join(pluginDir, "node_modules/transitive-dep/index.js"),
'import { Effect as DepEffect } from "effect"; import { some as depSome } from "effect/Option"; export { DepEffect, depSome }',
)
await mkdir(path.join(pluginDir, "node_modules/@opencode/plugin"), { recursive: true })
await writeFile(
path.join(pluginDir, "node_modules/@opencode/plugin/package.json"),
JSON.stringify({ name: "@opencode/plugin", type: "module", exports: { "./effect": "./effect.js" } }),
)
await writeFile(
path.join(pluginDir, "node_modules/@opencode/plugin/effect.js"),
"export const Plugin = { define: (p) => p }",
)
const entry = path.join(pluginDir, "index.mjs")
await writeFile(
entry,
`import { Effect as PluginEffect, Schema as PluginSchema } from "effect"
import { some as pluginSome } from "effect/Option"
import { Plugin as PluginDef } from "@opencode/plugin/effect"
import { DepEffect, depSome } from "transitive-dep"
export { PluginEffect, PluginSchema, pluginSome, PluginDef, DepEffect, depSome }`,
)
const script = path.join(root, "probe.ts")
await writeFile(
script,
`import assert from "node:assert/strict"
import { Effect, Option, Schema } from ${JSON.stringify(Bun.resolveSync("effect", import.meta.dir))}
import { Plugin } from ${JSON.stringify(path.join(import.meta.dir, "../src/effect/index.ts"))}
import { Host } from ${JSON.stringify(path.join(import.meta.dir, "../src/host.ts"))}
const loaded = await Host.load(${JSON.stringify(pathToFileURL(entry).href)}) as Record<string, unknown>
assert.equal(loaded.PluginEffect, Effect)
assert.equal(loaded.PluginSchema, Schema)
assert.equal(loaded.pluginSome, Option.some)
assert.equal(loaded.PluginDef, Plugin)
assert.equal(loaded.DepEffect, Effect)
assert.equal(loaded.depSome, Option.some)
console.log("node host load passed")`,
)
const build = await Bun.build({
entrypoints: [script],
target: "node",
format: "esm",
outdir: root,
naming: "probe.mjs",
})
assert.equal(build.success, true)
const child = Bun.spawn(["node", "--no-warnings", path.join(root, "probe.mjs")], {
cwd: path.join(import.meta.dir, ".."),
stdout: "pipe",
stderr: "pipe",
})
const [stdout, stderr, exit] = await Promise.all([
new Response(child.stdout).text(),
new Response(child.stderr).text(),
child.exited,
])
assert.deepEqual({ stdout, stderr, exit }, { stdout: "node host load passed\n", stderr: "", exit: 0 })
} finally {
await rm(root, { recursive: true, force: true })
}
})
})
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/protocol",
"version": "2.0.23",
"version": "2.0.22",
"type": "module",
"license": "MIT",
"repository": {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/schema",
"version": "2.0.23",
"version": "2.0.22",
"type": "module",
"license": "MIT",
"repository": {
Loaded 100 of 118 files, more files were not shown because too many files have changed in this diff. Show more