mirror of
https://github.com/anomalyco/opencode.git
synced 2026-10-05 15:06:16 +00:00
Compare commits
36
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
494af523d1 | ||
|
|
cb389aa679 | ||
|
|
30f4119459 | ||
|
|
4d17cf719f | ||
|
|
498b1deda3 | ||
|
|
25c54cbdf7 | ||
|
|
ae57dd0cf5 | ||
|
|
e86ec98711 | ||
|
|
c542a43503 | ||
|
|
2db9c2e1b3 | ||
|
|
3a861ff8b0 | ||
|
|
22803892bc | ||
|
|
69ba898a7b | ||
|
|
375ff827aa | ||
|
|
ed747091e9 | ||
|
|
44193bd71f | ||
|
|
238d4cf5ca | ||
|
|
3e77d5c170 | ||
|
|
eaf80d99e5 | ||
|
|
0a46301e36 | ||
|
|
c789aa16d1 | ||
|
|
a515e3bf4b | ||
|
|
8380c864bf | ||
|
|
247eb5cc9a | ||
|
|
ecfa884ff8 | ||
|
|
0d536d2d7d | ||
|
|
715de7af15 | ||
|
|
077444b94b | ||
|
|
5589ca43f1 | ||
|
|
453dd2ad01 | ||
|
|
44579a5d98 | ||
|
|
f8cafd8362 | ||
|
|
9155b7b791 | ||
|
|
9c66a0acb0 | ||
|
|
ee730bdd91 | ||
|
|
cfbdefa041 |
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
|
||||
|
||||
@@ -42,6 +42,7 @@ const RESPECTS_INLINE_HINTS = new Set([
|
||||
"alibaba-messages",
|
||||
"anthropic-messages",
|
||||
"anthropic-compatible-messages",
|
||||
"bedrock-mantle-messages",
|
||||
"cloudflare-ai-gateway-messages",
|
||||
"google-vertex-messages",
|
||||
"meta-messages",
|
||||
|
||||
@@ -584,10 +584,7 @@ const serverToolResultType = (name: string): AnthropicServerToolResultType | und
|
||||
return undefined
|
||||
}
|
||||
|
||||
const lowerServerToolResult = Effect.fn("AnthropicMessages.lowerServerToolResult")(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.fn("AnthropicMessages.lowerMedia")(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>()
|
||||
@@ -847,7 +822,7 @@ const splitsLocalToolResults = (messages: LLMRequest["messages"], index: number)
|
||||
return pending.size > 0
|
||||
}
|
||||
|
||||
const lowerNativeSystemUpdate = Effect.fn("AnthropicMessages.lowerNativeSystemUpdate")(function* (
|
||||
const lowerNativeSystemUpdate = Effect.fnUntraced(function* (
|
||||
message: LLMRequest["messages"][number],
|
||||
breakpoints: Cache.Breakpoints,
|
||||
) {
|
||||
@@ -862,12 +837,34 @@ const lowerNativeSystemUpdate = Effect.fn("AnthropicMessages.lowerNativeSystemUp
|
||||
}
|
||||
})
|
||||
|
||||
const lowerMessages = Effect.fn("AnthropicMessages.lowerMessages")(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.fn("AnthropicMessages.lowerMessages")(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.fn("AnthropicMessages.lowerMessages")(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,10 +976,14 @@ const lowerMessages = Effect.fn("AnthropicMessages.lowerMessages")(function* (
|
||||
messages[messages.length - 1] = { role: "user", content: [...previous.content, ...content] }
|
||||
else messages.push({ role: "user", content })
|
||||
}
|
||||
yield* releaseHeld()
|
||||
|
||||
return messages
|
||||
})
|
||||
|
||||
// TODO: Move per-model capability heuristics (`supportsEffortUpdates`, `supportsNativeSystemUpdates`,
|
||||
// `supportsThinkingBlockBinding`) into explicit model/provider `compatibility` metadata so the protocol
|
||||
// only reads `request.model.compatibility`.
|
||||
// Per-turn effort started with Claude Opus 5 and every Claude 5.1 model; later versions of any family inherit it.
|
||||
const supportsEffortUpdates = (model: LLMRequest["model"]) => {
|
||||
const override = model.compatibility?.supportsEffortUpdates
|
||||
@@ -1300,7 +1295,7 @@ const onContentBlockStart = (
|
||||
return [{ ...state, lifecycle: Lifecycle.stepStart(state.lifecycle, events) }, [...events, result]]
|
||||
}
|
||||
|
||||
const onContentBlockDelta = Effect.fn("AnthropicMessages.onContentBlockDelta")(function* (
|
||||
const onContentBlockDelta = Effect.fnUntraced(function* (
|
||||
state: ParserState,
|
||||
event: AnthropicEvent & { readonly delta: AnthropicStreamDelta },
|
||||
) {
|
||||
@@ -1368,7 +1363,7 @@ const onContentBlockDelta = Effect.fn("AnthropicMessages.onContentBlockDelta")(f
|
||||
return [state, NO_EVENTS] satisfies StepResult
|
||||
})
|
||||
|
||||
const onContentBlockStop = Effect.fn("AnthropicMessages.onContentBlockStop")(function* (
|
||||
const onContentBlockStop = Effect.fnUntraced(function* (
|
||||
state: ParserState,
|
||||
event: AnthropicEvent,
|
||||
) {
|
||||
@@ -1439,7 +1434,7 @@ const onMessageDelta = (
|
||||
]
|
||||
}
|
||||
|
||||
const onMessageStop = Effect.fn("AnthropicMessages.onMessageStop")(function* (state: ParserState) {
|
||||
const onMessageStop = Effect.fnUntraced(function* (state: ParserState) {
|
||||
if (Object.keys(state.compactions).length)
|
||||
return yield* ProviderShared.eventError(ADAPTER, "Response ended with an incomplete compaction block")
|
||||
const result = yield* ToolStream.finishAll(ADAPTER, state.tools)
|
||||
|
||||
@@ -285,7 +285,7 @@ const lowerToolCall = (part: ToolCallPart, normalizeID: (id: string) => string):
|
||||
},
|
||||
})
|
||||
|
||||
const lowerToolResultContent = Effect.fn("BedrockConverse.lowerToolResultContent")(function* (
|
||||
const lowerToolResultContent = Effect.fnUntraced(function* (
|
||||
part: ToolResultPart,
|
||||
documentNames: Set<string>,
|
||||
) {
|
||||
@@ -305,7 +305,7 @@ const lowerToolResultContent = Effect.fn("BedrockConverse.lowerToolResultContent
|
||||
return content
|
||||
})
|
||||
|
||||
const lowerToolResult = Effect.fn("BedrockConverse.lowerToolResult")(function* (
|
||||
const lowerToolResult = Effect.fnUntraced(function* (
|
||||
part: ToolResultPart,
|
||||
documentNames: Set<string>,
|
||||
normalizeID: (id: string) => string,
|
||||
@@ -322,7 +322,7 @@ const lowerToolResult = Effect.fn("BedrockConverse.lowerToolResult")(function* (
|
||||
// Keep Claude and Nova tool-result images inline; put other models' images beside the result.
|
||||
const keepToolImagesInline = (id: string) => id.includes("anthropic.claude-") || id.includes("amazon.nova-")
|
||||
|
||||
const lowerMessages = Effect.fn("BedrockConverse.lowerMessages")(function* (
|
||||
const lowerMessages = Effect.fnUntraced(function* (
|
||||
request: LLMRequest,
|
||||
breakpoints: BedrockCache.Breakpoints,
|
||||
) {
|
||||
|
||||
@@ -75,7 +75,7 @@ const usesSse = (request: MediaProtocol.Addressed<Request>) => request.mode ===
|
||||
|
||||
const CONTAINERS: Readonly<Record<string, "raw" | "wav" | "mp3">> = { pcm: "raw", wav: "wav", mp3: "mp3" }
|
||||
|
||||
const outputFormat = Effect.fn("CartesiaSpeech.outputFormat")(function* (request: MediaProtocol.Addressed<Request>) {
|
||||
const outputFormat = Effect.fnUntraced(function* (request: MediaProtocol.Addressed<Request>) {
|
||||
const sse = usesSse(request)
|
||||
const format = request.format ?? (sse ? "pcm" : "mp3")
|
||||
const container = CONTAINERS[format]
|
||||
@@ -121,7 +121,7 @@ const fromRequest = Effect.fn("CartesiaSpeech.fromRequest")(function* (request:
|
||||
// 6. Stream parsing
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const onEvent = Effect.fn("CartesiaSpeech.onEvent")(function* (state: State, frame: string) {
|
||||
const onEvent = Effect.fnUntraced(function* (state: State, frame: string) {
|
||||
const event = yield* decodeEvent(frame)
|
||||
if (event.type === "chunk" && event.data !== undefined) return SpeechStream.delta(state, event.data)
|
||||
if (event.type === "timestamps" && event.word_timestamps !== undefined) {
|
||||
@@ -140,7 +140,7 @@ const onEvent = Effect.fn("CartesiaSpeech.onEvent")(function* (state: State, fra
|
||||
return [state, []] as const
|
||||
})
|
||||
|
||||
const finish = Effect.fn("CartesiaSpeech.finish")(function* (
|
||||
const finish = Effect.fnUntraced(function* (
|
||||
state: State,
|
||||
context: MediaProtocol.ResponseContext<Request>,
|
||||
) {
|
||||
|
||||
@@ -252,7 +252,7 @@ const mapUsage = (usage: typeof NativeUsage.Type) =>
|
||||
})
|
||||
|
||||
// Lifecycle deltas open blocks on demand and ends are no-ops for closed blocks, so content-start needs no handling.
|
||||
const step = Effect.fn("CohereChat.step")(function* (state: State, event: Event) {
|
||||
const step = Effect.fnUntraced(function* (state: State, event: Event) {
|
||||
const events: LLMEvent[] = []
|
||||
switch (event.type) {
|
||||
case "message-start":
|
||||
|
||||
@@ -95,7 +95,7 @@ const OUTPUT_FORMATS: Readonly<Record<string, string>> = {
|
||||
}
|
||||
|
||||
/** WAV is served only by the non-streaming endpoints. */
|
||||
const outputFormat = Effect.fn("ElevenLabsSpeech.outputFormat")(function* (request: MediaProtocol.Addressed<Request>) {
|
||||
const outputFormat = Effect.fnUntraced(function* (request: MediaProtocol.Addressed<Request>) {
|
||||
const format = request.providerOptions?.outputFormat ?? OUTPUT_FORMATS[request.format ?? "mp3"]
|
||||
if (format === undefined)
|
||||
return yield* route.unsupported(
|
||||
@@ -138,7 +138,7 @@ const path = (request: MediaProtocol.Addressed<Request>) =>
|
||||
// 6. Stream parsing
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const onRecord = Effect.fn("ElevenLabsSpeech.onRecord")(function* (state: State, frame: string) {
|
||||
const onRecord = Effect.fnUntraced(function* (state: State, frame: string) {
|
||||
const record = yield* decodeRecord(frame)
|
||||
const [next, events] = SpeechStream.delta(state, record.audio_base64)
|
||||
const alignment = record.alignment
|
||||
@@ -169,7 +169,7 @@ const describeOutput = (format: string) => {
|
||||
return encoding === undefined ? SpeechStream.container(codec, sampleRate) : SpeechStream.pcm(encoding, sampleRate)
|
||||
}
|
||||
|
||||
const finish = Effect.fn("ElevenLabsSpeech.finish")(function* (
|
||||
const finish = Effect.fnUntraced(function* (
|
||||
state: State,
|
||||
context: MediaProtocol.ResponseContext<Request>,
|
||||
) {
|
||||
|
||||
@@ -283,7 +283,7 @@ const lowerToolConfig = (toolChoice: NonNullable<LLMRequest["toolChoice"]>) =>
|
||||
tool: (name) => ({ functionCallingConfig: { mode: "ANY" as const, allowedFunctionNames: [name] } }),
|
||||
})
|
||||
|
||||
const lowerContentPart = Effect.fn("Gemini.lowerContentPart")(function* (part: TextPart | MediaPart) {
|
||||
const lowerContentPart = Effect.fnUntraced(function* (part: TextPart | MediaPart) {
|
||||
if (part.type === "text") return { text: part.text }
|
||||
return yield* GeminiGenerateContent.mediaPart("Gemini", part.media)
|
||||
})
|
||||
@@ -302,7 +302,7 @@ const lowerToolCall = (part: ToolCallPart, omitIds: boolean, metadataKey: string
|
||||
thoughtSignature: thoughtSignature(part.providerMetadata, metadataKey),
|
||||
})
|
||||
|
||||
const lowerMessages = Effect.fn("Gemini.lowerMessages")(function* (request: LLMRequest) {
|
||||
const lowerMessages = Effect.fnUntraced(function* (request: LLMRequest) {
|
||||
const contents: GeminiContent[] = []
|
||||
const metadataKey = request.model.route.providerMetadataKey ?? String(request.model.provider)
|
||||
const omitCallIds = omitsFunctionCallIds(request.model.id)
|
||||
|
||||
@@ -182,7 +182,7 @@ type StepResult = readonly [ParserState, ReadonlyArray<LLMEvent>]
|
||||
// =============================================================================
|
||||
// Request Body Construction
|
||||
// =============================================================================
|
||||
const mediaContent = Effect.fn("GoogleInteractions.mediaContent")(function* (asset: Media.Asset) {
|
||||
const mediaContent = Effect.fnUntraced(function* (asset: Media.Asset) {
|
||||
if (
|
||||
asset.kind !== "image" &&
|
||||
asset.kind !== "audio" &&
|
||||
@@ -208,7 +208,7 @@ const signature = (metadata: ProviderMetadata | undefined, key: string) => {
|
||||
: undefined
|
||||
}
|
||||
|
||||
const lowerMessages = Effect.fn("GoogleInteractions.lowerMessages")(function* (request: LLMRequest) {
|
||||
const lowerMessages = Effect.fnUntraced(function* (request: LLMRequest) {
|
||||
const steps: InputStep[] = []
|
||||
const key = request.model.route.providerMetadataKey ?? String(request.model.provider)
|
||||
for (const message of request.messages) {
|
||||
@@ -275,7 +275,7 @@ const lowerMessages = Effect.fn("GoogleInteractions.lowerMessages")(function* (r
|
||||
return steps
|
||||
})
|
||||
|
||||
const lowerToolResult = Effect.fn("GoogleInteractions.lowerToolResult")(function* (part: ToolResultPart) {
|
||||
const lowerToolResult = Effect.fnUntraced(function* (part: ToolResultPart) {
|
||||
if (part.result.type === "json" && ProviderShared.isRecord(part.result.value)) return part.result.value
|
||||
if (part.result.type !== "content") return ProviderShared.toolResultText(part)
|
||||
|
||||
@@ -364,7 +364,7 @@ const mapUsage = (usage: RawUsage | undefined, key: string) => {
|
||||
})
|
||||
}
|
||||
|
||||
const onStart = Effect.fn("GoogleInteractions.onStart")(function* (
|
||||
const onStart = Effect.fnUntraced(function* (
|
||||
state: ParserState,
|
||||
index: number,
|
||||
step: OutputStep,
|
||||
@@ -402,7 +402,7 @@ const onStart = Effect.fn("GoogleInteractions.onStart")(function* (
|
||||
return [{ ...state, lifecycle, tools, steps: { ...state.steps, [index]: step } }, events] satisfies StepResult
|
||||
})
|
||||
|
||||
const onDelta = Effect.fn("GoogleInteractions.onDelta")(function* (
|
||||
const onDelta = Effect.fnUntraced(function* (
|
||||
state: ParserState,
|
||||
index: number,
|
||||
delta: typeof Delta.Type,
|
||||
@@ -447,7 +447,7 @@ const onDelta = Effect.fn("GoogleInteractions.onDelta")(function* (
|
||||
return yield* ProviderShared.eventError(ADAPTER, `Unsupported Interactions delta: ${delta.type}`, encodeJson(delta))
|
||||
})
|
||||
|
||||
const onStop = Effect.fn("GoogleInteractions.onStop")(function* (state: ParserState, index: number) {
|
||||
const onStop = Effect.fnUntraced(function* (state: ParserState, index: number) {
|
||||
const step = state.steps[index]
|
||||
if (!step) return yield* ProviderShared.eventError(ADAPTER, "Interactions step.stop without step.start")
|
||||
const events: LLMEvent[] = []
|
||||
@@ -465,7 +465,7 @@ const onStop = Effect.fn("GoogleInteractions.onStop")(function* (state: ParserSt
|
||||
return [{ ...state, tools: result.tools }, result.events ?? []] satisfies StepResult
|
||||
})
|
||||
|
||||
const step = Effect.fn("GoogleInteractions.step")(function* (state: ParserState, event: Event) {
|
||||
const step = Effect.fnUntraced(function* (state: ParserState, event: Event) {
|
||||
switch (event.event_type) {
|
||||
case "step.start":
|
||||
return yield* onStart(state, event.index, event.step)
|
||||
|
||||
@@ -94,7 +94,7 @@ const fromRequest = Effect.fn("GoogleSpeech.fromRequest")(function* (request: Me
|
||||
// 6. Stream parsing
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const step = Effect.fn("GoogleSpeech.step")(function* (state: State, frame: string) {
|
||||
const step = Effect.fnUntraced(function* (state: State, frame: string) {
|
||||
const chunk = yield* decodeChunk(frame)
|
||||
const blocked = GeminiGenerateContent.blocked(route.name, chunk, frame)
|
||||
if (blocked !== undefined) return yield* blocked
|
||||
|
||||
@@ -138,7 +138,7 @@ const turn = (part: Schema.Schema.Type<typeof AudioTranscription>) => {
|
||||
}
|
||||
}
|
||||
|
||||
const step = Effect.fn("GoogleTranscription.step")(function* (state: State, frame: string) {
|
||||
const step = Effect.fnUntraced(function* (state: State, frame: string) {
|
||||
const chunk = yield* decodeChunk(frame)
|
||||
const blocked = GeminiGenerateContent.blocked(route.name, chunk, frame)
|
||||
if (blocked !== undefined) return yield* blocked
|
||||
|
||||
@@ -95,7 +95,7 @@ const HOSTED_TOOLS = {
|
||||
image_generation_call: {
|
||||
name: "image_generation",
|
||||
input: () => ({}),
|
||||
result: Effect.fn("MetaResponses.imageResult")(function* (raw: ResponsesHostedTools.Item) {
|
||||
result: Effect.fnUntraced(function* (raw: ResponsesHostedTools.Item) {
|
||||
const item = yield* Schema.decodeUnknownEffect(ImageItem)(raw).pipe(
|
||||
Effect.mapError((cause) =>
|
||||
ProviderShared.eventError(
|
||||
@@ -136,7 +136,7 @@ const HOSTED_TOOLS = {
|
||||
},
|
||||
} satisfies ResponsesHostedTools.Definitions
|
||||
|
||||
const onEvent = Effect.fn("MetaResponses.onEvent")(function* (
|
||||
const onEvent = Effect.fnUntraced(function* (
|
||||
state: OpenResponses.ParserState,
|
||||
input: OpenResponses.Event,
|
||||
) {
|
||||
@@ -173,7 +173,7 @@ const onEvent = Effect.fn("MetaResponses.onEvent")(function* (
|
||||
] satisfies OpenResponses.StepResult
|
||||
})
|
||||
|
||||
const step = Effect.fn("MetaResponses.step")(function* (state: ParserState, input: OpenResponses.Event) {
|
||||
const step = Effect.fnUntraced(function* (state: ParserState, input: OpenResponses.Event) {
|
||||
const completedItems = new Set(state.completedItems)
|
||||
const event = OpenResponses.normalize(state, input)
|
||||
if (event.type === "response.output_item.done" && event.item && completedItems.has(event.item.id))
|
||||
|
||||
@@ -226,7 +226,7 @@ const MistralEvent = Schema.StructWithRest(
|
||||
type MistralEvent = Schema.Schema.Type<typeof MistralEvent>
|
||||
const MistralStreamEvent = Schema.Union([Schema.Literal(DONE), Protocol.jsonEvent(MistralEvent)])
|
||||
|
||||
const lowerMedia = Effect.fn("MistralChat.lowerMedia")(function* (part: MediaPart) {
|
||||
const lowerMedia = Effect.fnUntraced(function* (part: MediaPart) {
|
||||
const mime = part.media.mediaType.toLowerCase()
|
||||
const url =
|
||||
ProviderShared.mediaUrl(part.media) ??
|
||||
@@ -236,7 +236,7 @@ const lowerMedia = Effect.fn("MistralChat.lowerMedia")(function* (part: MediaPar
|
||||
return yield* ProviderShared.invalidRequest(`Mistral Chat does not support media type ${part.media.mediaType}`)
|
||||
})
|
||||
|
||||
const lowerUser = Effect.fn("MistralChat.lowerUser")(function* (message: LLMRequest["messages"][number]) {
|
||||
const lowerUser = Effect.fnUntraced(function* (message: LLMRequest["messages"][number]) {
|
||||
const content: MistralUserContent[] = []
|
||||
for (const part of message.content) {
|
||||
if (part.type === "text") {
|
||||
@@ -260,7 +260,7 @@ const lowerToolCall = (part: ToolCallPart, normalizeID: (id: string) => string):
|
||||
function: { name: part.name, arguments: ProviderShared.encodeJson(part.input) },
|
||||
})
|
||||
|
||||
const lowerAssistant = Effect.fn("MistralChat.lowerAssistant")(function* (
|
||||
const lowerAssistant = Effect.fnUntraced(function* (
|
||||
message: LLMRequest["messages"][number],
|
||||
normalizeID: (id: string) => string,
|
||||
prefix: boolean,
|
||||
@@ -298,7 +298,7 @@ const lowerAssistant = Effect.fn("MistralChat.lowerAssistant")(function* (
|
||||
}
|
||||
})
|
||||
|
||||
const lowerToolResults = Effect.fn("MistralChat.lowerToolResults")(function* (
|
||||
const lowerToolResults = Effect.fnUntraced(function* (
|
||||
message: LLMRequest["messages"][number],
|
||||
normalizeID: (id: string) => string,
|
||||
) {
|
||||
@@ -335,7 +335,7 @@ const lowerToolResults = Effect.fn("MistralChat.lowerToolResults")(function* (
|
||||
return output
|
||||
})
|
||||
|
||||
const lowerMessages = Effect.fn("MistralChat.lowerMessages")(function* (request: LLMRequest) {
|
||||
const lowerMessages = Effect.fnUntraced(function* (request: LLMRequest) {
|
||||
const normalizeID = MistralToolID.normalizer(request)
|
||||
const messages: MistralMessage[] =
|
||||
request.system.length === 0
|
||||
@@ -596,7 +596,7 @@ const toolText = (tool: MistralToolDelta) => {
|
||||
return value === null || value === undefined ? "" : ProviderShared.encodeJson(value)
|
||||
}
|
||||
|
||||
const appendTools = Effect.fn("MistralChat.appendTools")(function* (
|
||||
const appendTools = Effect.fnUntraced(function* (
|
||||
initial: ParserState,
|
||||
events: LLMEvent[],
|
||||
deltas: ReadonlyArray<MistralToolDelta>,
|
||||
@@ -662,7 +662,7 @@ const hasLateContent = (event: MistralEvent) => {
|
||||
)
|
||||
}
|
||||
|
||||
const step = Effect.fn("MistralChat.step")(function* (state: ParserState, event: MistralEvent) {
|
||||
const step = Effect.fnUntraced(function* (state: ParserState, event: MistralEvent) {
|
||||
if (event.error) {
|
||||
const body = ProviderShared.encodeJson(event)
|
||||
return yield* new AIError({
|
||||
@@ -726,7 +726,7 @@ const step = Effect.fn("MistralChat.step")(function* (state: ParserState, event:
|
||||
] as const
|
||||
})
|
||||
|
||||
const finishEvents = Effect.fn("MistralChat.finishEvents")(function* (state: ParserState) {
|
||||
const finishEvents = Effect.fnUntraced(function* (state: ParserState) {
|
||||
if (!state.finishReason)
|
||||
return yield* new AIError({
|
||||
reason: new InvalidProviderOutputError({
|
||||
|
||||
@@ -442,7 +442,7 @@ interface ReasoningStreamItem {
|
||||
// =============================================================================
|
||||
// Request Lowering
|
||||
// =============================================================================
|
||||
export const lowerTool = Effect.fn("OpenResponses.lowerTool")(function* (protocolName: string, tool: ToolDefinition) {
|
||||
export const lowerTool = Effect.fnUntraced(function* (protocolName: string, tool: ToolDefinition) {
|
||||
if (tool.native !== undefined)
|
||||
return yield* ProviderShared.invalidRequest(`${protocolName} does not support provider-native tool ${tool.name}`)
|
||||
return {
|
||||
@@ -507,7 +507,10 @@ const lowerReasoning = (part: ReasoningPart, providerMetadataKey: string): OpenR
|
||||
}
|
||||
}
|
||||
|
||||
const lowerMedia = Effect.fn("OpenResponses.lowerMedia")(function* (
|
||||
const decodeImageDetail = ProviderShared.validateWith(Schema.decodeUnknownEffect(OpenResponsesInputImage.fields.detail))
|
||||
const decodeMessageMetadata = ProviderShared.validateWith(Schema.decodeUnknownEffect(MessageMetadata))
|
||||
|
||||
const lowerMedia = Effect.fnUntraced(function* (
|
||||
part: MediaPart,
|
||||
request: LLMRequest,
|
||||
adapter: ProviderAdapter,
|
||||
@@ -516,9 +519,8 @@ const lowerMedia = Effect.fn("OpenResponses.lowerMedia")(function* (
|
||||
const media = part.media.inline()
|
||||
const providerMedia = adapter.lowerMedia?.({ part, media, request })
|
||||
if (providerMedia) return providerMedia
|
||||
const detail = yield* ProviderShared.validateWith(Schema.decodeUnknownEffect(OpenResponsesInputImage.fields.detail))(
|
||||
part.providerMetadata?.[metadataKey(request.model)]?.detail,
|
||||
)
|
||||
const rawDetail = part.providerMetadata?.[metadataKey(request.model)]?.detail
|
||||
const detail = rawDetail === undefined ? undefined : yield* decodeImageDetail(rawDetail)
|
||||
const mime = part.media.mediaType.toLowerCase()
|
||||
const url = ProviderShared.mediaUrl(part.media)
|
||||
const location = url ?? (yield* ProviderShared.requireInlineMedia(adapter.name, part.media)).dataUrl
|
||||
@@ -591,7 +593,7 @@ const lowerToolResultOutput = Effect.fnUntraced(function* (
|
||||
|
||||
const DEFAULT_EFFORT = "medium"
|
||||
|
||||
const lowerMessages = Effect.fn("OpenResponses.lowerMessages")(function* (
|
||||
const lowerMessages = Effect.fnUntraced(function* (
|
||||
request: LLMRequest,
|
||||
adapter: ProviderAdapter,
|
||||
) {
|
||||
@@ -599,9 +601,8 @@ const lowerMessages = Effect.fn("OpenResponses.lowerMessages")(function* (
|
||||
const providerMetadataKey = metadataKey(request.model)
|
||||
|
||||
for (const message of request.messages) {
|
||||
const metadata = yield* ProviderShared.validateWith(
|
||||
Schema.decodeUnknownEffect(Schema.UndefinedOr(MessageMetadata)),
|
||||
)(message.providerMetadata?.[providerMetadataKey])
|
||||
const rawMetadata = message.providerMetadata?.[providerMetadataKey]
|
||||
const metadata = rawMetadata === undefined ? undefined : yield* decodeMessageMetadata(rawMetadata)
|
||||
if (message.role === "system") {
|
||||
const update = effortUpdate(message)
|
||||
if (update) {
|
||||
@@ -755,7 +756,7 @@ const lowerMessages = Effect.fn("OpenResponses.lowerMessages")(function* (
|
||||
return input
|
||||
})
|
||||
|
||||
export const lowerConversation = Effect.fn("OpenResponses.lowerConversation")(function* (
|
||||
export const lowerConversation = Effect.fnUntraced(function* (
|
||||
request: LLMRequest,
|
||||
adapter: ProviderAdapter,
|
||||
) {
|
||||
@@ -1142,7 +1143,7 @@ const onReasoningSummaryPartDone = (state: ParserState, event: Event): StepResul
|
||||
]
|
||||
}
|
||||
|
||||
const onFunctionCallArgumentsDelta = Effect.fn("OpenResponses.onFunctionCallArgumentsDelta")(function* (
|
||||
const onFunctionCallArgumentsDelta = Effect.fnUntraced(function* (
|
||||
state: ParserState,
|
||||
event: Event,
|
||||
) {
|
||||
@@ -1173,7 +1174,7 @@ const onFunctionCallArgumentsDelta = Effect.fn("OpenResponses.onFunctionCallArgu
|
||||
return [{ ...state, lifecycle, tools: result.tools }, events] satisfies StepResult
|
||||
})
|
||||
|
||||
const onOutputItemDone = Effect.fn("OpenResponses.onOutputItemDone")(function* (
|
||||
const onOutputItemDone = Effect.fnUntraced(function* (
|
||||
state: ParserState,
|
||||
item: NormalizedEvent["item"],
|
||||
) {
|
||||
@@ -1309,7 +1310,7 @@ const onOutputItemDone = Effect.fn("OpenResponses.onOutputItemDone")(function* (
|
||||
return [state, NO_EVENTS] satisfies StepResult
|
||||
})
|
||||
|
||||
const onResponseFinish = Effect.fn("OpenResponses.onResponseFinish")(function* (state: ParserState, event: Event) {
|
||||
const onResponseFinish = Effect.fnUntraced(function* (state: ParserState, event: Event) {
|
||||
let current = state
|
||||
const events: LLMEvent[] = []
|
||||
if (event.type === "response.completed") {
|
||||
|
||||
@@ -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 => ({
|
||||
@@ -360,7 +362,7 @@ const lowerToolCall = (
|
||||
extra_content: decodeExtraContent(part.providerMetadata?.[options.providerMetadataKey]?.extraContent),
|
||||
})
|
||||
|
||||
const lowerMedia = Effect.fn("OpenAIChat.lowerMedia")(function* (part: MediaPart) {
|
||||
const lowerMedia = Effect.fnUntraced(function* (part: MediaPart) {
|
||||
// Chat Completions accepts PDFs, and no other documents, as inline `file` parts; file URLs are not supported.
|
||||
if (part.media.mediaType.toLowerCase() === "application/pdf")
|
||||
return {
|
||||
@@ -406,7 +408,7 @@ const lowerReasoningDetail = (detail: ReasoningDetail) => {
|
||||
|
||||
const isKimiDetail = (detail: { readonly type: string }) => detail.type === "summary" || detail.type === "encrypted"
|
||||
|
||||
const lowerUserMessage = Effect.fn("OpenAIChat.lowerUserMessage")(function* (
|
||||
const lowerUserMessage = Effect.fnUntraced(function* (
|
||||
message: OpenAIChatRequestMessage,
|
||||
options: LoweringOptions,
|
||||
) {
|
||||
@@ -430,7 +432,7 @@ const lowerUserMessage = Effect.fn("OpenAIChat.lowerUserMessage")(function* (
|
||||
return { role: "user" as const, content }
|
||||
})
|
||||
|
||||
const lowerAssistantMessage = Effect.fn("OpenAIChat.lowerAssistantMessage")(function* (
|
||||
const lowerAssistantMessage = Effect.fnUntraced(function* (
|
||||
message: OpenAIChatRequestMessage,
|
||||
configuredField: string | undefined,
|
||||
requireReasoning: boolean,
|
||||
@@ -495,7 +497,7 @@ const lowerAssistantMessage = Effect.fn("OpenAIChat.lowerAssistantMessage")(func
|
||||
return { ...result, [field]: reasoningText }
|
||||
})
|
||||
|
||||
const lowerToolMessages = Effect.fn("OpenAIChat.lowerToolMessages")(function* (
|
||||
const lowerToolMessages = Effect.fnUntraced(function* (
|
||||
message: OpenAIChatRequestMessage,
|
||||
options: LoweringOptions,
|
||||
) {
|
||||
@@ -532,19 +534,21 @@ const toolMessage = (toolCallID: string, text: string, cacheControl: OpenAIChatC
|
||||
content: cacheControl === undefined ? text : [{ type: "text" as const, text, cache_control: cacheControl }],
|
||||
})
|
||||
|
||||
const lowerMessage = Effect.fn("OpenAIChat.lowerMessage")(function* (
|
||||
const lowerMessage = Effect.fnUntraced(function* (
|
||||
message: OpenAIChatRequestMessage,
|
||||
reasoningField: string | undefined,
|
||||
requireReasoning: boolean,
|
||||
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
|
||||
})
|
||||
|
||||
const lowerMessages = Effect.fn("OpenAIChat.lowerMessages")(function* (request: LLMRequest, options: LoweringOptions) {
|
||||
const lowerMessages = Effect.fnUntraced(function* (request: LLMRequest, options: LoweringOptions) {
|
||||
const system: OpenAIChatMessage[] =
|
||||
request.system.length === 0
|
||||
? []
|
||||
@@ -855,7 +859,7 @@ export const fromRequest = Effect.fn("OpenAIChat.fromRequest")(function* (
|
||||
// Streaming parsers are small state machines: every event returns a new state
|
||||
// plus the common `LLMEvent`s produced by that event. Tool calls are accumulated
|
||||
// because OpenAI streams JSON arguments across multiple deltas.
|
||||
const mapFinishReason = Effect.fn("OpenAIChat.mapFinishReason")(function* (event: OpenAIChatEvent, reason: string) {
|
||||
const mapFinishReason = Effect.fnUntraced(function* (event: OpenAIChatEvent, reason: string) {
|
||||
switch (reason) {
|
||||
case "error":
|
||||
return yield* new AIError({
|
||||
@@ -1214,7 +1218,7 @@ const step = (state: ParserState, event: OpenAIChatEvent) =>
|
||||
] as const
|
||||
})
|
||||
|
||||
const finishEvents = Effect.fn("OpenAIChat.finishEvents")(function* (state: ParserState) {
|
||||
export const finishEvents = Effect.fnUntraced(function* (state: ParserState) {
|
||||
if (state.finishReason === undefined && state.requireFinishReason)
|
||||
return yield* new AIError({
|
||||
reason: new InvalidProviderOutputError({
|
||||
|
||||
@@ -208,7 +208,7 @@ const eventImage = (frame: string, label: string, data: string, format: string,
|
||||
info: info(format, size),
|
||||
})
|
||||
|
||||
const onEvent = Effect.fn("OpenAIImages.onEvent")(function* (state: State, frame: string) {
|
||||
const onEvent = Effect.fnUntraced(function* (state: State, frame: string) {
|
||||
const event = yield* decodeEvent(frame)
|
||||
const format = event.output_format
|
||||
if ("partial_image_index" in event) {
|
||||
@@ -229,7 +229,7 @@ const onEvent = Effect.fn("OpenAIImages.onEvent")(function* (state: State, frame
|
||||
] as const
|
||||
})
|
||||
|
||||
const onDocument = Effect.fn("OpenAIImages.onDocument")(function* (frame: Exclude<Frame, string>) {
|
||||
const onDocument = Effect.fnUntraced(function* (frame: Exclude<Frame, string>) {
|
||||
const invalid = (message: string, cause?: unknown) => route.frameError(message, frame.document, cause)
|
||||
const decoded = yield* decodeDocument(frame.document).pipe(
|
||||
Effect.mapError((cause) => invalid(`${route.name} returned an invalid response`, cause)),
|
||||
|
||||
@@ -157,7 +157,7 @@ const nativeImageTool = (tool: ToolDefinition) => {
|
||||
return Schema.is(OpenAIResponsesImageGenerationTool)(native) ? native : undefined
|
||||
}
|
||||
|
||||
const lowerTool = Effect.fn("OpenAIResponses.lowerTool")(function* (tool: ToolDefinition) {
|
||||
const lowerTool = Effect.fnUntraced(function* (tool: ToolDefinition) {
|
||||
const native = nativeImageToolInput(tool)
|
||||
if (native !== undefined) {
|
||||
if (Schema.is(OpenAIResponsesImageGenerationTool)(native)) return native
|
||||
@@ -168,7 +168,7 @@ const lowerTool = Effect.fn("OpenAIResponses.lowerTool")(function* (tool: ToolDe
|
||||
|
||||
// Native namespaces hold only function tools, so deeper levels flatten into
|
||||
// the leaf names the same way non-native protocols flatten the whole tree.
|
||||
const lowerToolEntry = Effect.fn("OpenAIResponses.lowerToolEntry")(function* (tool: ToolEntry) {
|
||||
const lowerToolEntry = Effect.fnUntraced(function* (tool: ToolEntry) {
|
||||
if (tool.type === "tool") return yield* lowerTool(tool)
|
||||
// OpenAI requires a namespace description; fall back to a generic one so a
|
||||
// missing description never blocks the request.
|
||||
@@ -237,7 +237,7 @@ const checkpointBody = {
|
||||
}),
|
||||
}
|
||||
|
||||
const hostedToolResult = Effect.fn("OpenAIResponses.hostedToolResult")(function* (item: ResponsesHostedTools.Item) {
|
||||
const hostedToolResult = Effect.fnUntraced(function* (item: ResponsesHostedTools.Item) {
|
||||
const isError = item.error !== undefined && item.error !== null
|
||||
if (item.type === "image_generation_call" && item.result) {
|
||||
yield* Effect.fromResult(Encoding.decodeBase64(item.result)).pipe(
|
||||
|
||||
@@ -94,7 +94,7 @@ const fromRequest = Effect.fn("OpenAISpeech.fromRequest")(function* (request: Me
|
||||
|
||||
const isSse = (body: MediaProtocol.Body) => body.type === "json" && body.value.stream_format === "sse"
|
||||
|
||||
const onEvent = Effect.fn("OpenAISpeech.onEvent")(function* (state: State, frame: string) {
|
||||
const onEvent = Effect.fnUntraced(function* (state: State, frame: string) {
|
||||
const event = yield* decodeEvent(frame)
|
||||
if (event.type === "speech.audio.delta") return SpeechStream.delta(state, event.audio)
|
||||
const usage = event.usage
|
||||
|
||||
@@ -210,7 +210,7 @@ const segment = (value: Schema.Schema.Type<typeof Segment>): TranscriptionSegmen
|
||||
speaker: value.speaker,
|
||||
})
|
||||
|
||||
const onEvent = Effect.fn("OpenAITranscription.onEvent")(function* (state: State, frame: string) {
|
||||
const onEvent = Effect.fnUntraced(function* (state: State, frame: string) {
|
||||
if (!EVENT_TYPES.has((yield* decodeEventType(frame)).type)) return [state, []] as const
|
||||
const event = yield* decodeEvent(frame)
|
||||
if (event.type === "error")
|
||||
|
||||
@@ -154,7 +154,7 @@ export const wrapSystemUpdate = (parts: ReadonlyArray<{ readonly text: string }>
|
||||
* raw retrieved, tool, or web content into privileged updates: keep untrusted
|
||||
* data in ordinary user/tool messages instead.
|
||||
*/
|
||||
export const systemUpdateText = Effect.fn("ProviderShared.systemUpdateText")(function* (
|
||||
export const systemUpdateText = Effect.fnUntraced(function* (
|
||||
route: string,
|
||||
message: LLMRequest["messages"][number],
|
||||
) {
|
||||
@@ -167,7 +167,7 @@ export const systemUpdateText = Effect.fn("ProviderShared.systemUpdateText")(fun
|
||||
})
|
||||
|
||||
/** Lower an unsupported privileged update into visible, in-order user text. */
|
||||
export const wrappedSystemUpdate = Effect.fn("ProviderShared.wrappedSystemUpdate")(function* (
|
||||
export const wrappedSystemUpdate = Effect.fnUntraced(function* (
|
||||
route: string,
|
||||
message: LLMRequest["messages"][number],
|
||||
) {
|
||||
|
||||
@@ -82,7 +82,7 @@ const endpoint = (model: string) => (model.startsWith("sd3") ? "sd3" : model)
|
||||
|
||||
const RESERVED_FORM_FIELDS = new Set(["image", "prompt", "mode", "model"])
|
||||
|
||||
const form = Effect.fn("StabilityImages.form")(function* (
|
||||
const form = Effect.fnUntraced(function* (
|
||||
identity: MediaProtocol.Identity,
|
||||
fields: Record<string, unknown>,
|
||||
native: Record<string, unknown> | undefined,
|
||||
|
||||
@@ -76,7 +76,7 @@ function documentName(filename: string | undefined, names: Set<string>) {
|
||||
return name
|
||||
}
|
||||
|
||||
const mediaBase64 = Effect.fn("BedrockMedia.mediaBase64")(function* (part: MediaPart) {
|
||||
const mediaBase64 = Effect.fnUntraced(function* (part: MediaPart) {
|
||||
const media = yield* ProviderShared.requireInlineMedia("Bedrock Converse", part.media)
|
||||
const bytes = yield* Effect.fromResult(Encoding.decodeBase64(media.base64)).pipe(
|
||||
Effect.mapError((cause) =>
|
||||
@@ -91,7 +91,7 @@ const mediaBase64 = Effect.fn("BedrockMedia.mediaBase64")(function* (part: Media
|
||||
// document block. Image MIME types not in `IMAGE_FORMATS` (e.g. `image/svg+xml`)
|
||||
// get an image-specific error so the caller knows it's a format-support issue,
|
||||
// not a kind-detection issue.
|
||||
export const lower = Effect.fn("BedrockMedia.lower")(function* (part: MediaPart, documentNames: Set<string>) {
|
||||
export const lower = Effect.fnUntraced(function* (part: MediaPart, documentNames: Set<string>) {
|
||||
const mime = part.media.mediaType.toLowerCase()
|
||||
const imageFormat = IMAGE_FORMATS[mime as keyof typeof IMAGE_FORMATS]
|
||||
if (imageFormat) {
|
||||
|
||||
@@ -11,7 +11,7 @@ interface State {
|
||||
readonly responseID?: string
|
||||
}
|
||||
|
||||
const onOutputItem = Effect.fn("ResponsesCheckpoint.onOutputItem")(function* (
|
||||
const onOutputItem = Effect.fnUntraced(function* (
|
||||
state: State,
|
||||
input: OpenResponses.Event,
|
||||
) {
|
||||
@@ -63,7 +63,7 @@ export const make = <Body>(body: RouteBody<Body>): TriggerCompactOperation =>
|
||||
checkpoints: {},
|
||||
}),
|
||||
terminal: OpenResponses.terminal,
|
||||
step: Effect.fn("ResponsesCheckpoint.step")(function* (state: State, event: OpenResponses.Event) {
|
||||
step: Effect.fnUntraced(function* (state: State, event: OpenResponses.Event) {
|
||||
if (event.response?.id && state.responseID && event.response.id !== state.responseID)
|
||||
return yield* ProviderShared.eventError(source.id, "Compaction response ID changed during execution")
|
||||
if (event.type === "response.created") return [{ ...state, responseID: event.response?.id }, []] as const
|
||||
|
||||
@@ -33,7 +33,7 @@ export const onDone: (
|
||||
state: OpenResponses.ParserState,
|
||||
item: Item,
|
||||
tools: Definitions,
|
||||
) => Effect.Effect<OpenResponses.StepResult, AIError> = Effect.fn("ResponsesHostedTools.onDone")(
|
||||
) => Effect.Effect<OpenResponses.StepResult, AIError> = Effect.fnUntraced(
|
||||
function* (state, item, tools) {
|
||||
const tool = tools[item.type]
|
||||
if (!tool) return [state, []] satisfies OpenResponses.StepResult
|
||||
|
||||
@@ -60,14 +60,21 @@ const inputStart = (tool: PendingTool) =>
|
||||
providerMetadata: tool.providerMetadata,
|
||||
})
|
||||
|
||||
const inputDelta = (tool: PendingTool, text: string) =>
|
||||
LLMEvent.toolInputDelta({
|
||||
id: tool.id,
|
||||
name: tool.name,
|
||||
namespace: tool.namespace,
|
||||
text,
|
||||
input: Option.getOrElse(parsePartialInput(tool.input), () => ({})),
|
||||
})
|
||||
const inputDelta = (tool: PendingTool, text: string): LLMEvent => {
|
||||
const raw = tool.input
|
||||
let parsed: unknown
|
||||
return {
|
||||
...LLMEvent.toolInputDelta({
|
||||
id: tool.id,
|
||||
name: tool.name,
|
||||
namespace: tool.namespace,
|
||||
text,
|
||||
}),
|
||||
get input() {
|
||||
return (parsed ??= Option.getOrElse(parsePartialInput(raw), () => ({})))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const toolCall = (route: string, tool: PendingTool, inputOverride?: string) => {
|
||||
const raw = inputOverride ?? tool.input
|
||||
|
||||
@@ -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"
|
||||
@@ -1,15 +1,20 @@
|
||||
import { Route, type RouteDefaultsInput } from "../route/client.js"
|
||||
import { Endpoint } from "../route/endpoint.js"
|
||||
import type { ProviderPackage } from "../provider-package.js"
|
||||
import { AnthropicMessages } from "../protocols/anthropic-messages.js"
|
||||
import { OpenAIChat } from "../protocols/openai-chat.js"
|
||||
import { OpenResponses } from "../protocols/open-responses.js"
|
||||
import { BedrockAuth, type Credentials } from "../protocols/utils/bedrock-auth.js"
|
||||
import { claudeVersion } from "../protocols/utils/claude-model.js"
|
||||
import { ProviderConfigurationError, ProviderID, type ModelID } from "../schema/index.js"
|
||||
import { withOpenAIOptions, type OpenAIProviderOptionsInput } from "./openai-options.js"
|
||||
|
||||
export const id = ProviderID.make("amazon-bedrock")
|
||||
|
||||
export type Config = RouteDefaultsInput & {
|
||||
export type OpenAIOptionsInput = OpenAIProviderOptionsInput
|
||||
export type MessagesOptionsInput = AnthropicMessages.ProviderOptionsInput
|
||||
|
||||
export type Config = Omit<RouteDefaultsInput, "providerOptions"> & {
|
||||
/** Bedrock API key. Falls back to `AWS_BEARER_TOKEN_BEDROCK`; bearer auth takes precedence over SigV4. */
|
||||
readonly apiKey?: string
|
||||
/** `sigv4` ignores `apiKey` fallbacks from the environment; `bearer` requires a token. */
|
||||
@@ -20,11 +25,11 @@ export type Config = RouteDefaultsInput & {
|
||||
/** Shared config profile for the default credential chain. */
|
||||
readonly profile?: string
|
||||
readonly region?: string
|
||||
readonly providerOptions?: OpenAIProviderOptionsInput
|
||||
readonly providerOptions?: OpenAIProviderOptionsInput | AnthropicMessages.ProviderOptionsInput
|
||||
}
|
||||
|
||||
export type Settings = ProviderPackage.Settings &
|
||||
OpenAIProviderOptionsInput & {
|
||||
export type Settings<Options = OpenAIProviderOptionsInput> = ProviderPackage.Settings &
|
||||
Options & {
|
||||
readonly apiKey?: string
|
||||
readonly auth?: "bearer" | "sigv4"
|
||||
readonly baseURL?: string
|
||||
@@ -34,6 +39,8 @@ export type Settings = ProviderPackage.Settings &
|
||||
readonly topP?: number
|
||||
}
|
||||
|
||||
export type MessagesSettings = Settings<AnthropicMessages.ProviderOptionsInput>
|
||||
|
||||
const responsesRoute = Route.make({
|
||||
id: "bedrock-mantle-responses",
|
||||
provider: id,
|
||||
@@ -50,12 +57,35 @@ const chatRoute = OpenAIChat.route.with({
|
||||
providerMetadataKey: "mantle",
|
||||
})
|
||||
|
||||
export const routes = [responsesRoute, chatRoute]
|
||||
const messagesRoute = Route.make({
|
||||
id: "bedrock-mantle-messages",
|
||||
provider: id,
|
||||
providerMetadataKey: "mantle",
|
||||
protocol: {
|
||||
...AnthropicMessages.protocol,
|
||||
// Mantle rejects mid-conversation `output_config` on Opus 5.0; support starts at 5.1+.
|
||||
supportsEffortUpdates: (request) => {
|
||||
const override = request.model.compatibility?.supportsEffortUpdates
|
||||
if (override !== undefined) return override
|
||||
const version = claudeVersion(request.model.id)
|
||||
return version !== undefined && (version.major > 5 || (version.major === 5 && version.minor >= 1))
|
||||
},
|
||||
},
|
||||
endpoint: Endpoint.path(AnthropicMessages.PATH),
|
||||
transport: AnthropicMessages.transport<AnthropicMessages.AnthropicMessagesBody>(),
|
||||
headers: () => ({ "anthropic-version": "2023-06-01" }),
|
||||
})
|
||||
|
||||
const configuredRoute = <Body, Prepared>(route: Route<Body, Prepared>, input: Config) => {
|
||||
export const routes = [responsesRoute, chatRoute, messagesRoute]
|
||||
|
||||
const configuredRoute = <Body, Prepared>(
|
||||
route: Route<Body, Prepared>,
|
||||
input: Config,
|
||||
defaultBaseURL = (region: string) => `https://bedrock-mantle.${region}.api.aws/v1`,
|
||||
) => {
|
||||
const region = BedrockAuth.resolveRegion(input)
|
||||
return route.with({
|
||||
endpoint: { baseURL: input.baseURL ?? `https://bedrock-mantle.${region}.api.aws/v1` },
|
||||
endpoint: { baseURL: input.baseURL ?? defaultBaseURL(region) },
|
||||
auth: BedrockAuth.resolveAuth(input, region, {
|
||||
service: "bedrock-mantle",
|
||||
name: "Bedrock Mantle",
|
||||
@@ -87,6 +117,11 @@ export const configure = (input: Config = {}) => {
|
||||
})
|
||||
const configuredResponsesRoute = configuredRoute(responsesRoute, input)
|
||||
const configuredChatRoute = configuredRoute(chatRoute, input)
|
||||
const configuredMessagesRoute = configuredRoute(
|
||||
messagesRoute,
|
||||
input,
|
||||
(region) => `https://bedrock-mantle.${region}.api.aws/anthropic/v1`,
|
||||
)
|
||||
const modelDefaults = defaults(input)
|
||||
const responses = (modelID: string | ModelID) =>
|
||||
configuredResponsesRoute
|
||||
@@ -96,11 +131,14 @@ export const configure = (input: Config = {}) => {
|
||||
configuredChatRoute
|
||||
.with(withOpenAIOptions(modelID, modelDefaults))
|
||||
.model<OpenAIProviderOptionsInput>({ id: modelID })
|
||||
const messages = (modelID: string | ModelID) =>
|
||||
configuredMessagesRoute.with(modelDefaults).model<AnthropicMessages.ProviderOptionsInput>({ id: modelID })
|
||||
|
||||
return {
|
||||
id,
|
||||
model: responses,
|
||||
chat,
|
||||
messages,
|
||||
responses,
|
||||
configure,
|
||||
}
|
||||
@@ -119,7 +157,7 @@ const fromSettings = ({
|
||||
region,
|
||||
topP,
|
||||
...providerOptions
|
||||
}: Settings) =>
|
||||
}: Settings<Config["providerOptions"]>) =>
|
||||
configure({
|
||||
apiKey,
|
||||
auth,
|
||||
@@ -137,6 +175,10 @@ export const chatModel: ProviderPackage.Definition<Settings, OpenAIProviderOptio
|
||||
modelID,
|
||||
settings,
|
||||
) => fromSettings(settings).chat(modelID)
|
||||
export const messagesModel: ProviderPackage.Definition<
|
||||
MessagesSettings,
|
||||
AnthropicMessages.ProviderOptionsInput
|
||||
>["model"] = (modelID, settings) => fromSettings(settings).messages(modelID)
|
||||
export const responsesModel: ProviderPackage.Definition<Settings, OpenAIProviderOptionsInput>["model"] = (
|
||||
modelID,
|
||||
settings,
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export { messagesModel as model } from "../../amazon-bedrock-mantle.js"
|
||||
export type { MessagesSettings as Settings } from "../../amazon-bedrock-mantle.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"
|
||||
@@ -50,7 +50,7 @@ export const gpt5DefaultOptions = (modelID: string): ProviderOptions | undefined
|
||||
export const openAIDefaultOptions = (modelID: string): ProviderOptions | undefined =>
|
||||
mergeProviderOptions(openAIProviderOptions({ store: false }), gpt5DefaultOptions(modelID))
|
||||
|
||||
export const withOpenAIOptions = <Options extends { readonly providerOptions?: OpenAIProviderOptionsInput }>(
|
||||
export const withOpenAIOptions = <Options extends { readonly providerOptions?: ProviderOptions }>(
|
||||
modelID: string,
|
||||
options: Options,
|
||||
): Omit<Options, "providerOptions"> & { readonly providerOptions?: ProviderOptions } => {
|
||||
|
||||
@@ -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"
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Effect, Stream } from "effect"
|
||||
import { makeParser, type Event } from "effect/unstable/encoding/Sse"
|
||||
import { makeParser } from "effect/unstable/encoding/Sse"
|
||||
import { AIError, InvalidProviderOutputError } from "../schema/index.js"
|
||||
|
||||
/**
|
||||
@@ -42,43 +42,39 @@ export const sseFraming = (
|
||||
Stream.decodeText(),
|
||||
Stream.mapAccumEffect(
|
||||
() => {
|
||||
const output: Event[] = []
|
||||
const output: string[] = []
|
||||
return {
|
||||
output,
|
||||
parser: makeParser((event) => {
|
||||
if (event._tag === "Event") output.push(event)
|
||||
if (
|
||||
event._tag === "Event" &&
|
||||
(events === undefined || events.has(event.event)) &&
|
||||
event.data.length > 0 &&
|
||||
// Some OpenAI-compatible proxies serialize an empty flush as a bare
|
||||
// `data: null`, between events or after `[DONE]`. No protocol has a
|
||||
// null event, so it carries nothing and must not abort the stream.
|
||||
event.data !== "null" &&
|
||||
// Vertex AI partner models (e.g. `xai/grok-4.6`) send their SSE
|
||||
// keepalive comment as `data: : keepalive` while reasoning.
|
||||
event.data !== ": keepalive" &&
|
||||
(event.data !== "[DONE]" || includeDone || (events !== undefined && event.event !== "message"))
|
||||
)
|
||||
output.push(event.data)
|
||||
}),
|
||||
}
|
||||
},
|
||||
(state, chunk) =>
|
||||
Effect.gen(function* () {
|
||||
const error = state.parser.feed(chunk)
|
||||
if (error)
|
||||
return yield* new AIError({
|
||||
reason: new InvalidProviderOutputError({
|
||||
route: "sse",
|
||||
message: error.message,
|
||||
body: chunk,
|
||||
cause: error,
|
||||
}),
|
||||
})
|
||||
return [state, state.output.splice(0)] as const
|
||||
}),
|
||||
(state, chunk) => {
|
||||
const error = state.parser.feed(chunk)
|
||||
if (!error) return Effect.succeed([state, state.output.splice(0)] as const)
|
||||
const reason = new InvalidProviderOutputError({
|
||||
route: "sse",
|
||||
message: error.message,
|
||||
body: chunk,
|
||||
cause: error,
|
||||
})
|
||||
return Effect.fail(new AIError({ reason }))
|
||||
},
|
||||
),
|
||||
Stream.filter(
|
||||
(event) =>
|
||||
(events === undefined || events.has(event.event)) &&
|
||||
event.data.length > 0 &&
|
||||
// Some OpenAI-compatible proxies serialize an empty flush as a bare
|
||||
// `data: null`, between events or after `[DONE]`. No protocol has a
|
||||
// null event, so it carries nothing and must not abort the stream.
|
||||
event.data !== "null" &&
|
||||
// Vertex AI partner models (e.g. `xai/grok-4.6`) send their SSE
|
||||
// keepalive comment as `data: : keepalive` while reasoning.
|
||||
event.data !== ": keepalive" &&
|
||||
(event.data !== "[DONE]" || includeDone || (events !== undefined && event.event !== "message")),
|
||||
),
|
||||
Stream.map((event) => event.data),
|
||||
)
|
||||
|
||||
/** Server-Sent Events framing. Used by every JSON-streaming HTTP provider. */
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import { LLM, LLMRequest, Message, ToolCallPart } from "../src/index.js"
|
||||
import { LLM, Message, ToolCallPart } from "../src/index.js"
|
||||
import { Auth, LLMClient } from "../src/route.js"
|
||||
import { compileRequest } from "../src/route/client.js"
|
||||
import { AnthropicMessages } from "../src/protocols/anthropic-messages.js"
|
||||
import { OpenAIResponses } from "../src/protocols/openai-responses.js"
|
||||
import { Gemini } from "../src/protocols/gemini.js"
|
||||
import { GoogleVertexMessages, OpenAI } from "../src/providers.js"
|
||||
import { applyCachePolicy } from "../src/cache-policy.js"
|
||||
import { AmazonBedrockMantle, GoogleVertexMessages, OpenAI } from "../src/providers.js"
|
||||
import { applyEffortUpdates } from "../src/effort-updates.js"
|
||||
import { it, testEffect } from "./lib/effect.js"
|
||||
import { dynamicResponse } from "./lib/http.js"
|
||||
@@ -172,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(
|
||||
@@ -242,6 +268,31 @@ describe("Anthropic Messages effort updates", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("strips markers for Opus 5.0 on Bedrock Mantle Messages while lowering Opus 5.5", () =>
|
||||
Effect.gen(function* () {
|
||||
const mantle = AmazonBedrockMantle.configure({ apiKey: "test", region: "us-east-1" })
|
||||
const opus50 = yield* compileRequest(
|
||||
LLM.request({
|
||||
model: mantle.messages("anthropic.claude-opus-5"),
|
||||
messages: conversation,
|
||||
providerOptions: { effort: "low" },
|
||||
}),
|
||||
)
|
||||
const opus55 = yield* compileRequest(
|
||||
LLM.request({
|
||||
model: mantle.messages("anthropic.claude-opus-5-5"),
|
||||
messages: conversation,
|
||||
providerOptions: { effort: "low" },
|
||||
}),
|
||||
)
|
||||
|
||||
expect(systemMessages(opus50.body)).toHaveLength(0)
|
||||
expect(opus50.body.output_config).toEqual({ effort: "low" })
|
||||
expect(systemMessages(opus55.body)).toEqual([{ role: "system", content: [], output_config: { effort: "low" } }])
|
||||
expect(opus55.body.output_config).toEqual({ effort: "high" })
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("strips markers on the Vertex Anthropic route, whose protocol wrapper does not forward support", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
|
||||
-29
@@ -1,29 +0,0 @@
|
||||
{
|
||||
"version": 1,
|
||||
"metadata": {
|
||||
"name": "anthropic-messages/accepts-malformed-assistant-tool-order-with-default-patch",
|
||||
"recordedAt": "2026-05-05T20:09:16.245Z",
|
||||
"tags": ["prefix:anthropic-messages", "provider:anthropic", "protocol:anthropic-messages", "tool"]
|
||||
},
|
||||
"interactions": [
|
||||
{
|
||||
"transport": "http",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "https://api.anthropic.com/v1/messages",
|
||||
"headers": {
|
||||
"anthropic-version": "2023-06-01",
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": "{\"model\":\"claude-haiku-4-5-20251001\",\"messages\":[{\"role\":\"assistant\",\"content\":[{\"type\":\"text\",\"text\":\"I will check the weather.\"}]},{\"role\":\"assistant\",\"content\":[{\"type\":\"tool_use\",\"id\":\"call_1\",\"name\":\"get_weather\",\"input\":{\"city\":\"Paris\"}}]},{\"role\":\"user\",\"content\":[{\"type\":\"tool_result\",\"tool_use_id\":\"call_1\",\"content\":\"{\\\"temperature\\\":\\\"72F\\\"}\"}]},{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"Use that result to answer briefly.\",\"cache_control\":{\"type\":\"ephemeral\"}}]}],\"tools\":[{\"name\":\"get_weather\",\"description\":\"Get weather\",\"input_schema\":{\"type\":\"object\",\"properties\":{}}}],\"stream\":true,\"max_tokens\":4096}"
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"content-type": "text/event-stream; charset=utf-8"
|
||||
},
|
||||
"body": "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"model\":\"claude-haiku-4-5-20251001\",\"id\":\"msg_01SikJVFaMR1XLMtavUhvuog\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"stop_reason\":null,\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":638,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":1,\"service_tier\":\"standard\",\"inference_geo\":\"not_available\"}} }\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"\"} }\n\nevent: ping\ndata: {\"type\": \"ping\"}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"The\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\" weather in Paris is currently 72°F.\"} }\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0}\n\nevent: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"end_turn\",\"stop_sequence\":null,\"stop_details\":null},\"usage\":{\"input_tokens\":638,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"output_tokens\":14} }\n\nevent: message_stop\ndata: {\"type\":\"message_stop\" }\n\n"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
-56
@@ -1,56 +0,0 @@
|
||||
{
|
||||
"version": 1,
|
||||
"metadata": {
|
||||
"name": "anthropic-messages/claude-opus-4-7-drives-a-tool-loop",
|
||||
"recordedAt": "2026-05-03T19:59:44.186Z",
|
||||
"tags": [
|
||||
"prefix:anthropic-messages",
|
||||
"provider:anthropic",
|
||||
"protocol:anthropic-messages",
|
||||
"tool",
|
||||
"tool-loop",
|
||||
"golden",
|
||||
"flagship"
|
||||
]
|
||||
},
|
||||
"interactions": [
|
||||
{
|
||||
"transport": "http",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "https://api.anthropic.com/v1/messages",
|
||||
"headers": {
|
||||
"anthropic-version": "2023-06-01",
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": "{\"model\":\"claude-opus-4-7\",\"system\":[{\"type\":\"text\",\"text\":\"Use the get_weather tool, then answer in one short sentence.\"}],\"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"What is the weather in Paris?\"}]}],\"tools\":[{\"name\":\"get_weather\",\"description\":\"Get current weather for a city.\",\"input_schema\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"],\"additionalProperties\":false}}],\"stream\":true,\"max_tokens\":80}"
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"content-type": "text/event-stream; charset=utf-8"
|
||||
},
|
||||
"body": "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"model\":\"claude-opus-4-7\",\"id\":\"msg_01DgAEgLgB1ZhavZon4qGE1t\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"stop_reason\":null,\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":798,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":0,\"service_tier\":\"standard\",\"inference_geo\":\"global\"}} }\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"tool_use\",\"id\":\"toolu_01M8nJQQMxqpv1VaPYuJKT4j\",\"name\":\"get_weather\",\"input\":{},\"caller\":{\"type\":\"direct\"}} }\n\nevent: ping\ndata: {\"type\": \"ping\"}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"{\\\"city\\\": \"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"\\\"Pa\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"ris\\\"}\"} }\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0 }\n\nevent: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"tool_use\",\"stop_sequence\":null,\"stop_details\":null},\"usage\":{\"input_tokens\":798,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"output_tokens\":66} }\n\nevent: message_stop\ndata: {\"type\":\"message_stop\" }\n\n"
|
||||
}
|
||||
},
|
||||
{
|
||||
"transport": "http",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "https://api.anthropic.com/v1/messages",
|
||||
"headers": {
|
||||
"anthropic-version": "2023-06-01",
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": "{\"model\":\"claude-opus-4-7\",\"system\":[{\"type\":\"text\",\"text\":\"Use the get_weather tool, then answer in one short sentence.\"}],\"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"What is the weather in Paris?\"}]},{\"role\":\"assistant\",\"content\":[{\"type\":\"tool_use\",\"id\":\"toolu_01M8nJQQMxqpv1VaPYuJKT4j\",\"name\":\"get_weather\",\"input\":{\"city\":\"Paris\"}}]},{\"role\":\"user\",\"content\":[{\"type\":\"tool_result\",\"tool_use_id\":\"toolu_01M8nJQQMxqpv1VaPYuJKT4j\",\"content\":\"{\\\"temperature\\\":22,\\\"condition\\\":\\\"sunny\\\"}\"}]}],\"tools\":[{\"name\":\"get_weather\",\"description\":\"Get current weather for a city.\",\"input_schema\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"],\"additionalProperties\":false}}],\"stream\":true,\"max_tokens\":80}"
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"content-type": "text/event-stream; charset=utf-8"
|
||||
},
|
||||
"body": "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"model\":\"claude-opus-4-7\",\"id\":\"msg_011KJqj32QjkrUAiBFxhmEoG\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"stop_reason\":null,\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":895,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":5,\"service_tier\":\"standard\",\"inference_geo\":\"global\"}} }\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"\"} }\n\nevent: ping\ndata: {\"type\": \"ping\"}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"Paris is curr\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"ently sunny at 22°C.\"} }\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0 }\n\nevent: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"end_turn\",\"stop_sequence\":null,\"stop_details\":null},\"usage\":{\"input_tokens\":895,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"output_tokens\":19}}\n\nevent: message_stop\ndata: {\"type\":\"message_stop\"}\n\n"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
-29
@@ -1,29 +0,0 @@
|
||||
{
|
||||
"version": 1,
|
||||
"metadata": {
|
||||
"name": "anthropic-messages/rejects-malformed-assistant-tool-order-without-patch",
|
||||
"recordedAt": "2026-05-05T20:08:42.597Z",
|
||||
"tags": ["prefix:anthropic-messages", "provider:anthropic", "protocol:anthropic-messages", "tool", "sad-path"]
|
||||
},
|
||||
"interactions": [
|
||||
{
|
||||
"transport": "http",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "https://api.anthropic.com/v1/messages",
|
||||
"headers": {
|
||||
"anthropic-version": "2023-06-01",
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": "{\"model\":\"claude-haiku-4-5-20251001\",\"messages\":[{\"role\":\"assistant\",\"content\":[{\"type\":\"tool_use\",\"id\":\"call_1\",\"name\":\"get_weather\",\"input\":{\"city\":\"Paris\"}},{\"type\":\"text\",\"text\":\"I will check the weather.\"}]},{\"role\":\"user\",\"content\":[{\"type\":\"tool_result\",\"tool_use_id\":\"call_1\",\"content\":\"{\\\"temperature\\\":\\\"72F\\\"}\"}]},{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"Use that result to answer briefly.\"}]}],\"tools\":[{\"name\":\"get_weather\",\"description\":\"Get weather\",\"input_schema\":{\"type\":\"object\",\"properties\":{}}}],\"stream\":true,\"max_tokens\":4096}"
|
||||
},
|
||||
"response": {
|
||||
"status": 400,
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": "{\"type\":\"error\",\"error\":{\"type\":\"invalid_request_error\",\"message\":\"messages.1: `tool_use` ids were found without `tool_result` blocks immediately after: call_1. Each `tool_use` block must have a corresponding `tool_result` block in the next message.\"},\"request_id\":\"req_011Cak2XdJgnzxKCY2BC2Beh\"}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
{
|
||||
"version": 1,
|
||||
"metadata": {
|
||||
"name": "anthropic-messages/streams-text",
|
||||
"recordedAt": "2026-04-28T21:18:45.535Z",
|
||||
"tags": ["prefix:anthropic-messages", "provider:anthropic", "protocol:anthropic-messages"]
|
||||
},
|
||||
"interactions": [
|
||||
{
|
||||
"transport": "http",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "https://api.anthropic.com/v1/messages",
|
||||
"headers": {
|
||||
"anthropic-version": "2023-06-01",
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": "{\"model\":\"claude-haiku-4-5-20251001\",\"system\":[{\"type\":\"text\",\"text\":\"You are concise.\"}],\"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"Reply with exactly: Hello!\"}]}],\"stream\":true,\"max_tokens\":20,\"temperature\":0}"
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"content-type": "text/event-stream; charset=utf-8"
|
||||
},
|
||||
"body": "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"model\":\"claude-haiku-4-5-20251001\",\"id\":\"msg_01UodR8c3ezAK8rAfi8HAs8g\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"stop_reason\":null,\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":18,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":2,\"service_tier\":\"standard\",\"inference_geo\":\"not_available\"}} }\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"\"} }\n\nevent: ping\ndata: {\"type\": \"ping\"}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"Hello!\"} }\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0 }\n\nevent: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"end_turn\",\"stop_sequence\":null,\"stop_details\":null},\"usage\":{\"input_tokens\":18,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"output_tokens\":5} }\n\nevent: message_stop\ndata: {\"type\":\"message_stop\" }\n\n"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
-29
@@ -1,29 +0,0 @@
|
||||
{
|
||||
"version": 1,
|
||||
"metadata": {
|
||||
"name": "anthropic-messages/streams-tool-call",
|
||||
"recordedAt": "2026-04-28T21:18:46.878Z",
|
||||
"tags": ["prefix:anthropic-messages", "provider:anthropic", "protocol:anthropic-messages", "tool"]
|
||||
},
|
||||
"interactions": [
|
||||
{
|
||||
"transport": "http",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "https://api.anthropic.com/v1/messages",
|
||||
"headers": {
|
||||
"anthropic-version": "2023-06-01",
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": "{\"model\":\"claude-haiku-4-5-20251001\",\"system\":[{\"type\":\"text\",\"text\":\"Call tools exactly as requested.\"}],\"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"Call get_weather with city exactly Paris.\"}]}],\"tools\":[{\"name\":\"get_weather\",\"description\":\"Get current weather for a city.\",\"input_schema\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"],\"additionalProperties\":false}}],\"tool_choice\":{\"type\":\"tool\",\"name\":\"get_weather\"},\"stream\":true,\"max_tokens\":80,\"temperature\":0}"
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"content-type": "text/event-stream; charset=utf-8"
|
||||
},
|
||||
"body": "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"model\":\"claude-haiku-4-5-20251001\",\"id\":\"msg_01RYgU7NUPMK4B9v8S7gVpCS\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"stop_reason\":null,\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":677,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":16,\"service_tier\":\"standard\",\"inference_geo\":\"not_available\"}} }\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"tool_use\",\"id\":\"toolu_012rmAruviySvUXSjgCPWVRu\",\"name\":\"get_weather\",\"input\":{},\"caller\":{\"type\":\"direct\"}} }\n\nevent: ping\ndata: {\"type\": \"ping\"}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"{\\\"city\\\":\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\" \\\"Paris\\\"}\"} }\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0 }\n\nevent: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"tool_use\",\"stop_sequence\":null,\"stop_details\":null},\"usage\":{\"input_tokens\":677,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"output_tokens\":33} }\n\nevent: message_stop\ndata: {\"type\":\"message_stop\" }\n\n"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"version": 1,
|
||||
"metadata": {
|
||||
"model": "anthropic.claude-opus-5-5",
|
||||
"tags": [
|
||||
"prefix:bedrock-mantle-messages",
|
||||
"provider:amazon-bedrock",
|
||||
"protocol:anthropic-messages",
|
||||
"reasoning",
|
||||
"effort-update"
|
||||
],
|
||||
"name": "bedrock-mantle-messages/applies-mid-conversation-effort-updates-and-thinking-block-binding-on-opus-5-5",
|
||||
"recordedAt": "2026-10-04T03:56:23.139Z"
|
||||
},
|
||||
"interactions": [
|
||||
{
|
||||
"transport": "http",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "https://bedrock-mantle.us-east-1.api.aws/anthropic/v1/messages",
|
||||
"headers": {
|
||||
"anthropic-beta": "interleaved-thinking-2025-05-14,thinking-binding-controls-2026-08-01",
|
||||
"anthropic-version": "2023-06-01",
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": "{\"model\":\"anthropic.claude-opus-5-5\",\"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"Compute 37 * 43 step by step, then reply with only the integer.\",\"cache_control\":{\"type\":\"ephemeral\"}}]}],\"stream\":true,\"max_tokens\":2048,\"thinking\":{\"type\":\"adaptive\",\"display\":\"summarized\",\"block_binding\":{\"prefix_mismatch_behavior\":\"drop_block\"}},\"output_config\":{\"effort\":\"high\"}}"
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"content-type": "text/event-stream; charset=utf-8"
|
||||
},
|
||||
"body": "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"model\":\"claude-opus-5-5\",\"id\":\"msg_bdrk_y7h2x5bz6w6ggmekuxekbi6rseptglsljkwgj36phisnsnu3syea\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"container\":null,\"stop_reason\":null,\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":31,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":2,\"service_tier\":\"standard\"},\"input_transformations\":[]}}\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"thinking\",\"thinking\":\"\",\"signature\":\"\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"They\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" want\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" only\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" the final\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" integer, so\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" I just\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" need\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" to compute 37\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"×43 \"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"using the dif\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"ference of squares: 40\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"²−3²\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" = 1600−9 =\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" 1591.\\n\\n\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"signature_delta\",\"signature\":\"CAQSogUKEggSEAEYAjgBQgh0aGlua2luZxIM48GlQ7IvNDW3jk2BGgw0Aq5cIs+m8gjYEIQiMFOyG984KYDLRzchzn2ApX6Rs3r7+CtXL0KeCRsXr1XKY3Y0UNK73Qrjyk94TGFPTSq9BCCZALbMiaeiIDqgB3H8jfrU26rP6kORoTJuF84ByBhXGI9AGo30c3RU2ZueXE/oLvRkK12g8r3TPql2fam1gcC/SeZ627fqGykV6G8WrPH7kthjbR45vnRuaqgFZmyA4C/XCxEH0zSSlVAhXf3Um9sjO/RD8lMJttQCAJSurbB5wmKbUQmZeOjIn8augHV/KmsGcFaHBjGLNfnwfEUwLEbRjNh5eIsmcs9dFfIeYHYNRgtLnBkERACoKPQLp2xU090/wCwrFHIGgLYbklR5wUhGqrAERhOAybqMxlNN3dwd7dV5Tbd16xEghDWb6wUtoxnNQdsUsV/0F/b8Ov9TSu93db/QJAkR74OBMtPuEu914SxtC4NBtqX5YZQAo//O4piaQGzFRVE7TahKzMS5bD111w/qz/cLpqEI/bZMqsXKdzBuMMOAox9TvgTumemdiiPu8tn2ONQeAnuNLjArrr7Jz1mbE9GqPl6aJW9HYWPb0BxCQ/hRXFz5d0v+okmmn5KCV2nD55wk2ySWz9OxIMQUsJ6uEUVOZPk7O0ZJ2ppoLAx6GYq2f55nNcUCsbKB4gB4+QlNp3Tf5IemDdZaY/tGW71dgpYI7iWYYEhqNtlC8pXpoEIbO31IvAHS9BGeQRYQQh1h0muEz4S1+YICPTdXnUmjc0uJ9MGBIKOuBsUhg+E4dzZzOfwS4KteNmJgjhj53WVdjJgRslemZle0tZphrTLLgvZpLZDVIYmOkIpbo2tf5Cflx/rf/NPg7RgB\"}}\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0}\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":1,\"content_block\":{\"type\":\"text\",\"text\":\"\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"text_delta\",\"text\":\"1591\"}}\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":1}\n\nevent: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"end_turn\",\"stop_sequence\":null,\"stop_details\":null,\"container\":null},\"usage\":{\"input_tokens\":31,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"output_tokens\":86,\"output_tokens_details\":{\"thinking_tokens\":82}}}\n\nevent: message_stop\ndata: {\"type\":\"message_stop\"}\n\n"
|
||||
}
|
||||
},
|
||||
{
|
||||
"transport": "http",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "https://bedrock-mantle.us-east-1.api.aws/anthropic/v1/messages",
|
||||
"headers": {
|
||||
"anthropic-beta": "interleaved-thinking-2025-05-14,mid-conversation-output-config-2026-07-01,thinking-binding-controls-2026-08-01",
|
||||
"anthropic-version": "2023-06-01",
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": "{\"model\":\"anthropic.claude-opus-5-5\",\"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"Compute 37 * 43 step by step, then reply with only the integer.\"}]},{\"role\":\"assistant\",\"content\":[{\"type\":\"thinking\",\"thinking\":\"They want only the final integer, so I just need to compute 37×43 using the difference of squares: 40²−3² = 1600−9 = 1591.\\n\\n\",\"signature\":\"CAQSogUKEggSEAEYAjgBQgh0aGlua2luZxIM48GlQ7IvNDW3jk2BGgw0Aq5cIs+m8gjYEIQiMFOyG984KYDLRzchzn2ApX6Rs3r7+CtXL0KeCRsXr1XKY3Y0UNK73Qrjyk94TGFPTSq9BCCZALbMiaeiIDqgB3H8jfrU26rP6kORoTJuF84ByBhXGI9AGo30c3RU2ZueXE/oLvRkK12g8r3TPql2fam1gcC/SeZ627fqGykV6G8WrPH7kthjbR45vnRuaqgFZmyA4C/XCxEH0zSSlVAhXf3Um9sjO/RD8lMJttQCAJSurbB5wmKbUQmZeOjIn8augHV/KmsGcFaHBjGLNfnwfEUwLEbRjNh5eIsmcs9dFfIeYHYNRgtLnBkERACoKPQLp2xU090/wCwrFHIGgLYbklR5wUhGqrAERhOAybqMxlNN3dwd7dV5Tbd16xEghDWb6wUtoxnNQdsUsV/0F/b8Ov9TSu93db/QJAkR74OBMtPuEu914SxtC4NBtqX5YZQAo//O4piaQGzFRVE7TahKzMS5bD111w/qz/cLpqEI/bZMqsXKdzBuMMOAox9TvgTumemdiiPu8tn2ONQeAnuNLjArrr7Jz1mbE9GqPl6aJW9HYWPb0BxCQ/hRXFz5d0v+okmmn5KCV2nD55wk2ySWz9OxIMQUsJ6uEUVOZPk7O0ZJ2ppoLAx6GYq2f55nNcUCsbKB4gB4+QlNp3Tf5IemDdZaY/tGW71dgpYI7iWYYEhqNtlC8pXpoEIbO31IvAHS9BGeQRYQQh1h0muEz4S1+YICPTdXnUmjc0uJ9MGBIKOuBsUhg+E4dzZzOfwS4KteNmJgjhj53WVdjJgRslemZle0tZphrTLLgvZpLZDVIYmOkIpbo2tf5Cflx/rf/NPg7RgB\"},{\"type\":\"text\",\"text\":\"1591\"}]},{\"role\":\"system\",\"content\":[],\"output_config\":{\"effort\":\"low\"}},{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"Add 9 to that result. Reply with only the integer.\",\"cache_control\":{\"type\":\"ephemeral\"}}]}],\"stream\":true,\"max_tokens\":2048,\"thinking\":{\"type\":\"adaptive\",\"display\":\"summarized\",\"block_binding\":{\"prefix_mismatch_behavior\":\"drop_block\"}},\"output_config\":{\"effort\":\"high\"}}"
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"content-type": "text/event-stream; charset=utf-8"
|
||||
},
|
||||
"body": "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"model\":\"claude-opus-5-5\",\"id\":\"msg_bdrk_yzpmbqh3aag3i7kymdm7ctqputcpeuln3cy44msgbdnctkcweena\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"container\":null,\"stop_reason\":null,\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":139,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":4,\"service_tier\":\"standard\"},\"input_transformations\":[]}}\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"1600\"}}\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0}\n\nevent: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"end_turn\",\"stop_sequence\":null,\"stop_details\":null,\"container\":null},\"usage\":{\"input_tokens\":139,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"output_tokens\":4,\"output_tokens_details\":{\"thinking_tokens\":0}}}\n\nevent: message_stop\ndata: {\"type\":\"message_stop\"}\n\n"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"version": 1,
|
||||
"metadata": {
|
||||
"model": "anthropic.claude-haiku-4-5",
|
||||
"tags": [
|
||||
"prefix:bedrock-mantle-messages",
|
||||
"provider:amazon-bedrock",
|
||||
"protocol:anthropic-messages",
|
||||
"reasoning",
|
||||
"system-update"
|
||||
],
|
||||
"name": "bedrock-mantle-messages/lowers-system-updates-to-wrapped-user-text-on-haiku-4-5-with-budget-thinking",
|
||||
"recordedAt": "2026-10-04T03:56:18.874Z"
|
||||
},
|
||||
"interactions": [
|
||||
{
|
||||
"transport": "http",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "https://bedrock-mantle.us-east-1.api.aws/anthropic/v1/messages",
|
||||
"headers": {
|
||||
"anthropic-beta": "interleaved-thinking-2025-05-14",
|
||||
"anthropic-version": "2023-06-01",
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": "{\"model\":\"anthropic.claude-haiku-4-5\",\"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"What is 19 multiplied by 23?\"},{\"type\":\"text\",\"text\":\"<system-update>\\nReply with only the integer.\\n</system-update>\",\"cache_control\":{\"type\":\"ephemeral\"}}]}],\"stream\":true,\"max_tokens\":2048,\"thinking\":{\"type\":\"enabled\",\"budget_tokens\":1024}}"
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"content-type": "text/event-stream; charset=utf-8"
|
||||
},
|
||||
"body": "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"model\":\"claude-haiku-4-5-20251001\",\"id\":\"msg_bdrk_prlwlyj7mdyfbm6ejq74ibwylnwd6deibs6j5xjq6lsc4dzuyzqa\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"container\":null,\"stop_reason\":null,\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":65,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":1,\"service_tier\":\"standard\"}}}\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"thinking\",\"thinking\":\"\",\"signature\":\"\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"19\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" × 23 = ?\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"\\n\\nLet me calculate\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" this:\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"\\n19 × 23 =\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" 19 × (20 +\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" 3)\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"\\n= 19 × 20\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" + 19 × 3\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"\\n= 380 + 57\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"\\n= 437\\n\\nOr\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" alternatively\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\":\\n19 × 23 \"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"= (\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"20 - 1) × \"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"23\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"\\n= 20 × 23 \"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"- 23\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"\\n= 460 - 23\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"\\n= 437\\n\\nSo\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" the\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" answer is 437.\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"signature_delta\",\"signature\":\"ErEDCoQBCBIQARgCKkBdVqTX3D5rRVz4O3SlBHJaAUb4Jv4CxOT9igcrHKXsDtU6Wwh9dw7QJbtvpCA0UJh0Wi0PCzu1Tlp8W8jmIU9wMhljbGF1ZGUtaGFpa3UtNC01LTIwMjUxMDAxOABCCHRoaW5raW5nWgwyMzI3NzE4NTY3ODGoAeKch9YGEgyP1xZ80Q63y4Uko9EaDHZo9a+qBGcqeBRLcSIw5/J5NpbvXm6aIqCN58pfjKvW9/35kh6B+t2jS/7dssFzks6J4m+5jtG7BRf9x65GKtkBOhxxxMXrJDYW8RJkXU9iKampxOVFPJuSrvSw43H1MF4V/dt2RFIXeKMNmIJ4umr4nTNVtSlaIEUt+uYmT/F0QL4wXB1Pd40e4VMcBqkJ3CmHkspPJgkyLursJIpDVJQ4TMCHDBzvEObimdAgoAzHM8ahMVOzb9nW9C7ZGE7OFinrq/uR+AzXR0N4zyBIenjcDF76n+OFm3ap3S7ngBQl6+2enaRiseO/UZ4UzgrYYkxk/V4oobGytu64Dn+ngeceb7P+uszDG8BsDs3zXWQdi2w56OvD6HGFVhgB\"}}\n\nevent: content_block_stop\ndata: {Line truncated
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"version": 1,
|
||||
"metadata": {
|
||||
"model": "anthropic.claude-opus-4-8",
|
||||
"tags": [
|
||||
"prefix:bedrock-mantle-messages",
|
||||
"provider:amazon-bedrock",
|
||||
"protocol:anthropic-messages",
|
||||
"tool",
|
||||
"tool-loop",
|
||||
"reasoning",
|
||||
"system-update"
|
||||
],
|
||||
"name": "bedrock-mantle-messages/replays-signed-thinking-through-a-tool-loop-and-native-system-update",
|
||||
"recordedAt": "2026-10-04T04:05:26.433Z"
|
||||
},
|
||||
"interactions": [
|
||||
{
|
||||
"transport": "http",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "https://bedrock-mantle.us-east-1.api.aws/anthropic/v1/messages",
|
||||
"headers": {
|
||||
"anthropic-beta": "interleaved-thinking-2025-05-14",
|
||||
"anthropic-version": "2023-06-01",
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": "{\"model\":\"anthropic.claude-opus-4-8\",\"system\":[{\"type\":\"text\",\"text\":\"You are a concise assistant.\",\"cache_control\":{\"type\":\"ephemeral\"}}],\"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"First calculate 37 * 43. Then call get_weather for Paris. After receiving the tool result, state both the product and the weather.\",\"cache_control\":{\"type\":\"ephemeral\"}}]}],\"tools\":[{\"name\":\"get_weather\",\"description\":\"Get the current weather in a city\",\"input_schema\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\",\"enum\":[\"Paris\"]}},\"required\":[\"city\"],\"additionalProperties\":false},\"cache_control\":{\"type\":\"ephemeral\"}}],\"stream\":true,\"max_tokens\":2048,\"thinking\":{\"type\":\"adaptive\",\"display\":\"summarized\"},\"output_config\":{\"effort\":\"medium\"}}"
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"content-type": "text/event-stream; charset=utf-8"
|
||||
},
|
||||
"body": "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"model\":\"claude-opus-4-8\",\"id\":\"msg_bdrk_fycfxoffsnlt6mj5wu2urbd2psp276vwn4pddgbaazfim6fxcvhq\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"container\":null,\"stop_reason\":null,\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":439,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":2,\"service_tier\":\"standard\"}}}\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"thinking\",\"thinking\":\"\",\"signature\":\"\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"37\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" times\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" 43\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" is 1591\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\", that\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" one\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"'s\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" quick to do\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" my\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"self. Now I'll go\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" ahead and call get\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"_weather.\\n\\n\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"signature_delta\",\"signature\":\"EqoDCngIEhABGAIqQIGpXRhzl8JEPJWxprG5sxaT1WjZ8/kEJgxZTN2qtzM5uC63GiumUjsHzfEIJikrldkFbwPbc0enJCXIoLr0G5UyDWNsYXVkZS1xdWluY2U4AEIIdGhpbmtpbmdaDDIzMjc3MTg1Njc4MagBhaGH1gYSDI7iCpVUnPgOaCirJBoMR20SZNRICFBH7f84IjDDaA7qNPHUlJN+eQa/aYqKn6Z2LobkjfWtscoBJs9sjR8qTy477EOozPy6lTkt1Pwq3wFcgWmJHdh5a0V2iRnSN4CtMEwxVPqC2im7vDCJDpNeAld8VmAO0u594guMJca+jme4lM/MW5oy3WrkmcL6Uc+qRByhUXhSqTWra8w9Xxf5S2nbPcT+FXjx1z1oDR1I8SIMzZrUM1DGOtQp7L/oWPadGwJYZ9EqKf9beRmSoc23kJKF5eA6+ZE9+HbS5gG1XxMDRFvx5rETS+mYMamOsfdowxWGWP7TzqHMhv+VGSCXoy2IXEIPBkXwen0K+dSAtgy/t21chzEmsGlBdBrAn7Ab0rCSqHvbZNZuA2T8CFPtGAE=\"}}\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0}\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":1,\"content_block\":{\"type\":\"text\",\"text\":\"\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"text_delta\",\"text\":\"37 × 43 \"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"text_delta\",\"text\":\"= 1591.\"}}\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":1}\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":2,\"content_block\":{\"type\":\"tool_use\",\"id\":\"toolu_bdrk_011We3Z1qCBXge7JLoyNwDat\",\"name\":\"get_weather\",\"input\":{}}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":2,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":2,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"{\\\"city\\\": \\\"Pa\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":2,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"ris\\\"}\"}}\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":2}\n\nevent: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"tool_use\",\"stop_sequence\":null,\"stop_details\":null,\"container\":null},\"usage\":{\"input_tokens\":439,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"output_tokens\":135,\"output_tokens_details\":{\"thinking_tokens\":73}}}\n\nevent: message_stop\ndata: {\"type\":\"message_stop\"}\n\n"
|
||||
}
|
||||
},
|
||||
{
|
||||
"transport": "http",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "https://bedrock-mantle.us-east-1.api.aws/anthropic/v1/messages",
|
||||
"headers": {
|
||||
"anthropic-beta": "interleaved-thinking-2025-05-14",
|
||||
"anthropic-version": "2023-06-01",
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": "{\"model\":\"anthropic.claude-opus-4-8\",\"system\":[{\"type\":\"text\",\"text\":\"You are a concise assistant.\",\"cache_control\":{\"type\":\"ephemeral\"}}],\"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"First calculate 37 * 43. Then call get_weather for Paris. After receiving the tool result, state both the product and the weather.\"}]},{\"role\":\"assistant\",\"content\":[{\"type\":\"thinking\",\"thinking\":\"37 times 43 is 1591, that one's quick to do myself. Now I'll go ahead and call get_weather.\\n\\n\",\"signature\":\"EqoDCngIEhABGAIqQIGpXRhzl8JEPJWxprG5sxaT1WjZ8/kEJgxZTN2qtzM5uC63GiumUjsHzfEIJikrldkFbwPbc0enJCXIoLr0G5UyDWNsYXVkZS1xdWluY2U4AEIIdGhpbmtpbmdaDDIzMjc3MTg1Njc4MagBhaGH1gYSDI7iCpVUnPgOaCirJBoMR20SZNRICFBH7f84IjDDaA7qNPHUlJN+eQa/aYqKn6Z2LobkjfWtscoBJs9sjR8qTy477EOozPy6lTkt1Pwq3wFcgWmJHdh5a0V2iRnSN4CtMEwxVPqC2im7vDCJDpNeAld8VmAO0u594guMJca+jme4lM/MW5oy3WrkmcL6Uc+qRByhUXhSqTWra8w9Xxf5S2nbPcT+FXjx1z1oDR1I8SIMzZrUM1DGOtQp7L/oWPadGwJYZ9EqKf9beRmSoc23kJKF5eA6+ZE9+HbS5gG1XxMDRFvx5rETS+mYMamOsfdowxWGWP7TzqHMhv+VGSCXoy2IXEIPBkXwen0K+dSAtgy/t21chzEmsGlBdBrAn7Ab0rCSqHvbZNZuA2T8CFPtGAE=\"},{\"type\":\"text\",\"text\":\"37 × 43 = 1591.\"},{\"type\":\"tool_use\",\"id\":\"toolu_bdrk_011We3Z1qCBXge7JLoyNwDat\",\"name\":\"get_weather\",\"input\":{\"city\":\"Paris\"}}]},{\"role\":\"user\",\"content\":[{\"type\":\"tool_result\",\"tool_use_id\":\"toolu_bdrk_011We3Z1qCBXge7JLoyNwDat\",\"content\":\"{\\\"condition\\\":\\\"sunny\\\",\\\"temperatureC\\\":18}\"}]},{\"role\":\"system\",\"content\":[{\"type\":\"text\",\"text\":\"Reply in French in one short sentence.\",\"cache_control\":{\"type\":\"ephemeral\"}}]}],\"tools\":[{\"name\":\"get_weather\",\"description\":\"Get the current weather in a city\",\"input_schema\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\",\"enum\":[\"Paris\"]}},\"required\":[\"city\"],\"additionalProperties\":false},\"cache_control\":{\"type\":\"ephemeral\"}}],\"stream\":true,\"max_tokens\":2048,\"thinking\":{\"type\":\"adaptive\",\"display\":\"summarized\"},\"output_config\":{\"effort\":\"medium\"}}"
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"content-type": "text/event-stream; charset=utf-8"
|
||||
},
|
||||
"body": "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"model\":\"claude-opus-4-8\",\"id\":\"msg_bdrk_uak5dibczxblk4xunblaiowmid7zhky7ozwinjhpjtcbtgzvuwyq\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"container\":null,\"stop_reason\":null,\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":611,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":3,\"service_tier\":\"standard\"}}}\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"Le produ\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"it est \"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"1591 \"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"et il\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\" f\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"ait beau à Paris av\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"ec 18°C.\"}}\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0}\n\nevent: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"end_turn\",\"stop_sequence\":null,\"stop_details\":null,\"container\":null},\"usage\":{\"input_tokens\":611,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"output_tokens\":28,\"output_tokens_details\":{\"thinking_tokens\":0}}}\n\nevent: message_stop\ndata: {\"type\":\"message_stop\"}\n\n"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"version": 1,
|
||||
"metadata": {
|
||||
"model": "anthropic.claude-opus-5",
|
||||
"tags": [
|
||||
"prefix:bedrock-mantle-messages",
|
||||
"provider:amazon-bedrock",
|
||||
"protocol:anthropic-messages",
|
||||
"reasoning",
|
||||
"effort-update"
|
||||
],
|
||||
"name": "bedrock-mantle-messages/strips-unsupported-mid-conversation-effort-updates-on-opus-5-0",
|
||||
"recordedAt": "2026-10-04T03:56:23.956Z"
|
||||
},
|
||||
"interactions": [
|
||||
{
|
||||
"transport": "http",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "https://bedrock-mantle.us-east-1.api.aws/anthropic/v1/messages",
|
||||
"headers": {
|
||||
"anthropic-beta": "interleaved-thinking-2025-05-14",
|
||||
"anthropic-version": "2023-06-01",
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": "{\"model\":\"anthropic.claude-opus-5\",\"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"What is 12 + 30?\"}]},{\"role\":\"assistant\",\"content\":[{\"type\":\"text\",\"text\":\"42\"}]},{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"Add 8 to that result. Reply with only the integer.\",\"cache_control\":{\"type\":\"ephemeral\"}}]}],\"stream\":true,\"max_tokens\":1024,\"thinking\":{\"type\":\"adaptive\",\"display\":\"summarized\"},\"output_config\":{\"effort\":\"low\"}}"
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"content-type": "text/event-stream; charset=utf-8"
|
||||
},
|
||||
"body": "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"model\":\"claude-opus-5\",\"id\":\"msg_bdrk_5mwa5agz3cvj44w72lno46ljoylvzjhipc4jmraridpzxktjquha\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"container\":null,\"stop_reason\":null,\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":38,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":1,\"service_tier\":\"standard\"}}}\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"50\"}}\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0}\n\nevent: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"end_turn\",\"stop_sequence\":null,\"stop_details\":null,\"container\":null},\"usage\":{\"input_tokens\":38,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"output_tokens\":3,\"output_tokens_details\":{\"thinking_tokens\":0}}}\n\nevent: message_stop\ndata: {\"type\":\"message_stop\"}\n\n"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
-28
@@ -1,28 +0,0 @@
|
||||
{
|
||||
"version": 1,
|
||||
"metadata": {
|
||||
"name": "openai-chat/continues-after-tool-result",
|
||||
"recordedAt": "2026-05-06T01:33:31.878Z",
|
||||
"tags": ["prefix:openai-chat", "provider:openai", "protocol:openai-chat", "tool"]
|
||||
},
|
||||
"interactions": [
|
||||
{
|
||||
"transport": "http",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "https://api.openai.com/v1/chat/completions",
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": "{\"model\":\"gpt-4o-mini\",\"messages\":[{\"role\":\"system\",\"content\":\"Answer using only the provided tool result.\"},{\"role\":\"user\",\"content\":\"What is the weather in Paris?\"},{\"role\":\"assistant\",\"content\":null,\"tool_calls\":[{\"id\":\"call_weather\",\"type\":\"function\",\"function\":{\"name\":\"get_weather\",\"arguments\":\"{\\\"city\\\":\\\"Paris\\\"}\"}}]},{\"role\":\"tool\",\"tool_call_id\":\"call_weather\",\"content\":\"{\\\"forecast\\\":\\\"sunny\\\",\\\"temperature_c\\\":22}\"}],\"stream\":true,\"stream_options\":{\"include_usage\":true},\"max_tokens\":40,\"temperature\":0}"
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"content-type": "text/event-stream; charset=utf-8"
|
||||
},
|
||||
"body": "data: {\"id\":\"chatcmpl-DcLQhErGVsn8x3hNFmX5A0yM0T9Km\",\"object\":\"chat.completion.chunk\",\"created\":1778031211,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_57133166c6\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":\"\",\"refusal\":null},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"gJ6VDZ2ZE\"}\n\ndata: {\"id\":\"chatcmpl-DcLQhErGVsn8x3hNFmX5A0yM0T9Km\",\"object\":\"chat.completion.chunk\",\"created\":1778031211,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_57133166c6\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"The\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"B2pU6Neg\"}\n\ndata: {\"id\":\"chatcmpl-DcLQhErGVsn8x3hNFmX5A0yM0T9Km\",\"object\":\"chat.completion.chunk\",\"created\":1778031211,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_57133166c6\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" weather\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"sa2\"}\n\ndata: {\"id\":\"chatcmpl-DcLQhErGVsn8x3hNFmX5A0yM0T9Km\",\"object\":\"chat.completion.chunk\",\"created\":1778031211,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_57133166c6\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" in\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"ENFjAfta\"}\n\ndata: {\"id\":\"chatcmpl-DcLQhErGVsn8x3hNFmX5A0yM0T9Km\",\"object\":\"chat.completion.chunk\",\"created\":1778031211,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_57133166c6\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" Paris\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"E1Kbi\"}\n\ndata: {\"id\":\"chatcmpl-DcLQhErGVsn8x3hNFmX5A0yM0T9Km\",\"object\":\"chat.completion.chunk\",\"created\":1778031211,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_57133166c6\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" is\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"NWj8HasA\"}\n\ndata: {\"id\":\"chatcmpl-DcLQhErGVsn8x3hNFmX5A0yM0T9Km\",\"object\":\"chat.completion.chunk\",\"created\":1778031211,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_57133166c6\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" sunny\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"irmMg\"}\n\ndata: {\"id\":\"chatcmpl-DcLQhErGVsn8x3hNFmX5A0yM0T9Km\",\"object\":\"chat.completion.chunk\",\"created\":1778031211,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_57133166c6\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" with\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"3eCMq6\"}\n\ndata: {\"id\":\"chatcmpl-DcLQhErGVsn8x3hNFmX5A0yM0T9Km\",\"object\":\"chat.completion.chunk\",\"created\":1778031211,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_57133166c6\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" a\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"XKMqPUsnt\"}\n\ndata: {\"id\":\"chatcmpl-DcLQhErGVsn8x3hNFmX5A0yM0T9Km\",\"object\":\"chat.completion.chunk\",\"created\":1778031211,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_57133166c6\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" temperature\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"BFVrBA09z9Y3lAC\"}\n\ndata: {\"id\":\"chatcmpl-DcLQhErGVsn8x3hNFmX5A0yM0T9Km\",\"object\":\"chat.completion.chunk\",\"created\":1778031211,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_57133166c6\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" of\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"AwG4puOX\"}\n\ndata: {\"id\":\"chatcmpl-DcLQhErGVsn8x3hNFmX5A0yM0T9Km\",\"object\":\"chat.completion.chunk\",\"created\":1778031211,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_57133166c6\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" \"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"pKQU39KXN6\"}\n\ndata: {\"id\":\"chatcmpl-DcLQhErGVsn8x3hNFmX5A0yM0T9Km\",\"object\":\"chat.completion.chunk\",\"created\":1778031211,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_57133166c6\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"22\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"xeTNA1JuE\"}\n\ndata: {\"id\":\"chatcmpl-DcLQhErGVsn8x3hNFmX5A0yM0T9Km\",\"object\":\"chat.compLine truncated
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
-46
@@ -1,46 +0,0 @@
|
||||
{
|
||||
"version": 1,
|
||||
"metadata": {
|
||||
"name": "openai-chat/drives-a-tool-loop-end-to-end",
|
||||
"recordedAt": "2026-05-06T01:33:29.747Z",
|
||||
"tags": ["prefix:openai-chat", "provider:openai", "protocol:openai-chat", "tool", "tool-loop"]
|
||||
},
|
||||
"interactions": [
|
||||
{
|
||||
"transport": "http",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "https://api.openai.com/v1/chat/completions",
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": "{\"model\":\"gpt-4o-mini\",\"messages\":[{\"role\":\"system\",\"content\":\"Use the get_weather tool, then answer in one short sentence.\"},{\"role\":\"user\",\"content\":\"What is the weather in Paris?\"}],\"tools\":[{\"type\":\"function\",\"function\":{\"name\":\"get_weather\",\"description\":\"Get current weather for a city.\",\"parameters\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"],\"additionalProperties\":false}}}],\"stream\":true,\"stream_options\":{\"include_usage\":true},\"max_tokens\":80,\"temperature\":0}"
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"content-type": "text/event-stream; charset=utf-8"
|
||||
},
|
||||
"body": "data: {\"id\":\"chatcmpl-DcLQeieQn9xQe2QqsLPi7rN15bnJF\",\"object\":\"chat.completion.chunk\",\"created\":1778031208,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_99cf176092\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":null,\"tool_calls\":[{\"index\":0,\"id\":\"call_tyZNHs2AudCbG4XJUEmX5Waw\",\"type\":\"function\",\"function\":{\"name\":\"get_weather\",\"arguments\":\"\"}}],\"refusal\":null},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"ayQl\"}\n\ndata: {\"id\":\"chatcmpl-DcLQeieQn9xQe2QqsLPi7rN15bnJF\",\"object\":\"chat.completion.chunk\",\"created\":1778031208,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_99cf176092\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"{\\\"\"}}]},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"TWZNUL5mYYtjWu\"}\n\ndata: {\"id\":\"chatcmpl-DcLQeieQn9xQe2QqsLPi7rN15bnJF\",\"object\":\"chat.completion.chunk\",\"created\":1778031208,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_99cf176092\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"city\"}}]},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"QidSCtgZRvDHL\"}\n\ndata: {\"id\":\"chatcmpl-DcLQeieQn9xQe2QqsLPi7rN15bnJF\",\"object\":\"chat.completion.chunk\",\"created\":1778031208,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_99cf176092\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"\\\":\\\"\"}}]},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"nupQO1L4GdWo\"}\n\ndata: {\"id\":\"chatcmpl-DcLQeieQn9xQe2QqsLPi7rN15bnJF\",\"object\":\"chat.completion.chunk\",\"created\":1778031208,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_99cf176092\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"Paris\"}}]},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"3W5B3hzGrFvl\"}\n\ndata: {\"id\":\"chatcmpl-DcLQeieQn9xQe2QqsLPi7rN15bnJF\",\"object\":\"chat.completion.chunk\",\"created\":1778031208,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_99cf176092\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"\\\"}\"}}]},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"JgscYuZR4Lmp5S\"}\n\ndata: {\"id\":\"chatcmpl-DcLQeieQn9xQe2QqsLPi7rN15bnJF\",\"object\":\"chat.completion.chunk\",\"created\":1778031208,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_99cf176092\",\"choices\":[{\"index\":0,\"delta\":{},\"logprobs\":null,\"finish_reason\":\"tool_calls\"}],\"usage\":null,\"obfuscation\":\"BtZF5TaQjX3UwLN\"}\n\ndata: {\"id\":\"chatcmpl-DcLQeieQn9xQe2QqsLPi7rN15bnJF\",\"object\":\"chat.completion.chunk\",\"created\":1778031208,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_99cf176092\",\"choices\":[],\"usage\":{\"prompt_tokens\":64,\"completion_tokens\":14,\"total_tokens\":78,\"prompt_tokens_details\":{\"cached_tokens\":0,\"audio_tokens\":0},\"completion_tokens_details\":{\"reasoning_tokens\":0,\"audio_tokens\":0,\"accepted_prediction_tokens\":0,\"rejected_prediction_tokens\":0}},\"obfuscation\":\"bZ51l7ptxM\"}\n\ndata: [DONE]\n\n"
|
||||
}
|
||||
},
|
||||
{
|
||||
"transport": "http",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "https://api.openai.com/v1/chat/completions",
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": "{\"model\":\"gpt-4o-mini\",\"messages\":[{\"role\":\"system\",\"content\":\"Use the get_weather tool, then answer in one short sentence.\"},{\"role\":\"user\",\"content\":\"What is the weather in Paris?\"},{\"role\":\"assistant\",\"content\":null,\"tool_calls\":[{\"id\":\"call_tyZNHs2AudCbG4XJUEmX5Waw\",\"type\":\"function\",\"function\":{\"name\":\"get_weather\",\"arguments\":\"{\\\"city\\\":\\\"Paris\\\"}\"}}]},{\"role\":\"tool\",\"tool_call_id\":\"call_tyZNHs2AudCbG4XJUEmX5Waw\",\"content\":\"{\\\"temperature\\\":22,\\\"condition\\\":\\\"sunny\\\"}\"}],\"tools\":[{\"type\":\"function\",\"function\":{\"name\":\"get_weather\",\"description\":\"Get current weather for a city.\",\"parameters\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"],\"additionalProperties\":false}}}],\"stream\":true,\"stream_options\":{\"include_usage\":true},\"max_tokens\":80,\"temperature\":0}"
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"content-type": "text/event-stream; charset=utf-8"
|
||||
},
|
||||
"body": "data: {\"id\":\"chatcmpl-DcLQfUuhXefq7QDmGNhpEN5IqEKMM\",\"object\":\"chat.completion.chunk\",\"created\":1778031209,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_99cf176092\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":\"\",\"refusal\":null},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"SCCu2B8Ri\"}\n\ndata: {\"id\":\"chatcmpl-DcLQfUuhXefq7QDmGNhpEN5IqEKMM\",\"object\":\"chat.completion.chunk\",\"created\":1778031209,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_99cf176092\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"The\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"vuE4h8te\"}\n\ndata: {\"id\":\"chatcmpl-DcLQfUuhXefq7QDmGNhpEN5IqEKMM\",\"object\":\"chat.completion.chunk\",\"created\":1778031209,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_99cf176092\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" weather\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"uzt\"}\n\ndata: {\"id\":\"chatcmpl-DcLQfUuhXefq7QDmGNhpEN5IqEKMM\",\"object\":\"chat.completion.chunk\",\"created\":1778031209,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_99cf176092\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" in\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"4vVdGuJc\"}\n\ndata: {\"id\":\"chatcmpl-DcLQfUuhXefq7QDmGNhpEN5IqEKMM\",\"object\":\"chat.completion.chunk\",\"created\":1778031209,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_99cf176092\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" Paris\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"hAfFt\"}\n\ndata: {\"id\":\"chatcmpl-DcLQfUuhXefq7QDmGNhpEN5IqEKMM\",\"object\":\"chat.completion.chunk\",\"created\":1778031209,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_99cf176092\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" is\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"uuNXNXne\"}\n\ndata: {\"id\":\"chatcmpl-DcLQfUuhXefq7QDmGNhpEN5IqEKMM\",\"object\":\"chat.completion.chunk\",\"created\":1778031209,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_99cf176092\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" sunny\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"HRMlI\"}\n\ndata: {\"id\":\"chatcmpl-DcLQfUuhXefq7QDmGNhpEN5IqEKMM\",\"object\":\"chat.completion.chunk\",\"created\":1778031209,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_99cf176092\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" with\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"Ii1R2u\"}\n\ndata: {\"id\":\"chatcmpl-DcLQfUuhXefq7QDmGNhpEN5IqEKMM\",\"object\":\"chat.completion.chunk\",\"created\":1778031209,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_99cf176092\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" a\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"ay3ddthfT\"}\n\ndata: {\"id\":\"chatcmpl-DcLQfUuhXefq7QDmGNhpEN5IqEKMM\",\"object\":\"chat.completion.chunk\",\"created\":1778031209,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_99cf176092\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" temperature\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"PtxyVsfiluBGiWj\"}\n\ndata: {\"id\":\"chatcmpl-DcLQfUuhXefq7QDmGNhpEN5IqEKMM\",\"object\":\"chat.completion.chunk\",\"created\":1778031209,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_99cf176092\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" of\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"WuI4V7O6\"}\n\ndata: {\"id\":\"chatcmpl-DcLQfUuhXefq7QDmGNhpEN5IqEKMM\",\"object\":\"chat.completion.chunk\",\"created\":1778031209,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_99cf176092\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" \"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"Z5wHwpykrS\"}\n\ndata: {\"id\":\"chatcmpl-DcLQfUuhXefq7QDmGNhpEN5IqEKMM\",\"object\":\"chat.completion.chunk\",\"created\":1778031209,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_99cf176092\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"22\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"Fi66TTzMb\"}\n\ndata: {\"id\":\"chatcmpl-DcLQfUuhXefq7QDmGNhpEN5IqEKMM\",\"object\":\"chat.compLine truncated
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
{
|
||||
"version": 1,
|
||||
"metadata": {
|
||||
"name": "openai-chat/streams-text",
|
||||
"recordedAt": "2026-05-06T01:33:30.542Z",
|
||||
"tags": ["prefix:openai-chat", "provider:openai", "protocol:openai-chat"]
|
||||
},
|
||||
"interactions": [
|
||||
{
|
||||
"transport": "http",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "https://api.openai.com/v1/chat/completions",
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": "{\"model\":\"gpt-4o-mini\",\"messages\":[{\"role\":\"system\",\"content\":\"You are concise.\"},{\"role\":\"user\",\"content\":\"Say hello in one short sentence.\"}],\"stream\":true,\"stream_options\":{\"include_usage\":true},\"max_tokens\":20,\"temperature\":0}"
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"content-type": "text/event-stream; charset=utf-8"
|
||||
},
|
||||
"body": "data: {\"id\":\"chatcmpl-DcLQgbFetadY4JFl0fHK0g7OYsCOL\",\"object\":\"chat.completion.chunk\",\"created\":1778031210,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_57133166c6\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":\"\",\"refusal\":null},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"g9SWm2h6J\"}\n\ndata: {\"id\":\"chatcmpl-DcLQgbFetadY4JFl0fHK0g7OYsCOL\",\"object\":\"chat.completion.chunk\",\"created\":1778031210,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_57133166c6\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"Hello\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"lVzwlh\"}\n\ndata: {\"id\":\"chatcmpl-DcLQgbFetadY4JFl0fHK0g7OYsCOL\",\"object\":\"chat.completion.chunk\",\"created\":1778031210,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_57133166c6\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"!\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"onzhziaLGv\"}\n\ndata: {\"id\":\"chatcmpl-DcLQgbFetadY4JFl0fHK0g7OYsCOL\",\"object\":\"chat.completion.chunk\",\"created\":1778031210,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_57133166c6\",\"choices\":[{\"index\":0,\"delta\":{},\"logprobs\":null,\"finish_reason\":\"stop\"}],\"usage\":null,\"obfuscation\":\"LzUj1\"}\n\ndata: {\"id\":\"chatcmpl-DcLQgbFetadY4JFl0fHK0g7OYsCOL\",\"object\":\"chat.completion.chunk\",\"created\":1778031210,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_57133166c6\",\"choices\":[],\"usage\":{\"prompt_tokens\":22,\"completion_tokens\":2,\"total_tokens\":24,\"prompt_tokens_details\":{\"cached_tokens\":0,\"audio_tokens\":0},\"completion_tokens_details\":{\"reasoning_tokens\":0,\"audio_tokens\":0,\"accepted_prediction_tokens\":0,\"rejected_prediction_tokens\":0}},\"obfuscation\":\"emMuPcvvOkI\"}\n\ndata: [DONE]\n\n"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
{
|
||||
"version": 1,
|
||||
"metadata": {
|
||||
"name": "openai-chat/streams-tool-call",
|
||||
"recordedAt": "2026-05-06T01:33:31.127Z",
|
||||
"tags": ["prefix:openai-chat", "provider:openai", "protocol:openai-chat", "tool"]
|
||||
},
|
||||
"interactions": [
|
||||
{
|
||||
"transport": "http",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "https://api.openai.com/v1/chat/completions",
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": "{\"model\":\"gpt-4o-mini\",\"messages\":[{\"role\":\"system\",\"content\":\"Call tools exactly as requested.\"},{\"role\":\"user\",\"content\":\"Call get_weather with city exactly Paris.\"}],\"tools\":[{\"type\":\"function\",\"function\":{\"name\":\"get_weather\",\"description\":\"Get current weather for a city.\",\"parameters\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"],\"additionalProperties\":false}}}],\"tool_choice\":{\"type\":\"function\",\"function\":{\"name\":\"get_weather\"}},\"stream\":true,\"stream_options\":{\"include_usage\":true},\"max_tokens\":80,\"temperature\":0}"
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"content-type": "text/event-stream; charset=utf-8"
|
||||
},
|
||||
"body": "data: {\"id\":\"chatcmpl-DcLQgGuIIwnMHqZMRCOwZMLir5SkK\",\"object\":\"chat.completion.chunk\",\"created\":1778031210,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_d0a1738203\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":null,\"tool_calls\":[{\"index\":0,\"id\":\"call_5wBV98AvGPwOyC6a2HtKh85w\",\"type\":\"function\",\"function\":{\"name\":\"get_weather\",\"arguments\":\"\"}}],\"refusal\":null},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"hrw8\"}\n\ndata: {\"id\":\"chatcmpl-DcLQgGuIIwnMHqZMRCOwZMLir5SkK\",\"object\":\"chat.completion.chunk\",\"created\":1778031210,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_d0a1738203\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"{\\\"\"}}]},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"MzOlaTohF20Sbb\"}\n\ndata: {\"id\":\"chatcmpl-DcLQgGuIIwnMHqZMRCOwZMLir5SkK\",\"object\":\"chat.completion.chunk\",\"created\":1778031210,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_d0a1738203\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"city\"}}]},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"QuYBQ5vYEUVxR\"}\n\ndata: {\"id\":\"chatcmpl-DcLQgGuIIwnMHqZMRCOwZMLir5SkK\",\"object\":\"chat.completion.chunk\",\"created\":1778031210,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_d0a1738203\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"\\\":\\\"\"}}]},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"spyXlsV2hl6l\"}\n\ndata: {\"id\":\"chatcmpl-DcLQgGuIIwnMHqZMRCOwZMLir5SkK\",\"object\":\"chat.completion.chunk\",\"created\":1778031210,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_d0a1738203\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"Paris\"}}]},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"Db1cjFKa6YAI\"}\n\ndata: {\"id\":\"chatcmpl-DcLQgGuIIwnMHqZMRCOwZMLir5SkK\",\"object\":\"chat.completion.chunk\",\"created\":1778031210,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_d0a1738203\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"\\\"}\"}}]},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"oPu35nrhXcjTL5\"}\n\ndata: {\"id\":\"chatcmpl-DcLQgGuIIwnMHqZMRCOwZMLir5SkK\",\"object\":\"chat.completion.chunk\",\"created\":1778031210,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_d0a1738203\",\"choices\":[{\"index\":0,\"delta\":{},\"logprobs\":null,\"finish_reason\":\"stop\"}],\"usage\":null,\"obfuscation\":\"63TVy\"}\n\ndata: {\"id\":\"chatcmpl-DcLQgGuIIwnMHqZMRCOwZMLir5SkK\",\"object\":\"chat.completion.chunk\",\"created\":1778031210,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_d0a1738203\",\"choices\":[],\"usage\":{\"prompt_tokens\":67,\"completion_tokens\":5,\"total_tokens\":72,\"prompt_tokens_details\":{\"cached_tokens\":0,\"audio_tokens\":0},\"completion_tokens_details\":{\"reasoning_tokens\":0,\"audio_tokens\":0,\"accepted_prediction_tokens\":0,\"rejected_prediction_tokens\":0}},\"obfuscation\":\"NxJjur40z4H\"}\n\ndata: [DONE]\n\n"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
-28
@@ -1,28 +0,0 @@
|
||||
{
|
||||
"version": 1,
|
||||
"metadata": {
|
||||
"name": "openai-compatible-chat/deepseek-streams-text",
|
||||
"recordedAt": "2026-04-28T21:18:49.498Z",
|
||||
"tags": ["prefix:openai-compatible-chat", "protocol:openai-compatible-chat", "provider:deepseek"]
|
||||
},
|
||||
"interactions": [
|
||||
{
|
||||
"transport": "http",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "https://api.deepseek.com/v1/chat/completions",
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": "{\"model\":\"deepseek-chat\",\"messages\":[{\"role\":\"system\",\"content\":\"You are concise.\"},{\"role\":\"user\",\"content\":\"Reply with exactly: Hello!\"}],\"stream\":true,\"stream_options\":{\"include_usage\":true},\"max_tokens\":20,\"temperature\":0}"
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"content-type": "text/event-stream; charset=utf-8"
|
||||
},
|
||||
"body": "data: {\"id\":\"0c811926-1e0c-4160-baf8-6e71247c8ad7\",\"object\":\"chat.completion.chunk\",\"created\":1777411128,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_058df29938_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":\"\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"0c811926-1e0c-4160-baf8-6e71247c8ad7\",\"object\":\"chat.completion.chunk\",\"created\":1777411128,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_058df29938_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"Hello\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"0c811926-1e0c-4160-baf8-6e71247c8ad7\",\"object\":\"chat.completion.chunk\",\"created\":1777411128,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_058df29938_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"!\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"0c811926-1e0c-4160-baf8-6e71247c8ad7\",\"object\":\"chat.completion.chunk\",\"created\":1777411128,\"model\":\"deepseek-v4-flash\",\"system_fingerprint\":\"fp_058df29938_prod0820_fp8_kvcache_20260402\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"\"},\"logprobs\":null,\"finish_reason\":\"stop\"}],\"usage\":{\"prompt_tokens\":14,\"completion_tokens\":2,\"total_tokens\":16,\"prompt_tokens_details\":{\"cached_tokens\":0},\"prompt_cache_hit_tokens\":0,\"prompt_cache_miss_tokens\":14}}\n\ndata: [DONE]\n\n"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
-53
@@ -1,53 +0,0 @@
|
||||
{
|
||||
"version": 1,
|
||||
"metadata": {
|
||||
"name": "openai-compatible-chat/groq-llama-3-3-70b-drives-a-tool-loop",
|
||||
"recordedAt": "2026-05-06T01:35:06.032Z",
|
||||
"tags": [
|
||||
"prefix:openai-compatible-chat",
|
||||
"protocol:openai-compatible-chat",
|
||||
"provider:groq",
|
||||
"tool",
|
||||
"tool-loop",
|
||||
"golden"
|
||||
]
|
||||
},
|
||||
"interactions": [
|
||||
{
|
||||
"transport": "http",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "https://api.groq.com/openai/v1/chat/completions",
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": "{\"model\":\"llama-3.3-70b-versatile\",\"messages\":[{\"role\":\"system\",\"content\":\"Use the get_weather tool, then answer in one short sentence.\"},{\"role\":\"user\",\"content\":\"What is the weather in Paris?\"}],\"tools\":[{\"type\":\"function\",\"function\":{\"name\":\"get_weather\",\"description\":\"Get current weather for a city.\",\"parameters\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"],\"additionalProperties\":false}}}],\"stream\":true,\"stream_options\":{\"include_usage\":true},\"max_tokens\":80,\"temperature\":0}"
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"content-type": "text/event-stream"
|
||||
},
|
||||
"body": "data: {\"id\":\"chatcmpl-74a8ff95-296e-4c98-8e51-4b23d5d7f261\",\"object\":\"chat.completion.chunk\",\"created\":1778031305,\"model\":\"llama-3.3-70b-versatile\",\"system_fingerprint\":\"fp_43d97c5965\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":null},\"logprobs\":null,\"finish_reason\":null}],\"x_groq\":{\"id\":\"req_01kqxes90afm8r12en80ez1vhw\",\"seed\":1587279809}}\n\ndata: {\"id\":\"chatcmpl-74a8ff95-296e-4c98-8e51-4b23d5d7f261\",\"object\":\"chat.completion.chunk\",\"created\":1778031305,\"model\":\"llama-3.3-70b-versatile\",\"system_fingerprint\":\"fp_43d97c5965\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"id\":\"4vgxtgdfg\",\"type\":\"function\",\"function\":{\"name\":\"get_weather\",\"arguments\":\"{\\\"city\\\":\\\"Paris\\\"}\"},\"index\":0}]},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-74a8ff95-296e-4c98-8e51-4b23d5d7f261\",\"object\":\"chat.completion.chunk\",\"created\":1778031305,\"model\":\"llama-3.3-70b-versatile\",\"system_fingerprint\":\"fp_43d97c5965\",\"choices\":[{\"index\":0,\"delta\":{},\"logprobs\":null,\"finish_reason\":\"tool_calls\"}],\"x_groq\":{\"id\":\"req_01kqxes90afm8r12en80ez1vhw\",\"usage\":{\"queue_time\":0.036768035,\"prompt_tokens\":237,\"prompt_time\":0.012356963,\"completion_tokens\":14,\"completion_time\":0.047052437,\"total_tokens\":251,\"total_time\":0.0594094}},\"usage\":{\"queue_time\":0.036768035,\"prompt_tokens\":237,\"prompt_time\":0.012356963,\"completion_tokens\":14,\"completion_time\":0.047052437,\"total_tokens\":251,\"total_time\":0.0594094}}\n\ndata: {\"id\":\"chatcmpl-74a8ff95-296e-4c98-8e51-4b23d5d7f261\",\"object\":\"chat.completion.chunk\",\"created\":1778031305,\"model\":\"llama-3.3-70b-versatile\",\"system_fingerprint\":\"fp_43d97c5965\",\"choices\":[],\"usage\":{\"queue_time\":0.036768035,\"prompt_tokens\":237,\"prompt_time\":0.012356963,\"completion_tokens\":14,\"completion_time\":0.047052437,\"total_tokens\":251,\"total_time\":0.0594094},\"service_tier\":\"on_demand\"}\n\ndata: [DONE]\n\n"
|
||||
}
|
||||
},
|
||||
{
|
||||
"transport": "http",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "https://api.groq.com/openai/v1/chat/completions",
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": "{\"model\":\"llama-3.3-70b-versatile\",\"messages\":[{\"role\":\"system\",\"content\":\"Use the get_weather tool, then answer in one short sentence.\"},{\"role\":\"user\",\"content\":\"What is the weather in Paris?\"},{\"role\":\"assistant\",\"content\":null,\"tool_calls\":[{\"id\":\"4vgxtgdfg\",\"type\":\"function\",\"function\":{\"name\":\"get_weather\",\"arguments\":\"{\\\"city\\\":\\\"Paris\\\"}\"}}]},{\"role\":\"tool\",\"tool_call_id\":\"4vgxtgdfg\",\"content\":\"{\\\"temperature\\\":22,\\\"condition\\\":\\\"sunny\\\"}\"}],\"tools\":[{\"type\":\"function\",\"function\":{\"name\":\"get_weather\",\"description\":\"Get current weather for a city.\",\"parameters\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"],\"additionalProperties\":false}}}],\"stream\":true,\"stream_options\":{\"include_usage\":true},\"max_tokens\":80,\"temperature\":0}"
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"content-type": "text/event-stream"
|
||||
},
|
||||
"body": "data: {\"id\":\"chatcmpl-52c0acaf-3f4b-45c8-8aa5-93a3b6adb045\",\"object\":\"chat.completion.chunk\",\"created\":1778031305,\"model\":\"llama-3.3-70b-versatile\",\"system_fingerprint\":\"fp_43d97c5965\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":\"\"},\"logprobs\":null,\"finish_reason\":null}],\"x_groq\":{\"id\":\"req_01kqxes966fm8r4q94e70a83gn\",\"seed\":524268521}}\n\ndata: {\"id\":\"chatcmpl-52c0acaf-3f4b-45c8-8aa5-93a3b6adb045\",\"object\":\"chat.completion.chunk\",\"created\":1778031305,\"model\":\"llama-3.3-70b-versatile\",\"system_fingerprint\":\"fp_43d97c5965\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"The\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-52c0acaf-3f4b-45c8-8aa5-93a3b6adb045\",\"object\":\"chat.completion.chunk\",\"created\":1778031305,\"model\":\"llama-3.3-70b-versatile\",\"system_fingerprint\":\"fp_43d97c5965\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" weather\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-52c0acaf-3f4b-45c8-8aa5-93a3b6adb045\",\"object\":\"chat.completion.chunk\",\"created\":1778031305,\"model\":\"llama-3.3-70b-versatile\",\"system_fingerprint\":\"fp_43d97c5965\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" in\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-52c0acaf-3f4b-45c8-8aa5-93a3b6adb045\",\"object\":\"chat.completion.chunk\",\"created\":1778031305,\"model\":\"llama-3.3-70b-versatile\",\"system_fingerprint\":\"fp_43d97c5965\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" Paris\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-52c0acaf-3f4b-45c8-8aa5-93a3b6adb045\",\"object\":\"chat.completion.chunk\",\"created\":1778031305,\"model\":\"llama-3.3-70b-versatile\",\"system_fingerprint\":\"fp_43d97c5965\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" is\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-52c0acaf-3f4b-45c8-8aa5-93a3b6adb045\",\"object\":\"chat.completion.chunk\",\"created\":1778031306,\"model\":\"llama-3.3-70b-versatile\",\"system_fingerprint\":\"fp_43d97c5965\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" sunny\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-52c0acaf-3f4b-45c8-8aa5-93a3b6adb045\",\"object\":\"chat.completion.chunk\",\"created\":1778031306,\"model\":\"llama-3.3-70b-versatile\",\"system_fingerprint\":\"fp_43d97c5965\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" with\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-52c0acaf-3f4b-45c8-8aa5-93a3b6adb045\",\"object\":\"chat.completion.chunk\",\"created\":1778031306,\"model\":\"llama-3.3-70b-versatile\",\"system_fingerprint\":\"fp_43d97c5965\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" a\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-52c0acaf-3f4b-45c8-8aa5-93a3b6adb045\",\"object\":\"chat.completion.chunk\",\"created\":1778031306,\"model\":\"llama-3.3-70b-versatile\",\"system_fingerprint\":\"fp_43d97c5965\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" temperature\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-52c0acaf-3f4b-45c8-8aa5-93a3b6adb045\",\"object\":\"chat.completion.chunk\",\"created\":1778031306,\"model\":\"llama-3.3-70b-versatile\",\"system_fingerprint\":\"fp_43d97c5965\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" of\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-52c0acaf-3f4b-45c8-8aa5-93a3b6adb045\",\"object\":\"chat.completion.chunk\",\"created\":1778031306,\"model\":\"llama-3.3-70b-versatile\",\"system_fingerprint\":\"fp_43d97c5965\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" \"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-52c0acaf-3f4b-45c8-8aa5-93a3b6adb045\",\"object\":\"chat.completion.chunk\",\"created\":1778031306,\"model\":\"llama-3.3-70b-versatile\",\"system_fingerprint\":\"fp_43d97c5965\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"22\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-52c0acaf-3f4b-45c8-8aa5-93a3b6adb045\",\"object\":\"chat.completion.chunk\",\"created\":1778031306,\"model\":\"llama-3.3-70b-versatile\",\"system_fingerprint\":\"fp_43d97c5965\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" degrees\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-52c0acaf-3f4b-45c8-8aa5-93a3b6adb045\",\"object\":\"chat.completion.chunk\",\"created\":1778031306,\"model\":\"llama-3.3-70b-versatile\",\"system_fingerprint\":\"fp_43d97c5965\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\".\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-52c0acaf-3f4b-45c8-8aa5-93a3b6adb045\",\"object\":\"chat.completion.chunk\",\"created\":1778031306,\"model\":\"llama-3.3-70b-versatile\",\"system_fingerprint\":\"fp_43d97c5965\",\"choices\":[{\"index\":0,\"delta\":{},\"logLine truncated
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
-28
@@ -1,28 +0,0 @@
|
||||
{
|
||||
"version": 1,
|
||||
"metadata": {
|
||||
"name": "openai-compatible-chat/groq-streams-text",
|
||||
"recordedAt": "2026-05-06T01:35:05.532Z",
|
||||
"tags": ["prefix:openai-compatible-chat", "protocol:openai-compatible-chat", "provider:groq"]
|
||||
},
|
||||
"interactions": [
|
||||
{
|
||||
"transport": "http",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "https://api.groq.com/openai/v1/chat/completions",
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": "{\"model\":\"llama-3.3-70b-versatile\",\"messages\":[{\"role\":\"system\",\"content\":\"You are concise.\"},{\"role\":\"user\",\"content\":\"Reply with exactly: Hello!\"}],\"stream\":true,\"stream_options\":{\"include_usage\":true},\"max_tokens\":20,\"temperature\":0}"
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"content-type": "text/event-stream"
|
||||
},
|
||||
"body": "data: {\"id\":\"chatcmpl-dd5aae9f-7032-44a7-aca8-01027903b4c9\",\"object\":\"chat.completion.chunk\",\"created\":1778031305,\"model\":\"llama-3.3-70b-versatile\",\"system_fingerprint\":\"fp_d42c28f9ce\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":\"\"},\"logprobs\":null,\"finish_reason\":null}],\"x_groq\":{\"id\":\"req_01kqxes8r3fmja0yhxvt665m6h\",\"seed\":687314058}}\n\ndata: {\"id\":\"chatcmpl-dd5aae9f-7032-44a7-aca8-01027903b4c9\",\"object\":\"chat.completion.chunk\",\"created\":1778031305,\"model\":\"llama-3.3-70b-versatile\",\"system_fingerprint\":\"fp_d42c28f9ce\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"Hello\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-dd5aae9f-7032-44a7-aca8-01027903b4c9\",\"object\":\"chat.completion.chunk\",\"created\":1778031305,\"model\":\"llama-3.3-70b-versatile\",\"system_fingerprint\":\"fp_d42c28f9ce\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"!\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-dd5aae9f-7032-44a7-aca8-01027903b4c9\",\"object\":\"chat.completion.chunk\",\"created\":1778031305,\"model\":\"llama-3.3-70b-versatile\",\"system_fingerprint\":\"fp_d42c28f9ce\",\"choices\":[{\"index\":0,\"delta\":{},\"logprobs\":null,\"finish_reason\":\"stop\"}],\"x_groq\":{\"id\":\"req_01kqxes8r3fmja0yhxvt665m6h\",\"usage\":{\"queue_time\":0.0381395,\"prompt_tokens\":45,\"prompt_time\":0.003985297,\"completion_tokens\":3,\"completion_time\":0.014171875,\"total_tokens\":48,\"total_time\":0.018157172}},\"usage\":{\"queue_time\":0.0381395,\"prompt_tokens\":45,\"prompt_time\":0.003985297,\"completion_tokens\":3,\"completion_time\":0.014171875,\"total_tokens\":48,\"total_time\":0.018157172}}\n\ndata: {\"id\":\"chatcmpl-dd5aae9f-7032-44a7-aca8-01027903b4c9\",\"object\":\"chat.completion.chunk\",\"created\":1778031305,\"model\":\"llama-3.3-70b-versatile\",\"system_fingerprint\":\"fp_d42c28f9ce\",\"choices\":[],\"usage\":{\"queue_time\":0.0381395,\"prompt_tokens\":45,\"prompt_time\":0.003985297,\"completion_tokens\":3,\"completion_time\":0.014171875,\"total_tokens\":48,\"total_time\":0.018157172},\"service_tier\":\"on_demand\"}\n\ndata: [DONE]\n\n"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
-28
@@ -1,28 +0,0 @@
|
||||
{
|
||||
"version": 1,
|
||||
"metadata": {
|
||||
"name": "openai-compatible-chat/groq-streams-tool-call",
|
||||
"recordedAt": "2026-05-06T01:35:05.706Z",
|
||||
"tags": ["prefix:openai-compatible-chat", "protocol:openai-compatible-chat", "provider:groq", "tool"]
|
||||
},
|
||||
"interactions": [
|
||||
{
|
||||
"transport": "http",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "https://api.groq.com/openai/v1/chat/completions",
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": "{\"model\":\"llama-3.3-70b-versatile\",\"messages\":[{\"role\":\"system\",\"content\":\"Call tools exactly as requested.\"},{\"role\":\"user\",\"content\":\"Call get_weather with city exactly Paris.\"}],\"tools\":[{\"type\":\"function\",\"function\":{\"name\":\"get_weather\",\"description\":\"Get current weather for a city.\",\"parameters\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"],\"additionalProperties\":false}}}],\"tool_choice\":{\"type\":\"function\",\"function\":{\"name\":\"get_weather\"}},\"stream\":true,\"stream_options\":{\"include_usage\":true},\"max_tokens\":80,\"temperature\":0}"
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"content-type": "text/event-stream"
|
||||
},
|
||||
"body": "data: {\"id\":\"chatcmpl-05380361-f8e4-444a-ae80-296b4d1d46f7\",\"object\":\"chat.completion.chunk\",\"created\":1778031305,\"model\":\"llama-3.3-70b-versatile\",\"system_fingerprint\":\"fp_0761e44d7b\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":null},\"logprobs\":null,\"finish_reason\":null}],\"x_groq\":{\"id\":\"req_01kqxes8v4fm7baf4smt42f0qn\",\"seed\":1846647562}}\n\ndata: {\"id\":\"chatcmpl-05380361-f8e4-444a-ae80-296b4d1d46f7\",\"object\":\"chat.completion.chunk\",\"created\":1778031305,\"model\":\"llama-3.3-70b-versatile\",\"system_fingerprint\":\"fp_0761e44d7b\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"id\":\"mcf2d8nn1\",\"type\":\"function\",\"function\":{\"name\":\"get_weather\",\"arguments\":\"{\\\"city\\\":\\\"Paris\\\"}\"},\"index\":0}]},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-05380361-f8e4-444a-ae80-296b4d1d46f7\",\"object\":\"chat.completion.chunk\",\"created\":1778031305,\"model\":\"llama-3.3-70b-versatile\",\"system_fingerprint\":\"fp_0761e44d7b\",\"choices\":[{\"index\":0,\"delta\":{},\"logprobs\":null,\"finish_reason\":\"tool_calls\"}],\"x_groq\":{\"id\":\"req_01kqxes8v4fm7baf4smt42f0qn\",\"usage\":{\"queue_time\":0.07684935,\"prompt_tokens\":249,\"prompt_time\":0.014815006,\"completion_tokens\":10,\"completion_time\":0.036435756,\"total_tokens\":259,\"total_time\":0.051250762}},\"usage\":{\"queue_time\":0.07684935,\"prompt_tokens\":249,\"prompt_time\":0.014815006,\"completion_tokens\":10,\"completion_time\":0.036435756,\"total_tokens\":259,\"total_time\":0.051250762}}\n\ndata: {\"id\":\"chatcmpl-05380361-f8e4-444a-ae80-296b4d1d46f7\",\"object\":\"chat.completion.chunk\",\"created\":1778031305,\"model\":\"llama-3.3-70b-versatile\",\"system_fingerprint\":\"fp_0761e44d7b\",\"choices\":[],\"usage\":{\"queue_time\":0.07684935,\"prompt_tokens\":249,\"prompt_time\":0.014815006,\"completion_tokens\":10,\"completion_time\":0.036435756,\"total_tokens\":259,\"total_time\":0.051250762},\"service_tier\":\"on_demand\"}\n\ndata: [DONE]\n\n"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
-54
@@ -1,54 +0,0 @@
|
||||
{
|
||||
"version": 1,
|
||||
"metadata": {
|
||||
"name": "openai-compatible-chat/openrouter-claude-opus-4-7-drives-a-tool-loop",
|
||||
"recordedAt": "2026-05-06T01:35:14.282Z",
|
||||
"tags": [
|
||||
"prefix:openai-compatible-chat",
|
||||
"protocol:openai-compatible-chat",
|
||||
"provider:openrouter",
|
||||
"tool",
|
||||
"tool-loop",
|
||||
"golden",
|
||||
"flagship"
|
||||
]
|
||||
},
|
||||
"interactions": [
|
||||
{
|
||||
"transport": "http",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "https://openrouter.ai/api/v1/chat/completions",
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": "{\"model\":\"anthropic/claude-opus-4.7\",\"messages\":[{\"role\":\"system\",\"content\":\"Use the get_weather tool exactly once, then answer in one short sentence.\"},{\"role\":\"user\",\"content\":\"What is the weather in Paris?\"}],\"tools\":[{\"type\":\"function\",\"function\":{\"name\":\"get_weather\",\"description\":\"Get current weather for a city.\",\"parameters\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"],\"additionalProperties\":false}}}],\"stream\":true,\"stream_options\":{\"include_usage\":true},\"max_tokens\":80,\"temperature\":0}"
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"content-type": "text/event-stream"
|
||||
},
|
||||
"body": ": OPENROUTER PROCESSING\n\n: OPENROUTER PROCESSING\n\n: OPENROUTER PROCESSING\n\ndata: {\"id\":\"gen-1778031311-S3NlfYGRwAnOoPoNrThK\",\"object\":\"chat.completion.chunk\",\"created\":1778031311,\"model\":\"anthropic/claude-4.7-opus-20260416\",\"provider\":\"Amazon Bedrock\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"role\":\"assistant\",\"tool_calls\":[{\"index\":0,\"id\":\"toolu_bdrk_01AVRkzbigpMbNJ3zjnuQ6ZE\",\"type\":\"function\",\"function\":{\"name\":\"get_weather\",\"arguments\":\"\"}}]},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"gen-1778031311-S3NlfYGRwAnOoPoNrThK\",\"object\":\"chat.completion.chunk\",\"created\":1778031311,\"model\":\"anthropic/claude-4.7-opus-20260416\",\"provider\":\"Amazon Bedrock\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"role\":\"assistant\",\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"\"}}]},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"gen-1778031311-S3NlfYGRwAnOoPoNrThK\",\"object\":\"chat.completion.chunk\",\"created\":1778031311,\"model\":\"anthropic/claude-4.7-opus-20260416\",\"provider\":\"Amazon Bedrock\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"role\":\"assistant\",\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"\"}}]},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"gen-1778031311-S3NlfYGRwAnOoPoNrThK\",\"object\":\"chat.completion.chunk\",\"created\":1778031311,\"model\":\"anthropic/claude-4.7-opus-20260416\",\"provider\":\"Amazon Bedrock\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"role\":\"assistant\",\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"{\\\"city\\\": \\\"P\"}}]},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"gen-1778031311-S3NlfYGRwAnOoPoNrThK\",\"object\":\"chat.completion.chunk\",\"created\":1778031311,\"model\":\"anthropic/claude-4.7-opus-20260416\",\"provider\":\"Amazon Bedrock\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"role\":\"assistant\",\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"ari\"}}]},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"gen-1778031311-S3NlfYGRwAnOoPoNrThK\",\"object\":\"chat.completion.chunk\",\"created\":1778031311,\"model\":\"anthropic/claude-4.7-opus-20260416\",\"provider\":\"Amazon Bedrock\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"role\":\"assistant\",\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"s\\\"}\"}}]},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"gen-1778031311-S3NlfYGRwAnOoPoNrThK\",\"object\":\"chat.completion.chunk\",\"created\":1778031311,\"model\":\"anthropic/claude-4.7-opus-20260416\",\"provider\":\"Amazon Bedrock\",\"service_tier\":\"standard\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"\",\"role\":\"assistant\"},\"finish_reason\":\"tool_calls\",\"native_finish_reason\":\"tool_use\"}]}\n\ndata: {\"id\":\"gen-1778031311-S3NlfYGRwAnOoPoNrThK\",\"object\":\"chat.completion.chunk\",\"created\":1778031311,\"model\":\"anthropic/claude-4.7-opus-20260416\",\"provider\":\"Amazon Bedrock\",\"service_tier\":\"standard\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"\",\"role\":\"assistant\"},\"finish_reason\":\"tool_calls\",\"native_finish_reason\":\"tool_use\"}],\"usage\":{\"prompt_tokens\":802,\"completion_tokens\":66,\"total_tokens\":868,\"cost\":0.00566,\"is_byok\":false,\"prompt_tokens_details\":{\"cached_tokens\":0,\"cache_write_tokens\":0,\"audio_tokens\":0,\"video_tokens\":0},\"cost_details\":{\"upstream_inference_cost\":0.00566,\"upstream_inference_prompt_cost\":0.00401,\"upstream_inference_completions_cost\":0.00165},\"completion_tokens_details\":{\"reasoning_tokens\":0,\"image_tokens\":0,\"audio_tokens\":0}}}\n\ndata: [DONE]\n\n"
|
||||
}
|
||||
},
|
||||
{
|
||||
"transport": "http",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "https://openrouter.ai/api/v1/chat/completions",
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": "{\"model\":\"anthropic/claude-opus-4.7\",\"messages\":[{\"role\":\"system\",\"content\":\"Use the get_weather tool exactly once, then answer in one short sentence.\"},{\"role\":\"user\",\"content\":\"What is the weather in Paris?\"},{\"role\":\"assistant\",\"content\":null,\"tool_calls\":[{\"id\":\"toolu_bdrk_01AVRkzbigpMbNJ3zjnuQ6ZE\",\"type\":\"function\",\"function\":{\"name\":\"get_weather\",\"arguments\":\"{\\\"city\\\":\\\"Paris\\\"}\"}}]},{\"role\":\"tool\",\"tool_call_id\":\"toolu_bdrk_01AVRkzbigpMbNJ3zjnuQ6ZE\",\"content\":\"{\\\"temperature\\\":22,\\\"condition\\\":\\\"sunny\\\"}\"}],\"tools\":[{\"type\":\"function\",\"function\":{\"name\":\"get_weather\",\"description\":\"Get current weather for a city.\",\"parameters\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"],\"additionalProperties\":false}}}],\"stream\":true,\"stream_options\":{\"include_usage\":true},\"max_tokens\":80,\"temperature\":0}"
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"content-type": "text/event-stream"
|
||||
},
|
||||
"body": ": OPENROUTER PROCESSING\n\ndata: {\"id\":\"gen-1778031313-XM4XZGmFyt6jg3GZ772w\",\"object\":\"chat.completion.chunk\",\"created\":1778031313,\"model\":\"anthropic/claude-4.7-opus-20260416\",\"provider\":\"Amazon Bedrock\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"It\",\"role\":\"assistant\"},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"gen-1778031313-XM4XZGmFyt6jg3GZ772w\",\"object\":\"chat.completion.chunk\",\"created\":1778031313,\"model\":\"anthropic/claude-4.7-opus-20260416\",\"provider\":\"Amazon Bedrock\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"'s sunny and\",\"role\":\"assistant\"},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"gen-1778031313-XM4XZGmFyt6jg3GZ772w\",\"object\":\"chat.completion.chunk\",\"created\":1778031313,\"model\":\"anthropic/claude-4.7-opus-20260416\",\"provider\":\"Amazon Bedrock\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" 22°C in\",\"role\":\"assistant\"},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"gen-1778031313-XM4XZGmFyt6jg3GZ772w\",\"object\":\"chat.completion.chunk\",\"created\":1778031313,\"model\":\"anthropic/claude-4.7-opus-20260416\",\"provider\":\"Amazon Bedrock\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" Paris.\",\"role\":\"assistant\"},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"gen-1778031313-XM4XZGmFyt6jg3GZ772w\",\"object\":\"chat.completion.chunk\",\"created\":1778031313,\"model\":\"anthropic/claude-4.7-opus-20260416\",\"provider\":\"Amazon Bedrock\",\"service_tier\":\"standard\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"\",\"role\":\"assistant\"},\"finish_reason\":\"stop\",\"native_finish_reason\":\"end_turn\"}]}\n\ndata: {\"id\":\"gen-1778031313-XM4XZGmFyt6jg3GZ772w\",\"object\":\"chat.completion.chunk\",\"created\":1778031313,\"model\":\"anthropic/claude-4.7-opus-20260416\",\"provider\":\"Amazon Bedrock\",\"service_tier\":\"standard\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"\",\"role\":\"assistant\"},\"finish_reason\":\"stop\",\"native_finish_reason\":\"end_turn\"}],\"usage\":{\"prompt_tokens\":899,\"completion_tokens\":19,\"total_tokens\":918,\"cost\":0.00497,\"is_byok\":false,\"prompt_tokens_details\":{\"cached_tokens\":0,\"cache_write_tokens\":0,\"audio_tokens\":0,\"video_tokens\":0},\"cost_details\":{\"upstream_inference_cost\":0.00497,\"upstream_inference_prompt_cost\":0.004495,\"upstream_inference_completions_cost\":0.000475},\"completion_tokens_details\":{\"reasoning_tokens\":0,\"image_tokens\":0,\"audio_tokens\":0}}}\n\ndata: [DONE]\n\n"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
-53
@@ -1,53 +0,0 @@
|
||||
{
|
||||
"version": 1,
|
||||
"metadata": {
|
||||
"name": "openai-compatible-chat/openrouter-gpt-4o-mini-drives-a-tool-loop",
|
||||
"recordedAt": "2026-05-06T01:35:08.922Z",
|
||||
"tags": [
|
||||
"prefix:openai-compatible-chat",
|
||||
"protocol:openai-compatible-chat",
|
||||
"provider:openrouter",
|
||||
"tool",
|
||||
"tool-loop",
|
||||
"golden"
|
||||
]
|
||||
},
|
||||
"interactions": [
|
||||
{
|
||||
"transport": "http",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "https://openrouter.ai/api/v1/chat/completions",
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": "{\"model\":\"openai/gpt-4o-mini\",\"messages\":[{\"role\":\"system\",\"content\":\"Use the get_weather tool exactly once, then answer in one short sentence.\"},{\"role\":\"user\",\"content\":\"What is the weather in Paris?\"}],\"tools\":[{\"type\":\"function\",\"function\":{\"name\":\"get_weather\",\"description\":\"Get current weather for a city.\",\"parameters\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"],\"additionalProperties\":false}}}],\"stream\":true,\"stream_options\":{\"include_usage\":true},\"max_tokens\":80,\"temperature\":0}"
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"content-type": "text/event-stream"
|
||||
},
|
||||
"body": ": OPENROUTER PROCESSING\n\ndata: {\"id\":\"gen-1778031307-FcHCDYW9unDVyRRL841T\",\"object\":\"chat.completion.chunk\",\"created\":1778031307,\"model\":\"openai/gpt-4o-mini\",\"provider\":\"OpenAI\",\"system_fingerprint\":\"fp_7e69b4ef44\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"role\":\"assistant\",\"tool_calls\":[{\"index\":0,\"id\":\"call_S63bjYITINemSHZ4Uqns7PIu\",\"type\":\"function\",\"function\":{\"name\":\"get_weather\",\"arguments\":\"\"}}]},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"gen-1778031307-FcHCDYW9unDVyRRL841T\",\"object\":\"chat.completion.chunk\",\"created\":1778031307,\"model\":\"openai/gpt-4o-mini\",\"provider\":\"OpenAI\",\"system_fingerprint\":\"fp_7e69b4ef44\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"role\":\"assistant\",\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"\"}}]},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"gen-1778031307-FcHCDYW9unDVyRRL841T\",\"object\":\"chat.completion.chunk\",\"created\":1778031307,\"model\":\"openai/gpt-4o-mini\",\"provider\":\"OpenAI\",\"system_fingerprint\":\"fp_7e69b4ef44\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"role\":\"assistant\",\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"{\\\"\"}}]},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"gen-1778031307-FcHCDYW9unDVyRRL841T\",\"object\":\"chat.completion.chunk\",\"created\":1778031307,\"model\":\"openai/gpt-4o-mini\",\"provider\":\"OpenAI\",\"system_fingerprint\":\"fp_7e69b4ef44\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"role\":\"assistant\",\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"city\"}}]},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"gen-1778031307-FcHCDYW9unDVyRRL841T\",\"object\":\"chat.completion.chunk\",\"created\":1778031307,\"model\":\"openai/gpt-4o-mini\",\"provider\":\"OpenAI\",\"system_fingerprint\":\"fp_7e69b4ef44\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"role\":\"assistant\",\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"\\\":\\\"\"}}]},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"gen-1778031307-FcHCDYW9unDVyRRL841T\",\"object\":\"chat.completion.chunk\",\"created\":1778031307,\"model\":\"openai/gpt-4o-mini\",\"provider\":\"OpenAI\",\"system_fingerprint\":\"fp_7e69b4ef44\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"role\":\"assistant\",\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"Paris\"}}]},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"gen-1778031307-FcHCDYW9unDVyRRL841T\",\"object\":\"chat.completion.chunk\",\"created\":1778031307,\"model\":\"openai/gpt-4o-mini\",\"provider\":\"OpenAI\",\"system_fingerprint\":\"fp_7e69b4ef44\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"role\":\"assistant\",\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"\\\"}\"}}]},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"gen-1778031307-FcHCDYW9unDVyRRL841T\",\"object\":\"chat.completion.chunk\",\"created\":1778031307,\"model\":\"openai/gpt-4o-mini\",\"provider\":\"OpenAI\",\"system_fingerprint\":\"fp_7e69b4ef44\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"\",\"role\":\"assistant\"},\"finish_reason\":\"tool_calls\",\"native_finish_reason\":\"tool_calls\"}]}\n\ndata: {\"id\":\"gen-1778031307-FcHCDYW9unDVyRRL841T\",\"object\":\"chat.completion.chunk\",\"created\":1778031307,\"model\":\"openai/gpt-4o-mini\",\"provider\":\"OpenAI\",\"system_fingerprint\":\"fp_7e69b4ef44\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"\",\"role\":\"assistant\"},\"finish_reason\":\"tool_calls\",\"native_finish_reason\":\"tool_calls\"}],\"usage\":{\"prompt_tokens\":66,\"completion_tokens\":14,\"total_tokens\":80,\"cost\":0.0000183,\"is_byok\":false,\"prompt_tokens_details\":{\"cached_tokens\":0,\"cache_write_tokens\":0,\"audio_tokens\":0,\"video_tokens\":0},\"cost_details\":{\"upstream_inference_cost\":0.0000183,\"upstream_inference_prompt_cost\":0.0000099,\"upstream_inference_completions_cost\":0.0000084},\"completion_tokens_details\":{\"reasoning_tokens\":0,\"image_tokens\":0,\"audio_tokens\":0}}}\n\ndata: [DONE]\n\n"
|
||||
}
|
||||
},
|
||||
{
|
||||
"transport": "http",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "https://openrouter.ai/api/v1/chat/completions",
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": "{\"model\":\"openai/gpt-4o-mini\",\"messages\":[{\"role\":\"system\",\"content\":\"Use the get_weather tool exactly once, then answer in one short sentence.\"},{\"role\":\"user\",\"content\":\"What is the weather in Paris?\"},{\"role\":\"assistant\",\"content\":null,\"tool_calls\":[{\"id\":\"call_S63bjYITINemSHZ4Uqns7PIu\",\"type\":\"function\",\"function\":{\"name\":\"get_weather\",\"arguments\":\"{\\\"city\\\":\\\"Paris\\\"}\"}}]},{\"role\":\"tool\",\"tool_call_id\":\"call_S63bjYITINemSHZ4Uqns7PIu\",\"content\":\"{\\\"temperature\\\":22,\\\"condition\\\":\\\"sunny\\\"}\"}],\"tools\":[{\"type\":\"function\",\"function\":{\"name\":\"get_weather\",\"description\":\"Get current weather for a city.\",\"parameters\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"],\"additionalProperties\":false}}}],\"stream\":true,\"stream_options\":{\"include_usage\":true},\"max_tokens\":80,\"temperature\":0}"
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"content-type": "text/event-stream"
|
||||
},
|
||||
"body": ": OPENROUTER PROCESSING\n\ndata: {\"id\":\"gen-1778031308-uNHYY6MdDXOs0BYMXXVb\",\"object\":\"chat.completion.chunk\",\"created\":1778031308,\"model\":\"openai/gpt-4o-mini\",\"provider\":\"OpenAI\",\"system_fingerprint\":\"fp_7e69b4ef44\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"The\",\"role\":\"assistant\"},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"gen-1778031308-uNHYY6MdDXOs0BYMXXVb\",\"object\":\"chat.completion.chunk\",\"created\":1778031308,\"model\":\"openai/gpt-4o-mini\",\"provider\":\"OpenAI\",\"system_fingerprint\":\"fp_7e69b4ef44\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" weather\",\"role\":\"assistant\"},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"gen-1778031308-uNHYY6MdDXOs0BYMXXVb\",\"object\":\"chat.completion.chunk\",\"created\":1778031308,\"model\":\"openai/gpt-4o-mini\",\"provider\":\"OpenAI\",\"system_fingerprint\":\"fp_7e69b4ef44\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" in\",\"role\":\"assistant\"},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"gen-1778031308-uNHYY6MdDXOs0BYMXXVb\",\"object\":\"chat.completion.chunk\",\"created\":1778031308,\"model\":\"openai/gpt-4o-mini\",\"provider\":\"OpenAI\",\"system_fingerprint\":\"fp_7e69b4ef44\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" Paris\",\"role\":\"assistant\"},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"gen-1778031308-uNHYY6MdDXOs0BYMXXVb\",\"object\":\"chat.completion.chunk\",\"created\":1778031308,\"model\":\"openai/gpt-4o-mini\",\"provider\":\"OpenAI\",\"system_fingerprint\":\"fp_7e69b4ef44\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" is\",\"role\":\"assistant\"},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"gen-1778031308-uNHYY6MdDXOs0BYMXXVb\",\"object\":\"chat.completion.chunk\",\"created\":1778031308,\"model\":\"openai/gpt-4o-mini\",\"provider\":\"OpenAI\",\"system_fingerprint\":\"fp_7e69b4ef44\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" sunny\",\"role\":\"assistant\"},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"gen-1778031308-uNHYY6MdDXOs0BYMXXVb\",\"object\":\"chat.completion.chunk\",\"created\":1778031308,\"model\":\"openai/gpt-4o-mini\",\"provider\":\"OpenAI\",\"system_fingerprint\":\"fp_7e69b4ef44\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" with\",\"role\":\"assistant\"},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"gen-1778031308-uNHYY6MdDXOs0BYMXXVb\",\"object\":\"chat.completion.chunk\",\"created\":1778031308,\"model\":\"openai/gpt-4o-mini\",\"provider\":\"OpenAI\",\"system_fingerprint\":\"fp_7e69b4ef44\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" a\",\"role\":\"assistant\"},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"gen-1778031308-uNHYY6MdDXOs0BYMXXVb\",\"object\":\"chat.completion.chunk\",\"created\":1778031308,\"model\":\"openai/gpt-4o-mini\",\"provider\":\"OpenAI\",\"system_fingerprint\":\"fp_7e69b4ef44\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" temperature\",\"role\":\"assistant\"},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"gen-1778031308-uNHYY6MdDXOs0BYMXXVb\",\"object\":\"chat.completion.chunk\",\"created\":1778031308,\"model\":\"openai/gpt-4o-mini\",\"provider\":\"OpenAI\",\"system_fingerprint\":\"fp_7e69b4ef44\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" of\",\"role\":\"assistant\"},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"gen-1778031308-uNHYY6MdDXOs0BYMXXVb\",\"object\":\"chat.completion.chunk\",\"created\":1778031308,\"model\":\"openai/gpt-4o-mini\",\"provider\":\"OpenAI\",\"system_fingerprint\":\"fp_7e69b4ef44\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" \",\"role\":\"assistant\"},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"gen-1778031308-uNHYY6MdDXOs0BYMXXVb\",\"object\":\"chat.completion.chunk\",\"created\":1778031308,\"model\":\"openai/gpt-4o-mini\",\"provider\":\"OpenAI\",\"system_fingerprint\":\"fp_7e69b4ef44\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"22\",\"role\":\"assistant\"},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"gen-1778031308-uNHYY6MdDXOs0BYMXXVb\",\"object\":\"chat.completion.chunk\",\"created\":1778031308,\"model\":\"openai/gpt-4o-mini\",\"provider\":\"OpenAI\",\"system_fingerprint\":\"fp_7e69b4ef44\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"°C\",\"role\":\"assistant\"},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"gen-1778031308-uNHYY6MdDXOs0BYMXXVb\",\"object\":\"chat.completion.chunk\",\"created\":1778031308,\"model\":\"openai/gpt-4o-mini\",\"provider\":\"OpenAI\",\"system_fingerprint\":\"fp_7e69b4ef44\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\".\",\"role\":\"assistant\"},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"idLine truncated
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
-54
@@ -1,54 +0,0 @@
|
||||
{
|
||||
"version": 1,
|
||||
"metadata": {
|
||||
"name": "openai-compatible-chat/openrouter-gpt-5-5-drives-a-tool-loop",
|
||||
"recordedAt": "2026-05-06T01:35:11.662Z",
|
||||
"tags": [
|
||||
"prefix:openai-compatible-chat",
|
||||
"protocol:openai-compatible-chat",
|
||||
"provider:openrouter",
|
||||
"tool",
|
||||
"tool-loop",
|
||||
"golden",
|
||||
"flagship"
|
||||
]
|
||||
},
|
||||
"interactions": [
|
||||
{
|
||||
"transport": "http",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "https://openrouter.ai/api/v1/chat/completions",
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": "{\"model\":\"openai/gpt-5.5\",\"messages\":[{\"role\":\"system\",\"content\":\"Use the get_weather tool exactly once, then answer in one short sentence.\"},{\"role\":\"user\",\"content\":\"What is the weather in Paris?\"}],\"tools\":[{\"type\":\"function\",\"function\":{\"name\":\"get_weather\",\"description\":\"Get current weather for a city.\",\"parameters\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"],\"additionalProperties\":false}}}],\"stream\":true,\"stream_options\":{\"include_usage\":true},\"max_tokens\":80,\"temperature\":0}"
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"content-type": "text/event-stream"
|
||||
},
|
||||
"body": ": OPENROUTER PROCESSING\n\n: OPENROUTER PROCESSING\n\ndata: {\"id\":\"gen-1778031308-dVa9axcHcOlG9GcilZkz\",\"object\":\"chat.completion.chunk\",\"created\":1778031308,\"model\":\"openai/gpt-5.5-20260423\",\"provider\":\"OpenAI\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"role\":\"assistant\",\"tool_calls\":[{\"index\":0,\"id\":\"call_4A7V7UN36HXCUUn8qAOQaKGw\",\"type\":\"function\",\"function\":{\"name\":\"get_weather\",\"arguments\":\"\"}}]},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"gen-1778031308-dVa9axcHcOlG9GcilZkz\",\"object\":\"chat.completion.chunk\",\"created\":1778031308,\"model\":\"openai/gpt-5.5-20260423\",\"provider\":\"OpenAI\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"role\":\"assistant\",\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"\"}}]},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"gen-1778031308-dVa9axcHcOlG9GcilZkz\",\"object\":\"chat.completion.chunk\",\"created\":1778031308,\"model\":\"openai/gpt-5.5-20260423\",\"provider\":\"OpenAI\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"role\":\"assistant\",\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"{\\\"\"}}]},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"gen-1778031308-dVa9axcHcOlG9GcilZkz\",\"object\":\"chat.completion.chunk\",\"created\":1778031308,\"model\":\"openai/gpt-5.5-20260423\",\"provider\":\"OpenAI\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"role\":\"assistant\",\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"city\"}}]},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"gen-1778031308-dVa9axcHcOlG9GcilZkz\",\"object\":\"chat.completion.chunk\",\"created\":1778031308,\"model\":\"openai/gpt-5.5-20260423\",\"provider\":\"OpenAI\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"role\":\"assistant\",\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"\\\":\\\"\"}}]},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"gen-1778031308-dVa9axcHcOlG9GcilZkz\",\"object\":\"chat.completion.chunk\",\"created\":1778031308,\"model\":\"openai/gpt-5.5-20260423\",\"provider\":\"OpenAI\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"role\":\"assistant\",\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"Paris\"}}]},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"gen-1778031308-dVa9axcHcOlG9GcilZkz\",\"object\":\"chat.completion.chunk\",\"created\":1778031308,\"model\":\"openai/gpt-5.5-20260423\",\"provider\":\"OpenAI\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"role\":\"assistant\",\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"\\\"}\"}}]},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"gen-1778031308-dVa9axcHcOlG9GcilZkz\",\"object\":\"chat.completion.chunk\",\"created\":1778031308,\"model\":\"openai/gpt-5.5-20260423\",\"provider\":\"OpenAI\",\"service_tier\":\"default\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"\",\"role\":\"assistant\"},\"finish_reason\":\"tool_calls\",\"native_finish_reason\":\"completed\"}]}\n\ndata: {\"id\":\"gen-1778031308-dVa9axcHcOlG9GcilZkz\",\"object\":\"chat.completion.chunk\",\"created\":1778031308,\"model\":\"openai/gpt-5.5-20260423\",\"provider\":\"OpenAI\",\"service_tier\":\"default\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"\",\"role\":\"assistant\"},\"finish_reason\":\"tool_calls\",\"native_finish_reason\":\"completed\"}],\"usage\":{\"prompt_tokens\":69,\"completion_tokens\":18,\"total_tokens\":87,\"cost\":0.000885,\"is_byok\":false,\"prompt_tokens_details\":{\"cached_tokens\":0,\"cache_write_tokens\":0,\"audio_tokens\":0,\"video_tokens\":0},\"cost_details\":{\"upstream_inference_cost\":0.000885,\"upstream_inference_prompt_cost\":0.000345,\"upstream_inference_completions_cost\":0.00054},\"completion_tokens_details\":{\"reasoning_tokens\":0,\"image_tokens\":0,\"audio_tokens\":0}}}\n\ndata: [DONE]\n\n"
|
||||
}
|
||||
},
|
||||
{
|
||||
"transport": "http",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "https://openrouter.ai/api/v1/chat/completions",
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": "{\"model\":\"openai/gpt-5.5\",\"messages\":[{\"role\":\"system\",\"content\":\"Use the get_weather tool exactly once, then answer in one short sentence.\"},{\"role\":\"user\",\"content\":\"What is the weather in Paris?\"},{\"role\":\"assistant\",\"content\":null,\"tool_calls\":[{\"id\":\"call_4A7V7UN36HXCUUn8qAOQaKGw\",\"type\":\"function\",\"function\":{\"name\":\"get_weather\",\"arguments\":\"{\\\"city\\\":\\\"Paris\\\"}\"}}]},{\"role\":\"tool\",\"tool_call_id\":\"call_4A7V7UN36HXCUUn8qAOQaKGw\",\"content\":\"{\\\"temperature\\\":22,\\\"condition\\\":\\\"sunny\\\"}\"}],\"tools\":[{\"type\":\"function\",\"function\":{\"name\":\"get_weather\",\"description\":\"Get current weather for a city.\",\"parameters\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"],\"additionalProperties\":false}}}],\"stream\":true,\"stream_options\":{\"include_usage\":true},\"max_tokens\":80,\"temperature\":0}"
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"content-type": "text/event-stream"
|
||||
},
|
||||
"body": ": OPENROUTER PROCESSING\n\n: OPENROUTER PROCESSING\n\ndata: {\"id\":\"gen-1778031310-JUYfFzDbun699uUYoA4N\",\"object\":\"chat.completion.chunk\",\"created\":1778031310,\"model\":\"openai/gpt-5.5-20260423\",\"provider\":\"OpenAI\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"Paris\",\"role\":\"assistant\"},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"gen-1778031310-JUYfFzDbun699uUYoA4N\",\"object\":\"chat.completion.chunk\",\"created\":1778031310,\"model\":\"openai/gpt-5.5-20260423\",\"provider\":\"OpenAI\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" is\",\"role\":\"assistant\"},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"gen-1778031310-JUYfFzDbun699uUYoA4N\",\"object\":\"chat.completion.chunk\",\"created\":1778031310,\"model\":\"openai/gpt-5.5-20260423\",\"provider\":\"OpenAI\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" sunny\",\"role\":\"assistant\"},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"gen-1778031310-JUYfFzDbun699uUYoA4N\",\"object\":\"chat.completion.chunk\",\"created\":1778031310,\"model\":\"openai/gpt-5.5-20260423\",\"provider\":\"OpenAI\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" and\",\"role\":\"assistant\"},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"gen-1778031310-JUYfFzDbun699uUYoA4N\",\"object\":\"chat.completion.chunk\",\"created\":1778031310,\"model\":\"openai/gpt-5.5-20260423\",\"provider\":\"OpenAI\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" \",\"role\":\"assistant\"},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"gen-1778031310-JUYfFzDbun699uUYoA4N\",\"object\":\"chat.completion.chunk\",\"created\":1778031310,\"model\":\"openai/gpt-5.5-20260423\",\"provider\":\"OpenAI\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"22\",\"role\":\"assistant\"},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"gen-1778031310-JUYfFzDbun699uUYoA4N\",\"object\":\"chat.completion.chunk\",\"created\":1778031310,\"model\":\"openai/gpt-5.5-20260423\",\"provider\":\"OpenAI\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"°C\",\"role\":\"assistant\"},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"gen-1778031310-JUYfFzDbun699uUYoA4N\",\"object\":\"chat.completion.chunk\",\"created\":1778031310,\"model\":\"openai/gpt-5.5-20260423\",\"provider\":\"OpenAI\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\".\",\"role\":\"assistant\"},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"gen-1778031310-JUYfFzDbun699uUYoA4N\",\"object\":\"chat.completion.chunk\",\"created\":1778031310,\"model\":\"openai/gpt-5.5-20260423\",\"provider\":\"OpenAI\",\"service_tier\":\"default\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"\",\"role\":\"assistant\"},\"finish_reason\":\"stop\",\"native_finish_reason\":\"completed\"}]}\n\ndata: {\"id\":\"gen-1778031310-JUYfFzDbun699uUYoA4N\",\"object\":\"chat.completion.chunk\",\"created\":1778031310,\"model\":\"openai/gpt-5.5-20260423\",\"provider\":\"OpenAI\",\"service_tier\":\"default\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"\",\"role\":\"assistant\"},\"finish_reason\":\"stop\",\"native_finish_reason\":\"completed\"}],\"usage\":{\"prompt_tokens\":108,\"completion_tokens\":12,\"total_tokens\":120,\"cost\":0.0009,\"is_byok\":false,\"prompt_tokens_details\":{\"cached_tokens\":0,\"cache_write_tokens\":0,\"audio_tokens\":0,\"video_tokens\":0},\"cost_details\":{\"upstream_inference_cost\":0.0009,\"upstream_inference_prompt_cost\":0.00054,\"upstream_inference_completions_cost\":0.00036},\"completion_tokens_details\":{\"reasoning_tokens\":0,\"image_tokens\":0,\"audio_tokens\":0}}}\n\ndata: [DONE]\n\n"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
-28
@@ -1,28 +0,0 @@
|
||||
{
|
||||
"version": 1,
|
||||
"metadata": {
|
||||
"name": "openai-compatible-chat/openrouter-streams-text",
|
||||
"recordedAt": "2026-05-06T01:35:06.767Z",
|
||||
"tags": ["prefix:openai-compatible-chat", "protocol:openai-compatible-chat", "provider:openrouter"]
|
||||
},
|
||||
"interactions": [
|
||||
{
|
||||
"transport": "http",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "https://openrouter.ai/api/v1/chat/completions",
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": "{\"model\":\"openai/gpt-4o-mini\",\"messages\":[{\"role\":\"system\",\"content\":\"You are concise.\"},{\"role\":\"user\",\"content\":\"Reply with exactly: Hello!\"}],\"stream\":true,\"stream_options\":{\"include_usage\":true},\"max_tokens\":20,\"temperature\":0}"
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"content-type": "text/event-stream"
|
||||
},
|
||||
"body": ": OPENROUTER PROCESSING\n\ndata: {\"id\":\"gen-1778031306-UD7bR0I1JNCsPvVzlXat\",\"object\":\"chat.completion.chunk\",\"created\":1778031306,\"model\":\"openai/gpt-4o-mini\",\"provider\":\"Azure\",\"system_fingerprint\":\"fp_eb37e061ec\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"Hello\",\"role\":\"assistant\"},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"gen-1778031306-UD7bR0I1JNCsPvVzlXat\",\"object\":\"chat.completion.chunk\",\"created\":1778031306,\"model\":\"openai/gpt-4o-mini\",\"provider\":\"Azure\",\"system_fingerprint\":\"fp_eb37e061ec\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"!\",\"role\":\"assistant\"},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"gen-1778031306-UD7bR0I1JNCsPvVzlXat\",\"object\":\"chat.completion.chunk\",\"created\":1778031306,\"model\":\"openai/gpt-4o-mini\",\"provider\":\"Azure\",\"system_fingerprint\":\"fp_eb37e061ec\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"\",\"role\":\"assistant\"},\"finish_reason\":\"stop\",\"native_finish_reason\":\"stop\"}]}\n\ndata: {\"id\":\"gen-1778031306-UD7bR0I1JNCsPvVzlXat\",\"object\":\"chat.completion.chunk\",\"created\":1778031306,\"model\":\"openai/gpt-4o-mini\",\"provider\":\"Azure\",\"system_fingerprint\":\"fp_eb37e061ec\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"\",\"role\":\"assistant\"},\"finish_reason\":\"stop\",\"native_finish_reason\":\"stop\"}],\"usage\":{\"prompt_tokens\":21,\"completion_tokens\":3,\"total_tokens\":24,\"cost\":0.00000495,\"is_byok\":false,\"prompt_tokens_details\":{\"cached_tokens\":0,\"cache_write_tokens\":0,\"audio_tokens\":0,\"video_tokens\":0},\"cost_details\":{\"upstream_inference_cost\":0.00000495,\"upstream_inference_prompt_cost\":0.00000315,\"upstream_inference_completions_cost\":0.0000018},\"completion_tokens_details\":{\"reasoning_tokens\":0,\"image_tokens\":0,\"audio_tokens\":0}}}\n\ndata: [DONE]\n\n"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
-28
@@ -1,28 +0,0 @@
|
||||
{
|
||||
"version": 1,
|
||||
"metadata": {
|
||||
"name": "openai-compatible-chat/openrouter-streams-tool-call",
|
||||
"recordedAt": "2026-05-06T01:35:07.466Z",
|
||||
"tags": ["prefix:openai-compatible-chat", "protocol:openai-compatible-chat", "provider:openrouter", "tool"]
|
||||
},
|
||||
"interactions": [
|
||||
{
|
||||
"transport": "http",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "https://openrouter.ai/api/v1/chat/completions",
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": "{\"model\":\"openai/gpt-4o-mini\",\"messages\":[{\"role\":\"system\",\"content\":\"Call tools exactly as requested.\"},{\"role\":\"user\",\"content\":\"Call get_weather with city exactly Paris.\"}],\"tools\":[{\"type\":\"function\",\"function\":{\"name\":\"get_weather\",\"description\":\"Get current weather for a city.\",\"parameters\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"],\"additionalProperties\":false}}}],\"tool_choice\":{\"type\":\"function\",\"function\":{\"name\":\"get_weather\"}},\"stream\":true,\"stream_options\":{\"include_usage\":true},\"max_tokens\":80,\"temperature\":0}"
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"content-type": "text/event-stream"
|
||||
},
|
||||
"body": ": OPENROUTER PROCESSING\n\ndata: {\"id\":\"gen-1778031306-HYzOq04JIk1hZQ4iaNjD\",\"object\":\"chat.completion.chunk\",\"created\":1778031306,\"model\":\"openai/gpt-4o-mini\",\"provider\":\"OpenAI\",\"system_fingerprint\":\"fp_b6580bbee1\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"role\":\"assistant\",\"tool_calls\":[{\"index\":0,\"id\":\"call_L7mHMq49ZSUTBHjLJfBIP2eT\",\"type\":\"function\",\"function\":{\"name\":\"get_weather\",\"arguments\":\"\"}}]},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"gen-1778031306-HYzOq04JIk1hZQ4iaNjD\",\"object\":\"chat.completion.chunk\",\"created\":1778031306,\"model\":\"openai/gpt-4o-mini\",\"provider\":\"OpenAI\",\"system_fingerprint\":\"fp_b6580bbee1\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"role\":\"assistant\",\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"\"}}]},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"gen-1778031306-HYzOq04JIk1hZQ4iaNjD\",\"object\":\"chat.completion.chunk\",\"created\":1778031306,\"model\":\"openai/gpt-4o-mini\",\"provider\":\"OpenAI\",\"system_fingerprint\":\"fp_b6580bbee1\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"role\":\"assistant\",\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"{\\\"\"}}]},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"gen-1778031306-HYzOq04JIk1hZQ4iaNjD\",\"object\":\"chat.completion.chunk\",\"created\":1778031306,\"model\":\"openai/gpt-4o-mini\",\"provider\":\"OpenAI\",\"system_fingerprint\":\"fp_b6580bbee1\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"role\":\"assistant\",\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"city\"}}]},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"gen-1778031306-HYzOq04JIk1hZQ4iaNjD\",\"object\":\"chat.completion.chunk\",\"created\":1778031306,\"model\":\"openai/gpt-4o-mini\",\"provider\":\"OpenAI\",\"system_fingerprint\":\"fp_b6580bbee1\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"role\":\"assistant\",\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"\\\":\\\"\"}}]},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"gen-1778031306-HYzOq04JIk1hZQ4iaNjD\",\"object\":\"chat.completion.chunk\",\"created\":1778031306,\"model\":\"openai/gpt-4o-mini\",\"provider\":\"OpenAI\",\"system_fingerprint\":\"fp_b6580bbee1\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"role\":\"assistant\",\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"Paris\"}}]},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"gen-1778031306-HYzOq04JIk1hZQ4iaNjD\",\"object\":\"chat.completion.chunk\",\"created\":1778031306,\"model\":\"openai/gpt-4o-mini\",\"provider\":\"OpenAI\",\"system_fingerprint\":\"fp_b6580bbee1\",\"choices\":[{\"index\":0,\"delta\":{\"content\":null,\"role\":\"assistant\",\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"\\\"}\"}}]},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"gen-1778031306-HYzOq04JIk1hZQ4iaNjD\",\"object\":\"chat.completion.chunk\",\"created\":1778031306,\"model\":\"openai/gpt-4o-mini\",\"provider\":\"OpenAI\",\"system_fingerprint\":\"fp_b6580bbee1\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"\",\"role\":\"assistant\"},\"finish_reason\":\"tool_calls\",\"native_finish_reason\":\"stop\"}]}\n\ndata: {\"id\":\"gen-1778031306-HYzOq04JIk1hZQ4iaNjD\",\"object\":\"chat.completion.chunk\",\"created\":1778031306,\"model\":\"openai/gpt-4o-mini\",\"provider\":\"OpenAI\",\"system_fingerprint\":\"fp_b6580bbee1\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"\",\"role\":\"assistant\"},\"finish_reason\":\"tool_calls\",\"native_finish_reason\":\"stop\"}],\"usage\":{\"prompt_tokens\":67,\"completion_tokens\":5,\"total_tokens\":72,\"cost\":0.00001305,\"is_byok\":false,\"prompt_tokens_details\":{\"cached_tokens\":0,\"cache_write_tokens\":0,\"audio_tokens\":0,\"video_tokens\":0},\"cost_details\":{\"upstream_inference_cost\":0.00001305,\"upstream_inference_prompt_cost\":0.00001005,\"upstream_inference_completions_cost\":0.000003},\"completion_tokens_details\":{\"reasoning_tokens\":0,\"image_tokens\":0,\"audio_tokens\":0}}}\n\ndata: [DONE]\n\n"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+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?\"}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
type ProviderMetadata,
|
||||
type ToolCallPart,
|
||||
ToolResultPart,
|
||||
type ToolResultValue,
|
||||
type Usage,
|
||||
} from "../../src/schema/index.js"
|
||||
import { type Tools, toDefinitions } from "../../src/tool.js"
|
||||
|
||||
@@ -39,9 +39,7 @@ describe("provider error classification", () => {
|
||||
]
|
||||
|
||||
expect(failures).toEqual(
|
||||
failures.map((failure) =>
|
||||
expect.objectContaining({ _tag: "InvalidRequest", classification: "payload-too-large" }),
|
||||
),
|
||||
failures.map(() => expect.objectContaining({ _tag: "InvalidRequest", classification: "payload-too-large" })),
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@@ -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" } },
|
||||
})
|
||||
@@ -7,67 +7,6 @@ const configuration = (provider: string, message: string) =>
|
||||
expect.objectContaining({ _tag: "ProviderConfiguration", provider, message })
|
||||
|
||||
describe("provider package entrypoints", () => {
|
||||
test("semantic API aliases expose the same contract", async () => {
|
||||
const modules = await Promise.all([
|
||||
import("@opencode/ai/providers/openai"),
|
||||
import("@opencode/ai/providers/openai/responses"),
|
||||
import("@opencode/ai/providers/openai/chat"),
|
||||
import("@opencode/ai/providers/anthropic"),
|
||||
import("@opencode/ai/providers/anthropic-compatible"),
|
||||
import("@opencode/ai/providers/openai-compatible"),
|
||||
import("@opencode/ai/providers/openai-compatible/responses"),
|
||||
import("@opencode/ai/providers/amazon-bedrock"),
|
||||
import("@opencode/ai/providers/azure"),
|
||||
import("@opencode/ai/providers/azure/responses"),
|
||||
import("@opencode/ai/providers/azure/chat"),
|
||||
import("@opencode/ai/providers/google"),
|
||||
import("@opencode/ai/providers/google-vertex"),
|
||||
import("@opencode/ai/providers/google-vertex/gemini"),
|
||||
import("@opencode/ai/providers/google-vertex/chat"),
|
||||
import("@opencode/ai/providers/google-vertex/responses"),
|
||||
import("@opencode/ai/providers/google-vertex/messages"),
|
||||
import("@opencode/ai/providers/openrouter"),
|
||||
import("@opencode/ai/providers/xai"),
|
||||
import("@opencode/ai/providers/amazon-bedrock/mantle"),
|
||||
import("@opencode/ai/providers/amazon-bedrock/mantle/chat"),
|
||||
import("@opencode/ai/providers/amazon-bedrock/mantle/responses"),
|
||||
import("@opencode/ai/providers/togetherai"),
|
||||
import("@opencode/ai/providers/cerebras"),
|
||||
import("@opencode/ai/providers/deepinfra"),
|
||||
import("@opencode/ai/providers/groq"),
|
||||
import("@opencode/ai/providers/baseten"),
|
||||
import("@opencode/ai/providers/deepseek"),
|
||||
import("@opencode/ai/providers/fireworks"),
|
||||
import("@opencode/ai/providers/cloudflare-ai-gateway"),
|
||||
import("@opencode/ai/providers/cloudflare-workers-ai"),
|
||||
import("@opencode/ai/providers/minimax"),
|
||||
import("@opencode/ai/providers/minimax/messages"),
|
||||
import("@opencode/ai/providers/minimax/chat"),
|
||||
import("@opencode/ai/providers/minimax/responses"),
|
||||
import("@opencode/ai/providers/moonshot"),
|
||||
import("@opencode/ai/providers/moonshot/chat"),
|
||||
import("@opencode/ai/providers/moonshot/messages"),
|
||||
import("@opencode/ai/providers/moonshot/responses"),
|
||||
import("@opencode/ai/providers/zai"),
|
||||
import("@opencode/ai/providers/zai/chat"),
|
||||
import("@opencode/ai/providers/zai-coding-plan"),
|
||||
import("@opencode/ai/providers/zai-coding-plan/chat"),
|
||||
import("@opencode/ai/providers/zai-coding-plan/messages"),
|
||||
import("@opencode/ai/providers/zai-coding-plan/responses"),
|
||||
import("@opencode/ai/providers/alibaba"),
|
||||
import("@opencode/ai/providers/alibaba/chat"),
|
||||
import("@opencode/ai/providers/alibaba/messages"),
|
||||
import("@opencode/ai/providers/alibaba/responses"),
|
||||
])
|
||||
|
||||
for (const module of modules) expect(module.model).toBeFunction()
|
||||
expect(modules[0].model).toBe(modules[1].model)
|
||||
expect(modules[8].model).toBe(modules[9].model)
|
||||
expect(modules[12].model).toBe(modules[13].model)
|
||||
expect(modules[19].model).toBe(modules[21].model)
|
||||
expect(modules[19].model).not.toBe(modules[20].model)
|
||||
})
|
||||
|
||||
test("maps Alibaba API entrypoints onto explicit regional routes", async () => {
|
||||
const modules = await Promise.all([
|
||||
import("@opencode/ai/providers/alibaba"),
|
||||
@@ -75,7 +14,6 @@ describe("provider package entrypoints", () => {
|
||||
import("@opencode/ai/providers/alibaba/messages"),
|
||||
import("@opencode/ai/providers/alibaba/responses"),
|
||||
])
|
||||
expect(modules[0].model).toBe(modules[1].model)
|
||||
const settings = {
|
||||
region: "eu-central-1",
|
||||
workspaceID: "llm-fixture",
|
||||
@@ -103,7 +41,6 @@ describe("provider package entrypoints", () => {
|
||||
import("@opencode/ai/providers/moonshot/messages"),
|
||||
import("@opencode/ai/providers/moonshot/responses"),
|
||||
])
|
||||
expect(modules[0].model).toBe(modules[1].model)
|
||||
const settings = {
|
||||
apiKey: "fixture",
|
||||
baseURL: "https://gateway.example/v1",
|
||||
@@ -147,7 +84,6 @@ describe("provider package entrypoints", () => {
|
||||
import("@opencode/ai/providers/minimax/chat"),
|
||||
import("@opencode/ai/providers/minimax/responses"),
|
||||
])
|
||||
expect(modules[0].model).toBe(modules[1].model)
|
||||
const settings = {
|
||||
apiKey: "fixture",
|
||||
baseURL: "https://gateway.example/v1",
|
||||
@@ -174,8 +110,6 @@ describe("provider package entrypoints", () => {
|
||||
import("@opencode/ai/providers/zai-coding-plan/messages"),
|
||||
import("@opencode/ai/providers/zai-coding-plan/responses"),
|
||||
])
|
||||
expect(modules[0].model).toBe(modules[1].model)
|
||||
expect(modules[2].model).toBe(modules[3].model)
|
||||
const routes = [
|
||||
"zai-chat",
|
||||
"zai-chat",
|
||||
@@ -458,7 +392,6 @@ describe("provider package entrypoints", () => {
|
||||
|
||||
test("selects Vertex entrypoints with the same model contract", async () => {
|
||||
const GoogleVertex = await import("@opencode/ai/providers/google-vertex")
|
||||
const GoogleVertexGemini = await import("@opencode/ai/providers/google-vertex/gemini")
|
||||
const GoogleVertexChat = await import("@opencode/ai/providers/google-vertex/chat")
|
||||
const GoogleVertexResponses = await import("@opencode/ai/providers/google-vertex/responses")
|
||||
const GoogleVertexMessages = await import("@opencode/ai/providers/google-vertex/messages")
|
||||
@@ -483,7 +416,6 @@ describe("provider package entrypoints", () => {
|
||||
project: "vertex-project",
|
||||
})
|
||||
|
||||
expect(GoogleVertexGemini.model).toBe(GoogleVertex.model)
|
||||
expect(gemini.route.id).toBe("google-vertex-gemini")
|
||||
expect(gemini.route.protocol).toBe("gemini")
|
||||
expect(gemini.route.endpoint.baseURL).toBe("https://aiplatform.googleapis.com/v1/publishers/google")
|
||||
|
||||
@@ -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." }] },
|
||||
])
|
||||
|
||||
@@ -21,7 +21,6 @@ import {
|
||||
import { LLMClient } from "../../src/route.js"
|
||||
import { compileRequest } from "../../src/route/client.js"
|
||||
import { AmazonBedrock } from "../../src/providers.js"
|
||||
import * as BedrockConverse from "../../src/protocols/bedrock-converse.js"
|
||||
import { it } from "../lib/effect.js"
|
||||
import { withProcessEnv } from "../lib/env.js"
|
||||
import { dynamicResponse, fixedResponse } from "../lib/http.js"
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import { HttpClientRequest } from "effect/unstable/http"
|
||||
import { LLM, Message } from "../../src/index.js"
|
||||
import { LLM, LLMEvent, LLMRequest, Message, ToolDefinition } from "../../src/index.js"
|
||||
import { AmazonBedrockMantle } from "../../src/providers.js"
|
||||
import { model } from "../../src/providers/amazon-bedrock/mantle.js"
|
||||
import { OpenResponses } from "../../src/protocols/open-responses.js"
|
||||
import { compileRequest, LLMClient } from "../../src/route/client.js"
|
||||
import { it } from "../lib/effect.js"
|
||||
@@ -19,14 +18,14 @@ const credentials = {
|
||||
}
|
||||
|
||||
describe("Amazon Bedrock Mantle provider", () => {
|
||||
it.effect("uses Responses by default and exposes Chat explicitly", () =>
|
||||
it.effect("uses Responses by default and exposes Chat and Messages explicitly", () =>
|
||||
Effect.gen(function* () {
|
||||
const provider = AmazonBedrockMantle.configure({ credentials })
|
||||
expect(provider.model).toBe(provider.responses)
|
||||
expect(AmazonBedrockMantle.model).toBe(AmazonBedrockMantle.responsesModel)
|
||||
expect(model).toBe(AmazonBedrockMantle.responsesModel)
|
||||
expect(provider.model("openai.gpt-oss-120b").route.transport).toBe(OpenResponses.httpTransport)
|
||||
const chat = yield* compileRequest(LLM.request({ model: provider.chat("openai.gpt-oss-120b"), prompt: "Hi" }))
|
||||
const messages = yield* compileRequest(
|
||||
LLM.request({ model: provider.messages("anthropic.claude-opus-4-8"), prompt: "Hi", cache: "none" }),
|
||||
)
|
||||
const responses = yield* compileRequest(
|
||||
LLM.request({ model: provider.model("openai.gpt-oss-120b"), prompt: "Hi" }),
|
||||
)
|
||||
@@ -36,6 +35,11 @@ describe("Amazon Bedrock Mantle provider", () => {
|
||||
protocol: "openai-chat",
|
||||
body: { model: "openai.gpt-oss-120b" },
|
||||
})
|
||||
expect(messages).toMatchObject({
|
||||
route: "bedrock-mantle-messages",
|
||||
protocol: "anthropic-messages",
|
||||
body: { model: "anthropic.claude-opus-4-8", stream: true },
|
||||
})
|
||||
expect(responses).toMatchObject({
|
||||
route: "bedrock-mantle-responses",
|
||||
protocol: "open-responses",
|
||||
@@ -43,43 +47,77 @@ describe("Amazon Bedrock Mantle provider", () => {
|
||||
})
|
||||
expect(provider.model("openai.gpt-oss-120b").route.providerMetadataKey).toBe("mantle")
|
||||
expect(provider.chat("openai.gpt-oss-120b").route.providerMetadataKey).toBe("mantle")
|
||||
expect(provider.messages("anthropic.claude-opus-4-8").route.providerMetadataKey).toBe("mantle")
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("preserves configured top-p generation defaults for Chat and Responses", () =>
|
||||
it.effect("preserves configured top-p generation defaults for Chat, Messages, and Responses", () =>
|
||||
Effect.gen(function* () {
|
||||
const settings = { apiKey: "test-key", topP: 0.8 }
|
||||
const chat = yield* compileRequest(
|
||||
LLM.request({ model: AmazonBedrockMantle.chatModel("openai.gpt-oss-safeguard-20b", settings), prompt: "Hi" }),
|
||||
)
|
||||
const messages = yield* compileRequest(
|
||||
LLM.request({ model: AmazonBedrockMantle.messagesModel("anthropic.claude-opus-4-8", settings), prompt: "Hi" }),
|
||||
)
|
||||
const responses = yield* compileRequest(
|
||||
LLM.request({ model: AmazonBedrockMantle.responsesModel("openai.gpt-oss-120b", settings), prompt: "Hi" }),
|
||||
)
|
||||
|
||||
expect(chat.body.top_p).toBe(0.8)
|
||||
expect(messages.body.top_p).toBe(0.8)
|
||||
expect(responses.body.top_p).toBe(0.8)
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("uses the Mantle endpoint and signing service", () =>
|
||||
it.effect("uses the Mantle endpoint and signing service across Responses and Messages", () =>
|
||||
Effect.gen(function* () {
|
||||
const seen: Array<{ readonly url: string; readonly authorization: string | undefined }> = []
|
||||
const model = AmazonBedrockMantle.configure({ credentials, region: "us-west-1" }).responses("openai.gpt-oss-120b")
|
||||
yield* LLMClient.generate(LLM.request({ model, prompt: "Hi" })).pipe(
|
||||
Effect.provide(
|
||||
dynamicResponse((input) =>
|
||||
Effect.gen(function* () {
|
||||
const request = yield* HttpClientRequest.toWeb(input.request)
|
||||
seen.push({ url: request.url, authorization: request.headers.get("authorization") ?? undefined })
|
||||
return input.respond("", { headers: { "content-type": "text/event-stream" } })
|
||||
}),
|
||||
const configured = AmazonBedrockMantle.configure({ credentials, region: "us-west-1" })
|
||||
for (const selected of [
|
||||
configured.responses("openai.gpt-oss-120b"),
|
||||
configured.messages("anthropic.claude-opus-4-8"),
|
||||
]) {
|
||||
yield* LLMClient.generate(LLM.request({ model: selected, prompt: "Hi" })).pipe(
|
||||
Effect.provide(
|
||||
dynamicResponse((input) =>
|
||||
Effect.gen(function* () {
|
||||
const request = yield* HttpClientRequest.toWeb(input.request)
|
||||
seen.push({ url: request.url, authorization: request.headers.get("authorization") ?? undefined })
|
||||
return input.respond("", { headers: { "content-type": "text/event-stream" } })
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
Effect.flip,
|
||||
Effect.flip,
|
||||
)
|
||||
}
|
||||
|
||||
expect(seen.map((item) => item.url)).toEqual([
|
||||
"https://bedrock-mantle.us-west-1.api.aws/v1/responses",
|
||||
"https://bedrock-mantle.us-west-1.api.aws/anthropic/v1/messages",
|
||||
])
|
||||
expect(seen.every((item) => item.authorization?.includes("/us-west-1/bedrock-mantle/aws4_request"))).toBe(true)
|
||||
}).pipe(withProcessEnv({ AWS_BEARER_TOKEN_BEDROCK: undefined })),
|
||||
)
|
||||
|
||||
it.effect("applies inline cache breakpoints on Mantle Messages", () =>
|
||||
Effect.gen(function* () {
|
||||
const model = AmazonBedrockMantle.configure({ apiKey: "test-key" }).messages("anthropic.claude-opus-4-8")
|
||||
const prepared = yield* compileRequest(
|
||||
LLM.request({
|
||||
model,
|
||||
system: "You are concise.",
|
||||
messages: [Message.user("Hello")],
|
||||
cache: "auto",
|
||||
}),
|
||||
)
|
||||
|
||||
expect(seen[0]?.url).toBe("https://bedrock-mantle.us-west-1.api.aws/v1/responses")
|
||||
expect(seen[0]?.authorization).toContain("/us-west-1/bedrock-mantle/aws4_request")
|
||||
expect(prepared.body.system).toEqual([
|
||||
{ type: "text", text: "You are concise.", cache_control: { type: "ephemeral" } },
|
||||
])
|
||||
expect(prepared.body.messages).toEqual([
|
||||
{ role: "user", content: [{ type: "text", text: "Hello", cache_control: { type: "ephemeral" } }] },
|
||||
])
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -201,3 +239,198 @@ describe("Amazon Bedrock Mantle recorded", () => {
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
const recordedMessages = recordedTests({
|
||||
prefix: "bedrock-mantle-messages",
|
||||
provider: "amazon-bedrock",
|
||||
protocol: "anthropic-messages",
|
||||
requires: ["AWS_BEARER_TOKEN_BEDROCK"],
|
||||
options: { redact: { allowRequestHeaders: ["anthropic-version", "anthropic-beta"] } },
|
||||
})
|
||||
|
||||
const mantleMessages = (modelID: string) =>
|
||||
AmazonBedrockMantle.configure({
|
||||
apiKey: process.env.AWS_BEARER_TOKEN_BEDROCK ?? "fixture",
|
||||
region: "us-east-1",
|
||||
}).messages(modelID)
|
||||
|
||||
const weatherTool = ToolDefinition.make({
|
||||
name: "get_weather",
|
||||
description: "Get the current weather in a city",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: { city: { type: "string", enum: ["Paris"] } },
|
||||
required: ["city"],
|
||||
additionalProperties: false,
|
||||
},
|
||||
})
|
||||
|
||||
describe("Amazon Bedrock Mantle Messages recorded", () => {
|
||||
recordedMessages.effect.with(
|
||||
"replays signed thinking through a tool loop and native system update",
|
||||
{ tags: ["tool", "tool-loop", "reasoning", "system-update"], metadata: { model: "anthropic.claude-opus-4-8" } },
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const model = mantleMessages("anthropic.claude-opus-4-8")
|
||||
const initial = LLM.request({
|
||||
model,
|
||||
system: "You are a concise assistant.",
|
||||
prompt:
|
||||
"First calculate 37 * 43. Then call get_weather for Paris. After receiving the tool result, state both the product and the weather.",
|
||||
tools: [weatherTool],
|
||||
providerOptions: {
|
||||
thinking: { type: "adaptive", display: "summarized" },
|
||||
effort: "medium",
|
||||
},
|
||||
generation: { maxTokens: 2048 },
|
||||
})
|
||||
const first = yield* LLMClient.generate(initial)
|
||||
expect(first.finishReason.normalized).toBe("tool-calls")
|
||||
expect(first.toolCalls).toMatchObject([{ name: "get_weather", input: { city: "Paris" } }])
|
||||
expect(first.reasoning.length).toBeGreaterThan(0)
|
||||
expect(first.events.some(LLMEvent.is.toolInputDelta)).toBe(true)
|
||||
const reasoningPart = first.message.content.find((part) => part.type === "reasoning")
|
||||
const signature = (reasoningPart?.providerMetadata?.mantle as { readonly signature?: unknown } | undefined)
|
||||
?.signature
|
||||
expect(typeof signature).toBe("string")
|
||||
|
||||
const followUp = LLMRequest.update(initial, {
|
||||
messages: [
|
||||
...initial.messages,
|
||||
first.message,
|
||||
...first.toolCalls.map((call) =>
|
||||
Message.tool({ id: call.id, name: call.name, result: { condition: "sunny", temperatureC: 18 } }),
|
||||
),
|
||||
Message.system("Reply in French in one short sentence."),
|
||||
],
|
||||
})
|
||||
const compiled = yield* compileRequest(followUp)
|
||||
expect(compiled.body.messages[1]?.content[0]).toEqual({
|
||||
type: "thinking",
|
||||
thinking: first.reasoning,
|
||||
signature: signature as string,
|
||||
})
|
||||
expect(compiled.body.messages.at(-1)).toEqual({
|
||||
role: "system",
|
||||
content: [
|
||||
{ type: "text", text: "Reply in French in one short sentence.", cache_control: { type: "ephemeral" } },
|
||||
],
|
||||
})
|
||||
|
||||
const second = yield* LLMClient.generate(followUp)
|
||||
expect(second.finishReason.normalized).toBe("stop")
|
||||
expect(second.text).toContain("1591")
|
||||
expect(second.text).toContain("18")
|
||||
}),
|
||||
120_000,
|
||||
)
|
||||
|
||||
recordedMessages.effect.with(
|
||||
"lowers system updates to wrapped user text on Haiku 4.5 with budget thinking",
|
||||
{ tags: ["reasoning", "system-update"], metadata: { model: "anthropic.claude-haiku-4-5" } },
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const request = LLM.request({
|
||||
model: mantleMessages("anthropic.claude-haiku-4-5"),
|
||||
messages: [Message.user("What is 19 multiplied by 23?"), Message.system("Reply with only the integer.")],
|
||||
providerOptions: {
|
||||
thinking: { type: "enabled", budgetTokens: 1024 },
|
||||
},
|
||||
generation: { maxTokens: 2048 },
|
||||
})
|
||||
const compiled = yield* compileRequest(request)
|
||||
expect(compiled.body.thinking).toEqual({ type: "enabled", budget_tokens: 1024 })
|
||||
expect(compiled.body.messages.some((message) => message.role === "system")).toBe(false)
|
||||
|
||||
const response = yield* LLMClient.generate(request)
|
||||
expect(response.finishReason.normalized).toBe("stop")
|
||||
expect(response.reasoning.length).toBeGreaterThan(0)
|
||||
expect(response.text.trim()).toContain("437")
|
||||
}),
|
||||
120_000,
|
||||
)
|
||||
|
||||
recordedMessages.effect.with(
|
||||
"applies mid-conversation effort updates and thinking block binding on Opus 5.5",
|
||||
{ tags: ["reasoning", "effort-update"], metadata: { model: "anthropic.claude-opus-5-5" } },
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const model = mantleMessages("anthropic.claude-opus-5-5")
|
||||
const firstRequest = LLM.request({
|
||||
model,
|
||||
prompt: "Compute 37 * 43 step by step, then reply with only the integer.",
|
||||
providerOptions: {
|
||||
thinking: { type: "adaptive", display: "summarized" },
|
||||
effort: "high",
|
||||
},
|
||||
generation: { maxTokens: 2048 },
|
||||
})
|
||||
const firstCompiled = yield* compileRequest(firstRequest)
|
||||
expect(firstCompiled.body.thinking).toEqual({
|
||||
type: "adaptive",
|
||||
display: "summarized",
|
||||
block_binding: { prefix_mismatch_behavior: "drop_block" },
|
||||
})
|
||||
|
||||
const first = yield* LLMClient.generate(firstRequest)
|
||||
expect(first.finishReason.normalized).toBe("stop")
|
||||
expect(first.reasoning.length).toBeGreaterThan(0)
|
||||
expect(first.text.replaceAll(",", "")).toContain("1591")
|
||||
|
||||
const secondRequest = LLM.request({
|
||||
model,
|
||||
messages: [
|
||||
...firstRequest.messages,
|
||||
first.message,
|
||||
Message.effort({ effort: "low", previous: "high" }),
|
||||
Message.user("Add 9 to that result. Reply with only the integer."),
|
||||
],
|
||||
providerOptions: {
|
||||
thinking: { type: "adaptive", display: "summarized" },
|
||||
effort: "low",
|
||||
},
|
||||
generation: { maxTokens: 2048 },
|
||||
})
|
||||
const secondCompiled = yield* compileRequest(secondRequest)
|
||||
expect(secondCompiled.body.output_config).toEqual({ effort: "high" })
|
||||
expect(secondCompiled.body.messages.filter((message) => message.role === "system")).toEqual([
|
||||
{ role: "system", content: [], output_config: { effort: "low" } },
|
||||
])
|
||||
|
||||
const second = yield* LLMClient.generate(secondRequest)
|
||||
expect(second.finishReason.normalized).toBe("stop")
|
||||
expect(second.text.replaceAll(",", "")).toContain("1600")
|
||||
}),
|
||||
120_000,
|
||||
)
|
||||
|
||||
recordedMessages.effect.with(
|
||||
"strips unsupported mid-conversation effort updates on Opus 5.0",
|
||||
{ tags: ["reasoning", "effort-update"], metadata: { model: "anthropic.claude-opus-5" } },
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const request = LLM.request({
|
||||
model: mantleMessages("anthropic.claude-opus-5"),
|
||||
messages: [
|
||||
Message.user("What is 12 + 30?"),
|
||||
Message.assistant("42"),
|
||||
Message.effort({ effort: "low", previous: "high" }),
|
||||
Message.user("Add 8 to that result. Reply with only the integer."),
|
||||
],
|
||||
providerOptions: {
|
||||
thinking: { type: "adaptive", display: "summarized" },
|
||||
effort: "low",
|
||||
},
|
||||
generation: { maxTokens: 1024 },
|
||||
})
|
||||
const compiled = yield* compileRequest(request)
|
||||
expect(compiled.body.output_config).toEqual({ effort: "low" })
|
||||
expect(compiled.body.messages.some((message) => message.role === "system")).toBe(false)
|
||||
|
||||
const response = yield* LLMClient.generate(request)
|
||||
expect(response.finishReason.normalized).toBe("stop")
|
||||
expect(response.text.trim()).toContain("50")
|
||||
}),
|
||||
120_000,
|
||||
)
|
||||
})
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { describe, expect } from "bun:test"
|
||||
import { ConfigProvider, Effect, Schema } from "effect"
|
||||
import { HttpClientRequest } from "effect/unstable/http"
|
||||
import { LLM, LLMEvent } from "../../src/index.js"
|
||||
|
||||
@@ -52,7 +52,6 @@ describe("experimental Evaluation recorded", () => {
|
||||
typesafe.effect("evaluates choice score and boolean questions", () =>
|
||||
assertEvaluation(
|
||||
TypeSafeAI.configure({ apiKey: process.env.TYPESAFE_API_KEY ?? "fixture" }).experimental.evaluation("jev-latest"),
|
||||
"typesafe",
|
||||
),
|
||||
)
|
||||
|
||||
@@ -61,7 +60,6 @@ describe("experimental Evaluation recorded", () => {
|
||||
OpenCodeZen.configure({ apiKey: process.env.OPENCODE_API_KEY ?? "fixture" }).experimental.evaluation(
|
||||
"jev-1.13-free",
|
||||
),
|
||||
"opencode",
|
||||
),
|
||||
)
|
||||
|
||||
@@ -70,15 +68,11 @@ describe("experimental Evaluation recorded", () => {
|
||||
OpenRouter.configure({ apiKey: process.env.OPENROUTER_API_KEY ?? "fixture" }).experimental.evaluation(
|
||||
"typesafe/jev-1.13",
|
||||
),
|
||||
"openrouter",
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
const assertEvaluation = <Options extends EvaluationOptions>(
|
||||
model: EvaluationModel<Options>,
|
||||
metadataKey: "typesafe" | "opencode" | "openrouter",
|
||||
) =>
|
||||
const assertEvaluation = <Options extends EvaluationOptions>(model: EvaluationModel<Options>) =>
|
||||
Effect.gen(function* () {
|
||||
const response = yield* Evaluation.run({ model, state, questions })
|
||||
expect(response.model).toContain("jev-")
|
||||
|
||||
@@ -43,6 +43,7 @@ describe("native OpenAI-compatible providers", () => {
|
||||
[Azure.configure({ resourceName: "resource", apiKey: "test" }).responses("model"), "azure"],
|
||||
[AmazonBedrock.configure({ apiKey: "test" }).model("model"), "bedrock"],
|
||||
[AmazonBedrockMantle.configure({ apiKey: "test" }).chat("model"), "mantle"],
|
||||
[AmazonBedrockMantle.configure({ apiKey: "test" }).messages("model"), "mantle"],
|
||||
[AmazonBedrockMantle.configure({ apiKey: "test" }).responses("model"), "mantle"],
|
||||
[Google.configure({ apiKey: "test" }).model("model"), "google"],
|
||||
[GoogleVertex.configure(vertex).model("model"), "vertex"],
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import { LLM, LLMEvent, LLMResponse, LanguageModel } from "../../src/index.js"
|
||||
import { OpenAIChat } from "../../src/protocols/openai-chat.js"
|
||||
import * as OpenAICompatible from "../../src/providers/openai-compatible.js"
|
||||
import * as OpenRouter from "../../src/providers/openrouter.js"
|
||||
import { LLMClient } from "../../src/route.js"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Effect, Schema } from "effect"
|
||||
import { HttpClientRequest } from "effect/unstable/http"
|
||||
import { LLM, LLMRequest, Message, ToolCallPart, ToolChoice, ToolDefinition } from "../../src/index.js"
|
||||
|
||||
@@ -3895,7 +3895,6 @@ describe("OpenAI Responses route", () => {
|
||||
|
||||
it.effect("preserves foreign hosted images as portable image content when storage is enabled", () =>
|
||||
Effect.gen(function* () {
|
||||
const item = { type: "image_generation_call", id: "ig_1", status: "completed", result: "AQID" }
|
||||
const prepared = yield* compileRequest(
|
||||
LLM.request({
|
||||
model: xaiModel,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}),
|
||||
)
|
||||
@@ -127,11 +127,6 @@ const assistantContent = (events: ReadonlyArray<LLMEvent>) =>
|
||||
export const expectFinish = (events: ReadonlyArray<LLMEvent>, reason: FinishReason) =>
|
||||
expect(events.at(-1)).toMatchObject({ type: "finish", reason: { normalized: reason } })
|
||||
|
||||
export const expectWeatherToolCall = (response: LLMResponse) =>
|
||||
expect(response.toolCalls).toMatchObject([
|
||||
{ type: "tool-call", id: expect.any(String), name: weatherToolName, input: { city: "Paris" } },
|
||||
])
|
||||
|
||||
export const expectWeatherToolLoop = (events: ReadonlyArray<LLMEvent>) => {
|
||||
const finishes = events.filter(LLMEvent.is.finish)
|
||||
expect(finishes).toHaveLength(1)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { fileURLToPath } from "node:url"
|
||||
import { expect, story } from "../../storybook/playwright/story"
|
||||
import { expect, sourceURL, story } from "../../storybook/playwright/story"
|
||||
|
||||
const source = (path: string) => `/@fs/${fileURLToPath(new URL(path, import.meta.url)).replaceAll("\\", "/")}`
|
||||
const source = (path: string) => sourceURL(new URL(path, import.meta.url))
|
||||
|
||||
const modules = {
|
||||
fixture: source("../../gui-extensions/src/browser/panel.fixture.tsx"),
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { fileURLToPath } from "node:url"
|
||||
import { expect, story } from "../../storybook/playwright/story"
|
||||
import { expect, sourceURL, story } from "../../storybook/playwright/story"
|
||||
|
||||
const source = (path: string) => `/@fs/${fileURLToPath(new URL(path, import.meta.url)).replaceAll("\\", "/")}`
|
||||
const source = (path: string) => sourceURL(new URL(path, import.meta.url))
|
||||
|
||||
const modules = {
|
||||
fixture: source("../../gui-extensions/src/browser/panel.fixture.tsx"),
|
||||
@@ -93,6 +92,7 @@ story("comments on a picked element over a still of the page", async ({ page })
|
||||
await picker.click()
|
||||
await expect(picker).toHaveAttribute("aria-pressed", "true")
|
||||
await expect(root.getByText("Picker: on", { exact: true })).toBeVisible()
|
||||
await expect(root.getByText("Session getter reads: 0", { exact: true })).toBeVisible()
|
||||
await expect(root.getByRole("status")).toHaveText(
|
||||
"Click an element in the page to comment on it. Press Escape to cancel.",
|
||||
)
|
||||
@@ -101,6 +101,7 @@ story("comments on a picked element over a still of the page", async ({ page })
|
||||
await expect(picker).toHaveAttribute("aria-pressed", "false")
|
||||
const editor = root.locator('[data-slot="browser-comment-editor"] textarea')
|
||||
await expect(editor).toBeFocused()
|
||||
await expect(root.getByText("Session getter reads: 0", { exact: true })).toBeVisible()
|
||||
await expect(root.locator('[data-slot="browser-comment-editor"]')).toContainText("button.primary")
|
||||
// The spotlight frames the picked element in surface pixels.
|
||||
await expect(root.locator('[data-slot="browser-comment-spotlight"]')).toHaveCSS("left", "48px")
|
||||
@@ -148,6 +149,44 @@ story("cancels the picker and a comment with Escape", async ({ page }) => {
|
||||
await expect(root.getByTestId("native-Alpha")).toHaveAttribute("data-visible", "true")
|
||||
})
|
||||
|
||||
story("cleans up the originating session's picker and comment after a session switch", async ({ page }) => {
|
||||
const root = page.getByTestId("browser-pane-fixture")
|
||||
const picker = root.getByRole("button", { name: "Select an element to comment on", exact: true })
|
||||
await picker.click()
|
||||
await expect(root.getByText("Picker Alpha: on", { exact: true })).toBeVisible()
|
||||
await root.getByRole("button", { name: "Beta", exact: true }).click()
|
||||
await expect(root.getByTestId("native-Beta")).toHaveAttribute("data-visible", "true")
|
||||
await expect(root.getByText("Picker Alpha: off", { exact: true })).toBeVisible()
|
||||
await expect(picker).toHaveAttribute("aria-pressed", "false")
|
||||
|
||||
await root.getByRole("button", { name: "Alpha", exact: true }).click()
|
||||
await picker.click()
|
||||
await root.getByRole("button", { name: "Pick element", exact: true }).click()
|
||||
await expect(root.locator('[data-slot="browser-comment-editor"] textarea')).toBeFocused()
|
||||
await root.getByRole("button", { name: "Beta", exact: true }).click()
|
||||
await expect(root.locator('[data-component="browser-comment"]')).toHaveCount(0)
|
||||
await expect(root.getByText("Highlights: clear", { exact: true })).toBeVisible()
|
||||
await expect(root.getByText("Highlight owners: Alpha", { exact: true })).toBeVisible()
|
||||
})
|
||||
|
||||
story("ends the native picker and highlight when the pane unmounts", async ({ page }) => {
|
||||
const root = page.getByTestId("browser-pane-fixture")
|
||||
const picker = root.getByRole("button", { name: "Select an element to comment on", exact: true })
|
||||
await picker.click()
|
||||
await expect(root.getByText("Picker: on", { exact: true })).toBeVisible()
|
||||
await root.getByRole("button", { name: "Unmount pane", exact: true }).click()
|
||||
await expect(root.locator("#browser-panel")).toHaveCount(0)
|
||||
await expect(root.getByText("Picker: off", { exact: true })).toBeVisible()
|
||||
|
||||
await root.getByRole("button", { name: "Alpha", exact: true }).click()
|
||||
await picker.click()
|
||||
await root.getByRole("button", { name: "Pick element", exact: true }).click()
|
||||
await expect(root.locator('[data-slot="browser-comment-editor"] textarea')).toBeFocused()
|
||||
await root.getByRole("button", { name: "Unmount pane", exact: true }).click()
|
||||
await expect(root.locator("#browser-panel")).toHaveCount(0)
|
||||
await expect(root.getByText("Highlights: clear", { exact: true })).toBeVisible()
|
||||
})
|
||||
|
||||
story("keeps a comment draft but drops its ref when the page navigates", async ({ page }) => {
|
||||
const root = page.getByTestId("browser-pane-fixture")
|
||||
await root.getByRole("button", { name: "Select an element to comment on", exact: true }).click()
|
||||
@@ -232,17 +271,22 @@ story("keeps the submitted URL visible until the browser reports navigation", as
|
||||
await expect(address).toHaveValue("https://example.com/")
|
||||
})
|
||||
|
||||
story("restores the current URL when a submitted navigation is blocked", async ({ page }) => {
|
||||
story("restores the current URL each time the same submitted navigation is blocked", async ({ page }) => {
|
||||
const root = page.getByTestId("browser-pane-fixture")
|
||||
await root.getByRole("button", { name: "Delay navigation", exact: true }).click()
|
||||
const address = root.getByRole("textbox", { name: "Browser address", exact: true })
|
||||
await address.fill("https://blocked.example/")
|
||||
await address.press("Enter")
|
||||
await expect(address).toHaveValue("https://blocked.example/")
|
||||
await root.getByRole("button", { name: "Block navigation", exact: true }).click()
|
||||
await expect(root.getByText("ERR_BLOCKED_BY_CLIENT", { exact: true })).toBeVisible()
|
||||
await expect(address).toHaveValue("https://alpha.example/")
|
||||
await expect(root.getByTestId("native-Alpha")).toHaveAttribute("data-visible", "true")
|
||||
|
||||
for (const submission of [1, 2]) {
|
||||
await story.step(`blocked submission ${submission}`, async () => {
|
||||
await address.fill("https://blocked.example/")
|
||||
await address.press("Enter")
|
||||
await expect(address).toHaveValue("https://blocked.example/")
|
||||
await root.getByRole("button", { name: "Block navigation", exact: true }).click()
|
||||
await expect(root.getByRole("alert")).toHaveText("Request failed")
|
||||
await expect(address).toHaveValue("https://alpha.example/")
|
||||
await expect(root.getByTestId("native-Alpha")).toHaveAttribute("data-visible", "true")
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
story("shows a themed failure state for only the failed tab and allows retry", async ({ page }) => {
|
||||
@@ -300,10 +344,12 @@ story("selects the full URL when the address field gains focus", async ({ page }
|
||||
await address.focus()
|
||||
await expect(address).toHaveJSProperty("selectionStart", 0)
|
||||
await expect(address).toHaveJSProperty("selectionEnd", "https://example.com/".length)
|
||||
// A click on the focused field places the caret instead of selecting the URL again.
|
||||
await address.press("ArrowRight")
|
||||
await address.click()
|
||||
await expect(address).toHaveJSProperty("selectionStart", 0)
|
||||
await expect(address).toHaveJSProperty("selectionEnd", "https://example.com/".length)
|
||||
await expect
|
||||
.poll(() => address.evaluate((input: HTMLInputElement) => input.selectionStart === input.selectionEnd))
|
||||
.toBe(true)
|
||||
})
|
||||
|
||||
story("keeps the current page visible while a submitted URL loads", async ({ page }) => {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { fileURLToPath } from "node:url"
|
||||
import type { Definition } from "@opencode/gui-extensions/sdk"
|
||||
import { expect, story } from "../../storybook/playwright/story"
|
||||
import { expect, sourceURL, story } from "../../storybook/playwright/story"
|
||||
|
||||
const source = (path: string) => `/@fs/${fileURLToPath(new URL(path, import.meta.url)).replaceAll("\\", "/")}`
|
||||
const source = (path: string) => sourceURL(new URL(path, import.meta.url))
|
||||
|
||||
const modules = {
|
||||
fixture: source("./extension-host.fixture.tsx"),
|
||||
|
||||
@@ -16,7 +16,16 @@ import {
|
||||
type StoreOptions,
|
||||
type Workspaces,
|
||||
} from "@opencode/gui-extensions/sdk"
|
||||
import { createMemo, createSignal, getOwner, onCleanup, runWithOwner, Show, type ParentProps } from "solid-js"
|
||||
import {
|
||||
createComponent,
|
||||
createMemo,
|
||||
createSignal,
|
||||
getOwner,
|
||||
onCleanup,
|
||||
runWithOwner,
|
||||
Show,
|
||||
type ParentProps,
|
||||
} from "solid-js"
|
||||
import { createStore, produce } from "solid-js/store"
|
||||
import { Schema } from "effect"
|
||||
import { render } from "solid-js/web"
|
||||
@@ -25,12 +34,15 @@ import { Persist, persisted } from "@/runtime/persistence/storage"
|
||||
import { ServerScope } from "@/runtime/server/scope"
|
||||
import { ExtensionHostProvider, useExtensionHost, type HostApiFactories } from "../src/runtime/extension/host"
|
||||
import { createHostApis } from "../src/runtime/extension/host-apis"
|
||||
import { createInstalled } from "../src/runtime/extension/installed"
|
||||
import { extensionEnabled } from "@opencode/gui-extensions/sdk/bridge"
|
||||
import { createRegion, RegionContent } from "../src/runtime/extension/panels"
|
||||
import { ExtensionSlot } from "../src/runtime/extension/render"
|
||||
import { persistedHandle } from "../src/runtime/extension/stores"
|
||||
import { LanguageProvider } from "../src/runtime/i18n/language"
|
||||
import { GuiExtensionsSettings } from "../src/runtime/extension/settings-page-dev"
|
||||
|
||||
export { Contract, createKeyed, Panel, Slot, Store } from "@opencode/gui-extensions/sdk"
|
||||
export { bindExtension, useExtension, Contract, createKeyed, Panel, Slot, Store } from "@opencode/gui-extensions/sdk"
|
||||
|
||||
export { Schema }
|
||||
|
||||
@@ -45,7 +57,7 @@ type Captured = { apis?: ReturnType<typeof createHostApis> }
|
||||
/** A value the fixture's storage holds as JSON. */
|
||||
type Json = string | number | boolean | null | readonly Json[] | { readonly [key: string]: Json }
|
||||
|
||||
export { createMemo, createSignal, getOwner, onCleanup, runWithOwner }
|
||||
export { createComponent, createMemo, createSignal, getOwner, onCleanup, runWithOwner }
|
||||
|
||||
const layout: Layout = {
|
||||
narrow: () => false,
|
||||
@@ -189,7 +201,10 @@ export function mountExtensionHost() {
|
||||
export function mountExtensions(input: {
|
||||
definitions: readonly Definition[]
|
||||
disabled?: readonly string[]
|
||||
bridge?: Parameters<typeof createInstalled>[0]
|
||||
stored?: Readonly<Record<string, Json>>
|
||||
/** Shows the production developer settings page for status-label contracts. */
|
||||
settings?: boolean
|
||||
}) {
|
||||
const held = Promise.withResolvers<void>()
|
||||
const [disabled, setDisabled] = createSignal<ReadonlySet<string>>(new Set(input.disabled ?? []))
|
||||
@@ -230,10 +245,24 @@ export function mountExtensions(input: {
|
||||
})
|
||||
|
||||
function MountedHost() {
|
||||
const installed = input.bridge ? createInstalled(input.bridge) : undefined
|
||||
|
||||
const disabledState = createMemo(() => {
|
||||
if (!installed) return disabled()
|
||||
|
||||
const state = installed.enableState()
|
||||
|
||||
if (!state) return undefined
|
||||
|
||||
return new Set(
|
||||
input.definitions.flatMap((definition) => (extensionEnabled(definition, state) ? [] : [definition.id])),
|
||||
)
|
||||
})
|
||||
|
||||
return (
|
||||
<ExtensionHostProvider
|
||||
definitions={input.definitions}
|
||||
disabled={disabled}
|
||||
disabled={disabledState}
|
||||
apis={fakeApis(storage)}
|
||||
whenMounted={mounted}
|
||||
>
|
||||
@@ -248,6 +277,9 @@ export function mountExtensions(input: {
|
||||
|
||||
return (
|
||||
<Show when={host.ready()}>
|
||||
<Show when={input.settings}>
|
||||
<GuiExtensionsSettings />
|
||||
</Show>
|
||||
<ExtensionSlot at="window.bottom" input={{}} />
|
||||
</Show>
|
||||
)
|
||||
@@ -362,15 +394,13 @@ export function mountSessionRegion(input: { definitions: readonly Definition[];
|
||||
}
|
||||
|
||||
const [view, setView] = createSignal(object("a"))
|
||||
const [routed, setRouted] = createSignal(true)
|
||||
|
||||
const route = {
|
||||
get session() {
|
||||
return view()
|
||||
},
|
||||
composer: { attach: () => void attached.push(view().id), update() {}, detach() {} },
|
||||
}
|
||||
|
||||
// SAFETY: the test's extension reads only `session` and `composer.attach` of the screen.
|
||||
// SAFETY: the test's extension reads only `composer.attach` of the screen.
|
||||
// oxlint-disable-next-line anti-slop/no-chained-type-assertions -- see SAFETY above
|
||||
const screen = route as unknown as SessionScreen
|
||||
|
||||
@@ -385,6 +415,7 @@ export function mountSessionRegion(input: { definitions: readonly Definition[];
|
||||
const region = createRegion({
|
||||
region: "side",
|
||||
view,
|
||||
screen,
|
||||
tabs: () => ({
|
||||
all,
|
||||
active: () => input.active,
|
||||
@@ -403,11 +434,13 @@ export function mountSessionRegion(input: { definitions: readonly Definition[];
|
||||
get session() {
|
||||
return view()
|
||||
},
|
||||
screen,
|
||||
}}
|
||||
/>
|
||||
<RegionContent
|
||||
region={region}
|
||||
view={view()}
|
||||
screen={screen}
|
||||
frame={{
|
||||
shown: () => true,
|
||||
present: () => true,
|
||||
@@ -429,7 +462,7 @@ export function mountSessionRegion(input: { definitions: readonly Definition[];
|
||||
<RealHost
|
||||
definitions={input.definitions}
|
||||
captured={captured}
|
||||
mounted={standIn({ current: view, screen: () => screen })}
|
||||
mounted={standIn({ current: () => (routed() ? view() : undefined), screen: () => screen })}
|
||||
>
|
||||
<Region />
|
||||
</RealHost>
|
||||
@@ -444,6 +477,8 @@ export function mountSessionRegion(input: { definitions: readonly Definition[];
|
||||
container,
|
||||
/** Routes a session: the screen creates a new session object for it. */
|
||||
route: (id: string) => setView(object(id)),
|
||||
/** The route leaves while its old screen has not unmounted yet. */
|
||||
leave: () => setRouted(false),
|
||||
/** The session each composer attachment reached, in order. */
|
||||
attached,
|
||||
status: (id: string) => hosts[0]?.state.status[id],
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
import { fileURLToPath } from "node:url"
|
||||
import type { Owner } from "solid-js"
|
||||
import type { Context, Contract, DialogHandle, Dialogs, SessionRef } from "@opencode/gui-extensions/sdk"
|
||||
import { expect, story } from "../../storybook/playwright/story"
|
||||
import type { Component, Owner } from "solid-js"
|
||||
import type {
|
||||
Context,
|
||||
Contract,
|
||||
DialogHandle,
|
||||
Dialogs,
|
||||
Live,
|
||||
PanelTab,
|
||||
SessionRef,
|
||||
SessionScreen,
|
||||
} from "@opencode/gui-extensions/sdk"
|
||||
import type { Bridge, BridgeMessage, EnableState, Installed } from "@opencode/gui-extensions/sdk/bridge"
|
||||
import { expect, sourceURL, story } from "../../storybook/playwright/story"
|
||||
|
||||
const fixture = `/@fs/${fileURLToPath(new URL("./extension-host.fixture.tsx", import.meta.url)).replaceAll("\\", "/")}`
|
||||
const fixture = sourceURL(new URL("./extension-host.fixture.tsx", import.meta.url))
|
||||
|
||||
/** What the scoped-registration case keeps from inside its extension. */
|
||||
type Scope = { owner?: Owner | null; end: () => void; ctx?: Context }
|
||||
@@ -34,29 +43,32 @@ story("an extension that finishes loading after the host unmounts is never set u
|
||||
expect(setups).toBe(0)
|
||||
})
|
||||
|
||||
story("an async setup that resolves after the host unmounts releases everything it registers", async ({ page }) => {
|
||||
story("async work from synchronous setup cannot register after the host unmounts", async ({ page }) => {
|
||||
const result = await page.evaluate(async (fixture) => {
|
||||
const { mountExtensionHost, onCleanup } = await import(fixture)
|
||||
const host = mountExtensionHost()
|
||||
const started = Promise.withResolvers<void>()
|
||||
const resume = Promise.withResolvers<void>()
|
||||
const done = Promise.withResolvers<void>()
|
||||
const cleaned: string[] = []
|
||||
const point = { kind: "point" as const, id: "fixture-point" }
|
||||
const resumed = { aborted: false }
|
||||
host.load(async (ctx: Context) => {
|
||||
host.load((ctx: Context) => {
|
||||
ctx.add(point, "before")
|
||||
onCleanup(() => void cleaned.push("owner"))
|
||||
started.resolve()
|
||||
await resume.promise
|
||||
// After an await there is no owner: the signal tells the setup it outlived its instance.
|
||||
resumed.aborted = ctx.signal.aborted
|
||||
ctx.add(point, "after")
|
||||
void resume.promise.then(() => {
|
||||
// A promise callback has no owner; the signal says the work outlived the instance.
|
||||
resumed.aborted = ctx.signal.aborted
|
||||
ctx.add(point, "after")
|
||||
done.resolve()
|
||||
})
|
||||
})
|
||||
await started.promise
|
||||
const before = host.entries(point.id)
|
||||
host.unmount()
|
||||
resume.resolve()
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
await done.promise
|
||||
|
||||
return { before, after: host.entries(point.id), cleaned, aborted: resumed.aborted }
|
||||
}, fixture)
|
||||
@@ -184,17 +196,23 @@ story(
|
||||
const result = await page.evaluate(async (fixture) => {
|
||||
const { mountSessionRegion, until, Panel, Slot } = await import(fixture)
|
||||
const mounts = { single: 0, grouped: 0, slot: 0 }
|
||||
const inputs: Partial<Record<keyof typeof mounts, { readonly session: { readonly id: string } }>> = {}
|
||||
|
||||
const inputs: Partial<
|
||||
Record<keyof typeof mounts, { readonly session: { readonly id: string }; readonly screen: SessionScreen }>
|
||||
> = {}
|
||||
|
||||
const kept: Kept = {}
|
||||
const node = () => document.createElement("p")
|
||||
|
||||
// One render of each kind the host has: a selected tab, a group that stays mounted, and a slot.
|
||||
const render = (kind: keyof typeof mounts) => (input: { readonly session: { readonly id: string } }) => {
|
||||
mounts[kind]++
|
||||
inputs[kind] = input
|
||||
const render =
|
||||
(kind: keyof typeof mounts) =>
|
||||
(input: { readonly session: { readonly id: string }; readonly screen: SessionScreen }) => {
|
||||
mounts[kind]++
|
||||
inputs[kind] = input
|
||||
|
||||
return node()
|
||||
}
|
||||
return node()
|
||||
}
|
||||
|
||||
const host = mountSessionRegion({
|
||||
active: "single:main",
|
||||
@@ -231,9 +249,13 @@ story(
|
||||
const seen = () => ({
|
||||
mounts: { ...mounts },
|
||||
sessions: [inputs.single?.session.id, inputs.grouped?.session.id, inputs.slot?.session.id],
|
||||
screens: [inputs.single?.screen === screen, inputs.grouped?.screen === screen, inputs.slot?.screen === screen],
|
||||
kept: session?.id,
|
||||
fresh: kept.ctx?.sessions.current() !== session,
|
||||
screen: { same: kept.ctx?.screen.current() === screen, session: screen?.session?.id },
|
||||
screen: {
|
||||
same: kept.ctx?.screen.current() === screen,
|
||||
aligned: !!kept.ctx?.screen.current() === !!kept.ctx?.sessions.current(),
|
||||
},
|
||||
})
|
||||
|
||||
const steps = [seen()]
|
||||
@@ -243,21 +265,28 @@ story(
|
||||
screen?.composer.attach({ type: "file", path: "notes.md" })
|
||||
host.route("a")
|
||||
steps.push(seen())
|
||||
host.leave()
|
||||
const home = seen()
|
||||
host.unmount()
|
||||
|
||||
return { steps, attached: host.attached }
|
||||
return { steps, home, attached: host.attached }
|
||||
}, fixture)
|
||||
|
||||
const step = (id: string, fresh: boolean) => ({
|
||||
mounts: { single: 1, grouped: 1, slot: 1 },
|
||||
sessions: [id, id, id],
|
||||
screens: [true, true, true],
|
||||
kept: "a",
|
||||
fresh,
|
||||
screen: { same: true, session: id },
|
||||
screen: { same: true, aligned: true },
|
||||
})
|
||||
|
||||
// Routing A again makes a new object for the new visit; the kept one still names A.
|
||||
expect(result).toEqual({ steps: [step("a", false), step("b", true), step("a", true)], attached: ["b"] })
|
||||
expect(result).toEqual({
|
||||
steps: [step("a", false), step("b", true), step("a", true)],
|
||||
home: { ...step("a", true), screen: { same: false, aligned: true } },
|
||||
attached: ["b"],
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
@@ -296,7 +325,9 @@ story(
|
||||
scope: { server: "local" },
|
||||
})
|
||||
|
||||
prefs.update((draft) => ({ count: draft.count + 1 }))
|
||||
prefs.update((draft) => {
|
||||
draft.count++
|
||||
})
|
||||
reads.state = createMemo(() => ctx.layout.state("fixture:main", session))
|
||||
reads.font = createMemo(() => ctx.appearance.font("mono"))
|
||||
reads.prefs = prefs
|
||||
@@ -341,6 +372,84 @@ story(
|
||||
},
|
||||
)
|
||||
|
||||
// Selection follows opt-in tier order, not the strip's pinned-first order. Unrelated regular tabs never opt in.
|
||||
const tiers: readonly { name: string; tabs: readonly PanelTab[]; active?: string; expected: string }[] = [
|
||||
{
|
||||
name: "regular tabs, in stored order",
|
||||
tabs: [
|
||||
{ id: "details", title: "Details" },
|
||||
{ id: "review", title: "Review", pinned: true, fallback: true },
|
||||
{ id: "context", title: "Context", first: true, fallback: true },
|
||||
{ id: "file-one", title: "File one", fallback: true },
|
||||
{ id: "file-two", title: "File two", fallback: true },
|
||||
],
|
||||
expected: "file-one",
|
||||
},
|
||||
{
|
||||
name: "first tabs before pinned tabs",
|
||||
tabs: [
|
||||
{ id: "details", title: "Details" },
|
||||
{ id: "review", title: "Review", pinned: true, fallback: true },
|
||||
{ id: "context", title: "Context", first: true, fallback: true },
|
||||
],
|
||||
expected: "context",
|
||||
},
|
||||
{
|
||||
name: "pinned tabs when no other tier opts in",
|
||||
tabs: [
|
||||
{ id: "details", title: "Details" },
|
||||
{ id: "review", title: "Review", pinned: true, fallback: true },
|
||||
],
|
||||
expected: "review",
|
||||
},
|
||||
{
|
||||
name: "no implicit fallback",
|
||||
tabs: [{ id: "details", title: "Details", fallback: false }],
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "a stored selection wins without opting in",
|
||||
tabs: [
|
||||
{ id: "details", title: "Details" },
|
||||
{ id: "file", title: "File", fallback: true },
|
||||
],
|
||||
active: "details:main",
|
||||
expected: "details",
|
||||
},
|
||||
]
|
||||
|
||||
tiers.forEach((row) => {
|
||||
story(`panel fallback: ${row.name}`, async ({ page }) => {
|
||||
const selected = await page.evaluate(
|
||||
async (input) => {
|
||||
const { mountSessionRegion, Panel, until } = await import(input.fixture)
|
||||
|
||||
const host = mountSessionRegion({
|
||||
active: input.row.active ?? "gone:main",
|
||||
definitions: input.row.tabs.map((tab) => ({
|
||||
id: tab.id,
|
||||
renderer: async () => ({
|
||||
default: (ctx: Context) => {
|
||||
const item = { ...tab, id: "main" }
|
||||
ctx.add(Panel, { id: "main", region: "side", list: () => [item], render: () => ctx.id })
|
||||
},
|
||||
}),
|
||||
})),
|
||||
})
|
||||
|
||||
await until(() => input.row.tabs.every((tab) => host.status(tab.id) === "active"))
|
||||
const result = host.container.textContent
|
||||
host.unmount()
|
||||
|
||||
return result
|
||||
},
|
||||
{ fixture, row },
|
||||
)
|
||||
|
||||
expect(selected).toBe(row.expected)
|
||||
})
|
||||
})
|
||||
|
||||
story("an extension reloaded while disabled starts when it is enabled again", async ({ page }) => {
|
||||
const result = await page.evaluate(async (fixture) => {
|
||||
const { mountExtensionHost } = await import(fixture)
|
||||
@@ -496,11 +605,27 @@ story("an extension that requires a contract starts once it is active and restar
|
||||
]
|
||||
|
||||
// Hard contracts gate startup: a consumer whose provider is disabled settles the gate without starting.
|
||||
const gated = mountExtensions({ definitions, disabled: ["provider"] })
|
||||
const gated = mountExtensions({ definitions: [definitions[1], definitions[0]], disabled: ["provider"] })
|
||||
await until(() => gated.ready())
|
||||
const blocked = { status: gated.status("consumer"), log: [...log] }
|
||||
gated.unmount()
|
||||
|
||||
const failed = mountExtensions({
|
||||
definitions: [
|
||||
{
|
||||
...definitions[0],
|
||||
renderer: async () => {
|
||||
throw new Error("provider failed")
|
||||
},
|
||||
},
|
||||
definitions[1],
|
||||
],
|
||||
})
|
||||
|
||||
await until(() => failed.ready())
|
||||
const failedProvider = { status: failed.status("consumer"), log: [...log] }
|
||||
failed.unmount()
|
||||
|
||||
const host = mountExtensions({ definitions })
|
||||
await new Promise((resolve) => setTimeout(resolve, 50))
|
||||
const waiting = { status: host.status("consumer"), log: [...log] }
|
||||
@@ -510,17 +635,232 @@ story("an extension that requires a contract starts once it is active and restar
|
||||
await until(() => log.length === 3)
|
||||
host.disable(["provider"])
|
||||
await until(() => log.length === 4)
|
||||
const outcome = { blocked, waiting, log, after: host.status("consumer") }
|
||||
const outcome = { blocked, failedProvider, waiting, log, after: host.status("consumer") }
|
||||
host.unmount()
|
||||
|
||||
return outcome
|
||||
}, fixture)
|
||||
|
||||
expect(result).toEqual({
|
||||
blocked: { status: "loading", log: [] },
|
||||
blocked: { status: "blocked", log: [] },
|
||||
failedProvider: { status: "blocked", log: [] },
|
||||
waiting: { status: "loading", log: [] },
|
||||
log: ["setup 1", "cleanup 1", "setup 2", "cleanup 2"],
|
||||
after: "loading",
|
||||
after: "blocked",
|
||||
})
|
||||
})
|
||||
|
||||
story("a contract component reads its provider context and preserves reactive props", async ({ page }) => {
|
||||
const result = await page.evaluate(async (fixture) => {
|
||||
const { bindExtension, createComponent, createSignal, Contract, mountExtensions, Slot, until, useExtension } =
|
||||
await import(fixture)
|
||||
|
||||
const View: Contract<{ View: Component<{ readonly label: string }> }, "provider.view"> =
|
||||
Contract.define("provider.view")
|
||||
|
||||
const observed = { context: "", read: () => "", change: () => {}, mounts: 0 }
|
||||
|
||||
const host = mountExtensions({
|
||||
definitions: [
|
||||
{
|
||||
id: "provider",
|
||||
provides: { view: View },
|
||||
renderer: async () => ({
|
||||
default: (ctx: Context) => {
|
||||
ctx.provide(View, {
|
||||
View: bindExtension((props: { readonly label: string }) => {
|
||||
observed.context = useExtension().id
|
||||
observed.read = () => props.label
|
||||
observed.mounts++
|
||||
|
||||
return document.createTextNode(observed.context)
|
||||
}),
|
||||
})
|
||||
},
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: "consumer",
|
||||
requires: { view: View },
|
||||
renderer: async () => ({
|
||||
default: (ctx: Context & { requires: { view: { View: Component<{ readonly label: string }> } } }) => {
|
||||
const [label, setLabel] = createSignal("before")
|
||||
observed.change = () => setLabel("after")
|
||||
ctx.add(Slot, {
|
||||
at: "window.bottom",
|
||||
render: () =>
|
||||
createComponent(ctx.requires.view.View, {
|
||||
get label() {
|
||||
return label()
|
||||
},
|
||||
}),
|
||||
})
|
||||
},
|
||||
}),
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
await until(() => host.container.textContent === "provider")
|
||||
const before = observed.read()
|
||||
observed.change()
|
||||
const outcome = { context: observed.context, mounts: observed.mounts, before, after: observed.read() }
|
||||
host.unmount()
|
||||
|
||||
return outcome
|
||||
}, fixture)
|
||||
|
||||
expect(result).toEqual({ context: "provider", mounts: 1, before: "before", after: "after" })
|
||||
})
|
||||
|
||||
story("developer settings show a blocked hard dependency instead of loading forever", async ({ page }, info) => {
|
||||
await page.evaluate(async (fixture) => {
|
||||
const { Contract, mountExtensions, until } = await import(fixture)
|
||||
const Tree = Contract.define("provider.tree")
|
||||
|
||||
const host = mountExtensions({
|
||||
settings: true,
|
||||
disabled: ["provider"],
|
||||
definitions: [
|
||||
{ id: "provider", provides: { tree: Tree }, renderer: async () => ({ default: () => {} }) },
|
||||
{ id: "consumer", requires: { tree: Tree }, renderer: async () => ({ default: () => {} }) },
|
||||
],
|
||||
})
|
||||
|
||||
await until(() => !!host.container.textContent?.includes("consumer"))
|
||||
}, fixture)
|
||||
const settings = page.getByRole("region", { name: "Extensions", exact: true })
|
||||
await settings.screenshot({ path: info.outputPath("requires-status.png") })
|
||||
await expect(settings.getByText("Blocked", { exact: true })).toBeVisible()
|
||||
await expect(settings.getByText("Loading", { exact: true })).toHaveCount(0)
|
||||
})
|
||||
|
||||
story("blocked chains expose their own reason and recover regardless of definition order", async ({ page }) => {
|
||||
const result = await page.evaluate(async (fixture) => {
|
||||
const { Contract, mountExtensions, until } = await import(fixture)
|
||||
const Root: Contract<number, "root.value"> = Contract.define("root.value")
|
||||
const Branch: Contract<number, "branch.value"> = Contract.define("branch.value")
|
||||
const observed = { read: (): Live<number> => ({ status: "pending" }), starts: 0 }
|
||||
|
||||
const host = mountExtensions({
|
||||
disabled: ["root"],
|
||||
definitions: [
|
||||
{
|
||||
id: "leaf",
|
||||
requires: { branch: Branch },
|
||||
renderer: async () => ({
|
||||
default: () => {
|
||||
observed.starts++
|
||||
},
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: "observer",
|
||||
uses: { branch: Branch },
|
||||
renderer: async () => ({
|
||||
default: (ctx: Context & { uses: { branch: () => Live<number> } }) => {
|
||||
observed.read = ctx.uses.branch
|
||||
},
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: "branch",
|
||||
requires: { root: Root },
|
||||
provides: { branch: Branch },
|
||||
renderer: async () => ({ default: (ctx: Context) => void ctx.provide(Branch, 2) }),
|
||||
},
|
||||
{
|
||||
id: "root",
|
||||
provides: { root: Root },
|
||||
renderer: async () => ({ default: (ctx: Context) => void ctx.provide(Root, 1) }),
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
await until(() => host.ready())
|
||||
|
||||
const blocked = {
|
||||
branch: host.status("branch"),
|
||||
leaf: host.status("leaf"),
|
||||
live: observed.read(),
|
||||
starts: observed.starts,
|
||||
}
|
||||
|
||||
host.disable([])
|
||||
await until(() => host.status("leaf") === "active")
|
||||
const recovered = { live: observed.read(), starts: observed.starts }
|
||||
host.unmount()
|
||||
|
||||
return { blocked, recovered }
|
||||
}, fixture)
|
||||
|
||||
expect(result).toEqual({
|
||||
blocked: { branch: "blocked", leaf: "blocked", live: { status: "inactive", reason: "blocked" }, starts: 0 },
|
||||
recovered: { live: { status: "active", value: 2, generation: 1 }, starts: 1 },
|
||||
})
|
||||
})
|
||||
|
||||
const invalidSetups = ["promise", "thenable"]
|
||||
|
||||
invalidSetups.forEach((kind) => {
|
||||
story(`window setup rejects a returned ${kind}`, async ({ page }) => {
|
||||
const result = await page.evaluate(
|
||||
async (input) => {
|
||||
const { mountExtensions, until } = await import(input.fixture)
|
||||
const resume = Promise.withResolvers<void>()
|
||||
const done = Promise.withResolvers<void>()
|
||||
const point = { kind: "point" as const, id: "invalid-setup" }
|
||||
const observed = { aborted: false }
|
||||
|
||||
const host = mountExtensions({
|
||||
definitions: [
|
||||
{
|
||||
id: "invalid",
|
||||
renderer: async () => ({
|
||||
default: (ctx: Context) => {
|
||||
ctx.add(point, "before")
|
||||
|
||||
if (input.kind === "thenable")
|
||||
return {
|
||||
then(resolve: () => void) {
|
||||
resolve()
|
||||
},
|
||||
}
|
||||
|
||||
return resume.promise.then(() => {
|
||||
observed.aborted = ctx.signal.aborted
|
||||
ctx.add(point, "after")
|
||||
done.resolve()
|
||||
})
|
||||
},
|
||||
}),
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
await until(() => host.ready())
|
||||
|
||||
const failed = {
|
||||
status: host.status("invalid"),
|
||||
error: host.failure("invalid")?.error.includes("Window setup must be synchronous"),
|
||||
entries: host.entries(point.id),
|
||||
}
|
||||
|
||||
resume.resolve()
|
||||
|
||||
if (input.kind === "promise") await done.promise
|
||||
const after = { entries: host.entries(point.id), aborted: observed.aborted }
|
||||
host.unmount()
|
||||
|
||||
return { failed, after }
|
||||
},
|
||||
{ fixture, kind },
|
||||
)
|
||||
|
||||
expect(result).toEqual({
|
||||
failed: { status: "failed", error: true, entries: 0 },
|
||||
after: { entries: 0, aborted: kind === "promise" },
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -556,3 +896,158 @@ story("declared global stores load before setup, so setup reads the stored value
|
||||
|
||||
expect(result).toEqual({ held: { status: "loading", seen: [] }, seen: [true] })
|
||||
})
|
||||
|
||||
story("startup enable state does not wait for the manager, and live updates beat older replies", async ({ page }) => {
|
||||
const result = await page.evaluate(async (fixture) => {
|
||||
const { Contract, mountExtensions, until } = await import(fixture)
|
||||
const Provider: Contract<number, "disabled.value"> = Contract.define("disabled.value")
|
||||
const Dependent: Contract<number, "dependent.value"> = Contract.define("dependent.value")
|
||||
const observed = { read: (): Live<number> => ({ status: "pending" }) }
|
||||
const initial = Promise.withResolvers<readonly EnableState[]>()
|
||||
const reply = Promise.withResolvers<readonly Installed[]>()
|
||||
const listeners = new Set<(message: BridgeMessage) => void>()
|
||||
const setups: string[] = []
|
||||
|
||||
const list = (disabled: readonly string[]): Installed[] =>
|
||||
["dependent", "enabled", "disabled"].map((id) => ({
|
||||
id,
|
||||
name: id,
|
||||
version: "1",
|
||||
builtin: true,
|
||||
enabled: !disabled.includes(id),
|
||||
}))
|
||||
|
||||
const bridge: Bridge = {
|
||||
packaged: false,
|
||||
call: async () => undefined,
|
||||
subscribe: async () => ({ available: false }),
|
||||
on: (listener) => {
|
||||
listeners.add(listener)
|
||||
|
||||
return () => void listeners.delete(listener)
|
||||
},
|
||||
embed() {},
|
||||
capture: async () => undefined,
|
||||
runMenubarItem() {},
|
||||
configure() {},
|
||||
manager: {
|
||||
initial: initial.promise,
|
||||
list: () => reply.promise,
|
||||
enable: async () => {},
|
||||
disable: async () => {},
|
||||
reload: async () => {},
|
||||
install: async () => {},
|
||||
remove: async () => {},
|
||||
source: async () => "",
|
||||
asset: () => "",
|
||||
},
|
||||
}
|
||||
|
||||
const host = mountExtensions({
|
||||
bridge,
|
||||
// The consumer comes first; the provider's old-id setting arrives asynchronously from the preload.
|
||||
definitions: [
|
||||
{
|
||||
id: "dependent",
|
||||
requires: { provider: Provider },
|
||||
provides: { value: Dependent },
|
||||
renderer: async () => ({
|
||||
default: (ctx: Context) => {
|
||||
setups.push("dependent")
|
||||
ctx.provide(Dependent, 2)
|
||||
},
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: "enabled",
|
||||
uses: { value: Dependent },
|
||||
renderer: async () => ({
|
||||
default: (ctx: Context & { uses: { value: () => Live<number> } }) => {
|
||||
setups.push("enabled")
|
||||
observed.read = ctx.uses.value
|
||||
},
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: "disabled",
|
||||
legacy: ["previous"],
|
||||
provides: { value: Provider },
|
||||
renderer: async () => ({
|
||||
default: (ctx: Context) => {
|
||||
setups.push("disabled")
|
||||
ctx.provide(Provider, 1)
|
||||
},
|
||||
}),
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 50))
|
||||
const before = [...setups]
|
||||
initial.resolve([{ id: "previous", enabled: false }])
|
||||
await until(() => host.ready())
|
||||
|
||||
const started = {
|
||||
setups: [...setups],
|
||||
status: host.status("disabled"),
|
||||
dependent: host.status("dependent"),
|
||||
live: observed.read(),
|
||||
}
|
||||
|
||||
const unknown: { ready: boolean; setups: string[]; status?: string }[] = []
|
||||
|
||||
// A failed startup reply must not temporarily activate the disabled extension while list() waits.
|
||||
for (const failure of ["unknown", "rejected"]) {
|
||||
const initial = failure === "unknown" ? Promise.resolve(undefined) : Promise.reject(new Error("snapshot failed"))
|
||||
const reply = Promise.withResolvers<readonly Installed[]>()
|
||||
|
||||
const failed = mountExtensions({
|
||||
bridge: { ...bridge, manager: { ...bridge.manager, initial, list: () => reply.promise } },
|
||||
definitions: [{ id: "disabled", renderer: async () => ({ default: () => void setups.push("wrongly enabled") }) }],
|
||||
})
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 50))
|
||||
const blocked = { ready: failed.ready(), setups: [...setups] }
|
||||
|
||||
reply.resolve(list(["disabled"]))
|
||||
await until(() => failed.ready())
|
||||
unknown.push({ ...blocked, status: failed.status("disabled") })
|
||||
failed.unmount()
|
||||
}
|
||||
|
||||
listeners.forEach((listener) => listener({ type: "extensions", list: list(["enabled"]) }))
|
||||
await until(
|
||||
() =>
|
||||
host.status("disabled") === "active" &&
|
||||
host.status("enabled") === "disabled" &&
|
||||
host.status("dependent") === "active",
|
||||
)
|
||||
reply.resolve(list(["disabled"]))
|
||||
await new Promise((resolve) => setTimeout(resolve, 50))
|
||||
|
||||
const updated = {
|
||||
setups: [...setups],
|
||||
status: [host.status("enabled"), host.status("disabled"), host.status("dependent")],
|
||||
}
|
||||
|
||||
host.unmount()
|
||||
|
||||
return { before, started, unknown, updated, listeners: listeners.size }
|
||||
}, fixture)
|
||||
|
||||
expect(result).toEqual({
|
||||
before: [],
|
||||
started: {
|
||||
setups: ["enabled"],
|
||||
status: "disabled",
|
||||
dependent: "blocked",
|
||||
live: { status: "inactive", reason: "blocked" },
|
||||
},
|
||||
unknown: [
|
||||
{ ready: false, setups: ["enabled"], status: "disabled" },
|
||||
{ ready: false, setups: ["enabled"], status: "disabled" },
|
||||
],
|
||||
updated: { setups: ["enabled", "disabled", "dependent"], status: ["disabled", "active", "active"] },
|
||||
listeners: 0,
|
||||
})
|
||||
})
|
||||
@@ -15,21 +15,25 @@ export function mountTimelineVirtualizer(input: { count: number; rowHeight: numb
|
||||
|
||||
function Fixture() {
|
||||
const [state, setState] = createStore({ pinned: true, ready: false })
|
||||
|
||||
const rows = Array.from(
|
||||
{ length: input.count },
|
||||
(_, index) => new TimelineRow.UserMessage({ userMessageID: `message-${index}` }),
|
||||
)
|
||||
|
||||
const rowByKey = new Map(rows.map((row) => [TimelineRow.key(row), row]))
|
||||
const indexes = new Map(rows.map((row, index) => [row.userMessageID, index]))
|
||||
let viewport!: HTMLDivElement
|
||||
let content!: HTMLDivElement
|
||||
let container!: HTMLDivElement
|
||||
|
||||
const timeline = createTimelineVirtualizer({
|
||||
sessionKey: () => "cold-reveal-fixture",
|
||||
projection: {
|
||||
rows: () => rows,
|
||||
rowByKey: () => rowByKey,
|
||||
activeMessageID: () => undefined,
|
||||
messageByID: () => new Map(),
|
||||
messageRowIndex: () => indexes,
|
||||
messageLastRowIndex: () => indexes,
|
||||
},
|
||||
@@ -62,6 +66,7 @@ export function mountTimelineVirtualizer(input: { count: number; rowHeight: numb
|
||||
host.dataset.observedHeight = String(entries[0].borderBoxSize[0].blockSize)
|
||||
host.dataset.viewportResizes = String(Number(host.dataset.viewportResizes) + 1)
|
||||
})
|
||||
|
||||
const reveal = new MutationObserver(() => {
|
||||
if (content.style.visibility === "hidden" || host.dataset.firstReveal) return
|
||||
// Capture the first reveal, not a later frame after geometry has recovered.
|
||||
@@ -71,11 +76,14 @@ export function mountTimelineVirtualizer(input: { count: number; rowHeight: numb
|
||||
pendingMarkdown: content.querySelectorAll('[data-component="markdown"]:not([data-markdown-ready])').length,
|
||||
viewportHeight: viewport.clientHeight,
|
||||
scrollTop: viewport.scrollTop,
|
||||
clipped: mounted
|
||||
.filter((element) => element.firstElementChild!.getBoundingClientRect().height > element.offsetHeight + 1)
|
||||
.map((element) => element.dataset.timelineKey),
|
||||
clipped: mounted.flatMap((element) =>
|
||||
element.firstElementChild!.getBoundingClientRect().height > element.offsetHeight + 1
|
||||
? [element.dataset.timelineKey]
|
||||
: [],
|
||||
),
|
||||
})
|
||||
})
|
||||
|
||||
onCleanup(() => {
|
||||
resize.disconnect()
|
||||
reveal.disconnect()
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { fileURLToPath } from "node:url"
|
||||
import { expect, story } from "../../storybook/playwright/story"
|
||||
import { expect, sourceURL, story } from "../../storybook/playwright/story"
|
||||
|
||||
const fixture = `/@fs/${fileURLToPath(new URL("./timeline-virtualizer.fixture.tsx", import.meta.url)).replaceAll("\\", "/")}`
|
||||
const fixture = sourceURL(new URL("./timeline-virtualizer.fixture.tsx", import.meta.url))
|
||||
|
||||
story.beforeEach(async ({ mount }) => {
|
||||
const component = await mount("opencode-composer-flow--mixed-attachments")
|
||||
|
||||
@@ -7,6 +7,7 @@ test.use({ permissions: ["clipboard-read", "clipboard-write"] })
|
||||
async function draft(page: Page) {
|
||||
const { editor } = await openDraft(page, { name: "ComposerDraft", provider: NO_PROVIDER })
|
||||
await editor.click()
|
||||
|
||||
return editor
|
||||
}
|
||||
|
||||
@@ -15,9 +16,11 @@ async function expectCaretVisible(input: Locator) {
|
||||
.poll(() =>
|
||||
input.evaluate((element) => {
|
||||
const selection = window.getSelection()
|
||||
|
||||
if (!selection?.isCollapsed || !selection.rangeCount || !element.contains(selection.anchorNode)) return false
|
||||
const caret = selection.getRangeAt(0).getBoundingClientRect()
|
||||
const viewport = (element.closest("[data-scrollable]") ?? element).getBoundingClientRect()
|
||||
|
||||
return caret.height > 0 && caret.top >= viewport.top - 1 && caret.bottom <= viewport.bottom + 1
|
||||
}),
|
||||
)
|
||||
@@ -28,11 +31,14 @@ test("keeps a 25000-line crash report editable in a new session", async ({ page
|
||||
const input = await draft(page)
|
||||
const text = "Thread 0 Crashed:\n" + "0 Example 0x0000000100000000 frame + 32\n".repeat(25000) + "End of report"
|
||||
await page.evaluate((text) => navigator.clipboard.writeText(text), text)
|
||||
|
||||
const events = await input.evaluateHandle((element) => {
|
||||
const events = { count: 0 }
|
||||
element.addEventListener("input", () => events.count++)
|
||||
|
||||
return events
|
||||
})
|
||||
|
||||
await page.keyboard.press("ControlOrMeta+V")
|
||||
await expect.poll(async () => (await input.innerText()) === text).toBe(true)
|
||||
expect(await events.evaluate((events) => events.count)).toBe(1)
|
||||
@@ -46,6 +52,7 @@ test("keeps a 25000-line crash report editable in a new session", async ({ page
|
||||
await expectCaretVisible(input)
|
||||
const thumb = await scroll.locator(".scroll-view__thumb").boundingBox()
|
||||
const bounds = await scroll.boundingBox()
|
||||
|
||||
if (!thumb || !bounds) throw new Error("Missing composer scrollbar bounds")
|
||||
await page.mouse.move(thumb.x + thumb.width / 2, thumb.y + thumb.height / 2)
|
||||
await page.mouse.down()
|
||||
@@ -58,6 +65,26 @@ test("keeps a 25000-line crash report editable in a new session", async ({ page
|
||||
await expectCaretVisible(input)
|
||||
})
|
||||
|
||||
test("a key-up never moves the caret back after later navigation", async ({ page }) => {
|
||||
const input = await draft(page)
|
||||
await input.pressSequentially("first line")
|
||||
await expect(input).toHaveText("first line")
|
||||
|
||||
// Navigation can land before the next frame; the cursor a key-up recorded must not overwrite it there.
|
||||
const offset = await input.evaluate(async (element) => {
|
||||
const text = document.createTreeWalker(element, NodeFilter.SHOW_TEXT).nextNode()!
|
||||
const selection = window.getSelection()!
|
||||
selection.collapse(text, 0)
|
||||
element.dispatchEvent(new KeyboardEvent("keyup", { key: "Home", bubbles: true }))
|
||||
selection.collapse(text, text.textContent!.length)
|
||||
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)))
|
||||
|
||||
return selection.anchorOffset
|
||||
})
|
||||
|
||||
expect(offset).toBe("first line".length)
|
||||
})
|
||||
|
||||
for (const [width, direction] of [
|
||||
[390, "rtl"],
|
||||
[1280, "ltr"],
|
||||
@@ -83,6 +110,7 @@ for (const [width, direction] of [
|
||||
|
||||
test("pastes plain text without markup and keeps native undo", async ({ page }) => {
|
||||
const input = await draft(page)
|
||||
|
||||
for (const text of [
|
||||
"single line <b> &",
|
||||
"first\nsecond",
|
||||
@@ -110,13 +138,16 @@ test("replaces only the selected text and leaves the caret after the paste", asy
|
||||
await page.keyboard.type("before replace after")
|
||||
await expect(input).toHaveText("before replace after")
|
||||
await page.evaluate(() => document.fonts.ready)
|
||||
|
||||
const word = await input.evaluate((element) => {
|
||||
const range = document.createRange()
|
||||
range.setStart(element.firstChild!, 7)
|
||||
range.setEnd(element.firstChild!, 14)
|
||||
const rect = range.getBoundingClientRect()
|
||||
|
||||
return { x: rect.x, y: rect.y + rect.height / 2, width: rect.width }
|
||||
})
|
||||
|
||||
await page.mouse.move(word.x, word.y)
|
||||
await page.mouse.down()
|
||||
await page.mouse.move(word.x + word.width, word.y, { steps: 5 })
|
||||
@@ -141,9 +172,11 @@ test("shows the dropzone and attaches a dropped file", async ({ page }) => {
|
||||
})
|
||||
const surface = page.locator('[data-component="new-session"]')
|
||||
const dropzone = page.locator('[data-component="session-dropzone"]')
|
||||
|
||||
const transfer = await page.evaluateHandle(() => {
|
||||
const value = new DataTransfer()
|
||||
value.items.add(new File(["Dropzone fixture"], "dropzone.txt", { type: "text/plain" }))
|
||||
|
||||
return value
|
||||
})
|
||||
|
||||
@@ -194,6 +227,7 @@ test("lists slash commands in their built-in order", async ({ page }) => {
|
||||
// A sent message enables /undo, /compact and /fork.
|
||||
pageMessages: () => ({ items: [{ id: "msg_slash", type: "user", text: "Hello", time: { created: T0 } }] }),
|
||||
})
|
||||
|
||||
await editor.fill("/")
|
||||
await expect(page.locator('[data-component="composer-suggestions"] [data-suggestion-id] bdi')).toHaveText([
|
||||
"/init",
|
||||
@@ -213,6 +247,7 @@ test("lists slash commands in their built-in order", async ({ page }) => {
|
||||
})
|
||||
|
||||
const followUp = "Add follow-up, / for commands, @ for context…"
|
||||
|
||||
for (const row of [
|
||||
{ state: "an idle", copy: "Ask anything, / for commands, @ for context…" },
|
||||
{ state: "a running", copy: followUp, sessionStatus: { ses_placeholder: { type: "running" } } },
|
||||
@@ -238,6 +273,7 @@ for (const row of [
|
||||
sessionStatus: row.sessionStatus,
|
||||
inbox: row.inbox,
|
||||
})
|
||||
|
||||
const scroll = page.locator('[data-component="composer-scroll"]')
|
||||
await expect(scroll).toHaveText(row.copy)
|
||||
await editor.pressSequentially("!")
|
||||
@@ -250,6 +286,7 @@ test("shows thinking on hover or a non-default selection while preserving keyboa
|
||||
name: "ComposerThinking",
|
||||
provider: provider({ id: "thinking-model", name: "Thinking Model", variants: { high: {} } }),
|
||||
})
|
||||
|
||||
const composer = page.locator('[data-component="composer"]')
|
||||
const control = composer.getByRole("button", { name: "Choose model variant" })
|
||||
const option = (name: string) => page.getByRole("menuitemradio", { name, exact: true })
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { OpenCodeEvent, SessionMessageInfo } from "@opencode/client/promise"
|
||||
import { expect, test, type Locator, type Page } from "@playwright/test"
|
||||
import { base64Encode } from "@opencode/util/encode"
|
||||
import { SERVER, project, provider, session, workspaceKey } from "../utils/app"
|
||||
import { SERVER, holdRoute, project, provider, session, workspaceKey } from "../utils/app"
|
||||
import { mockOpenCodeServer } from "../utils/mock-server"
|
||||
import { fileDiff, fileNode, openSession } from "../utils/workspace"
|
||||
import { expectSessionTitle } from "../utils/waits"
|
||||
@@ -33,6 +33,85 @@ for (const view of ["desktop", "mobile"] as const) {
|
||||
})
|
||||
}
|
||||
|
||||
test("Git initialization belongs to its session and refreshes after a session switch", async ({ page }) => {
|
||||
const requests: { directory: string; provider?: string }[] = []
|
||||
|
||||
const workspace = await openSession(page, {
|
||||
name: "ReviewInitSwitch",
|
||||
project: { vcs: undefined },
|
||||
onVcsInit: (input) => requests.push(input),
|
||||
sessions: [
|
||||
{ id: "ses_init_a", title: "Initialize Alpha" },
|
||||
{ id: "ses_init_b", title: "Initialize Beta" },
|
||||
],
|
||||
seed: { panes: { ses_init_a: { review: true }, ses_init_b: { review: true } } },
|
||||
})
|
||||
|
||||
// The successful init finishes while another session is routed.
|
||||
const request = await holdRoute(page, (url) => url.pathname === "/api/vcs/init", { method: "POST" })
|
||||
const panel = page.locator("#review-panel")
|
||||
const init = panel.getByRole("button", { name: "Create Git repository", exact: true })
|
||||
const pending = panel.getByRole("button", { name: "Creating Git repository…", exact: true })
|
||||
await expect(init).toBeEnabled()
|
||||
await init.click()
|
||||
await request.arrived
|
||||
await expect(pending).toBeDisabled()
|
||||
|
||||
await page.locator("[data-titlebar-tab-slot]", { hasText: "Initialize Beta" }).click()
|
||||
await expectSessionTitle(page, "Initialize Beta")
|
||||
await expect(init).toBeEnabled()
|
||||
await expect(pending).toHaveCount(0)
|
||||
|
||||
await page.locator("[data-titlebar-tab-slot]", { hasText: "Initialize Alpha" }).click()
|
||||
await expectSessionTitle(page, "Initialize Alpha")
|
||||
await expect(pending).toBeDisabled()
|
||||
await page.locator("[data-titlebar-tab-slot]", { hasText: "Initialize Beta" }).click()
|
||||
await expectSessionTitle(page, "Initialize Beta")
|
||||
await expect(init).toBeEnabled()
|
||||
|
||||
const response = page.waitForResponse((item) => new URL(item.url()).pathname === "/api/vcs/init")
|
||||
// The worktree event can refresh project-wide UI by itself; the completed action must also reload Alpha's cache.
|
||||
const refreshed = page.waitForResponse((item) => new URL(item.url()).pathname === "/api/session/ses_init_a")
|
||||
request.release()
|
||||
expect((await response).status()).toBe(204)
|
||||
expect((await refreshed).status()).toBe(200)
|
||||
expect(requests).toEqual([{ directory: workspace.directory, provider: "git" }])
|
||||
await expect(panel.getByRole("button", { name: "Git changes", exact: true })).toBeVisible()
|
||||
await expect(init).toHaveCount(0)
|
||||
await expect(page.getByText("Request failed", { exact: true })).toHaveCount(0)
|
||||
|
||||
await page.locator("[data-titlebar-tab-slot]", { hasText: "Initialize Alpha" }).click()
|
||||
await expectSessionTitle(page, "Initialize Alpha")
|
||||
await expect(panel.getByRole("button", { name: "Git changes", exact: true })).toBeVisible()
|
||||
await expect(pending).toHaveCount(0)
|
||||
})
|
||||
|
||||
test("a rejected Git init releases its session's button without showing an error in another session", async ({ page }) => {
|
||||
await openSession(page, {
|
||||
name: "ReviewInitFailure",
|
||||
project: { vcs: undefined },
|
||||
sessions: [
|
||||
{ id: "ses_init_failure_a", title: "Initialize Alpha" },
|
||||
{ id: "ses_init_failure_b", title: "Initialize Beta" },
|
||||
],
|
||||
seed: { panes: { ses_init_failure_a: { review: true }, ses_init_failure_b: { review: true } } },
|
||||
})
|
||||
const request = await holdRoute(page, (url) => url.pathname === "/api/vcs/init", { method: "POST" })
|
||||
const init = page.locator("#review-panel").getByRole("button", { name: "Create Git repository", exact: true })
|
||||
await init.click()
|
||||
await request.arrived
|
||||
await page.locator("[data-titlebar-tab-slot]", { hasText: "Initialize Beta" }).click()
|
||||
await expectSessionTitle(page, "Initialize Beta")
|
||||
await expect(init).toBeEnabled()
|
||||
const response = page.waitForResponse((item) => new URL(item.url()).pathname === "/api/vcs/init")
|
||||
request.release()
|
||||
expect((await response).status()).toBe(501)
|
||||
await expect(page.getByText("Request failed", { exact: true })).toHaveCount(0)
|
||||
await page.locator("[data-titlebar-tab-slot]", { hasText: "Initialize Alpha" }).click()
|
||||
await expectSessionTitle(page, "Initialize Alpha")
|
||||
await expect(init).toBeEnabled()
|
||||
})
|
||||
|
||||
test("open file tab browses, searches, and tracks missing files", async ({ page }) => {
|
||||
const searches: { query: string; dirs?: string; limit?: number }[] = []
|
||||
const directory = "C:/OpenCode/ReviewOpenFile"
|
||||
@@ -754,6 +833,32 @@ test("a chosen mode the session stops offering shows as Git, and comes back when
|
||||
await expect(file).toHaveText("beta.ts")
|
||||
})
|
||||
|
||||
test("names a single change source and keeps the review header rows at 48px", async ({ page }) => {
|
||||
// Turn changes are offered only for Git, so another VCS offers one source.
|
||||
await openSession(page, { name: "ReviewHeader", project: { vcs: "hg" }, vcsDiff: [fileDiff("src/alpha.ts")] })
|
||||
await page.getByRole("button", { name: "Toggle review", exact: true }).click()
|
||||
const panel = page.locator("#review-panel")
|
||||
const toggle = panel.getByRole("button", { name: "Toggle file tree" })
|
||||
const header = panel.locator('[data-slot="session-review-v2-sidebar-header"]')
|
||||
const toolbar = panel.locator('[data-slot="session-review-v2-toolbar"]')
|
||||
|
||||
await expect(header.getByText("Git changes", { exact: true })).toBeVisible()
|
||||
await expect(panel.getByRole("button", { name: "Git changes" })).toHaveCount(0)
|
||||
await expect(toggle).toHaveAttribute("aria-expanded", "true")
|
||||
await expect(header).toHaveCSS("height", "48px")
|
||||
await expect(toolbar).toHaveCSS("height", "48px")
|
||||
|
||||
await toggle.click()
|
||||
await expect(toggle).toHaveAttribute("aria-expanded", "false")
|
||||
await expect(toolbar.getByText("Git changes", { exact: true })).toBeVisible()
|
||||
await expect(toolbar).toHaveCSS("height", "48px")
|
||||
|
||||
await toggle.click()
|
||||
await expect(toggle).toHaveAttribute("aria-expanded", "true")
|
||||
await expect(header).toHaveCSS("height", "48px")
|
||||
await expect(toolbar).toHaveCSS("height", "48px")
|
||||
})
|
||||
|
||||
for (const direction of ["ltr", "rtl"] as const) {
|
||||
test(`review toggle stays at the header edge in ${direction}`, async ({ page }) => {
|
||||
await page.setViewportSize({ width: 1000, height: 900 })
|
||||
|
||||
@@ -29,12 +29,14 @@ for (const direction of ["ltr", "rtl"] as const) {
|
||||
await expect(page.locator('[data-slot="titlebar-v2"]').getByRole("button", { name: "Status" })).toHaveCount(0)
|
||||
const titleBounds = await header.getByRole("heading").boundingBox()
|
||||
expect(titleBounds).not.toBeNull()
|
||||
|
||||
for (const editing of [false, true]) {
|
||||
if (editing) {
|
||||
await header.getByRole("heading").click()
|
||||
await expect(header.getByRole("textbox")).toHaveValue(fixture.expected.targetTitle)
|
||||
await expect(header.getByRole("textbox")).toBeFocused()
|
||||
}
|
||||
|
||||
await expect(header.locator('[data-slot="session-title-child"]')).toHaveCSS("padding-left", "4px")
|
||||
await expect(header.locator('[data-slot="session-title-child"]')).toHaveCSS("padding-right", "4px")
|
||||
await expect
|
||||
@@ -44,8 +46,11 @@ for (const direction of ["ltr", "rtl"] as const) {
|
||||
control.boundingBox(),
|
||||
),
|
||||
)
|
||||
|
||||
if (!icon || !title || !menu || !sidebar || !summary || !titleBounds) return false
|
||||
|
||||
if (Math.abs(title.y - titleBounds.y) > 0.5 || Math.abs(title.height - titleBounds.height) > 0.5) return false
|
||||
|
||||
return direction === "ltr"
|
||||
? Math.abs(title.x - icon.x - icon.width - 2) <= 0.5 &&
|
||||
Math.abs(menu.x - title.x - title.width - 2) <= 0.5 &&
|
||||
@@ -58,6 +63,7 @@ for (const direction of ["ltr", "rtl"] as const) {
|
||||
})
|
||||
.toBe(true)
|
||||
}
|
||||
|
||||
await header.getByRole("textbox").press("Escape")
|
||||
await expect(header.getByRole("heading")).toHaveText(fixture.expected.targetTitle)
|
||||
|
||||
@@ -70,6 +76,7 @@ for (const direction of ["ltr", "rtl"] as const) {
|
||||
await more.click()
|
||||
const options = page.getByRole("menu")
|
||||
await expect(options.getByRole("menuitem")).toHaveText(["Rename", "Export…", "Delete…"])
|
||||
|
||||
if (direction === "ltr") {
|
||||
await expect
|
||||
.poll(async () => {
|
||||
@@ -77,20 +84,24 @@ for (const direction of ["ltr", "rtl"] as const) {
|
||||
header.getByRole("button", { name: "More options", exact: true, includeHidden: true }).boundingBox(),
|
||||
options.boundingBox(),
|
||||
])
|
||||
|
||||
return button && menu ? Math.abs(button.x - menu.x) : Infinity
|
||||
})
|
||||
.toBeLessThanOrEqual(1)
|
||||
}
|
||||
|
||||
await expect
|
||||
.poll(() =>
|
||||
options.evaluate((element) => {
|
||||
const menu = element.getBoundingClientRect()
|
||||
const rtl = getComputedStyle(element).direction === "rtl"
|
||||
|
||||
return Math.min(
|
||||
...Array.from(element.querySelectorAll('[data-slot="menu-v2-item-content"]'), (label) => {
|
||||
const range = document.createRange()
|
||||
range.selectNodeContents(label)
|
||||
const text = range.getBoundingClientRect()
|
||||
|
||||
return rtl ? text.left - menu.left : menu.right - text.right
|
||||
}),
|
||||
)
|
||||
@@ -103,6 +114,7 @@ for (const direction of ["ltr", "rtl"] as const) {
|
||||
const menu = element.getBoundingClientRect()
|
||||
const divider = element.querySelector('[data-slot="menu-v2-separator"]')?.getBoundingClientRect()
|
||||
const rows = Array.from(element.querySelectorAll('[role="menuitem"]'), (row) => row.getBoundingClientRect())
|
||||
|
||||
return (
|
||||
!!divider &&
|
||||
Math.abs(divider.left - menu.left) <= 0.5 &&
|
||||
@@ -135,6 +147,7 @@ test.describe("rename", () => {
|
||||
renames.length = 0
|
||||
page.on("request", (request) => {
|
||||
const match = new URL(request.url()).pathname.match(/^\/api\/session\/([^/]+)$/)
|
||||
|
||||
if (request.method() === "PATCH" && match) renames.push({ sessionID: match[1]!, body: request.postDataJSON() })
|
||||
})
|
||||
await mockStressTimeline(page)
|
||||
@@ -149,8 +162,11 @@ test.describe("rename", () => {
|
||||
const input = page.locator('input[data-slot="session-title-child"]')
|
||||
await expect(input).toBeFocused()
|
||||
await input.fill("Renamed session")
|
||||
|
||||
if (commit === "Enter") await input.press("Enter")
|
||||
|
||||
if (commit === "blur") await input.press("Tab")
|
||||
|
||||
if (commit === "click outside") await page.locator('[data-component="composer-editor"]').click()
|
||||
await expect(heading(page, "Renamed session")).toBeVisible()
|
||||
await expect(tabs(page).filter({ hasText: "Renamed session" })).toBeVisible()
|
||||
@@ -252,6 +268,7 @@ test.describe("revert", () => {
|
||||
},
|
||||
{ id: "msg_second", type: "user", text: "Second prompt", time: { created: 4 } },
|
||||
] satisfies SessionMessageInfo[]
|
||||
|
||||
const workspace = {
|
||||
name: "SessionMessageRevert",
|
||||
pageMessages: () => ({ items: messages }),
|
||||
@@ -260,18 +277,54 @@ test.describe("revert", () => {
|
||||
test("reverts directly to the selected user message", async ({ page }) => {
|
||||
const staged: { sessionID: string; messageID: string }[] = []
|
||||
const { session } = await openSession(page, { ...workspace, onRevertStage: (input) => staged.push(input) })
|
||||
const settles: string[] = []
|
||||
page.on("request", (request) => {
|
||||
const path = new URL(request.url()).pathname
|
||||
const step = ["/interrupt", "/wait", "/revert/stage"].find((suffix) => path.endsWith(`/${session.id}${suffix}`))
|
||||
|
||||
if (step) settles.push(step)
|
||||
})
|
||||
const message = page.locator('[data-message-id="msg_second"]')
|
||||
await message.hover()
|
||||
|
||||
const response = page.waitForResponse(
|
||||
(response) =>
|
||||
response.request().method() === "POST" &&
|
||||
new URL(response.url()).pathname === `/api/session/${session.id}/revert/stage`,
|
||||
)
|
||||
|
||||
await message.getByRole("button", { name: "Revert message" }).click()
|
||||
expect((await response).ok()).toBe(true)
|
||||
|
||||
await expect(page.getByRole("textbox", { name: "Prompt" })).toHaveText("Second prompt")
|
||||
expect(staged).toEqual([{ sessionID: session.id, messageID: "msg_second" }])
|
||||
// Interrupt acknowledges before the run settles, and the server refuses to stage while it is active.
|
||||
expect(settles).toEqual(["/interrupt", "/wait", "/revert/stage"])
|
||||
})
|
||||
|
||||
test("redo restores every reverted message at once, as in the TUI", async ({ page }) => {
|
||||
const staged: { sessionID: string; messageID: string }[] = []
|
||||
|
||||
const { editor } = await openSession(page, {
|
||||
...workspace,
|
||||
sessions: [{ id: "ses_revert_redo", title: "Session message revert", revert: { messageID: "msg_first" } }],
|
||||
onRevertStage: (input) => staged.push(input),
|
||||
})
|
||||
|
||||
const cleared = page.waitForResponse(
|
||||
(response) =>
|
||||
response.request().method() === "DELETE" &&
|
||||
new URL(response.url()).pathname === "/api/session/ses_revert_redo/revert",
|
||||
)
|
||||
|
||||
await editor.pressSequentially("/redo")
|
||||
await expect(
|
||||
page.locator('[data-component="composer-suggestions"] [data-suggestion-id][data-active]'),
|
||||
).toContainText("/redo")
|
||||
await editor.press("Enter")
|
||||
|
||||
expect((await cleared).ok()).toBe(true)
|
||||
expect(staged).toEqual([])
|
||||
})
|
||||
|
||||
test("hides revert actions in a child session", async ({ page }) => {
|
||||
|
||||
@@ -23,23 +23,30 @@ for (const endpoint of ["/api/location", "/api/agent"]) {
|
||||
let requests = 0
|
||||
await page.route("**/api/**", (route) => {
|
||||
const url = new URL(route.request().url())
|
||||
|
||||
if (url.pathname !== endpoint || url.searchParams.get("location[directory]") !== directory)
|
||||
return route.fallback()
|
||||
requests++
|
||||
|
||||
if (recover && requests > 1) return route.fallback()
|
||||
|
||||
return route.fulfill({ status: 500, body: "", headers: { "access-control-allow-origin": "*" } })
|
||||
})
|
||||
|
||||
const failure = page.waitForResponse(
|
||||
(response) => new URL(response.url()).pathname === endpoint && response.status() === 500,
|
||||
)
|
||||
|
||||
const settled = page.waitForResponse((response) => {
|
||||
const url = new URL(response.url())
|
||||
|
||||
return (
|
||||
url.pathname === endpoint &&
|
||||
url.searchParams.get("location[directory]") === directory &&
|
||||
(recover ? response.ok() : requests === 3 && response.status() === 500)
|
||||
)
|
||||
})
|
||||
|
||||
await page.goto(`/server/${base64Encode(fixture.serverKey)}/session/${sessionID}`)
|
||||
await failure
|
||||
await expect(page.getByText("Keep working in this worktree", { exact: true })).toBeVisible()
|
||||
@@ -86,10 +93,12 @@ test("follows a live session move while the agent catalog is still loading", asy
|
||||
})
|
||||
await page.route("**/api/**", async (route) => {
|
||||
const url = new URL(route.request().url())
|
||||
|
||||
if (url.pathname === "/api/agent" && url.searchParams.get("location[directory]") === directory) {
|
||||
requested.resolve()
|
||||
await release.promise
|
||||
}
|
||||
|
||||
return route.fallback()
|
||||
})
|
||||
await page.goto(`/server/${base64Encode(fixture.serverKey)}/session/${sessionID}`)
|
||||
@@ -99,10 +108,13 @@ test("follows a live session move while the agent catalog is still loading", asy
|
||||
await prompt.fill("Keep this draft")
|
||||
await expect(page.getByText("Session location unavailable", { exact: true })).toHaveCount(0)
|
||||
await transport.waitForConnection()
|
||||
|
||||
const resolved = page.waitForResponse((response) => {
|
||||
const url = new URL(response.url())
|
||||
|
||||
return url.pathname === "/api/agent" && url.searchParams.get("location[directory]") === destination && response.ok()
|
||||
})
|
||||
|
||||
session.directory = destination
|
||||
await transport.send({
|
||||
id: "evt_location_moved_while_loading",
|
||||
@@ -112,10 +124,13 @@ test("follows a live session move while the agent catalog is still loading", asy
|
||||
data: { sessionID, location: { directory: destination }, projectID: fixture.project.id },
|
||||
})
|
||||
await resolved
|
||||
|
||||
const delayed = page.waitForResponse((response) => {
|
||||
const url = new URL(response.url())
|
||||
|
||||
return url.pathname === "/api/agent" && url.searchParams.get("location[directory]") === directory && response.ok()
|
||||
})
|
||||
|
||||
release.resolve()
|
||||
await delayed
|
||||
await expect(prompt).toBeEditable()
|
||||
@@ -127,6 +142,76 @@ test("follows a live session move while the agent catalog is still loading", asy
|
||||
expect(recovery).toEqual([])
|
||||
})
|
||||
|
||||
test("keeps the side panel's tabs and selection when the session moves", async ({ page }) => {
|
||||
const directory = "/projects/tabs-before-move"
|
||||
const destination = "/projects/tabs-after-move"
|
||||
const sessionID = "ses_location_moved_tabs"
|
||||
const session = { id: sessionID, projectID: fixture.project.id, directory, title: "Moved tabs" }
|
||||
const transport = await installSseTransport(page, { server: fixture.serverKey })
|
||||
await mockOpenCodeServer(page, {
|
||||
directory: fixture.directory,
|
||||
project: fixture.project,
|
||||
provider: fixture.provider,
|
||||
sessions: [session],
|
||||
fileList: () => [],
|
||||
findFiles: (input) => ["README.md", "NOTES.md"].filter((name) => name.startsWith(input.query)),
|
||||
fileContent: (path) => ({ type: "text", content: `contents:${session.directory}:${path}` }),
|
||||
pageMessages: () => ({
|
||||
items: [{ id: "msg_saved", type: "user", text: "Keep my tabs", time: { created: 1 } }],
|
||||
}),
|
||||
})
|
||||
await page.goto(`/server/${base64Encode(fixture.serverKey)}/session/${sessionID}`)
|
||||
await expect(page.getByRole("textbox", { name: "Prompt", exact: true })).toBeEditable()
|
||||
|
||||
const panel = page.getByRole("complementary", { name: "Review and files" })
|
||||
const files = panel.getByRole("tab", { name: /\.md$/ })
|
||||
const notes = panel.getByRole("tab", { name: "NOTES.md", exact: true })
|
||||
|
||||
// A lost selection would fall back to the first file tab, README.md, so NOTES.md stays selected only if it carries.
|
||||
for (const name of ["README", "NOTES"]) {
|
||||
await page.keyboard.press("ControlOrMeta+Shift+P")
|
||||
|
||||
const dialog = page.getByRole("dialog")
|
||||
const search = dialog.getByRole("textbox")
|
||||
await search.fill(name)
|
||||
await expect(dialog.getByRole("option", { name: `/ ${name}.md`, exact: true })).toHaveAttribute(
|
||||
"aria-selected",
|
||||
"true",
|
||||
)
|
||||
await search.press("Enter")
|
||||
await expect(dialog).toHaveCount(0)
|
||||
await expect(panel.getByRole("tab", { name: `${name}.md`, exact: true })).toHaveAttribute("aria-selected", "true")
|
||||
}
|
||||
|
||||
await expect(files).toHaveText(["README.md", "NOTES.md"])
|
||||
await expect(panel.getByText(`contents:${directory}:NOTES.md`, { exact: true })).toBeVisible()
|
||||
|
||||
await transport.waitForConnection()
|
||||
|
||||
const resolved = page.waitForResponse((response) => {
|
||||
const url = new URL(response.url())
|
||||
|
||||
return (
|
||||
url.pathname === "/api/location" && url.searchParams.get("location[directory]") === destination && response.ok()
|
||||
)
|
||||
})
|
||||
|
||||
session.directory = destination
|
||||
await transport.send({
|
||||
id: "evt_location_moved_tabs",
|
||||
type: "session.moved",
|
||||
created: 2,
|
||||
durable: { aggregateID: sessionID, seq: 1, version: 1 },
|
||||
data: { sessionID, location: { directory: destination }, projectID: fixture.project.id },
|
||||
})
|
||||
await resolved
|
||||
await expect(page.locator('[data-type="location-switched"]').getByText(destination, { exact: true })).toBeVisible()
|
||||
await expect(files).toHaveText(["README.md", "NOTES.md"])
|
||||
await expect(notes).toHaveAttribute("aria-selected", "true")
|
||||
// The carried tab shows the file in the new directory.
|
||||
await expect(panel.getByText(`contents:${destination}:NOTES.md`, { exact: true })).toBeVisible()
|
||||
})
|
||||
|
||||
test("refreshes a session moved during disconnection without losing the draft", async ({ page }) => {
|
||||
const recovery = recoveryRequests(page)
|
||||
const directory = "/projects/before-reconnect"
|
||||
@@ -149,12 +234,15 @@ test("refreshes a session moved during disconnection without losing the draft",
|
||||
await expect(prompt).toBeEditable()
|
||||
await prompt.fill("Draft before disconnect")
|
||||
const connection = await transport.waitForConnection()
|
||||
|
||||
const resolved = page.waitForResponse((response) => {
|
||||
const url = new URL(response.url())
|
||||
|
||||
return (
|
||||
url.pathname === "/api/location" && url.searchParams.get("location[directory]") === destination && response.ok()
|
||||
)
|
||||
})
|
||||
|
||||
session.directory = destination
|
||||
await transport.close()
|
||||
await transport.waitForConnection({ after: connection.id })
|
||||
@@ -186,9 +274,11 @@ test("ignores an old failed location read after reconnecting", async ({ page })
|
||||
await page.route("**/api/location?**", async (route) => {
|
||||
if (new URL(route.request().url()).searchParams.get("location[directory]") !== directory) return route.fallback()
|
||||
requests++
|
||||
|
||||
if (requests > 1) return route.fallback()
|
||||
requested.resolve()
|
||||
await release.promise
|
||||
|
||||
return route.fulfill({
|
||||
status: 500,
|
||||
body: "",
|
||||
@@ -202,12 +292,15 @@ test("ignores an old failed location read after reconnecting", async ({ page })
|
||||
await prompt.fill("Keep typing here")
|
||||
const connection = await transport.waitForConnection()
|
||||
const metadata = page.waitForResponse((response) => new URL(response.url()).pathname === `/api/session/${sessionID}`)
|
||||
|
||||
const resolved = page.waitForResponse((response) => {
|
||||
const url = new URL(response.url())
|
||||
|
||||
return (
|
||||
url.pathname === "/api/location" && url.searchParams.get("location[directory]") === directory && response.ok()
|
||||
)
|
||||
})
|
||||
|
||||
await transport.close()
|
||||
await transport.waitForConnection({ after: connection.id })
|
||||
await metadata
|
||||
@@ -227,7 +320,9 @@ function recoveryRequests(page: Page) {
|
||||
const requests: string[] = []
|
||||
page.on("request", (request) => {
|
||||
const path = new URL(request.url()).pathname
|
||||
|
||||
if (request.method() === "POST" && /^\/api\/(session\/[^/]+\/move|worktree)$/.test(path)) requests.push(path)
|
||||
})
|
||||
|
||||
return requests
|
||||
}
|
||||
@@ -1,10 +1,26 @@
|
||||
import { expect, test, type Page } from "@playwright/test"
|
||||
import type { OpenCodeEvent, SessionInboxInfo, SessionMessageInfo } from "@opencode/client/promise"
|
||||
import { PromptInput } from "@opencode/schema/prompt-input"
|
||||
import { SessionInbox } from "@opencode/schema/session-inbox"
|
||||
import { SessionMessage } from "@opencode/schema/session-message"
|
||||
import { Schema } from "effect"
|
||||
import { provider } from "../utils/app"
|
||||
import type { MockServerConfig } from "../utils/mock-server"
|
||||
import { openSession } from "../utils/workspace"
|
||||
|
||||
const sessionID = "ses_session_queue_regression"
|
||||
|
||||
// The session.prompt payload, as the server's endpoint validates it.
|
||||
const PromptBody = Schema.Struct({
|
||||
id: SessionMessage.ID.pipe(Schema.optional),
|
||||
...PromptInput.Prompt.fields,
|
||||
metadata: SessionInbox.UserPayload.fields.metadata,
|
||||
delivery: SessionInbox.Delivery.pipe(Schema.optional),
|
||||
resume: Schema.Boolean.pipe(Schema.optional),
|
||||
})
|
||||
|
||||
const decodePromptBody = Schema.decodeUnknownSync(PromptBody)
|
||||
|
||||
type InboxRow = {
|
||||
id: string
|
||||
sessionID: string
|
||||
@@ -12,11 +28,26 @@ type InboxRow = {
|
||||
type: "user"
|
||||
payload: {
|
||||
text: string
|
||||
metadata?: Record<string, unknown>
|
||||
metadata?: typeof PromptBody.Type.metadata
|
||||
files?: Extract<SessionInboxInfo, { type: "user" }>["payload"]["files"]
|
||||
agents?: Extract<SessionInboxInfo, { type: "user" }>["payload"]["agents"]
|
||||
}
|
||||
delivery: "steer" | "queue"
|
||||
delivery: SessionInbox.Delivery
|
||||
}
|
||||
|
||||
function storedFile(file: NonNullable<typeof PromptBody.Type.files>[number]) {
|
||||
const inline = file.uri.startsWith("data:")
|
||||
|
||||
const stored: NonNullable<InboxRow["payload"]["files"]>[number] = {
|
||||
data: inline ? file.uri.slice(file.uri.indexOf(",") + 1) : "",
|
||||
mime: inline ? file.uri.slice("data:".length, file.uri.indexOf(";")) : "text/plain",
|
||||
source: inline ? { type: "inline" } : { type: "uri", uri: file.uri },
|
||||
name: file.name,
|
||||
description: file.description,
|
||||
mention: file.mention,
|
||||
}
|
||||
|
||||
return stored
|
||||
}
|
||||
|
||||
function createQueueMock(seed: string[], messages: SessionMessageInfo[] = []) {
|
||||
@@ -28,16 +59,20 @@ function createQueueMock(seed: string[], messages: SessionMessageInfo[] = []) {
|
||||
payload: { text },
|
||||
delivery: "queue",
|
||||
}))
|
||||
|
||||
const events: OpenCodeEvent[] = []
|
||||
const prompts: Record<string, unknown>[] = []
|
||||
const prompts: (typeof PromptBody.Type)[] = []
|
||||
const compactions: SessionInboxInfo[] = []
|
||||
const changes: { inboxID: string; action: "cancel" | "steer" | "queue" }[] = []
|
||||
const log: string[] = []
|
||||
let sequence = 0
|
||||
|
||||
const emit = <Type extends OpenCodeEvent["type"]>(
|
||||
type: Type,
|
||||
data: Extract<OpenCodeEvent, { type: Type }>["data"],
|
||||
) => {
|
||||
sequence += 1
|
||||
// SAFETY: `type` selects `data` from the same OpenCodeEvent member, so the pair is that member.
|
||||
events.push({
|
||||
id: `evt_queue_${sequence}`,
|
||||
type,
|
||||
@@ -46,28 +81,37 @@ function createQueueMock(seed: string[], messages: SessionMessageInfo[] = []) {
|
||||
data,
|
||||
} as OpenCodeEvent)
|
||||
}
|
||||
|
||||
return {
|
||||
rows,
|
||||
compactions,
|
||||
prompts,
|
||||
changes,
|
||||
log,
|
||||
messages,
|
||||
emit,
|
||||
events: () => events.splice(0),
|
||||
onPrompt: (input: { sessionID: string; body: Record<string, unknown> }) => {
|
||||
prompts.push(input.body)
|
||||
log.push(`prompt:${String(input.body.delivery ?? "steer")}`)
|
||||
onPrompt: (input: Parameters<NonNullable<MockServerConfig["onPrompt"]>>[0]) => {
|
||||
const body = decodePromptBody(input.body)
|
||||
prompts.push(body)
|
||||
log.push(`prompt:${body.delivery ?? "steer"}`)
|
||||
|
||||
const row: InboxRow = {
|
||||
id: typeof input.body.id === "string" ? input.body.id : `inb_mock_${sequence}`,
|
||||
id: body.id ?? `inb_mock_${sequence}`,
|
||||
sessionID: input.sessionID,
|
||||
time: { created: Date.now() },
|
||||
type: "user",
|
||||
payload: {
|
||||
text: typeof input.body.text === "string" ? input.body.text : "",
|
||||
...(input.body.metadata === undefined ? {} : { metadata: input.body.metadata as Record<string, unknown> }),
|
||||
},
|
||||
delivery: input.body.delivery === "queue" ? "queue" : "steer",
|
||||
payload: { text: body.text },
|
||||
delivery: body.delivery ?? "steer",
|
||||
}
|
||||
|
||||
if (body.metadata !== undefined) row.payload.metadata = body.metadata
|
||||
|
||||
// Store attachments the way the server materializes them, so re-admissions round-trip.
|
||||
if (body.files !== undefined) row.payload.files = body.files.map(storedFile)
|
||||
|
||||
if (body.agents !== undefined)
|
||||
row.payload.agents = body.agents.map((agent) => ({ name: agent.name, mention: agent.mention }))
|
||||
rows.push(row)
|
||||
emit("session.inbox.enqueued", {
|
||||
sessionID: input.sessionID,
|
||||
@@ -75,17 +119,32 @@ function createQueueMock(seed: string[], messages: SessionMessageInfo[] = []) {
|
||||
item: { type: "user", payload: row.payload, delivery: row.delivery },
|
||||
})
|
||||
},
|
||||
onCompact: (input: Parameters<NonNullable<MockServerConfig["onCompact"]>>[0]) => {
|
||||
log.push("compact")
|
||||
compactions.push({
|
||||
id: Schema.decodeUnknownSync(SessionMessage.ID)(input.body.id),
|
||||
sessionID: input.sessionID,
|
||||
time: { created: Date.now() },
|
||||
type: "compaction",
|
||||
payload: {},
|
||||
delivery: "steer",
|
||||
})
|
||||
},
|
||||
onInboxChange: (input: { sessionID: string; inboxID: string; action: "cancel" | "steer" | "queue" }) => {
|
||||
changes.push({ inboxID: input.inboxID, action: input.action })
|
||||
log.push(`${input.action}:${input.inboxID}`)
|
||||
const index = rows.findIndex((row) => row.id === input.inboxID)
|
||||
const row = rows[index]
|
||||
|
||||
if (!row) return
|
||||
|
||||
if (input.action === "cancel") {
|
||||
rows.splice(index, 1)
|
||||
emit("session.inbox.cancelled", { sessionID: input.sessionID, inboxID: input.inboxID })
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
row.delivery = input.action
|
||||
emit("session.inbox.delivery.changed", {
|
||||
sessionID: input.sessionID,
|
||||
@@ -96,21 +155,38 @@ function createQueueMock(seed: string[], messages: SessionMessageInfo[] = []) {
|
||||
}
|
||||
}
|
||||
|
||||
async function openQueue(page: Page, mock: ReturnType<typeof createQueueMock>, followUpBehavior?: "queue" | "steer") {
|
||||
async function openQueue(
|
||||
page: Page,
|
||||
mock: ReturnType<typeof createQueueMock>,
|
||||
followUpBehavior?: "queue" | "steer",
|
||||
revert?: string,
|
||||
) {
|
||||
const model = { id: "queue-model", name: "Queue Model" }
|
||||
await openSession(page, {
|
||||
|
||||
const options: Parameters<typeof openSession>[1] = {
|
||||
name: "SessionQueueRegression",
|
||||
sessions: [{ id: sessionID, title: "Session queue regression", model: { id: model.id, providerID: "opencode" } }],
|
||||
sessions: [
|
||||
{
|
||||
id: sessionID,
|
||||
title: "Session queue regression",
|
||||
model: { id: model.id, providerID: "opencode" },
|
||||
revert: revert ? { messageID: revert } : undefined,
|
||||
},
|
||||
],
|
||||
provider: provider(model),
|
||||
pageMessages: () => ({ items: mock.messages }),
|
||||
sessionStatus: () => ({ [sessionID]: { type: "running" } }),
|
||||
inbox: () => mock.rows.map((row) => ({ ...row, payload: { ...row.payload } })),
|
||||
inbox: () => [...mock.rows.map((row) => ({ ...row, payload: { ...row.payload } })), ...mock.compactions],
|
||||
onPrompt: mock.onPrompt,
|
||||
onCompact: mock.onCompact,
|
||||
onInboxChange: mock.onInboxChange,
|
||||
events: mock.events,
|
||||
...(followUpBehavior ? { seed: { settings: { general: { followUpBehavior } } } } : {}),
|
||||
})
|
||||
}
|
||||
|
||||
if (followUpBehavior) options.seed = { settings: { general: { followUpBehavior } } }
|
||||
await openSession(page, options)
|
||||
const composer = page.locator('[data-component="composer"]')
|
||||
|
||||
return {
|
||||
composer,
|
||||
input: composer.locator('[data-component="composer-editor"]'),
|
||||
@@ -118,6 +194,20 @@ async function openQueue(page: Page, mock: ReturnType<typeof createQueueMock>, f
|
||||
}
|
||||
}
|
||||
|
||||
async function runSlash(page: Page, input: Awaited<ReturnType<typeof openQueue>>["input"], name: string) {
|
||||
await input.pressSequentially(`/${name}`)
|
||||
await expect(page.locator('[data-component="composer-suggestions"] [data-suggestion-id][data-active]')).toContainText(
|
||||
`/${name}`,
|
||||
)
|
||||
await input.press("Enter")
|
||||
}
|
||||
|
||||
function userRow(page: Page, messageID: string) {
|
||||
return page.locator(
|
||||
`[data-timeline-virtual-content] [data-timeline-row="UserMessage"][data-message-id="${messageID}"]`,
|
||||
)
|
||||
}
|
||||
|
||||
test("follow-up preference controls Enter while Mod+Enter uses the alternate delivery", async ({ page }) => {
|
||||
const mock = createQueueMock([])
|
||||
const view = await openQueue(page, mock, "queue")
|
||||
@@ -135,6 +225,9 @@ test("follow-up preference controls Enter while Mod+Enter uses the alternate del
|
||||
|
||||
test("dragging reorders queued prompts", async ({ page }) => {
|
||||
const mock = createQueueMock(["first queued prompt", "second queued prompt", "third queued prompt"])
|
||||
mock.rows[0].payload.files = [
|
||||
{ data: "aGk=", mime: "text/plain", source: { type: "uri", uri: "file:///repo/main.ts" }, name: "main.ts" },
|
||||
]
|
||||
const view = await openQueue(page, mock)
|
||||
await expect(view.rows).toHaveCount(3)
|
||||
|
||||
@@ -143,6 +236,7 @@ test("dragging reorders queued prompts", async ({ page }) => {
|
||||
await first.getByRole("button", { name: "Reorder queued prompt" }).hover()
|
||||
await page.mouse.down()
|
||||
const target = await third.boundingBox()
|
||||
|
||||
if (!target) throw new Error("The target queue row is not visible")
|
||||
await page.mouse.move(target.x + target.width / 2, target.y + target.height / 2, { steps: 10 })
|
||||
await page.mouse.up()
|
||||
@@ -157,6 +251,8 @@ test("dragging reorders queued prompts", async ({ page }) => {
|
||||
"third queued prompt",
|
||||
"first queued prompt",
|
||||
])
|
||||
// A re-admitted file keeps the URI it came from instead of becoming an inline snapshot.
|
||||
expect(mock.prompts[2].files).toMatchObject([{ uri: "file:///repo/main.ts", name: "main.ts" }])
|
||||
expect(mock.changes).toEqual([
|
||||
{ inboxID: "inb_seed_1", action: "cancel" },
|
||||
{ inboxID: "inb_seed_2", action: "cancel" },
|
||||
@@ -164,6 +260,56 @@ test("dragging reorders queued prompts", async ({ page }) => {
|
||||
])
|
||||
})
|
||||
|
||||
for (const change of ["reorder", "edit"] as const) {
|
||||
test(`a ${change} that cannot re-admit a prompt leaves the queue unchanged`, async ({ page }) => {
|
||||
const order = ["first queued prompt", "second queued prompt", "third queued prompt"]
|
||||
const mock = createQueueMock(order)
|
||||
const admit = mock.onPrompt
|
||||
// The last prompt's re-admission fails after the server admitted it, so its response is lost.
|
||||
mock.onPrompt = (input) => {
|
||||
admit(input)
|
||||
|
||||
if (input.body.text === order[2]) throw new Error("Connection lost after admission")
|
||||
}
|
||||
|
||||
const view = await openQueue(page, mock)
|
||||
await expect(view.rows).toHaveCount(3)
|
||||
|
||||
if (change === "reorder") {
|
||||
const first = view.rows.filter({ hasText: order[0] })
|
||||
const second = view.rows.filter({ hasText: order[1] })
|
||||
await first.getByRole("button", { name: "Reorder queued prompt" }).hover()
|
||||
await page.mouse.down()
|
||||
const target = await second.boundingBox()
|
||||
|
||||
if (!target) throw new Error("The target queue row is not visible")
|
||||
await page.mouse.move(target.x + target.width / 2, target.y + target.height / 2, { steps: 10 })
|
||||
await page.mouse.up()
|
||||
}
|
||||
|
||||
if (change === "edit") {
|
||||
await view.rows.getByText(order[1], { exact: true }).click()
|
||||
await expect(view.input).toHaveText(order[1])
|
||||
await expect(view.input).toBeFocused()
|
||||
await view.input.press("End")
|
||||
await view.input.pressSequentially(", edited")
|
||||
await view.input.press("Enter")
|
||||
}
|
||||
|
||||
// The replacements admitted before the failure are withdrawn; the originals stay in place.
|
||||
await expect.poll(() => mock.log.includes(`cancel:${mock.prompts.at(-1)?.id}`)).toBe(true)
|
||||
await expect.poll(() => mock.changes.length).toBe(mock.prompts.length)
|
||||
expect(mock.changes.every((entry) => entry.action === "cancel" && !entry.inboxID.startsWith("inb_seed_"))).toBe(
|
||||
true,
|
||||
)
|
||||
await expect(view.rows.locator('[data-action="session-queue-edit"]')).toHaveText(order)
|
||||
expect(mock.rows.map((row) => row.payload.text)).toEqual(order)
|
||||
|
||||
// A failed edit keeps its draft in the composer so nothing typed is lost.
|
||||
if (change === "edit") await expect(view.input).toHaveText("second queued prompt, edited")
|
||||
})
|
||||
}
|
||||
|
||||
test("editing restores the existing draft and replaces only the original queue position", async ({ page }) => {
|
||||
const mock = createQueueMock(["first queued prompt", "tighten the error copy", "third queued prompt"])
|
||||
const view = await openQueue(page, mock)
|
||||
@@ -180,7 +326,9 @@ test("editing restores the existing draft and replaces only the original queue p
|
||||
await original.click()
|
||||
await expect(view.input).toHaveText("tighten the error copy")
|
||||
await expect(view.input).toBeFocused()
|
||||
await view.input.fill("tighten the error copy and add a retry hint")
|
||||
// fill() on a composer that holds text can append, so extend the loaded text instead.
|
||||
await view.input.press("End")
|
||||
await view.input.pressSequentially(" and add a retry hint")
|
||||
await expect(view.input).toHaveText("tighten the error copy and add a retry hint")
|
||||
await view.input.press("Enter")
|
||||
|
||||
@@ -190,16 +338,123 @@ test("editing restores the existing draft and replaces only the original queue p
|
||||
"third queued prompt",
|
||||
])
|
||||
await expect(view.input).toHaveText("my in-progress draft")
|
||||
// One rewrite from the edited position: the replacement, then the prompts after it.
|
||||
expect(mock.prompts.map((prompt) => prompt.text)).toEqual([
|
||||
"tighten the error copy and add a retry hint",
|
||||
"tighten the error copy and add a retry hint",
|
||||
"third queued prompt",
|
||||
])
|
||||
expect(mock.prompts.every((prompt) => prompt.delivery === "queue" && prompt.resume === false)).toBe(true)
|
||||
expect(mock.changes.map((change) => change.action)).toEqual(["cancel", "cancel", "cancel"])
|
||||
expect(mock.changes).toEqual([
|
||||
{ inboxID: "inb_seed_2", action: "cancel" },
|
||||
{ inboxID: "inb_seed_3", action: "cancel" },
|
||||
])
|
||||
expect(mock.log[0]).toBe("prompt:queue")
|
||||
})
|
||||
|
||||
for (const delivery of ["queue", "steer"] as const) {
|
||||
test(`editing a prompt the server delivered meanwhile keeps the edit draft (${delivery})`, async ({ page }) => {
|
||||
const mock = createQueueMock(["first queued prompt", "second queued prompt"])
|
||||
const view = await openQueue(page, mock, "queue")
|
||||
await view.rows.getByText("second queued prompt", { exact: true }).click()
|
||||
await expect(view.input).toHaveText("second queued prompt")
|
||||
await expect(view.input).toBeFocused()
|
||||
await view.input.press("End")
|
||||
await view.input.pressSequentially(", edited")
|
||||
// The server delivers the original before the edit lands; this client has not heard yet.
|
||||
mock.rows.splice(1, 1)
|
||||
await view.input.press(delivery === "queue" ? "Enter" : "ControlOrMeta+Enter")
|
||||
|
||||
await expect(page.getByText("Request failed")).toBeVisible()
|
||||
await expect(view.input).toHaveText("second queued prompt, edited")
|
||||
expect(mock.prompts).toEqual([])
|
||||
expect(mock.changes).toEqual([])
|
||||
})
|
||||
}
|
||||
|
||||
for (const change of ["reorder", "edit"] as const) {
|
||||
test(`a staged revert refuses a queue ${change}, whose admission would commit it`, async ({ page }) => {
|
||||
const order = ["first queued prompt", "second queued prompt"]
|
||||
|
||||
const mock = createQueueMock(order, [
|
||||
{ id: "msg_queue_delivered", type: "user", text: "First prompt", time: { created: 1 } },
|
||||
])
|
||||
|
||||
const view = await openQueue(page, mock, undefined, "msg_queue_delivered")
|
||||
await expect(view.rows).toHaveCount(2)
|
||||
|
||||
if (change === "reorder") {
|
||||
await view.rows.filter({ hasText: order[0] }).getByRole("button", { name: "Reorder queued prompt" }).hover()
|
||||
await page.mouse.down()
|
||||
const target = await view.rows.filter({ hasText: order[1] }).boundingBox()
|
||||
|
||||
if (!target) throw new Error("The target queue row is not visible")
|
||||
await page.mouse.move(target.x + target.width / 2, target.y + target.height / 2, { steps: 10 })
|
||||
await page.mouse.up()
|
||||
}
|
||||
|
||||
if (change === "edit") await view.rows.getByText(order[1], { exact: true }).click()
|
||||
|
||||
await expect(page.getByText("Redo the revert before you reorder or edit queued prompts")).toBeVisible()
|
||||
await expect(view.rows.locator('[data-action="session-queue-edit"]')).toHaveText(order)
|
||||
await expect(view.input).toHaveText("")
|
||||
expect(mock.prompts).toEqual([])
|
||||
expect(mock.changes).toEqual([])
|
||||
})
|
||||
}
|
||||
|
||||
test("editing drops a file whose mention was deleted and keeps unmentioned context", async ({ page }) => {
|
||||
const mock = createQueueMock(["inspect @main.ts here"])
|
||||
mock.rows[0].payload.files = [
|
||||
{
|
||||
data: "aGk=",
|
||||
mime: "text/plain",
|
||||
source: { type: "uri", uri: "file:///repo/main.ts" },
|
||||
name: "main.ts",
|
||||
mention: { start: 8, end: 16, text: "@main.ts" },
|
||||
},
|
||||
{ data: "bm90ZXM=", mime: "text/plain", source: { type: "uri", uri: "file:///repo/notes.md" }, name: "notes.md" },
|
||||
]
|
||||
const view = await openQueue(page, mock)
|
||||
await view.rows.getByText("inspect @main.ts here", { exact: true }).click()
|
||||
await expect(view.input).toHaveText("inspect @main.ts here")
|
||||
// The edit focuses the composer a frame after loading it; keys sent earlier miss it.
|
||||
await expect(view.input).toBeFocused()
|
||||
await view.input.press("ControlOrMeta+a")
|
||||
await view.input.pressSequentially("inspect here")
|
||||
await view.input.press("Enter")
|
||||
|
||||
await expect.poll(() => mock.prompts.length).toBe(1)
|
||||
expect(mock.prompts[0].files?.map((file) => file.uri)).toEqual(["file:///repo/notes.md"])
|
||||
})
|
||||
|
||||
test("editing a comment-only prompt keeps its notes once", async ({ page }) => {
|
||||
const note = "The user made the following comment regarding line 2 of /repo/app.ts: check the guard"
|
||||
const mock = createQueueMock([note])
|
||||
mock.rows[0].payload.metadata = {
|
||||
displayText: "",
|
||||
comments: [
|
||||
{
|
||||
path: "/repo/app.ts",
|
||||
comment: "check the guard",
|
||||
selection: { startLine: 2, startChar: 0, endLine: 2, endChar: 0 },
|
||||
origin: "review",
|
||||
},
|
||||
],
|
||||
}
|
||||
const view = await openQueue(page, mock)
|
||||
// With no display text, the editor shows the note itself, so the edit owns it as text.
|
||||
await view.rows.getByText(note, { exact: true }).click()
|
||||
await expect(view.input).toHaveText(note)
|
||||
await view.input.press("End")
|
||||
await view.input.pressSequentially(", please")
|
||||
await expect(view.input).toHaveText(`${note}, please`)
|
||||
await view.input.press("Enter")
|
||||
|
||||
await expect.poll(() => mock.prompts.length).toBe(1)
|
||||
expect(mock.prompts[0].text).toBe(`${note}, please`)
|
||||
expect(mock.prompts[0].metadata).toMatchObject({ displayText: `${note}, please`, comments: [] })
|
||||
})
|
||||
|
||||
test("Undo cancels only the selected queued prompt and focuses the restored input", async ({ page }) => {
|
||||
await page.setViewportSize({ width: 390, height: 844 })
|
||||
const text = "Review the detailed error report and check every step of the retry path ".repeat(4)
|
||||
@@ -241,6 +496,47 @@ test("Undo appends to an existing draft and restores inline attachments", async
|
||||
expect(mock.changes).toEqual([{ inboxID: "inb_seed_1", action: "cancel" }])
|
||||
})
|
||||
|
||||
test("Undo of a comment-only prompt keeps the draft text and restores the comment once", async ({ page }) => {
|
||||
const comment = "check the guard"
|
||||
const mock = createQueueMock([`The user made the following comment regarding line 2 of /repo/app.ts: ${comment}`])
|
||||
mock.rows[0].payload.metadata = {
|
||||
displayText: "",
|
||||
comments: [
|
||||
{
|
||||
path: "/repo/app.ts",
|
||||
comment,
|
||||
selection: { startLine: 2, startChar: 0, endLine: 2, endChar: 0 },
|
||||
origin: "review",
|
||||
},
|
||||
],
|
||||
}
|
||||
mock.rows[0].payload.files = [
|
||||
{
|
||||
data: "aGk=",
|
||||
mime: "text/plain",
|
||||
source: { type: "uri", uri: "file:///repo/app.ts?start=2&end=2" },
|
||||
name: "app.ts",
|
||||
},
|
||||
]
|
||||
const view = await openQueue(page, mock)
|
||||
await view.input.fill("my draft")
|
||||
await view.rows.getByRole("button", { name: "Undo" }).click()
|
||||
|
||||
await expect(view.rows).toHaveCount(0)
|
||||
await expect(view.input).toHaveText("my draft")
|
||||
const cards = view.composer.locator('[data-component="composer-attachments"] [data-component="attachment-card"]')
|
||||
await expect(cards).toHaveCount(1)
|
||||
await expect(cards).toContainText(comment)
|
||||
|
||||
// The resubmission carries the draft text with no stray break and the comment's file exactly once.
|
||||
await view.input.press("Enter")
|
||||
await expect.poll(() => mock.prompts.length).toBe(1)
|
||||
expect(mock.prompts[0].text).toBe(
|
||||
`my draft\nThe user made the following comment regarding line 2 of /repo/app.ts: ${comment}`,
|
||||
)
|
||||
expect(mock.prompts[0].files?.map((file) => file.uri)).toEqual(["file:///repo/app.ts?start=2&end=2"])
|
||||
})
|
||||
|
||||
test("Undo preserves mentioned file and agent references on resubmission", async ({ page }) => {
|
||||
const mock = createQueueMock(["inspect @main.ts with @build"])
|
||||
mock.rows[0].payload.files = [
|
||||
@@ -258,23 +554,256 @@ test("Undo preserves mentioned file and agent references on resubmission", async
|
||||
await expect(view.input).toHaveText("inspect @main.ts with @build")
|
||||
await view.input.press("Enter")
|
||||
await expect.poll(() => mock.prompts.length).toBe(1)
|
||||
// Like the TUI, a restored mention points at the file it named rather than a snapshot of it.
|
||||
expect(mock.prompts[0].files).toMatchObject([
|
||||
{ uri: "data:text/plain;base64,aGk=", mention: { text: "@main.ts", start: 8, end: 16 } },
|
||||
{ uri: "file:///repo/main.ts", mention: { text: "@main.ts", start: 8, end: 16 } },
|
||||
])
|
||||
expect(mock.prompts[0].agents).toMatchObject([{ name: "build", mention: { text: "@build" } }])
|
||||
})
|
||||
|
||||
test("Undo does not discard hidden file context", async ({ page }) => {
|
||||
const mock = createQueueMock(["inspect this file"])
|
||||
mock.rows[0].payload.files = [
|
||||
{ data: "aGk=", mime: "text/plain", source: { type: "uri", uri: "file:///repo/main.ts" }, name: "main.ts" },
|
||||
for (const delivery of ["queue", "steer"] as const) {
|
||||
test(`${delivery === "queue" ? "Undo" : "/undo of a pending steer"} keeps unmentioned file context`, async ({
|
||||
page,
|
||||
}) => {
|
||||
// Another client (for example ACP) can attach a file without mentioning it.
|
||||
const mock = createQueueMock(["inspect this file"])
|
||||
const inboxID = mock.rows[0].id
|
||||
mock.rows[0].delivery = delivery
|
||||
mock.rows[0].payload.files = [
|
||||
{
|
||||
data: "aGk=",
|
||||
mime: "text/plain",
|
||||
source: { type: "uri", uri: "file:///repo/main.ts" },
|
||||
name: "main.ts",
|
||||
description: "the failing version",
|
||||
},
|
||||
]
|
||||
const view = await openQueue(page, mock)
|
||||
|
||||
if (delivery === "queue") await view.rows.getByRole("button", { name: "Undo" }).click()
|
||||
|
||||
if (delivery === "steer") {
|
||||
await expect(userRow(page, inboxID)).toContainText("inspect this file")
|
||||
await runSlash(page, view.input, "undo")
|
||||
}
|
||||
|
||||
// Like the TUI, the file returns attached without a mention, as a chip rather than prompt text.
|
||||
const chip = view.composer.locator('[data-slot="composer-context-file"]')
|
||||
|
||||
await expect(view.input).toHaveText("inspect this file")
|
||||
await expect(chip).toHaveCount(1)
|
||||
await expect(chip).toContainText("main.ts")
|
||||
expect(mock.changes).toEqual([{ inboxID, action: "cancel" }])
|
||||
|
||||
// Like the TUI's blank Enter, a chip alone sends nothing: without text the draft reads as blank.
|
||||
await expect(view.input).toBeFocused()
|
||||
await view.input.press("ControlOrMeta+a")
|
||||
await view.input.press("Backspace")
|
||||
await expect(view.input).toHaveText("")
|
||||
await expect(view.composer.locator('[data-action="composer-submit"]')).toHaveAttribute("aria-label", "Stop")
|
||||
await expect(chip).toHaveCount(1)
|
||||
|
||||
await view.input.pressSequentially("inspect this file")
|
||||
await view.input.press("Enter")
|
||||
await expect.poll(() => mock.prompts.length).toBe(1)
|
||||
expect(mock.prompts[0].files).toMatchObject([
|
||||
{ uri: "file:///repo/main.ts", name: "main.ts", description: "the failing version" },
|
||||
])
|
||||
expect(mock.prompts[0].files?.[0]?.mention).toBeUndefined()
|
||||
await expect(chip).toHaveCount(0)
|
||||
})
|
||||
}
|
||||
|
||||
test("reverting to another prompt replaces the file chips of an earlier restore", async ({ page }) => {
|
||||
const mock = createQueueMock(
|
||||
[],
|
||||
[
|
||||
{ id: "msg_queue_first", type: "user", text: "First prompt", time: { created: 1 } },
|
||||
{
|
||||
id: "msg_queue_second",
|
||||
type: "user",
|
||||
text: "Second prompt",
|
||||
files: [
|
||||
{ data: "aGk=", mime: "text/plain", source: { type: "uri", uri: "file:///repo/main.ts" }, name: "main.ts" },
|
||||
],
|
||||
time: { created: 2 },
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
const view = await openQueue(page, mock)
|
||||
const chip = view.composer.locator('[data-slot="composer-context-file"]')
|
||||
|
||||
for (const [id, text, chips] of [
|
||||
["msg_queue_second", "Second prompt", 1],
|
||||
["msg_queue_first", "First prompt", 0],
|
||||
] as const) {
|
||||
const row = userRow(page, id)
|
||||
|
||||
await row.hover()
|
||||
await row.getByRole("button", { name: "Revert message" }).click()
|
||||
await expect(view.input).toHaveText(text)
|
||||
await expect(chip).toHaveCount(chips)
|
||||
}
|
||||
})
|
||||
|
||||
test("/undo withdraws a pending steer without interrupting the running session", async ({ page }) => {
|
||||
const mock = createQueueMock([])
|
||||
const view = await openQueue(page, mock, "steer")
|
||||
const stops: string[] = []
|
||||
page.on("request", (request) => {
|
||||
const path = new URL(request.url()).pathname
|
||||
|
||||
if (path.endsWith("/interrupt") || path.endsWith("/wait") || path.endsWith("/revert/stage")) stops.push(path)
|
||||
})
|
||||
const text = "U2: Also check the retry path."
|
||||
await view.input.fill(text)
|
||||
await view.input.press("Enter")
|
||||
await expect.poll(() => mock.rows.map((row) => row.delivery)).toEqual(["steer"])
|
||||
await expect(view.input).toHaveText("")
|
||||
|
||||
const inboxID = mock.rows[0].id
|
||||
const pending = userRow(page, inboxID)
|
||||
|
||||
await expect(pending).toContainText(text)
|
||||
await runSlash(page, view.input, "undo")
|
||||
|
||||
await expect(pending).toHaveCount(0)
|
||||
await expect(view.input).toHaveText(text)
|
||||
expect(mock.changes).toEqual([{ inboxID, action: "cancel" }])
|
||||
expect(stops).toEqual([])
|
||||
})
|
||||
|
||||
test("reverting a delivered prompt leaves queued prompts alone, as in the TUI", async ({ page }) => {
|
||||
const mock = createQueueMock(
|
||||
["U2: queued follow-up"],
|
||||
[{ id: "msg_queue_delivered", type: "user", text: "First prompt", time: { created: 1 } }],
|
||||
)
|
||||
|
||||
const view = await openQueue(page, mock)
|
||||
const delivered = userRow(page, "msg_queue_delivered")
|
||||
|
||||
const staged = page.waitForResponse(
|
||||
(response) => new URL(response.url()).pathname === `/api/session/${sessionID}/revert/stage`,
|
||||
)
|
||||
|
||||
await delivered.hover()
|
||||
await delivered.getByRole("button", { name: "Revert message" }).click()
|
||||
expect((await staged).ok()).toBe(true)
|
||||
|
||||
await expect(view.input).toHaveText("First prompt")
|
||||
expect(mock.changes).toEqual([])
|
||||
await expect(view.rows).toHaveCount(1)
|
||||
})
|
||||
|
||||
test("a shell command cannot wait in the queue, as in the TUI", async ({ page }) => {
|
||||
const mock = createQueueMock([])
|
||||
const view = await openQueue(page, mock, "queue")
|
||||
const shells: string[] = []
|
||||
page.on("request", (request) => {
|
||||
if (new URL(request.url()).pathname.endsWith(`/${sessionID}/shell`)) shells.push(request.url())
|
||||
})
|
||||
|
||||
await view.input.pressSequentially("!")
|
||||
await view.input.pressSequentially("git status")
|
||||
await view.input.press("Enter")
|
||||
|
||||
await expect(page.getByText("This prompt cannot be queued")).toBeVisible()
|
||||
await expect(view.input).toHaveText("git status")
|
||||
expect(shells).toEqual([])
|
||||
expect(mock.prompts).toEqual([])
|
||||
})
|
||||
|
||||
test("/compact runs with the composer model and shows a queued compaction, as in the TUI", async ({ page }) => {
|
||||
const mock = createQueueMock(
|
||||
[],
|
||||
[{ id: "msg_queue_delivered", type: "user", text: "First prompt", time: { created: 1 } }],
|
||||
)
|
||||
|
||||
const view = await openQueue(page, mock)
|
||||
const models: unknown[] = []
|
||||
page.on("request", (request) => {
|
||||
if (new URL(request.url()).pathname !== `/api/session/${sessionID}/model`) return
|
||||
mock.log.push("model")
|
||||
models.push(request.postDataJSON())
|
||||
})
|
||||
|
||||
await expect(userRow(page, "msg_queue_delivered")).toContainText("First prompt")
|
||||
await runSlash(page, view.input, "compact")
|
||||
|
||||
await expect(page.locator('[data-timeline-row="CompactionQueued"]')).toHaveText("Session compaction queued")
|
||||
// The row is optimistic; the model switch still precedes the admission.
|
||||
await expect.poll(() => mock.log).toEqual(["model", "compact"])
|
||||
expect(models).toMatchObject([{ model: { id: "queue-model", providerID: "opencode" } }])
|
||||
})
|
||||
|
||||
for (const action of ["Move to queue", "Delete"] as const) {
|
||||
test(`${action} on a pending steer replaces Revert, as in the TUI`, async ({ page }) => {
|
||||
const mock = createQueueMock(["U2: Also check the retry path."])
|
||||
const inboxID = mock.rows[0].id
|
||||
mock.rows[0].delivery = "steer"
|
||||
const view = await openQueue(page, mock)
|
||||
const pending = userRow(page, inboxID)
|
||||
|
||||
await expect(pending).toContainText("U2: Also check the retry path.")
|
||||
await pending.hover()
|
||||
await expect(pending.getByRole("button", { name: "Revert message" })).toHaveCount(0)
|
||||
await pending.getByRole("button", { name: action }).click()
|
||||
|
||||
await expect(pending).toHaveCount(0)
|
||||
expect(mock.changes).toEqual([{ inboxID, action: action === "Delete" ? "cancel" : "queue" }])
|
||||
// Neither action returns the prompt to the composer; a moved steer waits in the queue.
|
||||
await expect(view.input).toHaveText("")
|
||||
await expect(view.rows).toHaveCount(action === "Delete" ? 0 : 1)
|
||||
|
||||
if (action === "Move to queue") await expect(view.rows).toContainText("U2: Also check the retry path.")
|
||||
})
|
||||
}
|
||||
|
||||
test("/undo returns a pending steer's review comment to the composer", async ({ page }) => {
|
||||
const display = "tighten this"
|
||||
const comment = "check the guard"
|
||||
|
||||
const mock = createQueueMock([
|
||||
`${display}\nThe user made the following comment regarding line 2 of /repo/app.ts: ${comment}`,
|
||||
])
|
||||
|
||||
const row = mock.rows[0]
|
||||
row.delivery = "steer"
|
||||
row.payload.metadata = {
|
||||
displayText: display,
|
||||
comments: [
|
||||
{
|
||||
path: "/repo/app.ts",
|
||||
comment,
|
||||
selection: { startLine: 2, startChar: 0, endLine: 2, endChar: 0 },
|
||||
origin: "review",
|
||||
},
|
||||
],
|
||||
}
|
||||
// The comment's context file, which a resubmission regenerates from the restored comment.
|
||||
row.payload.files = [
|
||||
{
|
||||
data: "aGk=",
|
||||
mime: "text/plain",
|
||||
source: { type: "uri", uri: "file:///repo/app.ts?start=2&end=2" },
|
||||
name: "app.ts",
|
||||
},
|
||||
]
|
||||
const view = await openQueue(page, mock)
|
||||
await view.rows.getByRole("button", { name: "Undo" }).click()
|
||||
await expect(page.getByText("Edit this prompt in the queue to preserve its file context")).toBeVisible()
|
||||
await expect(view.rows).toHaveCount(1)
|
||||
await expect(view.input).toHaveText("")
|
||||
expect(mock.changes).toEqual([])
|
||||
|
||||
const pending = userRow(page, row.id)
|
||||
|
||||
await expect(pending).toContainText(display)
|
||||
await runSlash(page, view.input, "undo")
|
||||
|
||||
await expect(pending).toHaveCount(0)
|
||||
await expect(view.input).toHaveText(display)
|
||||
// The comment card is the only card: its context file is regenerated from it, not restored twice.
|
||||
const cards = view.composer.locator('[data-component="composer-attachments"] [data-component="attachment-card"]')
|
||||
await expect(cards).toHaveCount(1)
|
||||
await expect(cards).toContainText(comment)
|
||||
expect(mock.changes).toEqual([{ inboxID: row.id, action: "cancel" }])
|
||||
})
|
||||
|
||||
for (const delivery of ["steer", "queue"] as const) {
|
||||
@@ -285,6 +814,7 @@ for (const delivery of ["steer", "queue"] as const) {
|
||||
const userID = "msg_queue_initial_user"
|
||||
const assistantID = "msg_queue_continued_assistant"
|
||||
const followUp = "U2: Also check the retry path."
|
||||
|
||||
const mock = createQueueMock(
|
||||
[],
|
||||
[
|
||||
@@ -300,6 +830,7 @@ for (const delivery of ["steer", "queue"] as const) {
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
const view = await openQueue(page, mock, delivery)
|
||||
const transcript = page.locator("[data-timeline-virtual-content]")
|
||||
const thinking = transcript.locator('[data-timeline-row="Thinking"]')
|
||||
@@ -313,6 +844,7 @@ for (const delivery of ["steer", "queue"] as const) {
|
||||
|
||||
const inboxID = mock.rows[0].id
|
||||
const pending = transcript.locator(`[data-timeline-row="UserMessage"][data-message-id="${inboxID}"]`)
|
||||
|
||||
if (delivery === "queue") {
|
||||
const queued = view.rows.filter({ hasText: followUp })
|
||||
await expect(queued).toBeVisible()
|
||||
@@ -322,6 +854,7 @@ for (const delivery of ["steer", "queue"] as const) {
|
||||
await queued.getByRole("button", { name: "Steer", exact: true }).click()
|
||||
await expect.poll(() => mock.changes).toEqual([{ inboxID, action: "steer" }])
|
||||
}
|
||||
|
||||
await expect(view.rows).toHaveCount(0)
|
||||
await expect(pending).toContainText(followUp)
|
||||
await expect(thinking).toHaveCount(0)
|
||||
@@ -334,6 +867,7 @@ for (const delivery of ["steer", "queue"] as const) {
|
||||
model,
|
||||
started: Date.now(),
|
||||
})
|
||||
|
||||
for (const tool of [
|
||||
{ id: "tool_queue_read", name: "read", input: { path: "src/queue.ts" } },
|
||||
{ id: "tool_queue_grep", name: "grep", input: { pattern: "retry", path: "src" } },
|
||||
@@ -348,6 +882,7 @@ for (const delivery of ["steer", "queue"] as const) {
|
||||
executed: true,
|
||||
})
|
||||
}
|
||||
|
||||
mock.emit("session.step.ended", {
|
||||
sessionID,
|
||||
assistantMessageID: assistantID,
|
||||
@@ -377,6 +912,7 @@ for (const delivery of ["steer", "queue"] as const) {
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const boxes = await Promise.all([tools.boundingBox(), pending.boundingBox()])
|
||||
|
||||
return boxes.every((box) => box !== null) && boxes[0]!.y + boxes[0]!.height <= boxes[1]!.y
|
||||
})
|
||||
.toBe(true)
|
||||
@@ -395,9 +931,11 @@ for (const delivery of ["steer", "queue"] as const) {
|
||||
mock.emit("session.step.started", { ...later, agent: "build", model, started: Date.now() })
|
||||
mock.emit("session.text.started", { ...later, ordinal: 0 })
|
||||
mock.emit("session.text.ended", { ...later, ordinal: 0, text: "A3: Now checking the retry path for U2." })
|
||||
|
||||
const response = transcript
|
||||
.locator('[data-timeline-row="AssistantPart"]')
|
||||
.filter({ hasText: "A3: Now checking the retry path for U2." })
|
||||
|
||||
await expect(response).toHaveAttribute("data-message-id", inboxID)
|
||||
await expect(thinking).toHaveCount(0)
|
||||
await expect(tools.or(pending).or(response)).toHaveText([
|
||||
|
||||
@@ -134,6 +134,27 @@ test("a session in a worktree subfolder names its worktree and lists cached work
|
||||
list.release()
|
||||
})
|
||||
|
||||
test("a subagent shows its details but cannot move to another worktree", async ({ page }) => {
|
||||
const root = "C:/OpenCode/SmokeWorktrees"
|
||||
|
||||
await mockStressTimeline(page, {
|
||||
sessions: fixture.sessions.map((item) =>
|
||||
item.id === fixture.targetID
|
||||
? { ...item, parentID: fixture.sourceID, directory: `${root}/feature` }
|
||||
: { ...item },
|
||||
),
|
||||
worktrees: [{ directory: fixture.directory }, { directory: `${root}/feature`, strategy: "git" }],
|
||||
})
|
||||
await page.goto(sessionHref(fixture.targetID))
|
||||
|
||||
const summary = page.getByRole("dialog", { name: "Session details", exact: true })
|
||||
|
||||
await page.getByRole("button", { name: "Session details", exact: true }).click()
|
||||
await expect(summary.getByText("feature", { exact: true })).toBeVisible()
|
||||
await expect(summary.getByRole("button", { name: "feature", exact: true })).toHaveCount(0)
|
||||
await expect(summary.getByRole("button", { name: "Extensions", exact: true })).toBeVisible()
|
||||
})
|
||||
|
||||
test("the details follow the routed session when it moves to another worktree", async ({ page }) => {
|
||||
const root = "C:/OpenCode/SmokeWorktrees"
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { expect, test, type Page } from "@playwright/test"
|
||||
import type { SessionMessageInfo } from "@opencode/client/promise"
|
||||
import type { OpenCodeEvent, SessionMessageInfo } from "@opencode/client/promise"
|
||||
import {
|
||||
NO_PROVIDER,
|
||||
REMOTE_SERVER,
|
||||
@@ -240,7 +240,111 @@ test("a remote tab stays busy while a child session runs", async ({ page }) => {
|
||||
await expect(tabA.locator('[data-component="session-progress-indicator-v2"]')).toHaveCount(0)
|
||||
})
|
||||
|
||||
test("inactive tabs load attention, but read transcript and inbox only on selection", async ({ page }) => {
|
||||
test("inactive tabs stay busy while work waits in their inbox, and pulse on a new prompt, as in the TUI", async ({
|
||||
page,
|
||||
}) => {
|
||||
const workspace = await openSession(page, {
|
||||
name: "TabInbox",
|
||||
sessions: [a, b, c],
|
||||
inbox: [
|
||||
{
|
||||
id: "inb_tab_b",
|
||||
sessionID: b.id,
|
||||
time: { created: 1 },
|
||||
type: "user",
|
||||
payload: { text: "Queued follow-up" },
|
||||
delivery: "queue",
|
||||
},
|
||||
// Parked synthetic context, such as a user shell's output, waits without work.
|
||||
{
|
||||
id: "inb_tab_c",
|
||||
sessionID: c.id,
|
||||
time: { created: 1 },
|
||||
type: "synthetic",
|
||||
payload: { text: "Shell output", description: "Shell finished" },
|
||||
delivery: "steer",
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const tab = (id: string) => page.locator(`[data-titlebar-tab-slot]:has(a[href="${sessionHref(id)}"])`)
|
||||
const progress = '[data-component="session-progress-indicator-v2"]'
|
||||
|
||||
await expect(tab(b.id).locator(progress)).toBeVisible()
|
||||
await expect(tab(c.id).locator("[data-titlebar-tab-title]")).toHaveText(c.title)
|
||||
await expect(tab(c.id).locator(progress)).toHaveCount(0)
|
||||
await expect(tab(c.id).locator('[data-slot="tab-prompt-pulse"]')).toHaveCount(0)
|
||||
// The pulse lasts one animation, so record that it appeared rather than racing its removal.
|
||||
await tab(c.id).evaluate((element) => {
|
||||
const observer = new MutationObserver(() => {
|
||||
if (!element.querySelector('[data-slot="tab-prompt-pulse"]')) return
|
||||
element.setAttribute("data-test-pulsed", "")
|
||||
observer.disconnect()
|
||||
})
|
||||
|
||||
observer.observe(element, { childList: true, subtree: true })
|
||||
})
|
||||
|
||||
await workspace.push([
|
||||
{
|
||||
id: "evt_tab_c_prompt",
|
||||
created: 2,
|
||||
type: "session.inbox.enqueued",
|
||||
durable: { aggregateID: c.id, seq: 1, version: 1 },
|
||||
data: {
|
||||
sessionID: c.id,
|
||||
inboxID: "inb_tab_c_prompt",
|
||||
item: { type: "user", payload: { text: "Another client's prompt" }, delivery: "queue" },
|
||||
},
|
||||
} satisfies Extract<OpenCodeEvent, { type: "session.inbox.enqueued" }>,
|
||||
])
|
||||
|
||||
await expect(tab(c.id)).toHaveAttribute("data-test-pulsed", "")
|
||||
await expect(tab(c.id).locator('[data-slot="tab-prompt-pulse"]')).toHaveCount(0)
|
||||
await expect(tab(c.id).locator(progress)).toBeVisible()
|
||||
await expect(tab(a.id).locator('[data-slot="tab-prompt-pulse"]')).toHaveCount(0)
|
||||
})
|
||||
|
||||
test("selecting a tab with waiting work waits for its transcript instead of showing only the inbox", async ({
|
||||
page,
|
||||
}) => {
|
||||
await openSession(page, {
|
||||
name: "TabInboxTranscript",
|
||||
sessions: [a, b],
|
||||
pageMessages: (id) => ({
|
||||
items:
|
||||
id === b.id ? [{ id: "msg_tab_b_history", type: "user", text: "Earlier prompt", time: { created: 1 } }] : [],
|
||||
}),
|
||||
inbox: [
|
||||
{
|
||||
id: "inb_tab_b_steer",
|
||||
sessionID: b.id,
|
||||
time: { created: 2 },
|
||||
type: "user",
|
||||
payload: { text: "Pending steer" },
|
||||
delivery: "steer",
|
||||
},
|
||||
],
|
||||
})
|
||||
const tabB = page.locator(`[data-titlebar-tab-slot]:has(a[href="${sessionHref(b.id)}"])`)
|
||||
// The inactive tab read its inbox, which materializes the pending steer as a transcript row.
|
||||
await expect(tabB.locator('[data-component="session-progress-indicator-v2"]')).toBeVisible()
|
||||
|
||||
const transcript = await holdRoute(page, (url) => url.pathname === `/api/session/${b.id}/message`)
|
||||
await page.locator(`[data-titlebar-tab-link][href="${sessionHref(b.id)}"]`).click()
|
||||
await transcript.arrived
|
||||
await expect(page.locator("[data-timeline-virtual-content]")).toHaveCount(0)
|
||||
|
||||
transcript.release()
|
||||
await expect(page.locator('[data-timeline-row="UserMessage"][data-message-id="msg_tab_b_history"]')).toContainText(
|
||||
"Earlier prompt",
|
||||
)
|
||||
await expect(page.locator('[data-timeline-row="UserMessage"][data-message-id="inb_tab_b_steer"]')).toContainText(
|
||||
"Pending steer",
|
||||
)
|
||||
})
|
||||
|
||||
test("inactive tabs load attention and inbox, but read the transcript only on selection", async ({ page }) => {
|
||||
const reads: string[] = []
|
||||
const mutations: string[] = []
|
||||
const errors: string[] = []
|
||||
@@ -282,7 +386,7 @@ test("inactive tabs load attention, but read transcript and inbox only on select
|
||||
|
||||
const attention = Promise.all(
|
||||
[fixture.targetID, fixture.childID].flatMap((id) =>
|
||||
["permission", "form"].map((kind) =>
|
||||
["permission", "form", "inbox"].map((kind) =>
|
||||
page.waitForResponse((response) => new URL(response.url()).pathname === `/api/session/${id}/${kind}`),
|
||||
),
|
||||
),
|
||||
@@ -307,16 +411,14 @@ test("inactive tabs load attention, but read transcript and inbox only on select
|
||||
state.text,
|
||||
)
|
||||
|
||||
for (const id of [fixture.sourceID, fixture.targetID]) {
|
||||
expect(reads.filter((path) => path === `/api/session/${id}/message`)).toHaveLength(1)
|
||||
// Every tab reads its inbox once, since waiting work keeps a tab busy; selection reuses that read.
|
||||
for (const id of [fixture.sourceID, fixture.targetID, fixture.childID])
|
||||
expect(reads.filter((path) => path === `/api/session/${id}/inbox`)).toHaveLength(1)
|
||||
}
|
||||
|
||||
expect(
|
||||
reads.filter(
|
||||
(path) => path === `/api/session/${fixture.childID}/message` || path === `/api/session/${fixture.childID}/inbox`,
|
||||
),
|
||||
).toEqual([])
|
||||
for (const id of [fixture.sourceID, fixture.targetID])
|
||||
expect(reads.filter((path) => path === `/api/session/${id}/message`)).toHaveLength(1)
|
||||
|
||||
expect(reads.filter((path) => path === `/api/session/${fixture.childID}/message`)).toEqual([])
|
||||
expect(mutations).toEqual([])
|
||||
expect(errors).toEqual([])
|
||||
})
|
||||
|
||||
@@ -48,7 +48,9 @@ test("routes typing to the composer unless the open terminal is focused", async
|
||||
await expect.poll(() => pty.sockets.length).toBe(1)
|
||||
pty.send("ready")
|
||||
await expect(terminal).toHaveCSS("opacity", "1")
|
||||
await page.evaluate(() => (document.activeElement as HTMLElement | null)?.blur())
|
||||
await page.evaluate(() => {
|
||||
if (document.activeElement instanceof HTMLElement) document.activeElement.blur()
|
||||
})
|
||||
await page.keyboard.type("a")
|
||||
await expect(editor).toBeFocused()
|
||||
await expect(editor).toHaveText("a")
|
||||
@@ -64,6 +66,7 @@ for (const mount of ["cached", "explicit"] as const) {
|
||||
await route.continue()
|
||||
})
|
||||
const cached = { id: "pty_cached", title: "Terminal 1" }
|
||||
|
||||
const { pty, sessions } = await mockWorkspace(page, {
|
||||
...workspace,
|
||||
pty: mount === "cached" ? { initial: [cached] } : {},
|
||||
@@ -76,14 +79,17 @@ for (const mount of ["cached", "explicit"] as const) {
|
||||
}
|
||||
: undefined,
|
||||
})
|
||||
|
||||
await page.goto(sessionHref(sessions[0]!.id), { waitUntil: "commit" })
|
||||
await expectSessionTitle(page, "TerminalInput")
|
||||
const editor = page.locator('[data-component="composer-editor"]')
|
||||
const terminal = page.locator('[data-component="terminal"]')
|
||||
|
||||
if (mount === "explicit") {
|
||||
await expect(editor).toBeEditable()
|
||||
await page.keyboard.press("Control+Backquote")
|
||||
}
|
||||
|
||||
await expect(terminal).toBeVisible()
|
||||
await ghostty.promise
|
||||
await editor.click()
|
||||
@@ -95,6 +101,7 @@ for (const mount of ["cached", "explicit"] as const) {
|
||||
pty.send("ready")
|
||||
await expect(terminal).toHaveCSS("opacity", "1")
|
||||
await expect(editor).toBeFocused()
|
||||
|
||||
if (mount === "cached") expect(pty.created).toEqual([])
|
||||
})
|
||||
}
|
||||
@@ -113,8 +120,40 @@ test("focuses a terminal created from the new-terminal button", async ({ page })
|
||||
await expect.poll(() => active.evaluate((element) => element.contains(document.activeElement))).toBe(true)
|
||||
})
|
||||
|
||||
test("closing the last restored terminal does not create a hidden replacement", async ({ page }) => {
|
||||
const cached = { id: "pty_cached", title: "Terminal 1" }
|
||||
|
||||
const { pty } = await openSession(page, {
|
||||
...workspace,
|
||||
pty: { initial: [cached] },
|
||||
seed: {
|
||||
panes: { ses_terminalinput: { terminal: true, terminalHeight: 320 } },
|
||||
storage: { [legacyTerminalKey()]: { active: cached.id, all: [{ ...cached, titleNumber: 1 }] } },
|
||||
},
|
||||
})
|
||||
|
||||
const terminal = page.locator('[data-component="terminal"]')
|
||||
await expect(terminal.locator("textarea")).toHaveCount(1)
|
||||
await expect.poll(() => pty.sockets.length).toBe(1)
|
||||
expect(pty.created).toEqual([])
|
||||
|
||||
await page.locator("#terminal-panel").getByRole("button", { name: "Close terminal", exact: true }).click()
|
||||
await expect(terminal).toBeHidden()
|
||||
await expect.poll(() => pty.removed).toEqual([cached.id])
|
||||
expect(pty.created).toEqual([])
|
||||
|
||||
await page.keyboard.press("Control+Backquote")
|
||||
await expect(terminal.locator("textarea")).toHaveCount(1)
|
||||
await expect.poll(() => pty.created.map((item) => item.id)).toEqual(["pty_1"])
|
||||
await expect(page.locator("#terminal-panel").getByRole("tab", { name: "Terminal 1", exact: true })).toHaveAttribute(
|
||||
"aria-selected",
|
||||
"true",
|
||||
)
|
||||
})
|
||||
|
||||
function legacyTerminalKey() {
|
||||
const dir = base64Encode("C:/OpenCode/TerminalInput")
|
||||
const head = dir.slice(0, 12).replace(/[^a-zA-Z0-9._-]/g, "-")
|
||||
|
||||
return `opencode.workspace.${head}.${checksum(dir) ?? "0"}.dat:workspace:terminal`
|
||||
}
|
||||
@@ -28,7 +28,9 @@ import {
|
||||
} from "../utils/timeline"
|
||||
|
||||
const detailed = timelinePresets[2].value
|
||||
|
||||
const user = { id: "msg_user", type: "user", text: "Run it", time: { created: 1 } } satisfies SessionMessageInfo
|
||||
|
||||
const completed = {
|
||||
id: "msg_assistant",
|
||||
type: "assistant",
|
||||
@@ -89,28 +91,34 @@ test.describe("static projection", () => {
|
||||
],
|
||||
{ summary: { diffs: Array.from({ length: 11 }, (_, index) => summaryDiff(index)) } },
|
||||
)
|
||||
|
||||
const aborted = assistantMessage([{ id: "prt_before_abort", type: "text", text: "Before interruption" }], {
|
||||
id: "msg_1001_assistant_aborted",
|
||||
error: { type: "MessageAbortedError", message: "Stopped" },
|
||||
})
|
||||
|
||||
const failed = assistantMessage([{ id: "prt_after_abort", type: "text", text: "After interruption" }], {
|
||||
id: "msg_1002_assistant_failed",
|
||||
error: { type: "APIError", message: "Visible provider failure" },
|
||||
created: 1700000003000,
|
||||
})
|
||||
|
||||
const nextUser = userMessage([userText("Second turn", { id: "prt_second_user" })], {
|
||||
id: "msg_2000_second_user",
|
||||
created: 1700000005000,
|
||||
})
|
||||
|
||||
const nextAssistant = assistantMessage([{ id: "prt_second_text", type: "text", text: "Second response" }], {
|
||||
id: "msg_2001_second_assistant",
|
||||
parentID: "msg_2000_second_user",
|
||||
created: 1700000006000,
|
||||
})
|
||||
|
||||
const timeline = await setupTimeline(page, {
|
||||
settings: { timelineDetail: detailed },
|
||||
messages: [firstUser, aborted, failed, nextUser, nextAssistant],
|
||||
})
|
||||
|
||||
await timeline.send(status("idle"))
|
||||
const scroller = page.locator(".scroll-view__viewport", { has: page.locator("[data-timeline-row]") })
|
||||
await scroller.evaluate((element) => (element.scrollTop = 0))
|
||||
@@ -140,6 +148,7 @@ test.describe("static projection", () => {
|
||||
url: "data:text/plain;base64,bm90ZXM=",
|
||||
},
|
||||
])
|
||||
|
||||
message.metadata = {
|
||||
displayText: "what's goin on here",
|
||||
comments: [
|
||||
@@ -198,6 +207,7 @@ test.describe("static projection", () => {
|
||||
toolPart("prt_skill_break", "read", "completed", { path: "src/a.ts" }),
|
||||
toolPart("prt_skill_last", "skill", "completed", { id: "opencode" }),
|
||||
]
|
||||
|
||||
await setupTimeline(page, {
|
||||
settings: { timelineDetail: detailed },
|
||||
messages: [userMessage(), assistantMessage(parts)],
|
||||
@@ -217,6 +227,7 @@ test.describe("static projection", () => {
|
||||
toolPart("prt_collapsed_patch", "patch", "completed", { patchText: "Update src/value.ts" }),
|
||||
toolPart("prt_collapsed_read", "read", "completed", { path: "src/value.ts" }),
|
||||
]
|
||||
|
||||
await setupTimeline(page, {
|
||||
messages: [userMessage(), assistantMessage(parts)],
|
||||
settings: { shellToolPartsExpanded: true },
|
||||
@@ -235,6 +246,7 @@ test.describe("static projection", () => {
|
||||
|
||||
test("combines adjacent patch calls and repeated files into one group", async ({ page }) => {
|
||||
const [first, second] = ["prt_patch_first", "prt_patch_second"]
|
||||
|
||||
const timeline = await setupTimeline(page, {
|
||||
settings: { timelineDetail: { ...detailed, edit: { placement: "separate", details: "collapsed" } } },
|
||||
messages: [
|
||||
@@ -258,6 +270,7 @@ test("combines adjacent patch calls and repeated files into one group", async ({
|
||||
await expect(initialFile.getByRole("button")).toHaveAttribute("aria-expanded", "true")
|
||||
await initial.evaluate((element) => {
|
||||
const row = element.closest<HTMLElement>("[data-timeline-key]")
|
||||
|
||||
if (row) row.dataset.patchRow = "stable"
|
||||
})
|
||||
|
||||
@@ -292,6 +305,7 @@ test("combines adjacent patch calls and repeated files into one group", async ({
|
||||
test("keeps a failed patch in Used without losing the surviving file choice", async ({ page }) => {
|
||||
const failed = "prt_grouped_patch_failed"
|
||||
const surviving = "prt_grouped_patch_surviving"
|
||||
|
||||
const timeline = await setupTimeline(page, {
|
||||
settings: { timelineDetail: detailed },
|
||||
messages: [
|
||||
@@ -321,6 +335,7 @@ test("keeps a failed patch in Used without losing the surviving file choice", as
|
||||
await expect(file).toHaveAttribute("aria-expanded", "true")
|
||||
await group.evaluate((element) => {
|
||||
const row = element.closest<HTMLElement>("[data-timeline-key]")
|
||||
|
||||
if (row) row.dataset.groupIdentity = "preserved"
|
||||
})
|
||||
|
||||
@@ -331,9 +346,11 @@ test("keeps a failed patch in Used without losing the surviving file choice", as
|
||||
)
|
||||
|
||||
const failedRow = page.locator("[data-timeline-key]", { has: page.locator(`[data-timeline-part-id="${failed}"]`) })
|
||||
|
||||
const survivingRow = page.locator("[data-timeline-key]", {
|
||||
has: page.locator(`[data-timeline-part-id="${surviving}"]`),
|
||||
})
|
||||
|
||||
await expect(group).toHaveAttribute("data-timeline-part-ids", `${failed},${surviving}`)
|
||||
await expect(used).toHaveAttribute("aria-expanded", "true")
|
||||
await expect(failedRow).toHaveAttribute("data-timeline-key", /^assistant-part:context:/)
|
||||
@@ -351,15 +368,21 @@ test("does not remount an edit diff when a sibling part arrives", async ({ page
|
||||
const attachShadow = Element.prototype.attachShadow
|
||||
Element.prototype.attachShadow = function (init) {
|
||||
count += 1
|
||||
|
||||
return attachShadow.call(this, init)
|
||||
}
|
||||
;(window as Window & { __shadowRoots?: { reset(): void; count(): number } }).__shadowRoots = {
|
||||
|
||||
// SAFETY: this init script owns `__shadowRoots`; the probe reads below run only after it installed it.
|
||||
const probe = window as Window & { __shadowRoots?: { reset(): void; count(): number } }
|
||||
|
||||
probe.__shadowRoots = {
|
||||
reset: () => {
|
||||
count = 0
|
||||
},
|
||||
count: () => count,
|
||||
}
|
||||
})
|
||||
|
||||
const timeline = await setupTimeline(page, {
|
||||
settings: { editToolPartsExpanded: true, shellToolPartsExpanded: true, showReasoningSummaries: true },
|
||||
messages: [
|
||||
@@ -401,8 +424,10 @@ test("does not remount an edit diff when a sibling part arrives", async ({ page
|
||||
),
|
||||
],
|
||||
})
|
||||
|
||||
const tool = page.locator(`[data-timeline-part-id="${editID}"]`)
|
||||
await expect(tool.locator('[data-component="file"][data-mode="diff"]')).toBeVisible()
|
||||
|
||||
const markers = () =>
|
||||
tool.evaluate((element) => {
|
||||
const nodes = [
|
||||
@@ -410,12 +435,15 @@ test("does not remount an edit diff when a sibling part arrives", async ({ page
|
||||
element.querySelector('[data-component="file"][data-mode="diff"]'),
|
||||
element.closest("[data-timeline-key]"),
|
||||
element.closest("[data-timeline-row]"),
|
||||
] as (HTMLElement | null)[]
|
||||
]
|
||||
|
||||
return {
|
||||
markers: nodes.map((node) => node?.dataset.timelineProbe),
|
||||
markers: nodes.map((node) => (node instanceof HTMLElement ? node.dataset.timelineProbe : undefined)),
|
||||
// SAFETY: the init script installed `__shadowRoots` before the page loaded.
|
||||
shadowRoots: (window as Window & { __shadowRoots?: { count(): number } }).__shadowRoots!.count(),
|
||||
}
|
||||
})
|
||||
|
||||
await tool.evaluate((element) => {
|
||||
;[
|
||||
element,
|
||||
@@ -426,7 +454,11 @@ test("does not remount an edit diff when a sibling part arrives", async ({ page
|
||||
if (!(node instanceof HTMLElement)) throw new Error("missing edit tool, diff, row, or frame")
|
||||
node.dataset.timelineProbe = "before"
|
||||
})
|
||||
;(window as Window & { __shadowRoots?: { reset(): void } }).__shadowRoots!.reset()
|
||||
|
||||
// SAFETY: the init script installed `__shadowRoots` before the page loaded.
|
||||
const probe = window as Window & { __shadowRoots?: { reset(): void } }
|
||||
|
||||
probe.__shadowRoots!.reset()
|
||||
})
|
||||
|
||||
await timeline.send(partUpdated(textPart("prt_sibling_text", "Streaming added a later assistant text part.")))
|
||||
@@ -438,10 +470,12 @@ for (const transition of ["reasoning-end", "idle", "retry"] as const) {
|
||||
test(`stops active Thinking on ${transition} without a following tool`, async ({ page }) => {
|
||||
const id = `prt_reasoning_stop_${transition}`
|
||||
const text = "## Inspecting stability\n\nThe timeline is ready for the next step."
|
||||
|
||||
const timeline = await setupTimeline(page, {
|
||||
messages: [userMessage(), assistantMessage([reasoningPart(id, text)], { completed: false })],
|
||||
settings: { timelineDetail: { ...detailed, thinking: { placement: "separate", details: "collapsed" } } },
|
||||
})
|
||||
|
||||
const part = page.locator(`[data-timeline-part-id="${renderedPartID(id)}"]`)
|
||||
const trigger = part.locator('[data-slot="collapsible-trigger"]')
|
||||
await expect(page.locator('[data-timeline-row="Thinking"]')).toBeVisible()
|
||||
@@ -465,6 +499,7 @@ for (const shellDefault of ["collapsed", "expanded"] as const) {
|
||||
const shellID = `prt_separate_shell_${shellDefault}`
|
||||
const output = (count: number) => Array.from({ length: count }, (_, index) => `line ${index + 1}`).join("\n")
|
||||
const assistant = assistantMessage([reasoningPart(reasoningID, "## Inspecting stability")], { completed: false })
|
||||
|
||||
const timeline = await setupTimeline(page, {
|
||||
messages: [userMessage(), assistant],
|
||||
settings: {
|
||||
@@ -475,9 +510,11 @@ for (const shellDefault of ["collapsed", "expanded"] as const) {
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const thought = page.locator(
|
||||
`[data-timeline-part-id="${renderedPartID(reasoningID)}"] [data-slot="collapsible-trigger"]`,
|
||||
)
|
||||
|
||||
const shellTrigger = page.locator(`[data-timeline-part-id="${shellID}"] [data-slot="collapsible-trigger"]`)
|
||||
const group = page.locator('[data-component="collapsed-tool-group"]')
|
||||
// One row opens the thought, the other opens and then closes it again.
|
||||
@@ -485,6 +522,7 @@ for (const shellDefault of ["collapsed", "expanded"] as const) {
|
||||
await expect(page.locator('[data-timeline-row="Thinking"]')).toBeVisible()
|
||||
await expect(thought).toHaveAttribute("aria-expanded", "false")
|
||||
await thought.click()
|
||||
|
||||
if (!thoughtOpen) await thought.click()
|
||||
await expect(thought).toHaveAttribute("aria-expanded", String(thoughtOpen))
|
||||
|
||||
@@ -518,6 +556,7 @@ test.describe("Working", () => {
|
||||
viewport: { width: 390, height: 900 },
|
||||
settings: { timelineDetail: { ...detailed, thinking: { placement: "separate", details: "collapsed" } } },
|
||||
})
|
||||
|
||||
const working = page.locator('[data-component="session-working"]')
|
||||
const shimmer = working.locator('[data-component="text-shimmer"]')
|
||||
await expect(working).toHaveCount(1)
|
||||
@@ -551,6 +590,7 @@ test.describe("Working", () => {
|
||||
],
|
||||
settings: { timelineDetail: detailed },
|
||||
})
|
||||
|
||||
const working = page.locator('[data-component="session-working"]')
|
||||
const group = page.locator('[data-component="collapsed-tool-group"]')
|
||||
await expect(working).toBeVisible()
|
||||
@@ -571,16 +611,20 @@ test.describe("Working", () => {
|
||||
timelineDetail: { ...timelinePresets[0].value, shell: { placement: "separate", details: "collapsed" } },
|
||||
},
|
||||
})
|
||||
|
||||
const working = page.locator('[data-component="session-working"]')
|
||||
await expect(working).toBeVisible()
|
||||
|
||||
const id = `prt_working_${name}`
|
||||
const input =
|
||||
name === "shell"
|
||||
? { command: "printf ready" }
|
||||
: name === "patch"
|
||||
? { patchText: "*** Begin Patch\n*** Add File: src/working.ts\n+export const ready = true\n*** End Patch" }
|
||||
: { agent: "general", description: "Inspect working indicator", prompt: "Inspect the timeline." }
|
||||
|
||||
const input = {
|
||||
shell: { command: "printf ready" },
|
||||
patch: {
|
||||
patchText: "*** Begin Patch\n*** Add File: src/working.ts\n+export const ready = true\n*** End Patch",
|
||||
},
|
||||
subagent: { agent: "general", description: "Inspect working indicator", prompt: "Inspect the timeline." },
|
||||
}[name]
|
||||
|
||||
await timeline.send(partUpdated(toolPart(id, name, "streaming", input)))
|
||||
const tool = page.locator(`[data-timeline-part-id="${id}"]`)
|
||||
await expect(tool).toBeVisible()
|
||||
@@ -590,9 +634,10 @@ test.describe("Working", () => {
|
||||
name === "patch"
|
||||
? { files: [{ ...patchFile("src/working.ts", "added"), patch: "@@ -0,0 +1 @@\n+export const ready = true" }] }
|
||||
: {}
|
||||
|
||||
await timeline.send(partUpdated(toolPart(id, name, "running", input, { metadata })))
|
||||
await expect(tool).toContainText(
|
||||
name === "shell" ? "printf ready" : name === "patch" ? "working.ts" : "Inspect working indicator",
|
||||
{ shell: "printf ready", patch: "working.ts", subagent: "Inspect working indicator" }[name],
|
||||
)
|
||||
await expect(working).toHaveCount(0)
|
||||
|
||||
@@ -627,14 +672,18 @@ test.describe("Working", () => {
|
||||
],
|
||||
})
|
||||
const working = page.locator('[data-component="session-working"]')
|
||||
|
||||
if (!grouped) {
|
||||
await expect(page.locator('[data-timeline-part-id="prt_background_active"]')).toBeVisible()
|
||||
await expect(working).toHaveCount(0)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const trigger = page
|
||||
.locator('[data-component="collapsed-tool-group"]')
|
||||
.getByRole("button", { name: "Used 2 Shell", exact: true, includeHidden: true })
|
||||
|
||||
await expect(trigger).toHaveAttribute("aria-expanded", "false")
|
||||
await expect(working).toBeVisible()
|
||||
await trigger.click()
|
||||
@@ -666,6 +715,7 @@ test.describe("Working", () => {
|
||||
const timeline = await setupTimeline(page, {
|
||||
messages: [userMessage(), assistantMessage([], { completed: false })],
|
||||
})
|
||||
|
||||
const working = page.locator('[data-component="session-working"]')
|
||||
await expect(working).toBeVisible()
|
||||
|
||||
@@ -708,7 +758,9 @@ test.describe("background shortcut", () => {
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const [cardBox, hintBox] = await Promise.all([card.boundingBox(), hint.boundingBox()])
|
||||
|
||||
if (!cardBox || !hintBox) return undefined
|
||||
|
||||
return { aligned: Math.abs(cardBox.x - hintBox.x) < 2, ordered: cardBox.y < hintBox.y }
|
||||
})
|
||||
.toEqual({ aligned: true, ordered: true })
|
||||
@@ -729,11 +781,11 @@ test.describe("background shortcut", () => {
|
||||
"prt_grouped_active",
|
||||
name,
|
||||
"running",
|
||||
name === "shell"
|
||||
? { command: "sleep 10" }
|
||||
: name === "subagent"
|
||||
? { agent: "general", description: "Inspect the timeline", prompt: "Inspect it." }
|
||||
: { filePath: "src/working.ts" },
|
||||
{
|
||||
read: { filePath: "src/working.ts" },
|
||||
shell: { command: "sleep 10" },
|
||||
subagent: { agent: "general", description: "Inspect the timeline", prompt: "Inspect it." },
|
||||
}[name],
|
||||
),
|
||||
],
|
||||
{ completed: false },
|
||||
@@ -745,19 +797,24 @@ test.describe("background shortcut", () => {
|
||||
const trigger = group.locator(':scope > [data-component="collapsible"] > [data-slot="collapsible-trigger"]')
|
||||
await expect(trigger).toHaveAttribute("aria-expanded", "false")
|
||||
await expect(working).toBeInViewport()
|
||||
|
||||
if (name !== "read") {
|
||||
await expect(backgroundHint(page)).toBeInViewport()
|
||||
await expect(page.locator('[data-component="session-background-hint-row"]')).toHaveCSS("height", "24px")
|
||||
}
|
||||
|
||||
await trigger.click()
|
||||
await expect(trigger).toHaveAttribute("aria-expanded", "true")
|
||||
|
||||
if (name === "subagent") await expect(group.getByText("Inspect the timeline", { exact: true })).toBeVisible()
|
||||
|
||||
if (name !== "subagent")
|
||||
await expect(group.locator('[data-component="text-shimmer"][data-active="true"]')).toBeVisible()
|
||||
await expect(working).toBeVisible()
|
||||
await trigger.click()
|
||||
await expect(trigger).toHaveAttribute("aria-expanded", "false")
|
||||
await expect(working).toBeVisible()
|
||||
|
||||
if (name !== "read") await expectBackgroundRequest(page)
|
||||
})
|
||||
}
|
||||
@@ -765,6 +822,7 @@ test.describe("background shortcut", () => {
|
||||
test("separates blocking and already-backgrounded work into two rows", async ({ page }) => {
|
||||
const backgroundID = "ses_background_existing"
|
||||
const blockingID = "ses_background_blocking"
|
||||
|
||||
const timeline = await setupTimeline(page, {
|
||||
settings: { timelineDetail: detailed },
|
||||
sessionMessages: [
|
||||
@@ -781,7 +839,7 @@ test.describe("background shortcut", () => {
|
||||
name: "subagent",
|
||||
state: {
|
||||
status: "completed",
|
||||
input: { description: "Background task" },
|
||||
input: { description: "Background task", agent: "explore" },
|
||||
content: [{ type: "text", text: "working" }],
|
||||
metadata: { sessionID: backgroundID, status: "running" },
|
||||
},
|
||||
@@ -814,7 +872,7 @@ test.describe("background shortcut", () => {
|
||||
name: "subagent",
|
||||
state: {
|
||||
status: "running",
|
||||
input: { description: "Foreground task" },
|
||||
input: { description: "Foreground task", agent: "build" },
|
||||
metadata: { sessionID: blockingID },
|
||||
},
|
||||
time: { created: 4 },
|
||||
@@ -825,8 +883,8 @@ test.describe("background shortcut", () => {
|
||||
],
|
||||
sessions: [
|
||||
session(),
|
||||
session({ id: backgroundID, parentID: sessionID, title: "Background task" }),
|
||||
session({ id: blockingID, parentID: sessionID, title: "Foreground task" }),
|
||||
session({ id: backgroundID, parentID: sessionID, title: "Background task", agent: "explore" }),
|
||||
session({ id: blockingID, parentID: sessionID, title: "Foreground task", agent: "build" }),
|
||||
],
|
||||
sessionStatus: {
|
||||
[sessionID]: { type: "busy" },
|
||||
@@ -855,27 +913,58 @@ test.describe("background shortcut", () => {
|
||||
})
|
||||
const backgroundCard = page.locator('[data-timeline-part-id="call_backgrounded"]')
|
||||
await expect(backgroundHint(page)).toBeVisible()
|
||||
|
||||
const used = page
|
||||
.locator('[data-timeline-part-ids="call_backgrounded,call_shell_backgrounded,call_blocking"]')
|
||||
.locator(':scope > [data-component="collapsible"] > [data-slot="collapsible-trigger"]')
|
||||
|
||||
await expect(used).toHaveText(/^Used\s*3\s*Agent, Shell$/)
|
||||
await expect(used).toHaveAttribute("aria-expanded", "false")
|
||||
await used.click()
|
||||
await page.getByRole("button", { name: "3 running", exact: true }).click()
|
||||
const list = page.getByRole("menu", { name: "3 running", exact: true })
|
||||
await expect(list.getByRole("menuitem")).toHaveText([
|
||||
/^Build\s*Foreground task$/,
|
||||
/^Explore\s*Background task$/,
|
||||
/^Shell\s*sleep 120$/,
|
||||
])
|
||||
await list.getByRole("menuitem", { name: /sleep 120/ }).click()
|
||||
await expect(list).toHaveCount(0)
|
||||
await expect(used).toHaveAttribute("aria-expanded", "true")
|
||||
await page.getByRole("button", { name: "Session details" }).click()
|
||||
const summary = page.getByRole("button", { name: "2 background tasks running", exact: true })
|
||||
await expect(summary).toContainText("2")
|
||||
await summary.click()
|
||||
const list = page.locator('[data-component="session-background-list"]')
|
||||
await expect(list).toContainText("Background task")
|
||||
await expect(list).toContainText("sleep 120")
|
||||
await expect(list).not.toContainText("Foreground task")
|
||||
const shellCall = page.locator('[data-timeline-part-id="call_shell_backgrounded"]')
|
||||
await expect(shellCall.locator('[data-slot="collapsible-trigger"]')).toHaveAttribute("aria-expanded", "true")
|
||||
await expect(shellCall).toBeInViewport()
|
||||
|
||||
await page.getByRole("button", { name: "3 running", exact: true }).click()
|
||||
|
||||
const interrupt = page.waitForRequest(
|
||||
(request) =>
|
||||
request.method() === "POST" && new URL(request.url()).pathname === `/api/session/${blockingID}/interrupt`,
|
||||
)
|
||||
|
||||
const foreground = list.getByRole("menuitem", { name: /Foreground task/ })
|
||||
|
||||
await foreground.hover()
|
||||
await foreground.getByRole("button", { name: "Interrupt subagent", exact: true }).click()
|
||||
await interrupt
|
||||
await expect(list).toBeVisible()
|
||||
|
||||
const kill = page.waitForRequest(
|
||||
(request) => request.method() === "DELETE" && new URL(request.url()).pathname === "/api/shell/shell_backgrounded",
|
||||
)
|
||||
|
||||
await list.getByRole("menuitem", { name: /sleep 120/ }).hover()
|
||||
await page.keyboard.press("Delete")
|
||||
await kill
|
||||
await page.keyboard.press("Escape")
|
||||
await expect(list).toHaveCount(0)
|
||||
await expect(backgroundCard).toContainText("Background task (background)")
|
||||
await expect(backgroundCard.locator('[data-component="session-progress-indicator-v2"]')).toBeVisible()
|
||||
await expect(
|
||||
page.locator('[data-timeline-part-id="call_shell_backgrounded"] [data-component="text-shimmer"]'),
|
||||
).toHaveAttribute("data-active", "true")
|
||||
|
||||
// SAFETY: the transport forwards this payload unchanged, and the client marks a session idle from
|
||||
// `type` and `data.sessionID` alone; the child session's event needs no durable metadata here.
|
||||
await timeline.transport.send({
|
||||
id: "evt_background_succeeded",
|
||||
created: Date.now(),
|
||||
@@ -885,6 +974,116 @@ test.describe("background shortcut", () => {
|
||||
await expect(backgroundCard.locator('[data-component="session-progress-indicator-v2"]')).toHaveCount(0)
|
||||
await expect(backgroundCard).toContainText("Background task (background)")
|
||||
})
|
||||
|
||||
test("moves between siblings and to the parent's shell from inside a subagent", async ({ page }) => {
|
||||
const siblings = [
|
||||
{ id: "ses_sibling_one", agent: "explore", description: "Draft TUI proposal" },
|
||||
{ id: "ses_sibling_two", agent: "build", description: "Fix context controls" },
|
||||
]
|
||||
|
||||
const timeline = await setupTimeline(page, {
|
||||
settings: { timelineDetail: detailed },
|
||||
sessionMessages: [
|
||||
user,
|
||||
{
|
||||
id: "msg_sibling_shell",
|
||||
type: "assistant",
|
||||
agent: "build",
|
||||
model: { id: "model", providerID: "provider" },
|
||||
content: [
|
||||
{
|
||||
type: "tool",
|
||||
id: "call_sibling_shell",
|
||||
name: "shell",
|
||||
state: {
|
||||
status: "completed",
|
||||
input: { command: "sleep 120" },
|
||||
content: [{ type: "text", text: "working" }],
|
||||
metadata: { shellID: "shell_sibling", status: "running" },
|
||||
},
|
||||
time: { created: 2, completed: 3 },
|
||||
},
|
||||
],
|
||||
time: { created: 2, completed: 3 },
|
||||
},
|
||||
{
|
||||
id: "msg_siblings",
|
||||
type: "assistant",
|
||||
agent: "build",
|
||||
model: { id: "model", providerID: "provider" },
|
||||
content: siblings.map((item) => ({
|
||||
type: "tool" as const,
|
||||
id: `call_${item.id}`,
|
||||
name: "subagent",
|
||||
state: {
|
||||
status: "running" as const,
|
||||
input: { description: item.description, agent: item.agent },
|
||||
metadata: { sessionID: item.id },
|
||||
},
|
||||
time: { created: 2 },
|
||||
})),
|
||||
time: { created: 2 },
|
||||
},
|
||||
],
|
||||
sessions: [
|
||||
session(),
|
||||
...siblings.map((item) =>
|
||||
session({ id: item.id, parentID: sessionID, title: item.description, agent: item.agent }),
|
||||
),
|
||||
],
|
||||
sessionStatus: Object.fromEntries(
|
||||
[sessionID, ...siblings.map((item) => item.id)].map((id) => [id, { type: "busy" }]),
|
||||
),
|
||||
})
|
||||
|
||||
await timeline.transport.send({
|
||||
id: "evt_sibling_shell_created",
|
||||
created: 3,
|
||||
type: "shell.created",
|
||||
location: { directory },
|
||||
data: {
|
||||
info: {
|
||||
id: "shell_sibling",
|
||||
status: "running",
|
||||
command: "sleep 120",
|
||||
cwd: directory,
|
||||
shell: "bash",
|
||||
file: "/tmp/sibling.out",
|
||||
metadata: { sessionID },
|
||||
time: { started: 2 },
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const header = page.locator("[data-session-title]")
|
||||
const trigger = header.getByRole("button", { name: "3 running", exact: true })
|
||||
const list = page.getByRole("menu", { name: "3 running", exact: true })
|
||||
|
||||
await trigger.click()
|
||||
await list.getByRole("menuitem", { name: /Draft TUI proposal/ }).click()
|
||||
await expect(page).toHaveURL(/\/session\/ses_sibling_one$/)
|
||||
|
||||
await trigger.click()
|
||||
await expect(list.getByRole("menuitem")).toHaveText([
|
||||
/^Explore\s*Draft TUI proposal/,
|
||||
/^Build\s*Fix context controls/,
|
||||
/^Shell\s*sleep 120/,
|
||||
])
|
||||
await expect(list.getByRole("menuitem", { name: /Draft TUI proposal/ })).toHaveAttribute("aria-current", "page")
|
||||
await expect(list.getByRole("menuitem", { name: /Fix context controls/ })).not.toHaveAttribute("aria-current")
|
||||
await list.getByRole("menuitem", { name: /Fix context controls/ }).click()
|
||||
await expect(page).toHaveURL(/\/session\/ses_sibling_two$/)
|
||||
|
||||
// The shell call lives in the parent: the row opens it there, expanded.
|
||||
await trigger.click()
|
||||
await list.getByRole("menuitem", { name: /sleep 120/ }).click()
|
||||
await expect(page).toHaveURL(new RegExp(`/session/${sessionID}$`))
|
||||
|
||||
const shellCall = page.locator('[data-timeline-part-id="call_sibling_shell"]')
|
||||
|
||||
await expect(shellCall.locator('[data-slot="collapsible-trigger"]')).toHaveAttribute("aria-expanded", "true")
|
||||
await expect(shellCall).toBeInViewport()
|
||||
})
|
||||
})
|
||||
|
||||
test.describe("compaction", () => {
|
||||
@@ -916,6 +1115,7 @@ test.describe("compaction", () => {
|
||||
.poll(async () => {
|
||||
const summary = await compaction.locator('[data-component="text-part"]').boundingBox()
|
||||
const status = await running.boundingBox()
|
||||
|
||||
return !!summary && !!status && status.y >= summary.y + summary.height
|
||||
})
|
||||
.toBe(true)
|
||||
@@ -937,6 +1137,7 @@ test.describe("compaction", () => {
|
||||
.poll(async () => {
|
||||
const summary = await compaction.locator('[data-component="text-part"]').boundingBox()
|
||||
const done = await compaction.getByText("Session compacted", { exact: true }).boundingBox()
|
||||
|
||||
return !!summary && !!done && done.y >= summary.y + summary.height
|
||||
})
|
||||
.toBe(true)
|
||||
@@ -971,16 +1172,21 @@ test.describe("compaction", () => {
|
||||
shown: undefined,
|
||||
},
|
||||
} as const
|
||||
|
||||
const labels = Object.values(outcomes).map((outcome) => outcome.label)
|
||||
|
||||
// Failed then cancelled share one history, so each boundary must keep its own outcome.
|
||||
for (const names of [["failed", "cancelled"], ["interrupted"]] as const) {
|
||||
test(`ends running compactions as ${names.join(", then ")}`, async ({ page }) => {
|
||||
const timeline = await setupTimeline(page, {
|
||||
sessionMessages: [user, completed],
|
||||
...(names[0] === "interrupted" ? { sessionStatus: { [sessionID]: { type: "busy" as const } } } : {}),
|
||||
})
|
||||
const timeline = await setupTimeline(
|
||||
page,
|
||||
names[0] === "interrupted"
|
||||
? { sessionMessages: [user, completed], sessionStatus: { [sessionID]: { type: "busy" } } }
|
||||
: { sessionMessages: [user, completed] },
|
||||
)
|
||||
|
||||
const compactions = page.locator('[data-component="session-compaction-message"]')
|
||||
|
||||
for (const [index, name] of names.entries()) {
|
||||
const outcome = outcomes[name]
|
||||
await timeline.send(compactionStarted({ sessionID, reason: outcome.reason, recent: "" }))
|
||||
@@ -988,36 +1194,45 @@ test.describe("compaction", () => {
|
||||
await expect(compactions).toHaveCount(index + 1)
|
||||
const compaction = compactions.nth(index)
|
||||
await expect(compaction).toContainText(outcome.partial)
|
||||
|
||||
if (name === "interrupted") {
|
||||
await expect(compaction.getByRole("status").getByLabel("Compacting", { exact: true })).toBeVisible()
|
||||
|
||||
const request = page.waitForRequest(
|
||||
(request) =>
|
||||
request.method() === "POST" && new URL(request.url()).pathname === `/api/session/${sessionID}/interrupt`,
|
||||
)
|
||||
|
||||
await page.getByRole("button", { name: "Stop", exact: true }).click()
|
||||
await request
|
||||
}
|
||||
|
||||
await timeline.send(compactionFailed({ sessionID, reason: outcome.reason, error: outcome.error }))
|
||||
await expect(compaction.getByText(outcome.label, { exact: true })).toBeVisible()
|
||||
}
|
||||
|
||||
await expect(compactions).toHaveCount(names.length)
|
||||
|
||||
for (const [index, name] of names.entries()) {
|
||||
const outcome = outcomes[name]
|
||||
const compaction = compactions.nth(index)
|
||||
await expect(compaction.getByText("Session compaction started", { exact: true })).toBeVisible()
|
||||
await expect(compaction.getByText(outcome.label, { exact: true })).toBeVisible()
|
||||
|
||||
for (const other of labels.filter((label) => label !== outcome.label))
|
||||
await expect(compaction.getByText(other, { exact: true })).toHaveCount(0)
|
||||
await expect(compaction.getByText("Session compacted", { exact: true })).toHaveCount(0)
|
||||
await expect(compaction.getByRole("status")).toHaveCount(0)
|
||||
await expect(compaction).not.toContainText(outcome.partial)
|
||||
|
||||
if (outcome.shown) await expect(compaction.getByText(outcome.shown, { exact: true })).toBeVisible()
|
||||
|
||||
if (!outcome.shown) await expect(compaction).not.toContainText(outcome.error.message)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
test("reducer-hardening: converges when idle arrives before final part and message completion", async ({ page }) => {
|
||||
const textID = "prt_event_order_text"
|
||||
const assistant = assistantMessage([textPart(textID, "Partial")], { completed: false })
|
||||
@@ -1048,11 +1263,13 @@ test("changes timeline presets and saves custom thinking details", async ({ page
|
||||
const slider = settings.getByRole("slider", { name: "Timeline detail", exact: true })
|
||||
await expect(slider).toBeEnabled()
|
||||
await slider.press("Home")
|
||||
|
||||
for (const [index, name] of ["Messages only", "Quiet", "Compact", "Detailed", "Everything"].entries()) {
|
||||
if (index) await slider.press("ArrowRight")
|
||||
await expect(slider).toHaveValue(String(index))
|
||||
await expect(slider).toHaveAttribute("aria-valuetext", name)
|
||||
}
|
||||
|
||||
await slider.press("End")
|
||||
await settings.getByRole("button", { name: "Advanced", exact: true }).click()
|
||||
const grouped = settings.getByRole("switch", { name: "Thinking grouped", exact: true })
|
||||
@@ -1132,6 +1349,7 @@ test.describe("shell completion", () => {
|
||||
],
|
||||
time: { created: 2, completed: 3 },
|
||||
}
|
||||
|
||||
const state = { finished: false, requests: 0 }
|
||||
// Larger than one server page (65,536 bytes), so the final output is read in two pages. The card shows its most
|
||||
// recent 64 KiB.
|
||||
@@ -1141,9 +1359,11 @@ test.describe("shell completion", () => {
|
||||
const reads: number[] = []
|
||||
page.on("request", (request) => {
|
||||
const url = new URL(request.url())
|
||||
|
||||
if (url.pathname === `/api/shell/${background.id}/output`)
|
||||
reads.push(Number(url.searchParams.get("cursor") ?? 0))
|
||||
})
|
||||
|
||||
const timeline = await setupTimeline(page, {
|
||||
viewport: { width: grouped ? 390 : 1400, height: 900 },
|
||||
settings: { shellToolPartsExpanded: !grouped },
|
||||
@@ -1156,15 +1376,19 @@ test.describe("shell completion", () => {
|
||||
shellOutput: ({ id }) => {
|
||||
if (id !== background.id) return "Checking project\n"
|
||||
state.requests++
|
||||
|
||||
return state.finished ? finished : "Checking project\n"
|
||||
},
|
||||
})
|
||||
|
||||
await page.clock.install()
|
||||
await page.reload()
|
||||
await timeline.transport.waitForConnection()
|
||||
|
||||
const groupTrigger = page
|
||||
.locator('[data-component="collapsed-tool-group"]')
|
||||
.locator(':scope > [data-component="collapsible"] > [data-slot="collapsible-trigger"]')
|
||||
|
||||
if (grouped) {
|
||||
await expect(page.locator('[data-component="collapsed-tool-group"]')).toHaveAttribute(
|
||||
"data-timeline-part-ids",
|
||||
@@ -1173,11 +1397,13 @@ test.describe("shell completion", () => {
|
||||
await expect(groupTrigger).toHaveAttribute("aria-expanded", "false")
|
||||
await groupTrigger.click()
|
||||
}
|
||||
|
||||
const card = page.locator(`[data-timeline-part-id="call_${background.id}"]`)
|
||||
const shimmer = card.locator('[data-component="text-shimmer"]')
|
||||
const other = page.locator('[data-timeline-part-id="call_sh_other"] [data-component="text-shimmer"]')
|
||||
await expect(shimmer).toHaveAttribute("data-active", "true")
|
||||
await expect(other).toHaveAttribute("data-active", "true")
|
||||
|
||||
if (grouped) await card.locator('[data-slot="collapsible-trigger"]').click()
|
||||
await expect(card.locator('[data-slot="bash-result"]')).toHaveText("Checking project")
|
||||
|
||||
@@ -1202,9 +1428,11 @@ test.describe("shell completion", () => {
|
||||
|
||||
reads.length = 0
|
||||
await page.reload()
|
||||
|
||||
if (grouped) await groupTrigger.click()
|
||||
await expect(shimmer).toHaveAttribute("data-active", "false")
|
||||
await expect(other).toHaveAttribute("data-active", "true")
|
||||
|
||||
// An expanded card reads the exited shell's final output from the start, again in two pages.
|
||||
if (grouped) return
|
||||
await expect(card.locator('[data-slot="bash-result"]')).toHaveText(tail)
|
||||
@@ -1240,6 +1468,7 @@ test.describe("shell completion", () => {
|
||||
],
|
||||
shellOutput: ({ id }) => (id === background.id ? "Checking project\n" : undefined),
|
||||
})
|
||||
|
||||
const card = page.locator('[data-timeline-part-id="call_foreground"]')
|
||||
const shimmer = card.locator('[data-component="text-shimmer"]')
|
||||
await expect(shimmer).toHaveAttribute("data-active", "true")
|
||||
@@ -1290,6 +1519,7 @@ async function expectBackgroundRequest(page: Page) {
|
||||
(request) =>
|
||||
request.method() === "POST" && new URL(request.url()).pathname === `/api/session/${sessionID}/background`,
|
||||
)
|
||||
|
||||
await page.keyboard.press("Control+b")
|
||||
await request
|
||||
}
|
||||
@@ -1298,12 +1528,11 @@ function patchFile(file: string, status: "added" | "modified" | "deleted") {
|
||||
return {
|
||||
file,
|
||||
status,
|
||||
patch:
|
||||
status === "added"
|
||||
? "@@ -0,0 +1 @@\n+export const after = true"
|
||||
: status === "deleted"
|
||||
? "@@ -1 +0,0 @@\n-export const before = true"
|
||||
: "@@ -1 +1 @@\n-export const before = true\n+export const after = true",
|
||||
patch: {
|
||||
added: "@@ -0,0 +1 @@\n+export const after = true",
|
||||
deleted: "@@ -1 +0,0 @@\n-export const before = true",
|
||||
modified: "@@ -1 +1 @@\n-export const before = true\n+export const after = true",
|
||||
}[status],
|
||||
additions: status === "deleted" ? 0 : 1,
|
||||
deletions: status === "added" ? 0 : 1,
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect, test, type Page } from "@playwright/test"
|
||||
import pkg from "../../package.json" with { type: "json" }
|
||||
import type { SeedInput } from "../utils/app"
|
||||
import { SERVER, type SeedInput } from "../utils/app"
|
||||
import { openHeldStart } from "../utils/direction"
|
||||
import { expectAppVisible } from "../utils/waits"
|
||||
import { mockWorkspace } from "../utils/workspace"
|
||||
@@ -111,7 +111,23 @@ test("Don't show these in the future turns release notes off and closes What's N
|
||||
await expect.poll(() => stored(page, RELEASE_NOTES)).toEqual({ enabled: false })
|
||||
})
|
||||
|
||||
test("What's New opened after the attachment mounts waits for the routes, then keeps focus over restored Settings", async ({ page }) => {
|
||||
test("a rejected update check shows one failure title without repeating it as a description", async ({ page }) => {
|
||||
await mockWorkspace(page, { name: "UpdateCheck", sessions: [] })
|
||||
await page.goto(
|
||||
`/e2e/utils/settings-wsl.html?${new URLSearchParams({ server: SERVER, mode: "stopped", updater: "" })}`,
|
||||
)
|
||||
const settings = page.getByTestId("settings-screen")
|
||||
const check = settings.getByRole("button", { name: "Check now", exact: true })
|
||||
await expect(check).toBeEnabled()
|
||||
await check.click()
|
||||
await expect(page.getByText("Request failed", { exact: true })).toBeVisible()
|
||||
await expect(page.getByText("Request failed", { exact: true })).toHaveCount(1)
|
||||
await expect(page.getByText("You're up to date", { exact: true })).toHaveCount(0)
|
||||
})
|
||||
|
||||
test("What's New opened after the attachment mounts waits for the routes, then keeps focus over restored Settings", async ({
|
||||
page,
|
||||
}) => {
|
||||
const held = Promise.withResolvers<void>()
|
||||
await serveChangelog(page, changelog, held.promise)
|
||||
await mockWorkspace(page, { name: "Whats New", sessions: [], seed: upgraded })
|
||||
|
||||
@@ -1,16 +1,31 @@
|
||||
import { Schema, SchemaGetter } from "effect"
|
||||
import { Predicate, Schema, SchemaGetter } from "effect"
|
||||
import { HttpApi, HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "effect/unstable/httpapi"
|
||||
import { Pty } from "@opencode/schema/pty"
|
||||
import { Worktree } from "@opencode/schema/worktree"
|
||||
|
||||
// Handlers answer plain fixture data: undefined properties are dropped, and other non-JSON values become null.
|
||||
const Json = Schema.Json.pipe(
|
||||
Schema.decodeTo(Schema.Unknown, {
|
||||
decode: SchemaGetter.passthrough(),
|
||||
encode: SchemaGetter.transform(jsonValue),
|
||||
encode: SchemaGetter.transform(function json(value): Schema.Json {
|
||||
if (value === null || Predicate.isString(value) || Predicate.isBoolean(value)) return value
|
||||
|
||||
if (Predicate.isNumber(value)) return Number.isFinite(value) ? value : null
|
||||
|
||||
if (Array.isArray(value)) return value.map(json)
|
||||
|
||||
if (!Predicate.isObject(value)) return null
|
||||
|
||||
return Object.fromEntries(
|
||||
Object.entries(value).flatMap(([key, item]) => (item === undefined ? [] : [[key, json(item)]])),
|
||||
)
|
||||
}),
|
||||
}),
|
||||
HttpApiSchema.asJson(),
|
||||
)
|
||||
|
||||
const JsonPayload = Schema.Unknown.pipe(HttpApiSchema.asJson())
|
||||
|
||||
const Query = Schema.Struct({
|
||||
directory: Schema.optional(Schema.String),
|
||||
parentID: Schema.optional(Schema.String),
|
||||
@@ -23,8 +38,11 @@ const Query = Schema.Struct({
|
||||
type: Schema.optional(Schema.String),
|
||||
mode: Schema.optional(Schema.String),
|
||||
})
|
||||
|
||||
const SessionParams = { sessionID: Schema.String }
|
||||
|
||||
const PtyParams = { ptyID: Pty.ID }
|
||||
|
||||
const NoContent = HttpApiSchema.NoContent
|
||||
|
||||
export class MockNotFound extends Schema.TaggedError<MockNotFound>()("MockNotFound", {
|
||||
@@ -45,10 +63,16 @@ export class MockShellNotFound extends Schema.TaggedError<MockShellNotFound>()("
|
||||
message: Schema.String,
|
||||
}) {}
|
||||
|
||||
// The server's error for a request without its password.
|
||||
export class MockUnauthorized extends Schema.TaggedError<MockUnauthorized>()("UnauthorizedError", {
|
||||
message: Schema.String,
|
||||
}) {}
|
||||
|
||||
// A mutation the scenario did not configure a handler for.
|
||||
export class MockUnsupported extends Schema.TaggedError<MockUnsupported>()("MockUnsupported", {
|
||||
message: Schema.String,
|
||||
}) {}
|
||||
|
||||
const Unsupported = MockUnsupported.pipe(HttpApiSchema.status(501))
|
||||
|
||||
const Group = HttpApiGroup.make("mock")
|
||||
@@ -195,6 +219,12 @@ const Group = HttpApiGroup.make("mock")
|
||||
error: MockShellNotFound.pipe(HttpApiSchema.status(404)),
|
||||
}),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.delete("shellRemove", "/api/shell/:id", {
|
||||
params: { id: Schema.String },
|
||||
success: NoContent,
|
||||
}),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.get("ptyList", "/api/pty", {
|
||||
success: Json,
|
||||
@@ -303,6 +333,13 @@ const Group = HttpApiGroup.make("mock")
|
||||
success: Json,
|
||||
}),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.post("sessionCompact", "/api/session/:sessionID/compact", {
|
||||
params: SessionParams,
|
||||
payload: JsonPayload,
|
||||
success: Json,
|
||||
}),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.post("sessionCommand", "/api/session/:sessionID/command", {
|
||||
params: SessionParams,
|
||||
@@ -368,6 +405,12 @@ const Group = HttpApiGroup.make("mock")
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.post("sessionInterrupt", "/api/session/:sessionID/interrupt", {
|
||||
params: SessionParams,
|
||||
success: Json,
|
||||
}),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.post("sessionWait", "/api/experimental/session/:sessionID/wait", {
|
||||
params: SessionParams,
|
||||
success: NoContent,
|
||||
}),
|
||||
@@ -409,13 +452,3 @@ const Group = HttpApiGroup.make("mock")
|
||||
)
|
||||
|
||||
export const MockApi = HttpApi.make("mock").add(Group)
|
||||
|
||||
function jsonValue(value: unknown): Schema.Json {
|
||||
if (value === null || typeof value === "string" || typeof value === "boolean") return value
|
||||
if (typeof value === "number") return Number.isFinite(value) ? value : null
|
||||
if (Array.isArray(value)) return value.map(jsonValue)
|
||||
if (!value || typeof value !== "object") return null
|
||||
return Object.fromEntries(
|
||||
Object.entries(value).flatMap(([key, item]) => (item === undefined ? [] : [[key, jsonValue(item)]])),
|
||||
)
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -29,6 +29,8 @@ export function mount(input: {
|
||||
ssh?: string | null
|
||||
storage?: string | null
|
||||
hold?: string | null
|
||||
/** Enables the updater Ipc; check and install reject with this message, which may be empty. */
|
||||
updater?: string | null
|
||||
}) {
|
||||
const root = document.getElementById("root")
|
||||
|
||||
@@ -174,6 +176,7 @@ export function mount(input: {
|
||||
const bridge: Bridge = {
|
||||
packaged: false,
|
||||
async call(request) {
|
||||
if (request.ipc === "updater" && input.updater != null) throw new Error(input.updater)
|
||||
const method = methods.get(request.ipc)?.[request.method]
|
||||
|
||||
if (!method) throw new Error("Unexpected fixture action")
|
||||
@@ -184,6 +187,8 @@ export function mount(input: {
|
||||
return result ?? null
|
||||
},
|
||||
async subscribe(ipc) {
|
||||
if (ipc === "updater" && input.updater != null) return { available: true, state: { status: "idle" } }
|
||||
|
||||
if (ipc === "wsl" || ipc === "ssh") return { available: true, state: snapshot(ipc) }
|
||||
|
||||
return { available: false }
|
||||
|
||||
Loaded 100 of 369 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user