mirror of
https://github.com/anomalyco/opencode.git
synced 2026-10-05 06:56:16 +00:00
Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
494af523d1 | ||
|
|
cb389aa679 | ||
|
|
30f4119459 | ||
|
|
4d17cf719f | ||
|
|
498b1deda3 | ||
|
|
25c54cbdf7 | ||
|
|
ae57dd0cf5 |
No files matched your search
@@ -383,7 +383,6 @@
|
||||
"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:",
|
||||
@@ -3327,8 +3326,6 @@
|
||||
|
||||
"@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=="],
|
||||
@@ -3369,8 +3366,6 @@
|
||||
|
||||
"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=="],
|
||||
@@ -5723,8 +5718,6 @@
|
||||
|
||||
"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=="],
|
||||
@@ -6393,12 +6386,6 @@
|
||||
|
||||
"@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=="],
|
||||
@@ -6703,12 +6690,6 @@
|
||||
|
||||
"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=="],
|
||||
@@ -7257,8 +7238,6 @@
|
||||
|
||||
"@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=="],
|
||||
@@ -7557,10 +7536,6 @@
|
||||
|
||||
"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=="],
|
||||
@@ -8439,8 +8414,6 @@
|
||||
|
||||
"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
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"nodeModules": {
|
||||
"x86_64-linux": "sha256-2RlbJRTEKSliuUbAE2lAktX63JFR/RKAuLkcCou8wb4=",
|
||||
"aarch64-linux": "sha256-P7DAE018lTJNGmttg87U9oaTAuw/wHlnMStdnXYKCr0=",
|
||||
"aarch64-darwin": "sha256-N+NfV1ObOTnW+ez7As+CS+6ci/iRGCQ6cslvyQFCp6E="
|
||||
"x86_64-linux": "sha256-4AqU8dPEwo0ukoyEX1SuDVzsXQ7G/fIPnxoL400zH4M=",
|
||||
"aarch64-linux": "sha256-p75sJYtR8a4oVKb1+Dgp8Kl+1onSmUw5NRv95bbV1+E=",
|
||||
"aarch64-darwin": "sha256-Psyvbw/lGulysQlhK/MqcHVIwx2aCDTr+Pfr+gRWsG4="
|
||||
}
|
||||
}
|
||||
@@ -106,6 +106,41 @@ 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
|
||||
|
||||
@@ -584,10 +584,7 @@ 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}`)
|
||||
@@ -657,10 +654,7 @@ 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)
|
||||
@@ -804,9 +798,6 @@ 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))
|
||||
@@ -815,25 +806,9 @@ const supportsNativeSystemUpdates = (request: LLMRequest) => {
|
||||
return version.major >= 5
|
||||
}
|
||||
|
||||
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")
|
||||
)
|
||||
}
|
||||
// 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 splitsLocalToolResults = (messages: LLMRequest["messages"], index: number) => {
|
||||
const pending = new Set<string>()
|
||||
@@ -862,12 +837,34 @@ const lowerNativeSystemUpdate = Effect.fnUntraced(function* (
|
||||
}
|
||||
})
|
||||
|
||||
const lowerMessages = Effect.fnUntraced(function* (
|
||||
request: LLMRequest,
|
||||
const lowerWrappedSystemUpdate = Effect.fnUntraced(function* (
|
||||
message: LLMRequest["messages"][number],
|
||||
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") {
|
||||
@@ -879,16 +876,8 @@ const lowerMessages = Effect.fnUntraced(function* (
|
||||
}
|
||||
if (splitsLocalToolResults(request.messages, index))
|
||||
return yield* invalid("Anthropic Messages system updates cannot split a local tool call from its tool result")
|
||||
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] })
|
||||
if (holdUpdates) held.push(message)
|
||||
else appendToUserTurn(messages, yield* lowerWrappedSystemUpdate(message, breakpoints))
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -964,7 +953,9 @@ const lowerMessages = Effect.fnUntraced(function* (
|
||||
`Anthropic Messages assistant messages only support text, reasoning, and tool-call content for now`,
|
||||
)
|
||||
}
|
||||
if (content.length > 0) messages.push({ role: "assistant", content })
|
||||
if (content.length === 0) continue
|
||||
yield* releaseHeld()
|
||||
messages.push({ role: "assistant", content })
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -985,6 +976,7 @@ const lowerMessages = Effect.fnUntraced(function* (
|
||||
messages[messages.length - 1] = { role: "user", content: [...previous.content, ...content] }
|
||||
else messages.push({ role: "user", content })
|
||||
}
|
||||
yield* releaseHeld()
|
||||
|
||||
return messages
|
||||
})
|
||||
|
||||
@@ -65,7 +65,7 @@ const ExtraContent = Schema.Struct({
|
||||
})
|
||||
const decodeExtraContent = (value: unknown) => Option.getOrUndefined(Schema.decodeUnknownOption(ExtraContent)(value))
|
||||
|
||||
const OpenAIChatAssistantToolCall = Schema.Struct({
|
||||
export 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>
|
||||
|
||||
const OpenAIChatMessage = Schema.Union([
|
||||
export 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.
|
||||
const OpenAIChatUsage = Schema.StructWithRest(
|
||||
export 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),
|
||||
})
|
||||
|
||||
const OpenAIChatToolCallDelta = Schema.Struct({
|
||||
export const OpenAIChatToolCallDelta = Schema.Struct({
|
||||
index: optionalNull(Schema.Number),
|
||||
id: optionalNull(Schema.String),
|
||||
function: optionalNull(OpenAIChatToolCallDeltaFunction),
|
||||
@@ -246,7 +246,7 @@ const OpenAIChatToolCallDelta = Schema.Struct({
|
||||
})
|
||||
type OpenAIChatToolCallDelta = Schema.Schema.Type<typeof OpenAIChatToolCallDelta>
|
||||
|
||||
const OpenAIChatDelta = Schema.StructWithRest(
|
||||
export const OpenAIChatDelta = Schema.StructWithRest(
|
||||
Schema.Struct({
|
||||
content: optionalNull(Schema.String),
|
||||
refusal: optionalNull(Schema.String),
|
||||
@@ -259,7 +259,7 @@ const OpenAIChatDelta = Schema.StructWithRest(
|
||||
[Schema.Record(Schema.String, Schema.Unknown)],
|
||||
)
|
||||
|
||||
const OpenAIChatChoice = Schema.StructWithRest(
|
||||
export const OpenAIChatChoice = Schema.StructWithRest(
|
||||
Schema.Struct({
|
||||
delta: optionalNull(OpenAIChatDelta),
|
||||
finish_reason: optionalNull(Schema.String),
|
||||
@@ -326,6 +326,8 @@ 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 => ({
|
||||
@@ -539,8 +541,10 @@ const lowerMessage = Effect.fnUntraced(function* (
|
||||
options: LoweringOptions & { readonly providerMetadataKey: string },
|
||||
) {
|
||||
if (message.role === "user") return [yield* lowerUserMessage(message, options)]
|
||||
if (message.role === "assistant")
|
||||
return [yield* lowerAssistantMessage(message, reasoningField, requireReasoning, options)]
|
||||
if (message.role === "assistant") {
|
||||
const lowered = yield* lowerAssistantMessage(message, reasoningField, requireReasoning, options)
|
||||
return [options.assistant?.(message, lowered) ?? lowered]
|
||||
}
|
||||
return (yield* lowerToolMessages(message, options)).messages
|
||||
})
|
||||
|
||||
@@ -1214,7 +1218,7 @@ const step = (state: ParserState, event: OpenAIChatEvent) =>
|
||||
] as const
|
||||
})
|
||||
|
||||
const finishEvents = Effect.fnUntraced(function* (state: ParserState) {
|
||||
export const finishEvents = Effect.fnUntraced(function* (state: ParserState) {
|
||||
if (state.finishReason === undefined && state.requireFinishReason)
|
||||
return yield* new AIError({
|
||||
reason: new InvalidProviderOutputError({
|
||||
|
||||
@@ -0,0 +1,374 @@
|
||||
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"
|
||||
@@ -40,6 +40,7 @@ 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"
|
||||
@@ -0,0 +1,62 @@
|
||||
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"
|
||||
@@ -171,6 +171,33 @@ 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(
|
||||
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"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?\"}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
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,42 +431,115 @@ describe("Anthropic Messages route", () => {
|
||||
(yield* compileRequest(
|
||||
LLM.request({
|
||||
model: opus48,
|
||||
messages: [Message.user("Before."), Message.system("One."), Message.system("Two.")],
|
||||
messages: [
|
||||
Message.user("Start."),
|
||||
Message.assistant("One."),
|
||||
Message.system("Update."),
|
||||
Message.assistant("Two."),
|
||||
],
|
||||
cache: "none",
|
||||
}),
|
||||
)).body.messages,
|
||||
).toEqual([
|
||||
{
|
||||
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>" },
|
||||
],
|
||||
},
|
||||
{ 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." }] },
|
||||
])
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("keeps a terminal Vertex system update in the tool-result turn", () =>
|
||||
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", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
LLM.request({
|
||||
model: vertexOpus48,
|
||||
model,
|
||||
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.user("Fix it."),
|
||||
Message.assistant("Done."),
|
||||
Message.system("Update."),
|
||||
Message.user("Next."),
|
||||
],
|
||||
cache: "none",
|
||||
}),
|
||||
)
|
||||
|
||||
expect(prepared.body.messages).toEqual([
|
||||
{
|
||||
role: "assistant",
|
||||
content: [{ type: "tool_use", id: "call_1", name: "lookup", input: {} }],
|
||||
},
|
||||
{ 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: "user",
|
||||
content: [
|
||||
@@ -477,55 +550,23 @@ 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 }] },
|
||||
]
|
||||
|
||||
it.effect("preserves folded tool-result system updates across multi-turn Vertex history", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
const terminal = yield* compileRequest(LLM.request({ model: vertexOpus48, messages: toolTurn, cache: "none" }))
|
||||
const history = yield* compileRequest(
|
||||
LLM.request({
|
||||
model: vertexOpus48,
|
||||
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."),
|
||||
],
|
||||
messages: [...toolTurn, Message.assistant("Acknowledged."), Message.user("Next step.")],
|
||||
cache: "none",
|
||||
}),
|
||||
)
|
||||
|
||||
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,
|
||||
},
|
||||
],
|
||||
},
|
||||
expect(terminal.body.messages).toEqual(lowered)
|
||||
expect(history.body.messages).toEqual([
|
||||
...lowered,
|
||||
{ role: "assistant", content: [{ type: "text", text: "Acknowledged." }] },
|
||||
{ role: "user", content: [{ type: "text", text: "Next step." }] },
|
||||
])
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
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)
|
||||
}
|
||||
}),
|
||||
)
|
||||
@@ -0,0 +1,370 @@
|
||||
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)
|
||||
}
|
||||
}),
|
||||
)
|
||||
@@ -5,6 +5,7 @@ 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"
|
||||
@@ -35,6 +36,7 @@ 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.
|
||||
@@ -105,7 +107,7 @@ export function SessionRunningMenu(props: {
|
||||
|
||||
return (
|
||||
<Show when={items().length > 0}>
|
||||
<Menu gutter={6} placement="bottom-start">
|
||||
<Menu gutter={6} placement="bottom-start" open={menu.open} onOpenChange={(open) => setMenu("open", open)}>
|
||||
<Menu.Trigger
|
||||
as="button"
|
||||
type="button"
|
||||
@@ -123,7 +125,12 @@ export function SessionRunningMenu(props: {
|
||||
class="group/running-item"
|
||||
classList={{ "!bg-v2-overlay-simple-overlay-hover": viewing(item) }}
|
||||
aria-current={viewing(item) ? "page" : undefined}
|
||||
onSelect={() => open(item)}
|
||||
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)
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if ((event.key !== "Delete" && event.key !== "Backspace") || !stoppable(item)) return
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
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"
|
||||
@@ -79,28 +78,6 @@ 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({
|
||||
@@ -147,14 +124,7 @@ 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,
|
||||
pluginRuntimeModulesPlugin,
|
||||
simulationGraphPlugin,
|
||||
],
|
||||
plugins: [appAssetsPlugin, solidPlugin, parcelWatcherPlugin, opencodePtyPlugin, simulationGraphPlugin],
|
||||
external: ["node-gyp"],
|
||||
format: "esm",
|
||||
minify: true,
|
||||
|
||||
@@ -157,10 +157,8 @@ 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, defaultEnsureTiming)
|
||||
).pipe(Effect.catch((cause) => Effect.logWarning("Failed to prepare persistent terminals for replacement", cause)))
|
||||
if (info !== undefined) yield* terminate(info, options, ensureTiming(options))
|
||||
})
|
||||
|
||||
function fallback() {
|
||||
@@ -298,9 +296,9 @@ const terminate = Effect.fnUntraced(function* (info: Info, options: { readonly f
|
||||
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)))
|
||||
}
|
||||
|
||||
@@ -129,7 +129,7 @@ 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, defaultEnsureTiming)
|
||||
if (info !== undefined) await terminate(info, options, ensureTiming(options))
|
||||
}
|
||||
|
||||
function fallback() {
|
||||
@@ -258,9 +258,9 @@ async function terminate(info: Info, options: { readonly file?: string }, timing
|
||||
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`)
|
||||
}
|
||||
|
||||
@@ -25,6 +25,16 @@ export async function serviceFixture() {
|
||||
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)
|
||||
@@ -54,3 +64,8 @@ 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)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { appendFile, rename, writeFile } from "node:fs/promises"
|
||||
import { appendFile, rename, rm, 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")
|
||||
@@ -91,6 +91,10 @@ const server = Bun.serve({
|
||||
},
|
||||
})
|
||||
|
||||
// Install handlers before publishing: a test may signal as soon as the registration appears.
|
||||
process.on("SIGTERM", () => void shutdown("SIGTERM"))
|
||||
process.on("SIGINT", () => void shutdown("SIGINT"))
|
||||
|
||||
await writeFile(
|
||||
registration + ".tmp",
|
||||
JSON.stringify({
|
||||
@@ -106,8 +110,12 @@ 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,9 +1,10 @@
|
||||
import { expect, test } from "bun:test"
|
||||
import { Service, type EnsureReason } from "../src/promise/service"
|
||||
import { serviceFixture } from "./fixture/service-fixture"
|
||||
import { expectPortAvailable, 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()
|
||||
@@ -171,3 +172,66 @@ 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)
|
||||
@@ -3,10 +3,11 @@ 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 { serviceFixture } from "./fixture/service-fixture"
|
||||
import { expectPortAvailable, 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()
|
||||
@@ -161,6 +162,67 @@ 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
|
||||
|
||||
@@ -134,7 +134,6 @@
|
||||
"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:"
|
||||
|
||||
@@ -70,6 +70,7 @@ 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) => ({
|
||||
|
||||
@@ -61,17 +61,9 @@ const layer = Layer.effect(
|
||||
release: holdUnsafe(),
|
||||
})
|
||||
})
|
||||
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(
|
||||
const exit = yield* Effect.suspend(() =>
|
||||
plugin.effect({ ...host, storage: PluginHost.storage(kv, plugin.id) }),
|
||||
).pipe(
|
||||
grouped,
|
||||
inherit,
|
||||
Effect.updateContext((context: Context.Context<never>) =>
|
||||
|
||||
@@ -7,19 +7,13 @@ import { Permission } from "../permission.js"
|
||||
|
||||
const PROMPT_EXPLORE = `You are a file search specialist. You excel at thoroughly navigating and exploring codebases.
|
||||
|
||||
Your strengths:
|
||||
- Rapidly finding files using glob patterns
|
||||
- Searching code and text with powerful regex patterns
|
||||
- Reading and analyzing file contents
|
||||
|
||||
Guidelines:
|
||||
- Use Glob for broad file pattern matching
|
||||
- Use Grep for searching file contents with regex
|
||||
- Use Read when you know the specific file path you need to read
|
||||
- Your role is EXCLUSIVELY to search and analyze
|
||||
- Parallelize independent tool calls for searches and reads whenever possible
|
||||
- Adapt your search approach based on the thoroughness level specified by the caller
|
||||
- Return file paths as absolute paths in your final response
|
||||
- For clear communication, avoid using emojis
|
||||
- Do not create any files, or run bash commands that modify the user's system state in any way
|
||||
- You MUST NOT create, modify, delete, move, or copy files, including temporary files and reports
|
||||
- Shell commands MUST be read-only. NEVER run commands that write files or change system state
|
||||
|
||||
Complete the user's search request efficiently and report your findings clearly.`
|
||||
|
||||
@@ -115,6 +109,7 @@ export const Plugin = define({
|
||||
...Permission.merge(
|
||||
[
|
||||
{ action: "*", resource: "*", effect: "deny" },
|
||||
{ action: "shell", resource: "*", effect: "allow" },
|
||||
{ action: "grep", resource: "*", effect: "allow" },
|
||||
{ action: "glob", resource: "*", effect: "allow" },
|
||||
{ action: "webfetch", resource: "*", effect: "allow" },
|
||||
|
||||
@@ -128,17 +128,6 @@ const load = Effect.fn("PluginModule.load")(function* (
|
||||
target: operation.target,
|
||||
...(installed?.version ? { version: installed.version } : {}),
|
||||
},
|
||||
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
|
||||
}),
|
||||
effect: (host) => plugin.effect({ ...host, options: operation.options }),
|
||||
} satisfies Generation
|
||||
})
|
||||
@@ -26,7 +26,6 @@ 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"
|
||||
@@ -60,7 +59,6 @@ export const ProviderPlugins: PluginInternal.InternalPlugin[] = [
|
||||
PromptCacheKeyPlugin,
|
||||
SapAICorePlugin,
|
||||
VercelPlugin,
|
||||
VenicePlugin,
|
||||
VLLMPlugin,
|
||||
XAIPlugin,
|
||||
ZenmuxPlugin,
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
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)
|
||||
},
|
||||
})
|
||||
@@ -97,6 +97,7 @@ 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")],
|
||||
|
||||
@@ -19,9 +19,11 @@ 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"
|
||||
@@ -30,6 +32,7 @@ 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"
|
||||
@@ -181,8 +184,11 @@ 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
|
||||
|
||||
@@ -202,24 +208,33 @@ 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" }
|
||||
const ceiling = calculateCeiling(context.model.limit, settings.buffer)
|
||||
if (trigger.reason === "auto" && !due(context, ceiling)) 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)
|
||||
// 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({ ...context.model.limit, context: UNKNOWN_WINDOW }, settings.buffer)
|
||||
: calculateCeiling({ ...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 compaction =
|
||||
context.model.compaction?.type === "native"
|
||||
? compactNatively(trigger, budget, settings.keep)
|
||||
: summarize(trigger, budget, settings.keep)
|
||||
const selected = model === context.model ? trigger : { ...trigger, context: { ...context, model } }
|
||||
const compaction = native
|
||||
? compactNatively(selected, budget, settings.keep)
|
||||
: summarize(selected, budget, settings.keep)
|
||||
return yield* compaction.pipe(
|
||||
Effect.matchEffect({
|
||||
onSuccess: (result) => publish(trigger, result),
|
||||
onFailure: (failure) => publish(trigger, failure),
|
||||
onSuccess: (result) => publish(selected, result),
|
||||
onFailure: (failure) => publish(selected, failure),
|
||||
}),
|
||||
)
|
||||
})
|
||||
@@ -662,7 +677,7 @@ export const layer = Layer.effect(
|
||||
export const node = makeLocationNode({
|
||||
service: Service,
|
||||
layer,
|
||||
deps: [Bus.node, Database.node, llmClient, SessionModelRequest.node],
|
||||
deps: [Agent.node, Bus.node, Database.node, llmClient, Model.node, SessionModelRequest.node, SessionRunnerModel.node],
|
||||
})
|
||||
|
||||
/** History loads from the latest completed compaction, so a previous one is always the first message. */
|
||||
|
||||
@@ -220,6 +220,14 @@ 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")
|
||||
@@ -583,6 +591,7 @@ 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,
|
||||
@@ -602,7 +611,7 @@ const PROTOCOLS: Readonly<Record<string, Protocol>> = {
|
||||
"@opencode/ai/providers/cohere": cohere,
|
||||
"@opencode/ai/providers/openrouter": openrouter,
|
||||
|
||||
[Provider.aisdk("venice-ai-sdk-provider")]: openaiChat,
|
||||
[Provider.aisdk("venice-ai-sdk-provider")]: veniceChat,
|
||||
"@opencode/ai/providers/cloudflare-ai-gateway": cloudflareAIGateway,
|
||||
[Provider.aisdk("@ai-sdk/gateway")]: vercelGateway,
|
||||
[Provider.aisdk("@jerome-benoit/sap-ai-provider-v2")]: sapAICore,
|
||||
|
||||
@@ -203,6 +203,8 @@ describe("Agent", () => {
|
||||
expect(Permission.evaluate("external_directory", path.join(global.config, "*"), permissions).effect).toBe("allow")
|
||||
expect(Permission.evaluate("external_directory", path.join(global.tmp, "*"), permissions).effect).toBe("allow")
|
||||
const explore = yield* agent.get(Agent.ID.make("explore"))
|
||||
expect(Permission.evaluate("shell", "git log -5", explore?.permissions ?? []).effect).toBe("allow")
|
||||
expect(Permission.evaluate("edit", "src/index.ts", explore?.permissions ?? []).effect).toBe("deny")
|
||||
expect(Permission.evaluate("read", ".env", explore?.permissions ?? []).effect).toBe("ask")
|
||||
expect(Permission.evaluate("read", ".env.local", explore?.permissions ?? []).effect).toBe("ask")
|
||||
expect(Permission.evaluate("read", ".env.example", explore?.permissions ?? []).effect).toBe("allow")
|
||||
|
||||
@@ -46,6 +46,7 @@ const it = testEffect(
|
||||
}),
|
||||
),
|
||||
Config.node.replace(config),
|
||||
Location.node.replace(Location.boundNode({ directory: AbsolutePath.make("/tmp") })),
|
||||
]),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -1,27 +1,17 @@
|
||||
import { expect } from "bun:test"
|
||||
import { cp, mkdir, readFile, writeFile } from "node:fs/promises"
|
||||
import path from "node:path"
|
||||
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 { Deferred, Effect, Exit, Fiber, Layer, Schedule, Scope, Stream } from "effect"
|
||||
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.mergeAll(
|
||||
PluginTestLayer,
|
||||
Layer.merge(
|
||||
AppNodeBuilder.build(Npm.node, [Global.node.replace(tempGlobalLayer)]),
|
||||
Watcher.layer().pipe(Layer.provide(Watcher.nativeLayer)),
|
||||
),
|
||||
@@ -100,252 +90,3 @@ 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,7 +7,6 @@ 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"
|
||||
@@ -18,7 +17,6 @@ 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,6 +18,7 @@ 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"
|
||||
@@ -87,7 +88,11 @@ const it = testEffect(
|
||||
SessionModelRequest.node,
|
||||
PluginHooks.node,
|
||||
]),
|
||||
[Bus.node.replace(Bus.configured({ persist: true })), llmClient.replace(client)],
|
||||
[
|
||||
Bus.node.replace(Bus.configured({ persist: true })),
|
||||
llmClient.replace(client),
|
||||
Location.node.replace(Location.boundNode({ directory: AbsolutePath.make("/project") })),
|
||||
],
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ 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"
|
||||
@@ -40,7 +41,10 @@ const it = testEffect(
|
||||
PluginHooks.node,
|
||||
llmClient,
|
||||
]),
|
||||
[Bus.node.replace(Bus.configured({ persist: true }))],
|
||||
[
|
||||
Bus.node.replace(Bus.configured({ persist: true })),
|
||||
Location.node.replace(Location.boundNode({ directory: AbsolutePath.make("/project") })),
|
||||
],
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
# `@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.
|
||||
@@ -17,12 +17,6 @@
|
||||
"./*": "./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",
|
||||
|
||||
@@ -140,8 +140,6 @@ 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:
|
||||
|
||||
@@ -29,14 +29,6 @@ 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
|
||||
|
||||
@@ -14,7 +14,6 @@ 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"
|
||||
@@ -1,6 +1,3 @@
|
||||
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"
|
||||
|
||||
@@ -2,7 +2,6 @@ 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
|
||||
@@ -44,7 +43,6 @@ export function resolve(target: Target): Entrypoints {
|
||||
return { server: entry(["server", ""]), tui: entry(["tui"]), rpc: entry(["rpc"]) }
|
||||
}
|
||||
|
||||
export async function load(entrypoint: string): Promise<unknown> {
|
||||
await ensurePluginRuntime(entrypoint)
|
||||
export function load(entrypoint: string): Promise<unknown> {
|
||||
return importModule(entrypoint)
|
||||
}
|
||||
@@ -15,7 +15,6 @@ 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"
|
||||
@@ -1,46 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,306 +0,0 @@
|
||||
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)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
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 +0,0 @@
|
||||
export function ensurePluginRuntime(_entrypoint?: string) {}
|
||||
@@ -3,14 +3,12 @@ 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 = "") => {
|
||||
|
||||
@@ -4,12 +4,10 @@ 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)
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
import assert from "node:assert/strict"
|
||||
import { cp, mkdir, mkdtemp, realpath, rm, writeFile } from "node:fs/promises"
|
||||
import { mkdir, mkdtemp, 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.
|
||||
@@ -16,7 +11,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 realpath(await mkdtemp(path.join(tmpdir(), "opencode-host-")))
|
||||
const root = 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]) => {
|
||||
@@ -159,168 +154,3 @@ 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,11 +1,8 @@
|
||||
import { ensurePluginRuntime, pluginRuntimeModules } from "@opencode/plugin/runtime.bun"
|
||||
import { Plugin, PluginContextProvider, usePlugin } from "@opencode/plugin/tui"
|
||||
import { ensureRuntimePluginSupport } from "@opentui/solid/runtime-plugin-support/configure"
|
||||
|
||||
ensureRuntimePluginSupport({
|
||||
additional: {
|
||||
...pluginRuntimeModules(),
|
||||
"@opencode/plugin/tui": { Plugin, PluginContextProvider, usePlugin },
|
||||
},
|
||||
})
|
||||
ensurePluginRuntime()
|
||||
@@ -7,7 +7,6 @@ import "../src/plugin/runtime-plugin-support.bun"
|
||||
import { createPluginSources } from "../src/plugin/source"
|
||||
import { createSourceWatcher } from "../src/plugin/watch"
|
||||
import { createSignal } from "solid-js"
|
||||
import { Effect, Option, Schema } from "effect"
|
||||
import { Plugin } from "@opencode/plugin/tui"
|
||||
import { tmpdir } from "./fixture/fixture"
|
||||
|
||||
@@ -139,42 +138,6 @@ test("shared runtime and ordinary package identities survive plugin generations"
|
||||
}
|
||||
})
|
||||
|
||||
test("TUI plugins and their dependencies resolve effect and effect/* to the host copy", async () => {
|
||||
await using sources = await fixture()
|
||||
const entry = new URL("tui.ts", sources.url)
|
||||
await Bun.write(
|
||||
new URL("node_modules/effect/package.json", sources.url),
|
||||
'{"name":"effect","type":"module","exports":{".":"./index.js","./Option":"./Option.js"}}',
|
||||
)
|
||||
await Bun.write(
|
||||
new URL("node_modules/effect/index.js", sources.url),
|
||||
"export const Effect = { foreign: true }; export const Schema = { foreign: true }",
|
||||
)
|
||||
await Bun.write(new URL("node_modules/effect/Option.js", sources.url), "export const some = () => null")
|
||||
await Bun.write(
|
||||
new URL("node_modules/effect-helper/package.json", sources.url),
|
||||
'{"name":"effect-helper","type":"module","exports":{".":"./index.js"}}',
|
||||
)
|
||||
await Bun.write(
|
||||
new URL("node_modules/effect-helper/index.js", sources.url),
|
||||
'import { Effect as HelperEffect, Schema as HelperSchema } from "effect"; import { some as helperSome } from "effect/Option"; export { HelperEffect, HelperSchema, helperSome }',
|
||||
)
|
||||
await Bun.write(
|
||||
entry,
|
||||
`import { Effect as PluginEffect, Schema as PluginSchema } from "effect"
|
||||
import { some as pluginSome } from "effect/Option"
|
||||
import { HelperEffect, HelperSchema, helperSome } from "effect-helper"
|
||||
export { PluginEffect, PluginSchema, pluginSome, HelperEffect, HelperSchema, helperSome }`,
|
||||
)
|
||||
const loaded = (await sources.read(entry.href)).module as Record<string, unknown>
|
||||
expect(loaded.PluginEffect).toBe(Effect)
|
||||
expect(loaded.PluginSchema).toBe(Schema)
|
||||
expect(loaded.pluginSome).toBe(Option.some)
|
||||
expect(loaded.HelperEffect).toBe(Effect)
|
||||
expect(loaded.HelperSchema).toBe(Schema)
|
||||
expect(loaded.helperSome).toBe(Option.some)
|
||||
})
|
||||
|
||||
test("helper import.meta stays anchored to its source, including assets and resolution", async () => {
|
||||
await using sources = await fixture()
|
||||
const entry = new URL("tui.ts", sources.url)
|
||||
|
||||
@@ -3,13 +3,11 @@ title: "Effect"
|
||||
---
|
||||
|
||||
`@opencode/plugin/effect` is the Effect-native version of the OpenCode plugin API. Its context operations return
|
||||
Effects or Streams, callbacks return Effects, and plugin lifetime is represented by `Scope`.
|
||||
|
||||
OpenCode provides `effect` (including `effect/*` subpaths) and `@opencode/plugin` at runtime when loading plugins.
|
||||
Install them as peer/dev dependencies for local type-checking and testing rather than bundling them into your plugin.
|
||||
Effects or Streams, callbacks return Effects, and plugin lifetime is represented by `Scope`. Install `effect` with the
|
||||
plugin package.
|
||||
|
||||
```sh
|
||||
bun add --peer @opencode/plugin effect
|
||||
bun add @opencode/plugin effect
|
||||
```
|
||||
|
||||
Export an Effect plugin from `.opencode/plugins/` to load it automatically.
|
||||
@@ -1502,7 +1500,7 @@ type Transform<Input> = (callback: (input: Input) => void) => Effect.Effect<Regi
|
||||
## Publish
|
||||
|
||||
A package plugin uses the same default export as a local Effect plugin. Export the Effect implementation from the main
|
||||
entrypoint and declare `effect` as a `peerDependency`.
|
||||
entrypoint and declare both runtime dependencies.
|
||||
|
||||
```json title="package.json"
|
||||
{
|
||||
@@ -1513,23 +1511,12 @@ entrypoint and declare `effect` as a `peerDependency`.
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@opencode/plugin": "latest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"effect": ">=4.0.0-rc.112"
|
||||
"@opencode/plugin": "latest",
|
||||
"effect": "4.0.0-rc.111"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
OpenCode redirects imports of `effect`, `effect/*` subpaths, and `@opencode/plugin` (including from dependencies in the
|
||||
plugin's `node_modules`) to the host's runtime instances so fibers, loggers, and `Schema` parsers share one runtime
|
||||
copy.
|
||||
|
||||
- Do not bundle `effect` into the published plugin artifact. If you build with a bundler, mark `effect` and `effect/*`
|
||||
external; OpenCode rejects Effect plugins whose returned `Effect` comes from a bundled copy.
|
||||
- Because the plugin always runs against the host OpenCode release's `effect` instance, use `effect` APIs and module
|
||||
paths compatible with the OpenCode release you target.
|
||||
|
||||
The package entrypoint exports `Plugin.define` with an `effect` function.
|
||||
|
||||
```ts title="src/index.ts"
|
||||
|
||||
Reference in new issue
Block a user