Compare commits

..

46 Commits

Author SHA1 Message Date
Aiden Cline 95ca8a2aaf refactor(ai): reuse shared OpenRouter Responses implementation 2026-09-30 11:31:02 -05:00
Aiden Cline 25d302d02c refactor(ai): share OpenRouter Responses protocol 2026-09-30 11:31:01 -05:00
Aiden Cline 4ec4f274bd feat(ai): route OpenRouter models through native APIs 2026-09-30 11:31:01 -05:00
Aiden Cline 599bdc982e fix(ai): classify Amazon Nova input token rejections as context overflow (#52131) 2026-09-30 11:13:34 -05:00
James Long 82bb5ff88a fix(browser): hide browser tools unless a desktop is attached (#52309) 2026-09-30 11:42:23 -04:00
Simon Klee 0159356be6 tui: upgrade OpenTUI to 0.5.14 (#52316) 2026-09-30 15:16:51 +00:00
OpeOginni 9388b94a97 fix(tui): tighten expanded instruction group spacing (#52311) 2026-09-30 15:08:01 +00:00
James Long 02c3891d9b feat(core): let form cancellation carry a message (#52137) 2026-09-30 09:20:26 -04:00
Shoubhit Dash 1844c11104 fix(acp): follow server defaults and refresh the session catalog (#52286) 2026-09-30 18:36:56 +05:30
opencode-agent[bot] c0285b457a chore(core): refresh bundled models.dev snapshot 2026-09-30 12:25:26 +00:00
opencode-agent[bot] ffa4c4c730 feat(app): open referenced session IDs from markdown (#52231)
Co-authored-by: Brendonovich <14191578+Brendonovich@users.noreply.github.com>
2026-09-30 15:39:30 +08:00
Luke Parker 01efd22b2a feat(desktop): comment on page elements from the in-app browser (#52217) 2026-09-30 16:52:42 +10:00
opencode-agent[bot] dd68d6eb79 fix(app): select new-session project by resolved ID (#52230)
Co-authored-by: Brendonovich <14191578+Brendonovich@users.noreply.github.com>
2026-09-30 14:34:05 +08:00
Simon Klee 1243d09dbc tui: update OpenTUI to 0.5.13 (#52233) 2026-09-30 06:13:28 +00:00
Aiden Cline a80a00011d fix(ai): classify Workers AI context window rejections as context overflow (#52128) 2026-09-29 23:49:03 -05:00
opencode-agent[bot] bf97a0443f fix(app): alert only for open session tabs (#52204)
Co-authored-by: Brendonovich <14191578+Brendonovich@users.noreply.github.com>
2026-09-30 03:55:01 +00:00
Luke Parker 67b14e9191 feat(session-ui): keep open Used group header sticky (#52210) 2026-09-30 13:46:40 +10:00
Luke Parker 1b02abcfab feat(session-ui): group adjacent reads into one row (#52207) 2026-09-30 12:54:40 +10:00
Aiden Cline 74dbc509d7 fix(ai): place prompt cache breakpoints on OpenRouter Anthropic and Qwen requests (#52110) 2026-09-29 18:07:49 -05:00
Aiden Cline 760f87fab1 fix(core): pass through Copilot Responses settings (#52182) 2026-09-29 17:52:53 -05:00
opencode-agent[bot] df51166711 fix(ci): run V2 Discord notification after skipped build jobs (#52012)
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
2026-09-29 16:22:16 -05:00
opencode 8d8a7bc844 sync release versions for v2.0.20 2026-09-29 21:08:02 +00:00
opencode-agent[bot] a2fc1968be fix(stats): match document background to page theme (#52169) 2026-09-29 15:59:22 -05:00
Aiden Cline c2a39ac592 fix(core): stop promising a ChatGPT usage reset (#52168) 2026-09-29 15:50:09 -05:00
Aiden Cline 4deda18037 fix(app): align ChatGPT usage copy and links (#52163) 2026-09-29 15:18:45 -05:00
Aiden Cline 0f83038253 fix(core): align ChatGPT token sharing with updated partner guide (#52162) 2026-09-29 15:11:58 -05:00
Dax 4c33a253aa feat(cli): add auth export and auth import commands (#52139) 2026-09-29 16:09:01 -04:00
opencode-agent[bot] 135995d4fc test: stabilize V2 CI test suites (#52159)
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
Co-authored-by: jlongster <17031+jlongster@users.noreply.github.com>
2026-09-29 14:54:11 -05:00
Aiden Cline 4d9090790b feat(core): add separate ChatGPT token-sharing OAuth method (#52147) 2026-09-29 14:48:51 -05:00
Aiden Cline 621a5b0706 feat(app): show ChatGPT token-sharing guidance (#52160) 2026-09-29 14:45:59 -05:00
Aiden Cline 16ef62c851 feat(app): show ChatGPT usage limit dialog (#52153) 2026-09-29 14:38:48 -05:00
Aiden Cline b694d2bf26 fix(core): explain ChatGPT token-sharing errors (#52158) 2026-09-29 14:25:01 -05:00
Aiden Cline 89d37a0f6d fix(core): restrict database files to their owner (#52150) 2026-09-29 14:18:40 -05:00
James Long bc5ff1cfd9 fix(cli): answer subagent asks in run and keep parallel rejections continuing (#52146) 2026-09-29 15:03:06 -04:00
Aiden Cline c14a1f5acf feat(core): preserve provider response body in session errors (#52136) 2026-09-29 13:32:09 -05:00
Frank 7db941a892 docs(console): add GPT 6.1 Sol to model list 2026-09-29 14:28:47 -04:00
James Long 83fafda63a feat(cli): continue run after rejecting a permission ask (#51968) 2026-09-29 14:26:20 -04:00
opencode-agent[bot] a565ea8c74 feat(cli): allow disabling the background service (#52107)
Co-authored-by: jlongster <17031+jlongster@users.noreply.github.com>
2026-09-29 13:32:38 -04:00
Aiden Cline 3126808e0e fix(ai): defer Mistral thinking metadata until block end (#52008) 2026-09-29 11:38:06 -05:00
opencode-agent[bot] 948047a8ca fix(tui): use disclosure icon for low activity errors (#52097)
Co-authored-by: thdxr <826656+thdxr@users.noreply.github.com>
2026-09-29 11:10:32 -04:00
opencode-agent[bot] 0f931d3939 chore(core): refresh bundled models.dev snapshot 2026-09-29 12:22:53 +00:00
Kit Langton 7ef4a1a56e feat(tui): show recently closed tabs in new session menu 2026-09-29 03:35:31 -07:00
Victor Navarro b29dab3231 fix(core): drop SSO link from Console sign-in error (#52062) 2026-09-29 12:04:27 +02:00
Victor Navarro d76f968c60 feat(core): tell users how to sign back in to OpenCode Console (#51847) 2026-09-29 10:58:45 +02:00
opencode-agent[bot] b30c4d00d1 fix(ai): enable explicit caching on more message protocol routes (#51981)
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
2026-09-29 00:50:08 -05:00
Aiden Cline 6712cc4c2c fix(core): turn off Workers AI thinking through the chat template (#52017) 2026-09-29 00:45:17 -05:00
281 changed files with 8482 additions and 1884 deletions
+1 -1
View File
@@ -662,7 +662,7 @@ jobs:
needs:
- version
- publish
if: github.repository == 'anomalyco/opencode' && github.ref_name == 'v2' && needs.version.outputs.release && needs.publish.result == 'success'
if: ${{ !cancelled() && github.repository == 'anomalyco/opencode' && github.ref_name == 'v2' && needs.version.outputs.release && needs.publish.result == 'success' }}
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
# Unlike dev, V2 publishes a tag rather than a GitHub Release event.
+52 -51
View File
@@ -32,7 +32,7 @@
},
"packages/ai": {
"name": "@opencode/ai",
"version": "2.0.19",
"version": "2.0.20",
"dependencies": {
"@aws-sdk/credential-providers": "3.1057.0",
"@opencode/schema": "workspace:*",
@@ -54,7 +54,7 @@
},
"packages/app": {
"name": "@opencode/app",
"version": "2.0.19",
"version": "2.0.20",
"dependencies": {
"@corvu/drawer": "catalog:",
"@dnd-kit/abstract": "0.5.0",
@@ -112,7 +112,7 @@
},
"packages/cli": {
"name": "@opencode/cli",
"version": "2.0.19",
"version": "2.0.20",
"bin": {
"opencode": "./bin/opencode.cjs",
"opencode2": "./bin/opencode2.cjs",
@@ -178,7 +178,7 @@
},
"packages/client": {
"name": "@opencode/client",
"version": "2.0.19",
"version": "2.0.20",
"dependencies": {
"@opencode/protocol": "workspace:*",
"@opencode/schema": "workspace:*",
@@ -204,7 +204,7 @@
},
"packages/codemode": {
"name": "@opencode/codemode",
"version": "2.0.19",
"version": "2.0.20",
"dependencies": {
"acorn": "8.15.0",
"effect": "catalog:",
@@ -217,7 +217,7 @@
},
"packages/console/app": {
"name": "@opencode/console-app",
"version": "2.0.19",
"version": "2.0.20",
"dependencies": {
"@cloudflare/vite-plugin": "1.15.2",
"@ibm/plex": "6.4.1",
@@ -253,7 +253,7 @@
},
"packages/console/core": {
"name": "@opencode/console-core",
"version": "2.0.19",
"version": "2.0.20",
"dependencies": {
"@aws-sdk/client-sts": "3.782.0",
"@jsx-email/render": "1.1.1",
@@ -280,7 +280,7 @@
},
"packages/console/function": {
"name": "@opencode/console-function",
"version": "2.0.19",
"version": "2.0.20",
"dependencies": {
"@openauthjs/openauth": "0.0.0-20250322224806",
"@opencode/console-core": "workspace:*",
@@ -297,7 +297,7 @@
},
"packages/console/mail": {
"name": "@opencode/console-mail",
"version": "2.0.19",
"version": "2.0.20",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
@@ -321,7 +321,7 @@
},
"packages/console/support": {
"name": "@opencode/console-support",
"version": "2.0.19",
"version": "2.0.20",
"dependencies": {
"@cloudflare/vite-plugin": "1.15.2",
"@opencode/console-core": "workspace:*",
@@ -341,7 +341,7 @@
},
"packages/core": {
"name": "@opencode/core",
"version": "2.0.19",
"version": "2.0.20",
"dependencies": {
"@ai-sdk/cohere": "3.0.27",
"@ai-sdk/gateway": "3.0.104",
@@ -377,6 +377,7 @@
"https-proxy-agent": "7.0.6",
"ignore": "7.0.5",
"immer": "11.1.4",
"jose": "6.0.11",
"jsonc-parser": "3.3.1",
"mime-types": "3.0.2",
"tree-sitter-bash": "0.25.0",
@@ -409,7 +410,7 @@
},
"packages/desktop": {
"name": "@opencode/desktop",
"version": "2.0.19",
"version": "2.0.20",
"dependencies": {
"@zip.js/zip.js": "2.7.62",
"electron-context-menu": "5.0.0",
@@ -458,7 +459,7 @@
},
"packages/enterprise": {
"name": "@opencode/enterprise",
"version": "2.0.19",
"version": "2.0.20",
"dependencies": {
"@hono/standard-validator": "catalog:",
"@opencode-ai/sdk": "1.18.21",
@@ -495,7 +496,7 @@
},
"packages/function": {
"name": "@opencode/function",
"version": "2.0.19",
"version": "2.0.20",
"dependencies": {
"@octokit/auth-app": "8.0.1",
"@octokit/rest": "catalog:",
@@ -511,7 +512,7 @@
},
"packages/http-recorder": {
"name": "@opencode/http-recorder",
"version": "2.0.19",
"version": "2.0.20",
"dependencies": {
"@effect/platform-node-shared": "4.0.0-rc.112",
},
@@ -530,7 +531,7 @@
},
"packages/httpapi-codegen": {
"name": "@opencode/httpapi-codegen",
"version": "2.0.19",
"version": "2.0.20",
"dependencies": {
"effect": "catalog:",
"prettier": "3.6.2",
@@ -543,7 +544,7 @@
},
"packages/latex": {
"name": "@opencode/latex",
"version": "2.0.19",
"version": "2.0.20",
"dependencies": {
"@opencode/plugin": "workspace:*",
"@opentui/core": "catalog:",
@@ -557,7 +558,7 @@
},
"packages/merman": {
"name": "@opencode/merman",
"version": "2.0.19",
"version": "2.0.20",
"dependencies": {
"@opencode/plugin": "workspace:*",
"@opentui/core": "catalog:",
@@ -572,7 +573,7 @@
},
"packages/plugin": {
"name": "@opencode/plugin",
"version": "2.0.19",
"version": "2.0.20",
"dependencies": {
"@ai-sdk/provider": "3.0.8",
"@opencode/ai": "workspace:*",
@@ -598,8 +599,8 @@
},
"peerDependencies": {
"@opencode/theme": "workspace:*",
"@opentui/core": ">=0.5.12",
"@opentui/solid": ">=0.5.12",
"@opentui/core": ">=0.5.14",
"@opentui/solid": ">=0.5.14",
"solid-js": ">=1.9.0",
},
"optionalPeers": [
@@ -611,7 +612,7 @@
},
"packages/plugin-browser": {
"name": "@opencode/plugin-browser",
"version": "2.0.19",
"version": "2.0.20",
"dependencies": {
"@opencode/plugin": "workspace:*",
"@opencode/schema": "workspace:*",
@@ -641,7 +642,7 @@
},
"packages/protocol": {
"name": "@opencode/protocol",
"version": "2.0.19",
"version": "2.0.20",
"dependencies": {
"@opencode/schema": "workspace:*",
"effect": "catalog:",
@@ -656,7 +657,7 @@
},
"packages/schema": {
"name": "@opencode/schema",
"version": "2.0.19",
"version": "2.0.20",
"dependencies": {
"@standard-schema/spec": "catalog:",
"effect": "catalog:",
@@ -680,7 +681,7 @@
},
"packages/sdk": {
"name": "@opencode/sdk",
"version": "2.0.19",
"version": "2.0.20",
"dependencies": {
"@opencode/client": "workspace:*",
"@opencode/core": "workspace:*",
@@ -701,7 +702,7 @@
},
"packages/server": {
"name": "@opencode/server",
"version": "2.0.19",
"version": "2.0.20",
"dependencies": {
"@effect/platform-node": "catalog:",
"@effect/platform-node-shared": "catalog:",
@@ -723,7 +724,7 @@
},
"packages/session-ui": {
"name": "@opencode/session-ui",
"version": "2.0.19",
"version": "2.0.20",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode/client": "workspace:*",
@@ -758,7 +759,7 @@
},
"packages/simulation": {
"name": "@opencode/simulation",
"version": "2.0.19",
"version": "2.0.20",
"dependencies": {
"@opencode/ai": "workspace:*",
"@opencode/core": "workspace:*",
@@ -778,7 +779,7 @@
},
"packages/stats/app": {
"name": "@opencode/stats-app",
"version": "2.0.19",
"version": "2.0.20",
"dependencies": {
"@ibm/plex": "6.4.1",
"@kobalte/core": "catalog:",
@@ -812,7 +813,7 @@
},
"packages/stats/core": {
"name": "@opencode/stats-core",
"version": "2.0.19",
"version": "2.0.20",
"dependencies": {
"@aws-sdk/client-athena": "3.933.0",
"@planetscale/database": "1.19.0",
@@ -831,7 +832,7 @@
},
"packages/stats/server": {
"name": "@opencode/stats-server",
"version": "2.0.19",
"version": "2.0.20",
"dependencies": {
"@aws-sdk/client-firehose": "3.933.0",
"@effect/platform-node": "catalog:",
@@ -877,7 +878,7 @@
},
"packages/theme": {
"name": "@opencode/theme",
"version": "2.0.19",
"version": "2.0.20",
"dependencies": {
"@opentui/core": "catalog:",
"effect": "catalog:",
@@ -891,7 +892,7 @@
},
"packages/tui": {
"name": "@opencode/tui",
"version": "2.0.19",
"version": "2.0.20",
"dependencies": {
"@opencode/client": "workspace:*",
"@opencode/core": "workspace:*",
@@ -925,7 +926,7 @@
},
"packages/ui": {
"name": "@opencode/ui",
"version": "2.0.19",
"version": "2.0.20",
"dependencies": {
"@kobalte/core": "catalog:",
"@pierre/diffs": "catalog:",
@@ -960,7 +961,7 @@
},
"packages/util": {
"name": "@opencode/util",
"version": "2.0.19",
"version": "2.0.20",
"dependencies": {
"@effect/opentelemetry": "catalog:",
"@effect/platform-node": "catalog:",
@@ -998,7 +999,7 @@
},
"packages/web": {
"name": "@opencode/web",
"version": "2.0.19",
"version": "2.0.20",
"dependencies": {
"@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1",
@@ -1039,7 +1040,7 @@
},
"services/update": {
"name": "@opencode/update",
"version": "2.0.19",
"version": "2.0.20",
"dependencies": {
"jose": "6.0.11",
"semver": "catalog:",
@@ -1116,9 +1117,9 @@
"@npmcli/arborist": "9.4.0",
"@octokit/rest": "22.0.0",
"@openauthjs/openauth": "0.0.0-20250322224806",
"@opentui/core": "0.5.12",
"@opentui/keymap": "0.5.12",
"@opentui/solid": "0.5.12",
"@opentui/core": "0.5.14",
"@opentui/keymap": "0.5.14",
"@opentui/solid": "0.5.14",
"@pierre/diffs": "1.5.1",
"@playwright/test": "1.59.1",
"@sentry/solid": "10.71.0",
@@ -2254,27 +2255,27 @@
"@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.43.0", "", {}, "sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg=="],
"@opentui/core": ["@opentui/core@0.5.12", "", { "dependencies": { "bun-ffi-structs": "0.3.1", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.5.12", "@opentui/core-darwin-x64": "0.5.12", "@opentui/core-linux-arm64": "0.5.12", "@opentui/core-linux-arm64-musl": "0.5.12", "@opentui/core-linux-x64": "0.5.12", "@opentui/core-linux-x64-musl": "0.5.12", "@opentui/core-win32-arm64": "0.5.12", "@opentui/core-win32-x64": "0.5.12" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-ZXBE5gmvdovmV8zJQrOQf6E44v1tJRDEgrM2MYhEglzgXZ+smIUp95O8zeRYGsuIzQIiMPMgQqKtTJuzvAb7BQ=="],
"@opentui/core": ["@opentui/core@0.5.14", "", { "dependencies": { "bun-ffi-structs": "0.3.1", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.5.14", "@opentui/core-darwin-x64": "0.5.14", "@opentui/core-linux-arm64": "0.5.14", "@opentui/core-linux-arm64-musl": "0.5.14", "@opentui/core-linux-x64": "0.5.14", "@opentui/core-linux-x64-musl": "0.5.14", "@opentui/core-win32-arm64": "0.5.14", "@opentui/core-win32-x64": "0.5.14" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-tfQ+PWQyeBnYloB3diEcPqbILv14xemH5jjAEICfPuyNDtGBqrjhUtThrhbvFRuPMcj6IEeXrAk6VE8e91h0kg=="],
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.5.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-YdVnP0tAyerBNl0mIcmQEOotPeZzW1VnSXKBl5cyZ5e6nDd2Y+ui/8eRPpn1oqcamf1NCnzS4ohMgejOvna8Zg=="],
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.5.14", "", { "os": "darwin", "cpu": "arm64" }, "sha512-wWmw41wRMBoI0lN9mgyzGRwYujwwfNkBP6jYM99k4Bx2XkeSZ3sCeu5bwX9vEJPYqjiBShbDhc0NnVSP7NwzVg=="],
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.5.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-uRrQJdHmLUSj3PV23QPi3WSimYTTxcXnVouxF6U4xMXlOv4N3SxnHfVwMRQkPqbGOfvVWHeLE6FdK4C+ubU0sQ=="],
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.5.14", "", { "os": "darwin", "cpu": "x64" }, "sha512-7smHKDH8IhUaBsgYuAClMl2mHWu+yjpMrtdw3wEvBVtCzKMrHi/TJ5PtydO+IfUzR3BXpVubdbR1irD8BTcR/w=="],
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.5.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-XeKhuIaEtgipvuPHbl4qPOBj+Ut+2zObmsxMVM1jDcjz/FatG9PGeGQPx1G1SnvH2AgpT4K+eCu7DUF0+yIqoQ=="],
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.5.14", "", { "os": "linux", "cpu": "arm64" }, "sha512-xH1hP+NaLySEJeZkl21NlkZBMddMfQ1jU8NeX1AEBc2GNBOvDXU4Ud/xw87SrAvU1xG9K7/9C4oy4AmIMEpVGg=="],
"@opentui/core-linux-arm64-musl": ["@opentui/core-linux-arm64-musl@0.5.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-VZ2sNMw1d/r1SLPjUbOP9LKscKz1CQjID8adTL6gG8Lrrq+mYcIUxutyB+P/eG0J/7oRZLPR6OMt7dUOap6RTg=="],
"@opentui/core-linux-arm64-musl": ["@opentui/core-linux-arm64-musl@0.5.14", "", { "os": "linux", "cpu": "arm64" }, "sha512-mnBBAuTb92NiRLAjOD755tS8/tNQemDztbg9tMvoCT90G52FtVrRb31Ge6OrYqfm0c9DkZGhEBOhunsId/4zSA=="],
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.5.12", "", { "os": "linux", "cpu": "x64" }, "sha512-eZiCjEzwbb6qClPPfk32Nha9xmr9obt69Xj0+9SKsXxWLBKkjQEGOMRoh/R9ObaQF4aq8If1xV3VEY0sD9W9vg=="],
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.5.14", "", { "os": "linux", "cpu": "x64" }, "sha512-Hkk4kaDGMcn9bmJFJPW3/QOGGbPuWe3sCFV/CkiVb4+bCvcTm7EXQr4QTAA63BYy2dKE5bUFUi1zZlyeMkWpnA=="],
"@opentui/core-linux-x64-musl": ["@opentui/core-linux-x64-musl@0.5.12", "", { "os": "linux", "cpu": "x64" }, "sha512-WWW0hVBoSYZ3D6AgZ4u2Y5/u/IyIq2pDb+4yI3WgJ70Wyt6ofHy+6kRGRgbXFn1p+rPInAHjCXD2v6C7iEKSrA=="],
"@opentui/core-linux-x64-musl": ["@opentui/core-linux-x64-musl@0.5.14", "", { "os": "linux", "cpu": "x64" }, "sha512-ngJ+U2grOGEteeQvZdAJNqn09M+At5mfWInauK5aS427bea1yLo+e6hor/CRmbn9SxEEF+SwoyekaOrLPWyU7w=="],
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.5.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-aLbm6870Ybls6CYL4zMOCImTBPLZHZMUXJFGqMI44lIWxitkAtT6zg5lYA4oRqFRzzryDclxr29+hDgT3p3Blw=="],
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.5.14", "", { "os": "win32", "cpu": "arm64" }, "sha512-T9kNqKXg2jysmTsyyZ1A8LBQotFBM+iPjzyRslxyexqrs8a1UcmxbApEo+COtxQuqukMTbvwyqEdAT8vcmxEkQ=="],
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.5.12", "", { "os": "win32", "cpu": "x64" }, "sha512-KTwtwpfd2zF9opVh3SyRJYDd1o3Xv4XL8OZb8Zi+CqWUel6Y2IDCiVivCv8fGJt3J7wOIXXtuZI9ZUkLyKJCiQ=="],
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.5.14", "", { "os": "win32", "cpu": "x64" }, "sha512-mqKSkab8VdMLSmMdocna7+BTyMoIutkVXOV9lfmPrBO2g7Np5c6c4SJ4QIVZqPvast11XyT0/7FfXOYLKAS72w=="],
"@opentui/keymap": ["@opentui/keymap@0.5.12", "", { "dependencies": { "@opentui/core": "0.5.12" }, "peerDependencies": { "@opentui/react": "0.5.12", "@opentui/solid": "0.5.12", "react": ">=19.2.0", "solid-js": "1.9.12" }, "optionalPeers": ["@opentui/react", "@opentui/solid", "react", "solid-js"] }, "sha512-yWPvJjRhJTRoRSUucQq9Ua8ZW7n/2YQ/j6JxWq5Qekm4WuFiTplEkebR/Aj2/xA8tX68NOE5qv1LrY0Jk3NLNQ=="],
"@opentui/keymap": ["@opentui/keymap@0.5.14", "", { "dependencies": { "@opentui/core": "0.5.14" }, "peerDependencies": { "@opentui/react": "0.5.14", "@opentui/solid": "0.5.14", "react": ">=19.2.0", "solid-js": "1.9.12" }, "optionalPeers": ["@opentui/react", "@opentui/solid", "react", "solid-js"] }, "sha512-YGTAvRrpQTbRNV7GH0UxRuCSPxwnSooVdB+qPHHP+3ywc93+lhekljgP8dRivl32f3CiMqqm96Uj2PgwKxkC0Q=="],
"@opentui/solid": ["@opentui/solid@0.5.12", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.5.12", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.12", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.12" } }, "sha512-hAiVlVMtT7AkHGblKwcW1YAuXtxkSy1XSf/RRc4j3IlG3mTNX0bhJdnGOo3Xw14EqeZMp41Mcp5WzHAzMm/DzA=="],
"@opentui/solid": ["@opentui/solid@0.5.14", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.5.14", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.12", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.12" } }, "sha512-bBRl34mZ0wFGhjHX6y1VNiZmJ3DSm2DVPm0PNSVDpvq9qxRHuywEPTCn/Lgte0C9450tgf11cDqcE3pR9cZwVA=="],
"@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],
+4 -4
View File
@@ -2,7 +2,7 @@
"$schema": "https://json.schemastore.org/package.json",
"name": "opencode",
"description": "AI-powered development tool",
"version": "2.0.19",
"version": "2.0.20",
"private": true,
"type": "module",
"packageManager": "bun@1.4.2",
@@ -52,9 +52,9 @@
"@octokit/rest": "22.0.0",
"@hono/standard-validator": "0.2.0",
"@hono/zod-validator": "0.4.2",
"@opentui/core": "0.5.12",
"@opentui/keymap": "0.5.12",
"@opentui/solid": "0.5.12",
"@opentui/core": "0.5.14",
"@opentui/keymap": "0.5.14",
"@opentui/solid": "0.5.14",
"@tanstack/solid-virtual": "3.13.37",
"@shikijs/stream": "4.4.3",
"@standard-schema/spec": "1.1.0",
+11 -1
View File
@@ -1203,12 +1203,22 @@ LLM.request({
| ----------------------- | ------------------------------------------------------------------------- |
| Anthropic Messages | emits up to 4 `cache_control` markers (4-breakpoint cap enforced) |
| Bedrock Converse | emits up to 4 `cachePoint` blocks (4-breakpoint cap enforced) |
| OpenRouter | emits up to 4 `cache_control` markers |
| OpenRouter | Anthropic/Qwen emit explicit markers; other families use upstream caching |
| OpenAI Chat / Responses | no-op (implicit caching above 1024 tokens) |
| Gemini | no-op (implicit caching on 2.5+; explicit `CachedContent` is out-of-band) |
Normalized cache usage is read back into `response.usage.cacheReadInputTokens` and `cacheWriteInputTokens` across every provider.
OpenRouter selects its wire API by model family: `openai/*`, `x-ai/*`, and `meta/*` use Responses; `anthropic/*` uses
Anthropic Messages; other families use Chat Completions. All three Responses families share the existing OpenAI-shaped
Responses implementation, including namespace tools and hosted-tool events; there are no per-vendor OpenRouter
protocols. Leading-`~` model aliases follow the same routing. Explicit `openrouter.chat(id)`, `.responses(id)`, and
`.messages(id)` selectors are available for API-specific integrations.
Qwen retains Chat's explicit system/conversation cache markers, while Claude uses native Messages cache blocks.
OpenRouter Responses is stateless (`store: false`); every request includes full history, not `previous_response_id`.
Meta currently accepts only automatic tool selection on both its Chat and Responses upstreams; forced/disabled tool
choices can be rejected by the provider.
## Providers
Provider facades configure endpoint/auth/deployment details first, then expose model selectors that take only a model or deployment id. The selected model carries the executable route value used at runtime.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "2.0.19",
"version": "2.0.20",
"name": "@opencode/ai",
"type": "module",
"license": "MIT",
+23 -3
View File
@@ -38,13 +38,31 @@ const resolve = (policy: CachePolicy | undefined): CachePolicyObject => {
// prefix caching, Gemini's implicit + out-of-band CachedContent). Skip the
// whole policy pass for these — emitting hints would be harmless but pointless.
const RESPECTS_INLINE_HINTS = new Set([
"alibaba-messages",
"anthropic-messages",
"anthropic-compatible-messages",
"cloudflare-ai-gateway-messages",
"google-vertex-messages",
"meta-messages",
"minimax-messages",
"moonshot-messages",
"zai-coding-messages",
"bedrock-converse",
"openrouter",
"openrouter-messages",
])
// OpenRouter upstreams other than Anthropic and Alibaba Qwen cache without breakpoints. Gemini uses only the last
// breakpoint, so a conversation-tail breakpoint writes a new cache every step and costs more than none. Qwen ignores
// breakpoints on tool definitions and caches tools with the system prompt.
const openRouterPolicy = (modelID: string): CachePolicyObject => {
// `~anthropic/claude-sonnet-latest` style IDs are OpenRouter aliases for the latest model in a family.
const id = modelID.replace(/^~/, "")
if (id.startsWith("anthropic/")) return AUTO
if (id.startsWith("qwen/")) return { system: true, messages: { tail: 1 } }
return NONE
}
const makeHint = (ttlSeconds: number | undefined): CacheHint =>
ttlSeconds !== undefined ? new CacheHint({ type: "ephemeral", ttlSeconds }) : new CacheHint({ type: "ephemeral" })
@@ -150,9 +168,11 @@ const countHints = (request: LLMRequest) =>
export const applyCachePolicy = (request: LLMRequest): LLMRequest => {
if (!RESPECTS_INLINE_HINTS.has(request.model.route.id)) return request
if (request.model.route.id === "openrouter" && (request.cache === undefined || request.cache === "auto"))
return request
const policy = resolve(request.cache)
const policy =
(request.model.route.id === "openrouter" || request.model.route.id === "openrouter-messages") &&
(request.cache === undefined || request.cache === "auto")
? openRouterPolicy(request.model.id)
: resolve(request.cache)
if (!policy.tools && !policy.system && !policy.messages) return request
const hint = makeHint(policy.ttlSeconds)
+11 -6
View File
@@ -10,14 +10,19 @@ const WebSearch = Schema.Struct({
name: Schema.Literal("web_search"),
user_location: MetaResponses.WebSearch.fields.user_location,
})
const MetaCacheControl = Schema.Struct({
type: Schema.tag("ephemeral"),
ttl: Schema.optional(Schema.Literals(["5m", "1h"])),
})
const FunctionTool = Schema.Struct({
name: Schema.String,
description: Schema.String,
input_schema: JsonObject,
cache_control: Schema.optional(MetaCacheControl),
})
const Body = Schema.Struct({
...AnthropicMessages.AnthropicMessagesBody.fields,
tools: optionalArray(
Schema.Union([
Schema.Struct({ name: Schema.String, description: Schema.String, input_schema: JsonObject }),
WebSearch,
]),
),
tools: optionalArray(Schema.Union([FunctionTool, WebSearch])),
})
const fromRequest = Effect.fn("MetaMessages.fromRequest")(function* (request: LLMRequest) {
+10 -6
View File
@@ -426,6 +426,7 @@ interface ActiveContent {
readonly type: "text" | "reasoning"
readonly id: string
readonly thinking?: MistralThinkingContent
readonly thinkingUnits?: MistralThinkingUnit[]
}
export interface ParserState {
@@ -502,8 +503,8 @@ const closeActive = (state: ParserState, events: LLMEvent[]) => {
state.lifecycle,
events,
state.active.id,
thinkingMetadata(state.active.thinking ?? { type: "thinking", thinking: [] }),
thinkingText(state.active.thinking?.thinking ?? []),
thinkingMetadata({ ...state.active.thinking, type: "thinking", thinking: state.active.thinkingUnits ?? [] }),
thinkingText(state.active.thinkingUnits ?? []),
)
return { ...state, lifecycle, active: undefined }
}
@@ -524,20 +525,23 @@ const appendThinking = (state: ParserState, events: LLMEvent[], part: MistralOut
const current = state.active?.type === "reasoning" ? state : closeActive(state, events)
const units = thinkingUnits(part.thinking)
const active = current.active ?? { type: "reasoning" as const, id: `reasoning-${current.nextContent}` }
// Keep native units out of streamed events until the block is complete.
const accumulated = active.thinkingUnits ?? []
accumulated.push(...units)
const thinking = {
...active.thinking,
...part,
type: "thinking" as const,
thinking: [...(active.thinking?.thinking ?? []), ...units],
thinking: [],
}
const text = thinkingText(units)
return {
...current,
lifecycle:
text.length > 0
? Lifecycle.reasoningDelta(current.lifecycle, events, active.id, text, thinkingMetadata(thinking))
: Lifecycle.reasoningStart(current.lifecycle, events, active.id, thinkingMetadata(thinking)),
active: { ...active, thinking },
? Lifecycle.reasoningDelta(current.lifecycle, events, active.id, text)
: Lifecycle.reasoningStart(current.lifecycle, events, active.id),
active: { ...active, thinking, thinkingUnits: accumulated },
nextContent: current.active ? current.nextContent : current.nextContent + 1,
}
}
+17 -13
View File
@@ -163,7 +163,7 @@ export const CompactionItem = Schema.Struct({
encrypted_content: Schema.String,
})
// Kept out of the baseline `InputItem` union: only the OpenAI extension accepts it.
// Kept out of the baseline `InputItem` union; extensions such as OpenAI and OpenRouter opt in.
export const ConfigurationUpdate = Schema.Struct({
type: Schema.Literal("configuration_update"),
reasoning: Schema.Struct({ effort: OpenResponsesOptions.ReasoningEffort }),
@@ -298,18 +298,22 @@ const OpenResponsesBody = Schema.Struct({
})
export type OpenResponsesBody = Schema.Schema.Type<typeof OpenResponsesBody>
export const OpenResponsesUsage = Schema.Struct({
input_tokens: Schema.optional(Schema.Number),
input_tokens_details: optionalNull(
Schema.Struct({
cached_tokens: Schema.optional(Schema.Number),
cache_write_tokens: Schema.optional(Schema.Number),
}),
),
output_tokens: Schema.optional(Schema.Number),
output_tokens_details: optionalNull(Schema.Struct({ reasoning_tokens: Schema.optional(Schema.Number) })),
total_tokens: Schema.optional(Schema.Number),
})
// Retain gateway billing extensions in provider metadata without changing normalized token accounting.
export const OpenResponsesUsage = Schema.StructWithRest(
Schema.Struct({
input_tokens: Schema.optional(Schema.Number),
input_tokens_details: optionalNull(
Schema.Struct({
cached_tokens: Schema.optional(Schema.Number),
cache_write_tokens: Schema.optional(Schema.Number),
}),
),
output_tokens: Schema.optional(Schema.Number),
output_tokens_details: optionalNull(Schema.Struct({ reasoning_tokens: Schema.optional(Schema.Number) })),
total_tokens: Schema.optional(Schema.Number),
}),
[Schema.Record(Schema.String, Schema.Unknown)],
)
type OpenResponsesUsage = Schema.Schema.Type<typeof OpenResponsesUsage>
// The spec requires `id` on every output item, but some gateways drop it from
+47 -40
View File
@@ -20,7 +20,6 @@ import {
type LLMRequest,
type MediaPart,
type ReasoningPart,
type TextPart,
type ToolCallPart,
type ToolDefinition,
} from "../schema/index.js"
@@ -45,6 +44,7 @@ const OpenAIChatCacheControl = Schema.Struct({
type: Schema.Literal("ephemeral"),
ttl: Schema.optional(Schema.String),
})
type OpenAIChatCacheControl = Schema.Schema.Type<typeof OpenAIChatCacheControl>
const OpenAIChatFunction = Schema.Struct({
name: Schema.String,
@@ -121,12 +121,15 @@ const decodeReasoningDetail = Schema.decodeUnknownOption(ReasoningDetail)
const knownReasoningDetails = (details: ReadonlyArray<unknown>) =>
details.flatMap((detail) => Option.toArray(decodeReasoningDetail(detail)))
const OpenAIChatTextContent = Schema.Struct({
type: Schema.Literal("text"),
text: Schema.String,
cache_control: Schema.optional(OpenAIChatCacheControl),
})
type OpenAIChatTextContent = Schema.Schema.Type<typeof OpenAIChatTextContent>
const OpenAIChatUserContent = Schema.Union([
Schema.Struct({
type: Schema.Literal("text"),
text: Schema.String,
cache_control: Schema.optional(OpenAIChatCacheControl),
}),
OpenAIChatTextContent,
Schema.Struct({
type: Schema.Literal("image_url"),
image_url: Schema.Struct({ url: Schema.String }),
@@ -136,6 +139,7 @@ const OpenAIChatUserContent = Schema.Union([
file: Schema.Struct({ filename: Schema.String, file_data: Schema.String }),
}),
])
type OpenAIChatUserContent = Schema.Schema.Type<typeof OpenAIChatUserContent>
const OpenAIChatMessage = Schema.Union([
Schema.Struct({
@@ -149,21 +153,19 @@ const OpenAIChatMessage = Schema.Union([
Schema.StructWithRest(
Schema.Struct({
role: Schema.Literal("assistant"),
content: Schema.NullOr(Schema.String),
content: Schema.NullOr(Schema.Union([Schema.String, Schema.Array(OpenAIChatTextContent)])),
tool_calls: optionalArray(OpenAIChatAssistantToolCall),
reasoning_content: Schema.optional(Schema.String),
reasoning: Schema.optional(Schema.String),
reasoning_text: Schema.optional(Schema.String),
reasoning_details: Schema.optional(Schema.Unknown),
cache_control: Schema.optional(OpenAIChatCacheControl),
}),
[Schema.Record(Schema.String, Schema.Unknown)],
),
Schema.Struct({
role: Schema.Literal("tool"),
tool_call_id: Schema.String,
content: Schema.String,
cache_control: Schema.optional(OpenAIChatCacheControl),
content: Schema.Union([Schema.String, Schema.Array(OpenAIChatTextContent)]),
}),
]).pipe(Schema.toTaggedUnion("role"))
type OpenAIChatMessage = Schema.Schema.Type<typeof OpenAIChatMessage>
@@ -327,9 +329,7 @@ export interface ParserState {
// OpenAI Chat wire format. Keep provider quirks here instead of leaking native
// fields into `LLMRequest`.
interface LoweringOptions {
readonly cacheControl?: (
cache: CacheHint | undefined,
) => Schema.Schema.Type<typeof OpenAIChatCacheControl> | undefined
readonly cacheControl?: (cache: CacheHint | undefined) => OpenAIChatCacheControl | undefined
readonly toolCallID?: (id: string) => string
}
@@ -415,7 +415,7 @@ const lowerUserMessage = Effect.fn("OpenAIChat.lowerUserMessage")(function* (
message: OpenAIChatRequestMessage,
options: LoweringOptions,
) {
const content: Array<Schema.Schema.Type<typeof OpenAIChatUserContent>> = []
const content: OpenAIChatUserContent[] = []
for (const part of message.content) {
if (part.type === "text") {
content.push({ type: "text", text: part.text, cache_control: options.cacheControl?.(part.cache) })
@@ -441,14 +441,14 @@ const lowerAssistantMessage = Effect.fn("OpenAIChat.lowerAssistantMessage")(func
requireReasoning: boolean,
options: LoweringOptions & { readonly providerMetadataKey: string },
) {
const content: TextPart[] = []
const content: OpenAIChatTextContent[] = []
const reasoning: ReasoningPart[] = []
const toolCalls: OpenAIChatAssistantToolCall[] = []
for (const part of message.content) {
if (!ProviderShared.supportsContent(part, ["text", "reasoning", "tool-call"]))
return yield* ProviderShared.unsupportedContent("OpenAI Chat", "assistant", ["text", "reasoning", "tool-call"])
if (part.type === "text") {
content.push(part)
content.push({ type: "text", text: part.text, cache_control: options.cacheControl?.(part.cache) })
continue
}
if (part.type === "reasoning") {
@@ -486,14 +486,15 @@ const lowerAssistantMessage = Effect.fn("OpenAIChat.lowerAssistantMessage")(func
if (reasoning.length === 0) return nativeReasoning ?? (requireReasoning ? "" : undefined)
return text
})()
const cached = message.content.findLast((part) => "cache" in part && part.cache !== undefined)
const cacheControl = options.cacheControl?.(cached && "cache" in cached ? cached.cache : undefined)
const result = {
role: "assistant" as const,
content: content.length > 0 ? content.map((part) => part.text).join("") : toolCalls.length > 0 ? null : "",
content: (() => {
if (content.some((part) => part.cache_control !== undefined)) return content
if (content.length === 0 && toolCalls.length > 0) return null
return content.map((part) => part.text).join("")
})(),
...(toolCalls.length > 0 ? { tool_calls: toolCalls } : {}),
...(details !== undefined ? { reasoning_details: details } : {}),
...(cacheControl !== undefined ? { cache_control: cacheControl } : {}),
}
if (field === undefined || reasoningText === undefined) return result
return { ...result, [field]: reasoningText }
@@ -504,33 +505,38 @@ const lowerToolMessages = Effect.fn("OpenAIChat.lowerToolMessages")(function* (
options: LoweringOptions,
) {
const messages: OpenAIChatMessage[] = []
const attachments: Array<Schema.Schema.Type<typeof OpenAIChatUserContent>> = []
const attachments: OpenAIChatUserContent[] = []
for (const part of message.content) {
if (!ProviderShared.supportsContent(part, ["tool-result"]))
return yield* ProviderShared.unsupportedContent("OpenAI Chat", "tool", ["tool-result"])
if (part.result.type !== "content") {
messages.push({
role: "tool",
tool_call_id: options.toolCallID?.(part.id) ?? part.id,
content: ProviderShared.toolResultText(part),
cache_control: options.cacheControl?.(part.cache),
})
messages.push(
toolMessage(
options.toolCallID?.(part.id) ?? part.id,
ProviderShared.toolResultText(part),
options.cacheControl?.(part.cache),
),
)
continue
}
const content: ReadonlyArray<Tool.Content> = part.result.value
const text = content.filter((item) => item.type === "text").map((item) => item.text)
messages.push({
role: "tool",
tool_call_id: options.toolCallID?.(part.id) ?? part.id,
content: text.join("\n"),
cache_control: options.cacheControl?.(part.cache),
})
messages.push(
toolMessage(options.toolCallID?.(part.id) ?? part.id, text.join("\n"), options.cacheControl?.(part.cache)),
)
const files = content.filter((item) => item.type === "file")
attachments.push(...(yield* Effect.forEach(files, (item) => lowerMedia(ProviderShared.toolFileMedia(item)))))
}
return { messages, attachments }
})
// Chat cache breakpoints belong on text content parts, not on the message itself.
const toolMessage = (toolCallID: string, text: string, cacheControl: OpenAIChatCacheControl | undefined) => ({
role: "tool" as const,
tool_call_id: toolCallID,
content: cacheControl === undefined ? text : [{ type: "text" as const, text, cache_control: cacheControl }],
})
const lowerMessage = Effect.fn("OpenAIChat.lowerMessage")(function* (
message: OpenAIChatRequestMessage,
reasoningField: string | undefined,
@@ -589,7 +595,7 @@ const lowerMessages = Effect.fn("OpenAIChat.lowerMessages")(function* (request:
if (requireAssistantAfterTool && messages.at(-1)?.role === "tool")
messages.push({ role: "assistant", content: "Done." })
}
const pendingAttachments: Array<Schema.Schema.Type<typeof OpenAIChatUserContent>> = []
const pendingAttachments: OpenAIChatUserContent[] = []
const flushAttachments = () => {
if (pendingAttachments.length === 0) return
bridgeTools()
@@ -599,24 +605,25 @@ const lowerMessages = Effect.fn("OpenAIChat.lowerMessages")(function* (request:
if (message.role === "user") bridgeTools()
if (message.role === "system") {
const part = yield* ProviderShared.wrappedSystemUpdate("OpenAI Chat", message)
const cacheControl = options.cacheControl?.(part.cache)
if (pendingAttachments.length > 0) {
messages.push({
role: "user",
content: [
...pendingAttachments.splice(0),
{ type: "text", text: part.text, cache_control: options.cacheControl?.(part.cache) },
{ type: "text", text: part.text, cache_control: cacheControl },
],
})
continue
}
const previous = messages.at(-1)
if (previous?.role === "user" && typeof previous.content === "string")
messages[messages.length - 1] = options.cacheControl?.(part.cache)
messages[messages.length - 1] = cacheControl
? {
role: "user",
content: [
{ type: "text", text: previous.content },
{ type: "text", text: part.text, cache_control: options.cacheControl(part.cache) },
{ type: "text", text: part.text, cache_control: cacheControl },
],
}
: { role: "user", content: `${previous.content}\n${part.text}` }
@@ -625,15 +632,15 @@ const lowerMessages = Effect.fn("OpenAIChat.lowerMessages")(function* (request:
role: "user",
content: [
...previous.content,
{ type: "text", text: part.text, cache_control: options.cacheControl?.(part.cache) },
{ type: "text", text: part.text, cache_control: cacheControl },
],
}
else
messages.push(
options.cacheControl?.(part.cache)
cacheControl
? {
role: "user",
content: [{ type: "text", text: part.text, cache_control: options.cacheControl(part.cache) }],
content: [{ type: "text", text: part.text, cache_control: cacheControl }],
}
: { role: "user", content: part.text },
)
@@ -124,7 +124,7 @@ const OpenAIResponsesCoreFields = {
),
}
const OpenAIResponsesBody = Schema.Struct({
export const OpenAIResponsesBody = Schema.Struct({
...OpenAIResponsesCoreFields,
stream: Schema.Literal(true),
})
@@ -0,0 +1,179 @@
export * as OpenRouterWire from "./openrouter.js"
import { Option, Schema } from "effect"
import { LLMRequest, Message, type ContentPart, type ReasoningPart } from "../../schema/index.js"
import type { OpenAIResponses } from "../openai-responses.js"
import { isRecord, ProviderShared } from "../shared.js"
const ReplayDetail = Schema.Struct({
type: Schema.String,
text: Schema.optional(Schema.String),
signature: Schema.optional(Schema.NullOr(Schema.String)),
data: Schema.optional(Schema.String),
encrypted: Schema.optional(Schema.String),
})
const decodeReplayDetail = Schema.decodeUnknownOption(ReplayDetail)
// OpenRouter's generic Responses API supports chronological effort updates only on eligible models.
export function supportsEffortUpdates(request: LLMRequest) {
if (request.providerOptions?.contextManagement !== undefined) return false
if (request.providerOptions?.truncation === "auto" || request.http?.body?.truncation === "auto") return false
if (Schema.is(Schema.Struct({ mode: Schema.Literal("pro") }))(request.providerOptions?.reasoning)) return false
if (Schema.is(Schema.Struct({ mode: Schema.Literal("pro") }))(request.http?.body?.reasoning)) return false
return (
request.model.compatibility?.supportsEffortUpdates ??
/^~?openai\/(?:gpt-6-(?:astra|sol|luna)|gpt-6\.1-sol)$/i.test(request.model.id)
)
}
export function nativeRequest(request: LLMRequest, format: "responses" | "messages") {
const options = request.providerOptions ?? {}
const reasoning = isRecord(options.reasoning)
? fitReasoning(options.reasoning, request.generation?.maxTokens)
: undefined
const disabled = reasoning?.enabled === false || reasoning?.effort === "none"
return LLMRequest.update(request, {
providerOptions:
format === "responses"
? {
...options,
reasoningEffort:
options.reasoningEffort ?? (typeof reasoning?.effort === "string" ? reasoning.effort : undefined),
}
: {
...options,
effort:
options.effort ?? (typeof reasoning?.effort === "string" && !disabled ? reasoning.effort : undefined),
thinking:
options.thinking ??
(() => {
if (disabled) return { type: "disabled" }
if (typeof reasoning?.max_tokens === "number")
return { type: "enabled", budget_tokens: reasoning.max_tokens }
if (reasoning?.enabled === true || typeof reasoning?.effort === "string")
return { type: "adaptive", ...(reasoning.exclude === true ? { display: "omitted" } : {}) }
})(),
},
messages: request.messages.map((message) => {
if (
!message.content.some(
(part) => part.type === "reasoning" && part.providerMetadata?.openrouter?.reasoningDetails !== undefined,
)
)
return message
return Message.make({
...message,
// Chat stores signatures in reasoning_details; native APIs require individual signed blocks/items.
content: message.content.flatMap<ContentPart>((part) => {
if (part.type !== "reasoning") return [part]
const details = part.providerMetadata?.openrouter?.reasoningDetails
if (!Array.isArray(details)) return [part]
const replay = details.flatMap((detail) => Option.toArray(decodeReplayDetail(detail)))
if (format === "responses") {
const encrypted = replay.find(
(detail) => detail.type === "reasoning.encrypted" || detail.type === "encrypted",
)
return [
{
...part,
providerMetadata: {
...part.providerMetadata,
openrouter: {
...part.providerMetadata?.openrouter,
reasoningEncryptedContent:
part.providerMetadata?.openrouter?.reasoningEncryptedContent ??
encrypted?.data ??
encrypted?.encrypted,
},
},
},
]
}
const blocks = replay.flatMap<ReasoningPart>((detail) => {
const metadata = (() => {
if (detail.type === "reasoning.text" && detail.signature) return { signature: detail.signature }
if (detail.type === "reasoning.encrypted" && detail.data) return { redactedData: detail.data }
})()
if (!metadata) return []
return [
{
...part,
text: metadata.signature ? (detail.text ?? part.text) : "",
providerMetadata: {
...part.providerMetadata,
openrouter: { ...part.providerMetadata?.openrouter, ...metadata },
},
},
]
})
return blocks.length > 0 ? blocks : [part]
}),
})
}),
})
}
export function responsesOptions(request: LLMRequest, body: OpenAIResponses.OpenAIResponsesBody) {
const { usage: _, ...options } = bodyOptions(request.providerOptions, request.generation?.maxTokens)
return {
...options,
...body,
store: false as const,
// Native lowering may freeze effort at the history baseline; keep it instead of the current effort.
...(options.reasoning || body.reasoning
? {
reasoning: {
...options.reasoning,
...body.reasoning,
effort: body.reasoning?.effort,
summary:
body.reasoning?.summary ??
(typeof options.reasoning?.summary === "string" ? options.reasoning.summary : undefined),
},
}
: {}),
...(options.text || body.text ? { text: { ...options.text, ...body.text } } : {}),
}
}
export const bodyOptions = (input: unknown, maxTokens: number | undefined) => {
const openrouter = isRecord(input) ? input : {}
const {
usage,
models,
provider,
plugins,
web_search_options,
debug,
user,
reasoning,
text,
promptCacheKey,
...options
} = openrouter
return {
...options,
...(usage === undefined || usage === true
? { usage: { include: true } }
: usage === false
? { usage: { include: false } }
: isRecord(usage)
? { usage }
: {}),
...(Array.isArray(models) ? { models } : {}),
...(isRecord(provider) ? { provider } : {}),
...(Array.isArray(plugins) ? { plugins } : {}),
...(isRecord(web_search_options) ? { web_search_options } : {}),
...(isRecord(debug) ? { debug } : {}),
...(typeof user === "string" ? { user } : {}),
...(isRecord(reasoning) ? { reasoning: fitReasoning(reasoning, maxTokens) } : {}),
...(isRecord(text) ? { text } : {}),
}
}
// Anthropic and Alibaba require the thinking budget to remain below the output limit.
function fitReasoning(reasoning: Record<string, unknown>, maxTokens: number | undefined) {
return typeof reasoning.max_tokens === "number"
? { ...reasoning, max_tokens: ProviderShared.fitThinkingBudget(reasoning.max_tokens, maxTokens, 1_024) }
: reasoning
}
+4
View File
@@ -16,9 +16,13 @@ import {
const patterns = [
/prompt is too long/i,
/input is too long for requested model/i,
// Cloudflare Workers AI reports this as HTTP 413.
/exceeded this model context window limit/i,
/exceeds the context window/i,
/exceeds (?:the )?(?:model'?s )?maximum context length(?: of [\d,]+ tokens?|\s*\([\d,]+\))/i,
/input token count.*exceeds the maximum/i,
// Amazon Nova on Bedrock reports this as a mid-stream validationException.
/number of input tokens exceeds maximum length/i,
/tokens in request more than max tokens allowed/i,
/maximum prompt length is \d+/i,
/reduce the length of the messages/i,
+95 -39
View File
@@ -7,8 +7,12 @@ import { HttpOptions, ProviderID, type CacheHint, type ModelID, type OpenString
import type { ProviderPackage } from "../provider-package.js"
import { SystemOne } from "../experimental/system-one.js"
import { OpenAIChat } from "../protocols/openai-chat.js"
import { OpenAIResponses } from "../protocols/openai-responses.js"
import { AnthropicMessages } from "../protocols/anthropic-messages.js"
import { Framing } from "../route/framing.js"
import { JsonObject } from "../protocols/shared.js"
import { newBreakpoints, ttlBucket } from "../protocols/utils/cache.js"
import { isRecord, ProviderShared } from "../protocols/shared.js"
import { OpenRouterWire } from "../protocols/utils/openrouter.js"
export const id = ProviderID.make("openrouter")
const baseURL = "https://openrouter.ai/api/v1"
@@ -54,6 +58,7 @@ export interface OpenRouterOptions {
readonly plugins?: ReadonlyArray<OpenRouterPlugin>
readonly provider?: OpenRouterProviderRouting
readonly reasoning?: Readonly<{
[key: string]: unknown
enabled?: boolean
exclude?: boolean
effort?: OpenString<"none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max">
@@ -95,6 +100,18 @@ const OpenRouterBody = Schema.StructWithRest(Schema.Struct(OpenAIChat.bodyFields
])
export type OpenRouterBody = Schema.Schema.Type<typeof OpenRouterBody>
const OpenRouterResponsesBody = Schema.StructWithRest(
Schema.Struct({
...OpenAIResponses.OpenAIResponsesBody.fields,
store: Schema.Literal(false),
reasoning: Schema.optional(JsonObject),
text: Schema.optional(JsonObject),
}),
[JsonObject],
)
const OpenRouterMessagesBody = Schema.StructWithRest(AnthropicMessages.AnthropicMessagesBody, [JsonObject])
type OpenRouterMessagesBody = Schema.Schema.Type<typeof OpenRouterMessagesBody>
export const protocol = Protocol.make({
id: "openrouter-chat",
body: {
@@ -123,7 +140,7 @@ export const protocol = Protocol.make({
return {
...body,
messages,
...bodyOptions(request.providerOptions, request.generation?.maxTokens),
...OpenRouterWire.bodyOptions(request.providerOptions, request.generation?.maxTokens),
} as OpenRouterBody
}),
),
@@ -131,6 +148,37 @@ export const protocol = Protocol.make({
stream: OpenAIChat.protocol.stream,
})
const responsesProtocol = Protocol.make({
...OpenAIResponses.protocol,
id: "openrouter-responses",
supportsEffortUpdates: OpenRouterWire.supportsEffortUpdates,
body: {
schema: OpenRouterResponsesBody,
from: (request) =>
OpenAIResponses.protocol.body
.from(OpenRouterWire.nativeRequest(request, "responses"))
.pipe(Effect.map((body) => OpenRouterWire.responsesOptions(request, body))),
},
})
const messagesProtocol = Protocol.make({
...AnthropicMessages.protocol,
id: "openrouter-messages",
body: {
schema: OpenRouterMessagesBody,
from: (request) =>
AnthropicMessages.protocol.body.from(OpenRouterWire.nativeRequest(request, "messages")).pipe(
Effect.map((body) => {
const {
reasoning: _,
usage: _usage,
...options
} = OpenRouterWire.bodyOptions(request.providerOptions, body.max_tokens)
return { ...options, ...body }
}),
),
},
})
const cacheControl = () => {
const breakpoints = newBreakpoints(4)
return (cache: CacheHint | undefined) => {
@@ -143,36 +191,6 @@ const cacheControl = () => {
}
}
// OpenRouter forwards `reasoning.max_tokens` as the upstream thinking budget. Upstreams such as Anthropic and Alibaba
// reject one that is not below the output limit; 1,024 is Anthropic's minimum budget.
const fitReasoning = (reasoning: Record<string, unknown>, maxTokens: number | undefined) =>
typeof reasoning.max_tokens === "number"
? { ...reasoning, max_tokens: ProviderShared.fitThinkingBudget(reasoning.max_tokens, maxTokens, 1_024) }
: reasoning
const bodyOptions = (input: unknown, maxTokens: number | undefined) => {
const openrouter = isRecord(input) ? input : {}
const { usage, models, provider, plugins, web_search_options, debug, user, reasoning, promptCacheKey, ...options } =
openrouter
return {
...options,
...(usage === undefined || usage === true
? { usage: { include: true } }
: usage === false
? { usage: { include: false } }
: isRecord(usage)
? { usage }
: {}),
...(Array.isArray(models) ? { models } : {}),
...(isRecord(provider) ? { provider } : {}),
...(Array.isArray(plugins) ? { plugins } : {}),
...(isRecord(web_search_options) ? { web_search_options } : {}),
...(isRecord(debug) ? { debug } : {}),
...(typeof user === "string" ? { user } : {}),
...(isRecord(reasoning) ? { reasoning: fitReasoning(reasoning, maxTokens) } : {}),
}
}
export const route = Route.make({
id: ADAPTER,
provider: id,
@@ -182,19 +200,45 @@ export const route = Route.make({
framing: OpenAIChat.framing,
})
export const routes = [route]
const responsesRoute = Route.make({
id: "openrouter-responses",
provider: id,
providerMetadataKey: "openrouter",
protocol: responsesProtocol,
endpoint: Endpoint.path("/responses", { baseURL }),
framing: Framing.sse,
defaults: { providerOptions: { store: false, include: ["reasoning.encrypted_content"] } },
})
const messagesRoute = Route.make({
id: "openrouter-messages",
provider: id,
providerMetadataKey: "openrouter",
protocol: messagesProtocol,
endpoint: Endpoint.path("/messages", { baseURL }),
transport: AnthropicMessages.transport<OpenRouterMessagesBody>(),
})
const configuredRoute = (input: LanguageModelOptions) => {
export const routes = [route, responsesRoute, messagesRoute]
const routeOptions = (input: LanguageModelOptions) => {
const { apiKey: _, auth: _auth, baseURL: endpoint, ...rest } = input
return route.with({
return {
...rest,
endpoint: { baseURL: endpoint ?? baseURL },
auth: AuthOptions.bearer(input, "OPENROUTER_API_KEY"),
})
}
}
export const configure = (input: LanguageModelOptions = {}) => {
const route = configuredRoute(input)
const options = routeOptions(input)
const chatRoute = route.with(options)
const openResponses = responsesRoute.with(options)
const anthropicMessages = messagesRoute.with(options)
const chat = (modelID: string | ModelID) =>
chatRoute.model<OpenRouterProviderOptionsInput>({ id: modelID, compatibility: { supportsPromptCacheKey: true } })
const responses = (modelID: string | ModelID) => openResponses.model<OpenRouterProviderOptionsInput>({ id: modelID })
const messages = (modelID: string | ModelID) =>
anthropicMessages.model<OpenRouterProviderOptionsInput>({ id: modelID })
const evaluation = (modelID: string | ModelID) =>
SystemOne.model<OpenRouterEvaluationOptions>({
id: modelID,
@@ -207,14 +251,24 @@ export const configure = (input: LanguageModelOptions = {}) => {
})
return {
id,
model: (modelID: string | ModelID) =>
route.model<OpenRouterProviderOptionsInput>({ id: modelID, compatibility: { supportsPromptCacheKey: true } }),
model: (modelID: string | ModelID) => {
const model = String(modelID).replace(/^~/, "")
if (model.startsWith("anthropic/")) return messages(modelID)
if (["openai/", "x-ai/", "meta/"].some((prefix) => model.startsWith(prefix))) return responses(modelID)
return chat(modelID)
},
chat,
responses,
messages,
experimental: { evaluation },
configure,
}
}
export const provider = configure()
export const chat = provider.chat
export const responses = provider.responses
export const messages = provider.messages
export const experimental = provider.experimental
export const model: ProviderPackage.Definition<Settings, OpenRouterProviderOptionsInput>["model"] = (
modelID,
@@ -227,3 +281,5 @@ export const model: ProviderPackage.Definition<Settings, OpenRouterProviderOptio
http: body === undefined ? undefined : { body: { ...body } },
providerOptions,
}).model(modelID)
export * as OpenRouter from "./openrouter.js"
+39 -1
View File
@@ -3,7 +3,17 @@ import { Effect } from "effect"
import { CacheHint, LLM, Message } from "../src/index.js"
import { Auth } from "../src/route.js"
import { compileRequest } from "../src/route/client.js"
import { AmazonBedrock, AnthropicCompatible, GoogleVertexMessages } from "../src/providers.js"
import {
Alibaba,
AmazonBedrock,
AnthropicCompatible,
CloudflareAIGateway,
GoogleVertexMessages,
Meta,
MiniMax,
Moonshot,
ZAICodingPlan,
} from "../src/providers.js"
import * as AnthropicMessages from "../src/protocols/anthropic-messages.js"
import * as Gemini from "../src/protocols/gemini.js"
import * as OpenAIChat from "../src/protocols/openai-chat.js"
@@ -127,6 +137,34 @@ describe("applyCachePolicy", () => {
}),
)
const messagesModels = [
["alibaba-messages", Alibaba.configure({ region: "ap-southeast-1", apiKey: "test" }).messages("qwen3.8-max")],
[
"cloudflare-ai-gateway-messages",
CloudflareAIGateway.configure({ accountId: "test", gatewayId: "test", apiKey: "test" }).model(
"anthropic/claude-sonnet-4-6",
),
],
["meta-messages", Meta.configure({ apiKey: "test" }).messages("muse-spark-1.3")],
["minimax-messages", MiniMax.configure({ apiKey: "test" }).model("MiniMax-M3")],
["moonshot-messages", Moonshot.configure({ apiKey: "test" }).messages("kimi-k3")],
["zai-coding-messages", ZAICodingPlan.configure({ apiKey: "test" }).messages("glm-5.3")],
] as const
messagesModels.forEach(([route, model]) =>
it.effect(`'auto' emits cache markers on ${route}`, () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(LLM.request({ model, system: "Sys", prompt: "hi" }))
expect(prepared.route).toBe(route)
expect(prepared.body).toMatchObject({
system: [{ type: "text", text: "Sys", cache_control: { type: "ephemeral" } }],
messages: [{ role: "user", content: [{ type: "text", text: "hi", cache_control: { type: "ephemeral" } }] }],
})
}),
),
)
it.effect("'auto' is a no-op on OpenAI (implicit caching protocol)", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
@@ -10,7 +10,7 @@
"usage"
],
"name": "alibaba-messages/qwen-3-7-plus-streams-thinking-disabled",
"recordedAt": "2026-09-08T03:10:57.819Z"
"recordedAt": "2026-09-29T05:26:24.450Z"
},
"interactions": [
{
@@ -21,14 +21,14 @@
"headers": {
"content-type": "application/json"
},
"body": "{\"model\":\"qwen3.7-plus\",\"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"What is 173 multiplied by 219? Reply with only the final integer.\"}]}],\"stream\":true,\"max_tokens\":4096,\"thinking\":{\"type\":\"disabled\"}}"
"body": "{\"model\":\"qwen3.7-plus\",\"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"What is 173 multiplied by 219? Reply with only the final integer.\",\"cache_control\":{\"type\":\"ephemeral\"}}]}],\"stream\":true,\"max_tokens\":4096,\"thinking\":{\"type\":\"disabled\"}}"
},
"response": {
"status": 200,
"headers": {
"content-type": "text/event-stream"
},
"body": "event:ping\ndata:{\"type\":\"ping\"}\n\nevent:message_start\ndata:{\"message\":{\"model\":\"qwen3.7-plus\",\"id\":\"msg_c4d58b4f-a61d-9d0c-a9e4-cb45d34b1120\",\"role\":\"assistant\",\"type\":\"message\",\"content\":[],\"usage\":{\"input_tokens\":20,\"output_tokens\":0}},\"type\":\"message_start\"}\n\nevent:content_block_start\ndata:{\"type\":\"content_block_start\",\"content_block\":{\"type\":\"text\",\"text\":\"\"},\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"text_delta\",\"text\":\"3\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"text_delta\",\"text\":\"7887\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_stop\ndata:{\"type\":\"content_block_stop\",\"index\":0}\n\nevent:message_delta\ndata:{\"delta\":{\"stop_reason\":\"end_turn\"},\"type\":\"message_delta\",\"usage\":{\"output_tokens\":5,\"cache_creation_input_tokens\":0,\"input_tokens\":32,\"cache_read_input_tokens\":0,\"prompt_tokens_details\":{\"cached_tokens\":0}}}\n\nevent:message_stop\ndata:{\"type\":\"message_stop\"}\n\n"
"body": "event:ping\ndata:{\"type\":\"ping\"}\n\nevent:message_start\ndata:{\"message\":{\"model\":\"qwen3.7-plus\",\"id\":\"msg_39067759-5f01-9b3a-b2c1-09111797f7b5\",\"role\":\"assistant\",\"type\":\"message\",\"content\":[],\"usage\":{\"input_tokens\":20,\"output_tokens\":0}},\"type\":\"message_start\"}\n\nevent:content_block_start\ndata:{\"type\":\"content_block_start\",\"content_block\":{\"type\":\"text\",\"text\":\"\"},\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"text_delta\",\"text\":\"3\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"text_delta\",\"text\":\"788\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"text_delta\",\"text\":\"7\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_stop\ndata:{\"type\":\"content_block_stop\",\"index\":0}\n\nevent:message_delta\ndata:{\"delta\":{\"stop_reason\":\"end_turn\"},\"type\":\"message_delta\",\"usage\":{\"cache_creation\":{\"ephemeral_5m_input_tokens\":0},\"output_tokens\":5,\"cache_creation_input_tokens\":0,\"input_tokens\":32,\"cache_read_input_tokens\":0,\"prompt_tokens_details\":{\"cached_tokens\":0}}}\n\nevent:message_stop\ndata:{\"type\":\"message_stop\"}\n\n"
}
}
]
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -9,7 +9,7 @@
"structured-output"
],
"name": "alibaba-messages/qwen-3-8-max-follows-a-json-schema",
"recordedAt": "2026-09-08T03:11:30.530Z"
"recordedAt": "2026-09-29T05:26:41.466Z"
},
"interactions": [
{
@@ -20,14 +20,14 @@
"headers": {
"content-type": "application/json"
},
"body": "{\"model\":\"qwen3.8-max\",\"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"Return a JSON object with one key \\\"city\\\" set to the capital city of France.\"}]}],\"stream\":true,\"max_tokens\":1024,\"thinking\":{\"type\":\"disabled\"},\"output_config\":{\"format\":{\"type\":\"json_schema\",\"schema\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"],\"additionalProperties\":false}}}}"
"body": "{\"model\":\"qwen3.8-max\",\"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"Return a JSON object with one key \\\"city\\\" set to the capital city of France.\",\"cache_control\":{\"type\":\"ephemeral\"}}]}],\"stream\":true,\"max_tokens\":1024,\"thinking\":{\"type\":\"disabled\"},\"output_config\":{\"format\":{\"type\":\"json_schema\",\"schema\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"],\"additionalProperties\":false}}}}"
},
"response": {
"status": 200,
"headers": {
"content-type": "text/event-stream"
},
"body": "event:ping\ndata:{\"type\":\"ping\"}\n\nevent:message_start\ndata:{\"message\":{\"model\":\"qwen3.8-max\",\"id\":\"msg_16e067c1-984b-94d3-9abb-11792d794271\",\"role\":\"assistant\",\"type\":\"message\",\"content\":[],\"usage\":{\"input_tokens\":18,\"output_tokens\":0}},\"type\":\"message_start\"}\n\nevent:content_block_start\ndata:{\"type\":\"content_block_start\",\"content_block\":{\"type\":\"text\",\"text\":\"\"},\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"text_delta\",\"text\":\"{\\\"\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"text_delta\",\"text\":\"city\\\":\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"text_delta\",\"text\":\" \\\"Paris\\\"}\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_stop\ndata:{\"type\":\"content_block_stop\",\"index\":0}\n\nevent:message_delta\ndata:{\"delta\":{\"stop_reason\":\"end_turn\"},\"type\":\"message_delta\",\"usage\":{\"output_tokens\":6,\"cache_creation_input_tokens\":0,\"input_tokens\":32,\"cache_read_input_tokens\":0,\"prompt_tokens_details\":{\"cached_tokens\":0}}}\n\nevent:message_stop\ndata:{\"type\":\"message_stop\"}\n\n"
"body": "event:ping\ndata:{\"type\":\"ping\"}\n\nevent:message_start\ndata:{\"message\":{\"model\":\"qwen3.8-max\",\"id\":\"msg_61ee9b9d-a224-9b69-98f7-9f9ea3937b41\",\"role\":\"assistant\",\"type\":\"message\",\"content\":[],\"usage\":{\"input_tokens\":18,\"output_tokens\":0}},\"type\":\"message_start\"}\n\nevent:content_block_start\ndata:{\"type\":\"content_block_start\",\"content_block\":{\"type\":\"text\",\"text\":\"\"},\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"text_delta\",\"text\":\"{\\\"\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"text_delta\",\"text\":\"city\\\":\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"type\":\"text_delta\",\"text\":\" \\\"Paris\\\"}\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_stop\ndata:{\"type\":\"content_block_stop\",\"index\":0}\n\nevent:message_delta\ndata:{\"delta\":{\"stop_reason\":\"end_turn\"},\"type\":\"message_delta\",\"usage\":{\"cache_creation\":{\"ephemeral_5m_input_tokens\":0},\"output_tokens\":6,\"cache_creation_input_tokens\":0,\"input_tokens\":32,\"cache_read_input_tokens\":0,\"prompt_tokens_details\":{\"cached_tokens\":0}}}\n\nevent:message_stop\ndata:{\"type\":\"message_stop\"}\n\n"
}
}
]
@@ -10,7 +10,7 @@
"tool-choice"
],
"name": "alibaba-messages/qwen-3-8-max-obeys-named-tool-choice",
"recordedAt": "2026-09-08T03:11:06.590Z"
"recordedAt": "2026-09-29T05:26:40.212Z"
},
"interactions": [
{
@@ -21,14 +21,14 @@
"headers": {
"content-type": "application/json"
},
"body": "{\"model\":\"qwen3.8-max\",\"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"Find the current weather in Paris.\"}]}],\"tools\":[{\"name\":\"get_weather\",\"description\":\"Get weather in a city\",\"input_schema\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\",\"enum\":[\"Paris\"]}},\"required\":[\"city\"]}}],\"tool_choice\":{\"type\":\"tool\",\"name\":\"get_weather\"},\"stream\":true,\"max_tokens\":4096,\"thinking\":{\"type\":\"disabled\"}}"
"body": "{\"model\":\"qwen3.8-max\",\"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"Find the current weather in Paris.\",\"cache_control\":{\"type\":\"ephemeral\"}}]}],\"tools\":[{\"name\":\"get_weather\",\"description\":\"Get weather in a city\",\"input_schema\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\",\"enum\":[\"Paris\"]}},\"required\":[\"city\"]},\"cache_control\":{\"type\":\"ephemeral\"}}],\"tool_choice\":{\"type\":\"tool\",\"name\":\"get_weather\"},\"stream\":true,\"max_tokens\":4096,\"thinking\":{\"type\":\"disabled\"}}"
},
"response": {
"status": 200,
"headers": {
"content-type": "text/event-stream"
},
"body": "event:ping\ndata:{\"type\":\"ping\"}\n\nevent:message_start\ndata:{\"message\":{\"model\":\"qwen3.8-max\",\"id\":\"msg_0e8f1abf-f2bc-9a86-a6a7-14804ac17eac\",\"role\":\"assistant\",\"type\":\"message\",\"content\":[],\"usage\":{\"input_tokens\":45,\"output_tokens\":0}},\"type\":\"message_start\"}\n\nevent:content_block_start\ndata:{\"type\":\"content_block_start\",\"content_block\":{\"name\":\"get_weather\",\"input\":{},\"id\":\"toolu_cf9cab33261f4709ae096d8a\",\"type\":\"tool_use\"},\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"partial_json\":\"\",\"type\":\"input_json_delta\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"partial_json\":\"{\\\"city\\\": \\\"Paris\",\"type\":\"input_json_delta\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"partial_json\":\"\\\"\",\"type\":\"input_json_delta\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"partial_json\":\"}\",\"type\":\"input_json_delta\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_stop\ndata:{\"type\":\"content_block_stop\",\"index\":0}\n\nevent:message_delta\ndata:{\"delta\":{\"stop_reason\":\"end_turn\"},\"type\":\"message_delta\",\"usage\":{\"output_tokens\":19,\"cache_creation_input_tokens\":0,\"input_tokens\":288,\"cache_read_input_tokens\":0,\"prompt_tokens_details\":{\"cached_tokens\":0}}}\n\nevent:message_stop\ndata:{\"type\":\"message_stop\"}\n\n"
"body": "event:ping\ndata:{\"type\":\"ping\"}\n\nevent:message_start\ndata:{\"message\":{\"model\":\"qwen3.8-max\",\"id\":\"msg_dab3e1c5-e75e-978d-902e-1b0a82918cff\",\"role\":\"assistant\",\"type\":\"message\",\"content\":[],\"usage\":{\"input_tokens\":45,\"output_tokens\":0}},\"type\":\"message_start\"}\n\nevent:content_block_start\ndata:{\"type\":\"content_block_start\",\"content_block\":{\"name\":\"get_weather\",\"input\":{},\"id\":\"toolu_9a1c3c06a04744cc97df33a2\",\"type\":\"tool_use\"},\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"partial_json\":\"\",\"type\":\"input_json_delta\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"partial_json\":\"{\\\"city\\\": \\\"Paris\",\"type\":\"input_json_delta\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"partial_json\":\"\\\"\",\"type\":\"input_json_delta\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_delta\ndata:{\"delta\":{\"partial_json\":\"}\",\"type\":\"input_json_delta\"},\"type\":\"content_block_delta\",\"index\":0}\n\nevent:content_block_stop\ndata:{\"type\":\"content_block_stop\",\"index\":0}\n\nevent:message_delta\ndata:{\"delta\":{\"stop_reason\":\"end_turn\"},\"type\":\"message_delta\",\"usage\":{\"cache_creation\":{\"ephemeral_5m_input_tokens\":0},\"output_tokens\":19,\"cache_creation_input_tokens\":0,\"input_tokens\":288,\"cache_read_input_tokens\":0,\"prompt_tokens_details\":{\"cached_tokens\":0}}}\n\nevent:message_stop\ndata:{\"type\":\"message_stop\"}\n\n"
}
}
]
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -2,9 +2,16 @@
"version": 1,
"metadata": {
"model": "muse-spark-1.3",
"tags": ["prefix:meta-messages", "provider:meta", "protocol:meta-messages", "tool", "tool-loop", "reasoning"],
"tags": [
"prefix:meta-messages",
"provider:meta",
"protocol:meta-messages",
"tool",
"tool-loop",
"reasoning"
],
"name": "meta-messages/replays-encrypted-thinking-through-a-tool-loop",
"recordedAt": "2026-09-07T17:27:03.540Z"
"recordedAt": "2026-09-29T05:29:52.700Z"
},
"interactions": [
{
@@ -15,14 +22,14 @@
"headers": {
"content-type": "application/json"
},
"body": "{\"model\":\"muse-spark-1.3\",\"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"Look up the current weather in Paris using lookup_weather. After receiving the result, report Paris's weather in one short sentence.\"}]}],\"tools\":[{\"name\":\"lookup_weather\",\"description\":\"Look up current weather\",\"input_schema\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\",\"enum\":[\"Paris\"]}},\"required\":[\"city\"],\"additionalProperties\":false}}],\"tool_choice\":{\"type\":\"auto\"},\"stream\":true,\"max_tokens\":1024,\"thinking\":{\"type\":\"adaptive\",\"display\":\"omitted\"},\"output_config\":{\"effort\":\"low\"}}"
"body": "{\"model\":\"muse-spark-1.3\",\"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"Look up the current weather in Paris using lookup_weather. After receiving the result, report Paris's weather in one short sentence.\",\"cache_control\":{\"type\":\"ephemeral\"}}]}],\"tools\":[{\"name\":\"lookup_weather\",\"description\":\"Look up current weather\",\"input_schema\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\",\"enum\":[\"Paris\"]}},\"required\":[\"city\"],\"additionalProperties\":false},\"cache_control\":{\"type\":\"ephemeral\"}}],\"tool_choice\":{\"type\":\"auto\"},\"stream\":true,\"max_tokens\":1024,\"thinking\":{\"type\":\"adaptive\",\"display\":\"omitted\"},\"output_config\":{\"effort\":\"low\"}}"
},
"response": {
"status": 200,
"headers": {
"content-type": "text/event-stream"
},
"body": "event: message_start\ndata: {\"message\":{\"content\":[],\"id\":\"msg_6a9ef3e5a96d9a155dd4463b\",\"model\":\"muse-spark-1.3\",\"role\":\"assistant\",\"stop_reason\":null,\"stop_sequence\":null,\"type\":\"message\",\"usage\":{\"input_tokens\":0,\"output_tokens\":0}},\"type\":\"message_start\"}\n\nevent: content_block_start\ndata: {\"content_block\":{\"data\":\"Q-PaDgGyJ4hIKk41uslnSV0PGvrTkwJ5D-t5skfjtkIt-ABXsehMcLKJJ8RJHRKW2-XhkpPrex0aOkIdqWl99vpCgtOHIFFaSc4b3oGxA8XDx4T_2aKANfYrR1DYwYzGe6ZZ-DQnU0bnpVUzCcXkghkLdyTcJr2p8cvVo1rymFpB0wZsbQRBhxCMR6PrY4i0aOTe8_waq_Po1a4l3YzKnzZIIvYP090o5kltv7MAwqChXjQeTJlKq7ECFa6HVeoTa43oiLmoD2Bzih0VfvBvQfbSh2uDypRM7-f65sp35-VuZfwsM33ZTvefzDbd8zd0D50I6wcybsj8gulDhWWpY7cxoN1Xasy-ImvisACGeppIN66maIFq5dMT2s8_CQ8a2EQ0hw9kwJaIiygZCdRofs-T1yaGWvnQxL6MpNh6SXux9T1ZfuexNrXOhz5cR3_s1euhq3-hI3GDCUDLAtkjiigSE3w6PExrwHTHnvkuAOTP0Lw7DH4NtKv-RFFeZaQOk_0Bi2EV43tR0Hq_chtEDiSpoezDjexPWfdZvhRIaO6PwyJaQ8jVOaLUr6bh6jQ3IM19lv6R7lY4Neno8fLxMwTbf7v7q02lfhOaZe3jR1fqOFUq8e4VXV76_rJBgzThqPNqO8OI_betgX5d2KMcwWAkaZjmrP3rxA7g5xp_Q7bkFiBaaTd3HbYIk_cKbtLOS6TIIphb4SX6ADA2qJ0zoo98P2NOceZODT2WrLjKCHl5dT6b9l2feH9m21pW576ULfKhVSMzuy0cmmWI7rv2P4q-2Fw2klsDVJAc6q22bFjfDgzhybKuhuM_p1SYb8aswNrgggV-cqHWDpF1FdVpL5fHMO74l1uD8Sj_9wuuD0asMQustuvsnYq2EdI0lLvONFMApWCU0s3QA8_P0Iyf0YoCZOm5QGnA7l3O5nAPkFF4kxLSsgRe9zuP6A4oKBDLN8EHwZ3pB4WZGWpUT12cJxplmT3_n8xKaMgaz13qs27uvUm3wMI1seyfpkMwUPHmt1ftpk9f_1OAg3fvQgIkWPTp6K9NzVzEry6SP7zbPfls8yn529Qrbki7ZM_oz3xEvDg367ZCe97eVnjiqRrYsYM2MmDsQq9lUwkgXb84kpK6a4pEzX9NDvhTqhw7RI6RL9E-2Ki4ciUfHv5LVTZ0rNweCo6SYXC11f8FCeObIW_Esr2mqYpORFS4SDMU-4I1HQfD9z6cPjL22APpv9a7Pp1phI8x_aQZkPU7LkyBWyVNDeusI7wq_LC0UgJ0vCAguKcAvN7gxwpCo5MEEZivRuYBldEMwRo4he2WQQke9KEokqREdlPF3Lvaav0fqPfXqhJyHAq8bYBHWdzMjDJ_dJwhdbLQ-iNLmrK5JNg2vPDglWSznBXbrSX4iyMFk1Ni2Y1SQUIOOxS_InIwCg\",\"type\":\"redacted_thinking\"},\"index\":0,\"type\":\"content_block_start\"}\n\nevent: content_block_stop\ndata: {\"index\":0,\"type\":\"content_block_stop\"}\n\nevent: content_block_start\ndata: {\"content_block\":{\"text\":\"\",\"type\":\"text\"},\"index\":1,\"type\":\"content_block_start\"}\n\nevent: content_block_delta\ndata: {\"delta\":{\"text\":\"I'll look up the current weather in Paris.\",\"type\":\"text_delta\"},\"index\":1,\"type\":\"content_block_delta\"}\n\nevent: content_block_stop\ndata: {\"index\":1,\"type\":\"content_block_stop\"}\n\nevent: content_block_start\ndata: {\"content_block\":{\"id\":\"call_01a07ce8bb767c109c12f0a202e2ac19\",\"input\":{},\"name\":\"lookup_weather\",\"type\":\"tool_use\"},\"index\":2,\"type\":\"content_block_start\"}\n\nevent: content_block_delta\ndata: {\"delta\":{\"partial_json\":\"{\\\"city\\\":\\\"Paris\\\"}\",\"type\":\"input_json_delta\"},\"index\":2,\"type\":\"content_block_delta\"}\n\nevent: content_block_stop\ndata: {\"index\":2,\"type\":\"content_block_stop\"}\n\nevent: message_delta\ndata: {\"delta\":{\"stop_reason\":\"tool_use\",\"stop_sequence\":null},\"type\":\"message_delta\",\"usage\":{\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":113,\"input_tokens\":451,\"output_tokens\":155,\"output_tokens_details\":{\"thinking_tokens\":87}}}\n\nevent: message_stop\ndata: {\"type\":\"message_stop\"}\n\n"
"body": "event: message_start\ndata: {\"message\":{\"content\":[],\"id\":\"msg_6abb4ccec3772e65cd44417d\",\"model\":\"muse-spark-1.3\",\"role\":\"assistant\",\"stop_reason\":null,\"stop_sequence\":null,\"type\":\"message\",\"usage\":{\"input_tokens\":0,\"output_tokens\":0}},\"type\":\"message_start\"}\n\nevent: content_block_start\ndata: {\"content_block\":{\"data\":\"Q-PaDgFT_IXY3RXUUduHqe1_WSgTF5_gMh29lCBV6vhfXW34eU6nJO7gUfRPczMK4nZfQhrqQewRujwjtNKR69DHHqTP35Q0jDnuZ4GZX9qBZT1OH9JhcMl1FxvccpatEcuH2Nju9Rf12KFHAOm9eKK6wFiaZgEHcT1wPbRYVZVZ8VxLeKk-ED3wkmWFhUFJRLSgJguWBMvEMbtIUlAKsJ6qkUxClfQKMm3eft1yf9ajseT32eloGzBE48IhyXu6tpeUDs9UatFgZOHKtHmvtKsPqRw-ddbimLnMtdkexwa8xR7EVTVuwBezunqhssfYAdR4c4t6VDpACh2UL3LcVUF9b0reiwpUfdpYrwjpB3bTCWyzGjwvIwGeuoR6VrWHz5nqVbt3bBprJXqJ4IRLKAXFFlMhClkz0MFNPBufcXSdh9i9NjbNp5c8lXCSkhMfhYX9B9KbQKl3DxUCjEY_VZk0nQJwt9zQVwTzWPzl9fTkBWrdebpXCg-z4lPmU5GLyGY3MlF24eDoSfJfqibF1HHqztluSHi4XEulrwMh4dpZmLhlLxlFxwnC8LjY_Rg8uOyzeoCzsMj-R9WrZ6n9jLZrxJPVo17nFjsuSwt6zMRS9Cna6adGFso0jIKF4BNTcZXlVt0b_TWA9AY4xpGSaAQXKYRBdAZNzTK5hj-bvyfWVTBepTBKm7pAtJW2e05mVBFDqRwEr3kcKkMTRMEj8vztSX4sNjV7Sw6bDbPjb_uFeoaNkq1ua9N7ZkKsyLn80ysWQ6IL-B_AVM1toAI0bVVYQmHbRSz1MgybcT5vjK2TWfCgxBuqw1ARU3oEM1iWTvpuHkFDNGeZnT4IWc_z_vLtQVnqSlqXPtnAmJ0WJgvQGSBhojSa9QMxRz_l4LZHmXUtfdMdUHTbrZwbfoL6eIyZf43ah-sXILJWTX9wXuYqnaclwvuO3EFiDUioSIBxWvAj2HNe5HpzayuvJqneRAukiVynt9SOo79VcBKAFsHW_KhUz58KjndfvOFP7byXv_W6Xa00ve_3GEaCANjFIl7ratBEuFYtpbVTT7TGIyGmuPQTVnGe7CAcFiAvGSwoaMwqvdGheKxeR7_cZvSYREeUnL5mXev8-MKY6_AtddOanQhf9pshxAXrIUtVFqin5MGhL2j787niURU9-o_VhLgzaIqfjbfMKkzdZ3-ESyDloOeD3CwoAsD_AVIrQZQHYbpYzFYasH0VrItar07ItxWZt_sCehTOi7Jtk5G8e7fd1HtCWmrp2iArioSIp8aLOphI9lazM7taIpfF_y0EMt3x5ENSvhtEmSk4kCJQnNyRv21uHT4uD0XjdQJbHeoRhzHDxoLHUOqAX9fGBzMFO8ZsWJxl8ebiCJeJ9Jzzr9bXeUKH5cP2VymoXPs3AWBrTpw1WfADM5sjHDJLzw\",\"type\":\"redacted_thinking\"},\"index\":0,\"type\":\"content_block_start\"}\n\nevent: content_block_stop\ndata: {\"index\":0,\"type\":\"content_block_stop\"}\n\nevent: content_block_start\ndata: {\"content_block\":{\"text\":\"\",\"type\":\"text\"},\"index\":1,\"type\":\"content_block_start\"}\n\nevent: content_block_delta\ndata: {\"delta\":{\"text\":\"I'll look up the current weather in Paris.\",\"type\":\"text_delta\"},\"index\":1,\"type\":\"content_block_delta\"}\n\nevent: content_block_stop\ndata: {\"index\":1,\"type\":\"content_block_stop\"}\n\nevent: content_block_start\ndata: {\"content_block\":{\"id\":\"call_01a0eba40810714cbd0a8a84496badc7\",\"input\":{},\"name\":\"lookup_weather\",\"type\":\"tool_use\"},\"index\":2,\"type\":\"content_block_start\"}\n\nevent: content_block_delta\ndata: {\"delta\":{\"partial_json\":\"{\\\"city\\\":\\\"Paris\\\"}\",\"type\":\"input_json_delta\"},\"index\":2,\"type\":\"content_block_delta\"}\n\nevent: content_block_stop\ndata: {\"index\":2,\"type\":\"content_block_stop\"}\n\nevent: message_delta\ndata: {\"delta\":{\"stop_reason\":\"tool_use\",\"stop_sequence\":null},\"type\":\"message_delta\",\"usage\":{\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"input_tokens\":564,\"output_tokens\":97,\"output_tokens_details\":{\"thinking_tokens\":29}}}\n\nevent: message_stop\ndata: {\"type\":\"message_stop\"}\n\n"
}
},
{
@@ -33,14 +40,14 @@
"headers": {
"content-type": "application/json"
},
"body": "{\"model\":\"muse-spark-1.3\",\"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"Look up the current weather in Paris using lookup_weather. After receiving the result, report Paris's weather in one short sentence.\"}]},{\"role\":\"assistant\",\"content\":[{\"type\":\"redacted_thinking\",\"data\":\"Q-PaDgGyJ4hIKk41uslnSV0PGvrTkwJ5D-t5skfjtkIt-ABXsehMcLKJJ8RJHRKW2-XhkpPrex0aOkIdqWl99vpCgtOHIFFaSc4b3oGxA8XDx4T_2aKANfYrR1DYwYzGe6ZZ-DQnU0bnpVUzCcXkghkLdyTcJr2p8cvVo1rymFpB0wZsbQRBhxCMR6PrY4i0aOTe8_waq_Po1a4l3YzKnzZIIvYP090o5kltv7MAwqChXjQeTJlKq7ECFa6HVeoTa43oiLmoD2Bzih0VfvBvQfbSh2uDypRM7-f65sp35-VuZfwsM33ZTvefzDbd8zd0D50I6wcybsj8gulDhWWpY7cxoN1Xasy-ImvisACGeppIN66maIFq5dMT2s8_CQ8a2EQ0hw9kwJaIiygZCdRofs-T1yaGWvnQxL6MpNh6SXux9T1ZfuexNrXOhz5cR3_s1euhq3-hI3GDCUDLAtkjiigSE3w6PExrwHTHnvkuAOTP0Lw7DH4NtKv-RFFeZaQOk_0Bi2EV43tR0Hq_chtEDiSpoezDjexPWfdZvhRIaO6PwyJaQ8jVOaLUr6bh6jQ3IM19lv6R7lY4Neno8fLxMwTbf7v7q02lfhOaZe3jR1fqOFUq8e4VXV76_rJBgzThqPNqO8OI_betgX5d2KMcwWAkaZjmrP3rxA7g5xp_Q7bkFiBaaTd3HbYIk_cKbtLOS6TIIphb4SX6ADA2qJ0zoo98P2NOceZODT2WrLjKCHl5dT6b9l2feH9m21pW576ULfKhVSMzuy0cmmWI7rv2P4q-2Fw2klsDVJAc6q22bFjfDgzhybKuhuM_p1SYb8aswNrgggV-cqHWDpF1FdVpL5fHMO74l1uD8Sj_9wuuD0asMQustuvsnYq2EdI0lLvONFMApWCU0s3QA8_P0Iyf0YoCZOm5QGnA7l3O5nAPkFF4kxLSsgRe9zuP6A4oKBDLN8EHwZ3pB4WZGWpUT12cJxplmT3_n8xKaMgaz13qs27uvUm3wMI1seyfpkMwUPHmt1ftpk9f_1OAg3fvQgIkWPTp6K9NzVzEry6SP7zbPfls8yn529Qrbki7ZM_oz3xEvDg367ZCe97eVnjiqRrYsYM2MmDsQq9lUwkgXb84kpK6a4pEzX9NDvhTqhw7RI6RL9E-2Ki4ciUfHv5LVTZ0rNweCo6SYXC11f8FCeObIW_Esr2mqYpORFS4SDMU-4I1HQfD9z6cPjL22APpv9a7Pp1phI8x_aQZkPU7LkyBWyVNDeusI7wq_LC0UgJ0vCAguKcAvN7gxwpCo5MEEZivRuYBldEMwRo4he2WQQke9KEokqREdlPF3Lvaav0fqPfXqhJyHAq8bYBHWdzMjDJ_dJwhdbLQ-iNLmrK5JNg2vPDglWSznBXbrSX4iyMFk1Ni2Y1SQUIOOxS_InIwCg\"},{\"type\":\"text\",\"text\":\"I'll look up the current weather in Paris.\"},{\"type\":\"tool_use\",\"id\":\"call_01a07ce8bb767c109c12f0a202e2ac19\",\"name\":\"lookup_weather\",\"input\":{\"city\":\"Paris\"}}]},{\"role\":\"user\",\"content\":[{\"type\":\"tool_result\",\"tool_use_id\":\"call_01a07ce8bb767c109c12f0a202e2ac19\",\"content\":\"{\\\"condition\\\":\\\"sunny\\\"}\"}]}],\"tools\":[{\"name\":\"lookup_weather\",\"description\":\"Look up current weather\",\"input_schema\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\",\"enum\":[\"Paris\"]}},\"required\":[\"city\"],\"additionalProperties\":false}}],\"tool_choice\":{\"type\":\"none\"},\"stream\":true,\"max_tokens\":1024,\"thinking\":{\"type\":\"adaptive\",\"display\":\"omitted\"},\"output_config\":{\"effort\":\"low\"}}"
"body": "{\"model\":\"muse-spark-1.3\",\"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"Look up the current weather in Paris using lookup_weather. After receiving the result, report Paris's weather in one short sentence.\"}]},{\"role\":\"assistant\",\"content\":[{\"type\":\"redacted_thinking\",\"data\":\"Q-PaDgFT_IXY3RXUUduHqe1_WSgTF5_gMh29lCBV6vhfXW34eU6nJO7gUfRPczMK4nZfQhrqQewRujwjtNKR69DHHqTP35Q0jDnuZ4GZX9qBZT1OH9JhcMl1FxvccpatEcuH2Nju9Rf12KFHAOm9eKK6wFiaZgEHcT1wPbRYVZVZ8VxLeKk-ED3wkmWFhUFJRLSgJguWBMvEMbtIUlAKsJ6qkUxClfQKMm3eft1yf9ajseT32eloGzBE48IhyXu6tpeUDs9UatFgZOHKtHmvtKsPqRw-ddbimLnMtdkexwa8xR7EVTVuwBezunqhssfYAdR4c4t6VDpACh2UL3LcVUF9b0reiwpUfdpYrwjpB3bTCWyzGjwvIwGeuoR6VrWHz5nqVbt3bBprJXqJ4IRLKAXFFlMhClkz0MFNPBufcXSdh9i9NjbNp5c8lXCSkhMfhYX9B9KbQKl3DxUCjEY_VZk0nQJwt9zQVwTzWPzl9fTkBWrdebpXCg-z4lPmU5GLyGY3MlF24eDoSfJfqibF1HHqztluSHi4XEulrwMh4dpZmLhlLxlFxwnC8LjY_Rg8uOyzeoCzsMj-R9WrZ6n9jLZrxJPVo17nFjsuSwt6zMRS9Cna6adGFso0jIKF4BNTcZXlVt0b_TWA9AY4xpGSaAQXKYRBdAZNzTK5hj-bvyfWVTBepTBKm7pAtJW2e05mVBFDqRwEr3kcKkMTRMEj8vztSX4sNjV7Sw6bDbPjb_uFeoaNkq1ua9N7ZkKsyLn80ysWQ6IL-B_AVM1toAI0bVVYQmHbRSz1MgybcT5vjK2TWfCgxBuqw1ARU3oEM1iWTvpuHkFDNGeZnT4IWc_z_vLtQVnqSlqXPtnAmJ0WJgvQGSBhojSa9QMxRz_l4LZHmXUtfdMdUHTbrZwbfoL6eIyZf43ah-sXILJWTX9wXuYqnaclwvuO3EFiDUioSIBxWvAj2HNe5HpzayuvJqneRAukiVynt9SOo79VcBKAFsHW_KhUz58KjndfvOFP7byXv_W6Xa00ve_3GEaCANjFIl7ratBEuFYtpbVTT7TGIyGmuPQTVnGe7CAcFiAvGSwoaMwqvdGheKxeR7_cZvSYREeUnL5mXev8-MKY6_AtddOanQhf9pshxAXrIUtVFqin5MGhL2j787niURU9-o_VhLgzaIqfjbfMKkzdZ3-ESyDloOeD3CwoAsD_AVIrQZQHYbpYzFYasH0VrItar07ItxWZt_sCehTOi7Jtk5G8e7fd1HtCWmrp2iArioSIp8aLOphI9lazM7taIpfF_y0EMt3x5ENSvhtEmSk4kCJQnNyRv21uHT4uD0XjdQJbHeoRhzHDxoLHUOqAX9fGBzMFO8ZsWJxl8ebiCJeJ9Jzzr9bXeUKH5cP2VymoXPs3AWBrTpw1WfADM5sjHDJLzw\"},{\"type\":\"text\",\"text\":\"I'll look up the current weather in Paris.\"},{\"type\":\"tool_use\",\"id\":\"call_01a0eba40810714cbd0a8a84496badc7\",\"name\":\"lookup_weather\",\"input\":{\"city\":\"Paris\"}}]},{\"role\":\"user\",\"content\":[{\"type\":\"tool_result\",\"tool_use_id\":\"call_01a0eba40810714cbd0a8a84496badc7\",\"content\":\"{\\\"condition\\\":\\\"sunny\\\"}\",\"cache_control\":{\"type\":\"ephemeral\"}}]}],\"tools\":[{\"name\":\"lookup_weather\",\"description\":\"Look up current weather\",\"input_schema\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\",\"enum\":[\"Paris\"]}},\"required\":[\"city\"],\"additionalProperties\":false},\"cache_control\":{\"type\":\"ephemeral\"}}],\"tool_choice\":{\"type\":\"none\"},\"stream\":true,\"max_tokens\":1024,\"thinking\":{\"type\":\"adaptive\",\"display\":\"omitted\"},\"output_config\":{\"effort\":\"low\"}}"
},
"response": {
"status": 200,
"headers": {
"content-type": "text/event-stream"
},
"body": "event: message_start\ndata: {\"message\":{\"content\":[],\"id\":\"msg_6a9ef3e77f6ef36fdd8f4925\",\"model\":\"muse-spark-1.3\",\"role\":\"assistant\",\"stop_reason\":null,\"stop_sequence\":null,\"type\":\"message\",\"usage\":{\"input_tokens\":0,\"output_tokens\":0}},\"type\":\"message_start\"}\n\nevent: content_block_start\ndata: {\"content_block\":{\"data\":\"Q-PaDgH-GUZkiqnb_seFaP70it_GxAEIUjt2BLllpyzfckiBOUVwr72xGQsSnj0y9aOhoaQ24A_HB0MHh4XWyYziVdRgq6lDPLHkJlmMzbusZxUZUx7D7B8kD8HrBbiTeIFbFlHaM9MiZW_PpYY9fpkdc7aeuX0mORdK8XTT3JPjvW1HPp4Qqp_iEOl9G2iM2Y6xno8SqeTcbc4EQs1LePaKrq86dBPmXBjkgQbYfvIw57o0SBEyelcudtnzJnaluOyOQdV2Ytk_r_xrYSCoBwgbf2KBCOFjdeQyruuwVZZ32JJdVOpWLw8eopUwAo2xYOZP0g8S8hTmOFvDKuzXipm1OoAXwD8Swm6ED3IIo0hHA5xSfMygDCee47nd-EpShNPamkCKodfX1QvePEJsIQK2iTgkh8IGUeEEne5dxgLuXvEAbeqGDvEy6T7IoSgZnc-KPtH6SoWM_kgc_eF_oN73Nxg2prMyCqTUNg5Qs2WLPjA9wSLmmnCoiDr1bYNIuQyn6adgv0-nZZXETJoRAHJBj65Asa8kbLyCYesb192178xCj2aBSdwyj-jc0i328sa9STS7mUSI7KvOt0Yi3kllLs1aSnHW-ogsUJUM7tTf83VO9fRRU_aW4H6qr4OAr8jbBKSD3bxE0AekDd24ZS-8YIjiP1tMOAPZ2JGQrNqbxnaeqhCZzD2nl-E8TMXaAJIk4L3oZtV80xJiUW7mLEf_jQBPAWhph4ujbkDaufGvNrl4FXyBJ-XpKMIA5z0jAYU2b3ul5-Qn5Km7Oc8fPu1M_0XGAOKG-pF9ppr4y-an4B4mKDYoAiSHp3cjv-fW57D87wBfQPaJRjOzWhzqENO-5MlB0CEsnDtrLveE2ui8wSECszParUNk5SEaeSYvrXLY0QkaE7CTK9ljwTKjJM34r1a8o16KFxGuzMrloNlxIYIaO_suM9CDuf8wNwe8KkAV2Kt28g9LwkL5b3mtI7ZSjTdHYOOhFFMzs-sjXboQoGAJGOhMnCQNc3E5-goSzvOzEIXehSTs8Mzyq8d3C2k5F9PSgdO6ZouHVURDgbEXTqWqoqjQ8huk5AbrUX16QwhJambd6P-3i--Idz9CbIVuWIqiMm80TvYlMttobYxTyFDIJrKJTrVA0PkAOFvOMfYt27d7z2ZMrTf6Vtm_DNniuhVbvVU3ZcWbb4LO-XkwH3bfHB-sckpwURP34KzOMvogaqWaMAZlabdd-lQKCc7tMhmo9BrYwHHvLxAELD2qQnyym43Yo1iJNAyrqoUAFk6K029fDon7h9ybmKZdBxRtS3arUesY_91Xep0tA0Y1UYUKo8zKjK3Op_DbVyC5OoR8rxpumKiqk2PtG7RPf-WjEXGkleGqZtxix7ILSkQXxW6McY1r_6LAh2Nv0nUzqS019tTb7mx2OLyt2g\",\"type\":\"redacted_thinking\"},\"index\":0,\"type\":\"content_block_start\"}\n\nevent: content_block_stop\ndata: {\"index\":0,\"type\":\"content_block_stop\"}\n\nevent: content_block_start\ndata: {\"content_block\":{\"text\":\"\",\"type\":\"text\"},\"index\":1,\"type\":\"content_block_start\"}\n\nevent: content_block_delta\ndata: {\"delta\":{\"text\":\"It's sunny in Paris.\",\"type\":\"text_delta\"},\"index\":1,\"type\":\"content_block_delta\"}\n\nevent: content_block_stop\ndata: {\"index\":1,\"type\":\"content_block_stop\"}\n\nevent: message_delta\ndata: {\"delta\":{\"stop_reason\":\"end_turn\",\"stop_sequence\":null},\"type\":\"message_delta\",\"usage\":{\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"input_tokens\":210,\"output_tokens\":50,\"output_tokens_details\":{\"thinking_tokens\":35}}}\n\nevent: message_stop\ndata: {\"type\":\"message_stop\"}\n\n"
"body": "event: message_start\ndata: {\"message\":{\"content\":[],\"id\":\"msg_6abb4cd0b3f15768e1ae4287\",\"model\":\"muse-spark-1.3\",\"role\":\"assistant\",\"stop_reason\":null,\"stop_sequence\":null,\"type\":\"message\",\"usage\":{\"input_tokens\":0,\"output_tokens\":0}},\"type\":\"message_start\"}\n\nevent: content_block_start\ndata: {\"content_block\":{\"data\":\"Q-PaDgF70ZwJMdvZQh6urOFUjgHa2IrdJbBv8RFXDpivwOudmj9QoCi81uRWeArjn_7V2nNk6i6jlHMjN7ACeZUE-faBnl4bTI10Fs0w0SSgcdkMaume2hpXBrZB85wlEttQviz-Pc5EHhAzU5QFIrK83CQkhJENqcfY3NJW1-55JFqlXmWSFV0GtNp3clG7V0LR1Zaq54hqIUY5iwf1pB8VIXLxPca3LwXMGbQPgjOgj_WNKHEBGl6mIafeca1OlGuDrJPN9tWBhlfxJCkcRU0zl14NA8chEyr96N40KrPewjVAlWO3jSejy9V7PnglilNewhUXOEQ75w2tYiZDajBfz6iG_n27zD3uvgD3QQthdyeUhJqlBaX81zMIe39Ctfo-cX-62cKwE3_EHy_7SxkwY5Olbf_bwL4GGzR39MObZoyv8-ZU1kwwVuSLXuDFvIx6iaAMZ50U4xXYbOphAaw2rylql0_kiPlFS9V3Fr_Jvx4vplj-8xh9JI7Uu9t9O7ltECpN9Qfu32fqxzWy9-ZVIh15gzhyegNeICBOcz6b9s3CIFHnzRcU69Ru9-lbydKv7scII_2xxLd-wlHQDCXHXyTeTeZjbTRiul0u5J2m8gGefF-j0lVChJOh8F4vBltzYTFTiQoSP8KtL61VJOt6dLsxOQA0Rq4PR5Lcs2fAfJbKb7jYB23bmXvQQ1d76keuvq_yoSIhrSyxLvXRCxouoWttkI8QC0u7O5pZUIPVfiYMI-j3bbc63BFOu4wAGbckm8Txo-gAY8UYAAwMBX17SD5EcTR82njKmd1R0Y8XOJLiwZRlN0ou-syq8xhahVgd0VcyB1HKr1f9mbhJdqFkKGpWh9-SR6jT6fKCCFxeMA3caczpG5wP_q5SGgyUiK0GWYpHabjSeJWac-04TDOfqL4QwLEKuL4PjlINe9OZY1n1lPJa2_MS9YGt6qlgLe3uZSEBwHk-ul5_yRetU8bd7lYcwDtUHT6puEkdjqeYwYQ8XWkUjoYwvSIMB_2v_owa5p9gPzf5Fr_zWqG7pizrStNNHq1MjXQX8Pl4Z6e2LZ2NjItfjR1GKdErNEJWXyh0oHCQKD1fIOXl64lpQorAHUcKV2HZm_SkwV7kxvzAfhdeT9Fe3AXZmtPBS1m0ZMMYNMYsNKb813JnWu8LvNg_zgXLBWsiB-hl4kCk1NKVAsCoA0i8JUFnGGm_uQp6t06a5NQYEXvi5A0akZTacle4UTBflNdsIVR6tSaO_m49nnr-XYSIh3tEzfjOsXpFz5L_A9SBDdPSGpcMJrs00nl1wDpF78OKC8NhwkIx8ydwF5A-ecXf91SbgqzVgv2T-wzuziA1wjJbPsGzTxP_wULUc2P5-ftFAMpsiRHB_-7hq6ZGo3HwNWjK2DTTN_89u7RCjuErEVPXUv-2fQ\",\"type\":\"redacted_thinking\"},\"index\":0,\"type\":\"content_block_start\"}\n\nevent: content_block_stop\ndata: {\"index\":0,\"type\":\"content_block_stop\"}\n\nevent: content_block_start\ndata: {\"content_block\":{\"text\":\"\",\"type\":\"text\"},\"index\":1,\"type\":\"content_block_start\"}\n\nevent: content_block_delta\ndata: {\"delta\":{\"text\":\"It's sunny in Paris right now.\",\"type\":\"text_delta\"},\"index\":1,\"type\":\"content_block_delta\"}\n\nevent: content_block_stop\ndata: {\"index\":1,\"type\":\"content_block_stop\"}\n\nevent: message_delta\ndata: {\"delta\":{\"stop_reason\":\"end_turn\",\"stop_sequence\":null},\"type\":\"message_delta\",\"usage\":{\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"input_tokens\":152,\"output_tokens\":55,\"output_tokens_details\":{\"thinking_tokens\":38}}}\n\nevent: message_stop\ndata: {\"type\":\"message_stop\"}\n\n"
}
}
]
@@ -2,9 +2,16 @@
"version": 1,
"metadata": {
"model": "muse-spark-1.3",
"tags": ["prefix:meta-messages", "provider:meta", "protocol:meta-messages", "text", "reasoning", "adaptive"],
"tags": [
"prefix:meta-messages",
"provider:meta",
"protocol:meta-messages",
"text",
"reasoning",
"adaptive"
],
"name": "meta-messages/streams-text-with-adaptive-thinking",
"recordedAt": "2026-09-07T17:26:18.565Z"
"recordedAt": "2026-09-29T05:29:46.263Z"
},
"interactions": [
{
@@ -15,14 +22,14 @@
"headers": {
"content-type": "application/json"
},
"body": "{\"model\":\"muse-spark-1.3\",\"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"What is 173 multiplied by 219? Reply with only the final integer.\"}]}],\"stream\":true,\"max_tokens\":2048,\"thinking\":{\"type\":\"adaptive\",\"display\":\"omitted\"},\"output_config\":{\"effort\":\"low\"}}"
"body": "{\"model\":\"muse-spark-1.3\",\"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"What is 173 multiplied by 219? Reply with only the final integer.\",\"cache_control\":{\"type\":\"ephemeral\"}}]}],\"stream\":true,\"max_tokens\":2048,\"thinking\":{\"type\":\"adaptive\",\"display\":\"omitted\"},\"output_config\":{\"effort\":\"low\"}}"
},
"response": {
"status": 200,
"headers": {
"content-type": "text/event-stream"
},
"body": "event: message_start\ndata: {\"message\":{\"content\":[],\"id\":\"msg_6a9ef3b8e46a777cfdc94a93\",\"model\":\"muse-spark-1.3\",\"role\":\"assistant\",\"stop_reason\":null,\"stop_sequence\":null,\"type\":\"message\",\"usage\":{\"input_tokens\":0,\"output_tokens\":0}},\"type\":\"message_start\"}\n\nevent: content_block_start\ndata: {\"content_block\":{\"data\":\"Q-PaDgFtwwNGqSqKxPk9Zj-j9xogYigpDsDHrLKPV-DxmXcbhtXDPrGPuj5qB4l1FslhuEQFCA8zIRyNGaCmzM0JG4GOZe3LpFYM2iegBbi24l4V7souryAwUs_7PrInxObuguMwh2S66ML6PwlfVRVTAwPsgDSCl6Y9tkoDHr2KBRY2fzJhmjkotuFRK2LYvNp9pKi89JxnoIKK8B_UJc_B2r4vU_Txq9_rWTmo2eXFoGOSMZNaHpVrvACRQKC1jc6-8Pj5JkJN11_Cs2fHqlaCwOoPDVJxHXUXP1AMHU1mG0jR4qf9wUaz8kMmyCBBijlfQJ4G7nsQs7LJO9gS2ropmynDoO5IEIAFf7ekxLw54Fx3JigC0_qz4Jx4a04Ey8JBzWRlZNdqggpq2_I_VoT3S0zhCYDp8ZAJQs2JqvvF-ZmLcMPH1PmpkgRySqXl5TDxERIpdL3aOUzTl-PTHVMIGEcSAz84fFwU4p1kS5llGEGC6jHlClscEEto1Y-qthL6BoeDALzk7GHEMVQD0bfC4E5yGCh0_xlWqsG3XPj0wpYKBc0hfh7vjO1NpzPxCtaDjnRwR6TzSo6x_HrEL3VELArHhpYAXl30fNO-EepduZ3Hd_kWqWj_GhdIM4dRh2zoytbhIesVD7OvBqU47urKFnpXw4kswJ-NoVVNk00ysMMHwobT9BdJL9UCoXlNzyErPe8LkA9f0VrKkuLCg5FDr_DCEYXHXr7fdRmC3JTKuzvxffvPYppUt3hs5gbdXWgT2iZzunt4bSfFne7BPkj9OGREyfLcukvGAh_oqPLXuu0lert9wbTAqRUTyM5Y1nSjw3h2B3jv91mH78mGvhy8LeVNLkrSMxEa4GtrrUvlzkPnZayc_LE2Sr7JnQjhwylVNWvkcIAjstOZShjPKe4pUJwj-cUXmXZB7Zwr-Q9WLtprFgQoBvadd5DHD4aynsYEabKZVcOqhV8l2zyaUCGobUzdPqBC5kk5-VPmNTqm6Qs91mb1y_5ydmHjTWmSW6qtN_op-Xfwqr_hllvOjhnAvvDJWUOKH-YIgZw_k_db2mzW1hO093jSDMYv6wpiy_5XWMNJBjcH8hvR_pFjXIqScUnKPranEZvCyGfHR-UBNJ89s_JEE49chcIt8LiukBeXQ_TOe29kzWnT1tYtfGQXP8FY2Wukw5yhvP3coGp1-LPZRuw3BLP2tWmiijmyj5lMikp8MIeqOztN8zgsArZ9jq21EtDSNfpO_0C4GEOtCZQYvZHRpDeY966XDnN7Blenn_8FZ95M-M-1My8hsmF__JrnY4ZFZr2274f_FbyJaSqGj6H-YRuzawO2TVLWHxAr6v2LCiB_QX01PwUI1yaJmP0mI-Qz5zXgSHzfVzhF7l1zEnM8qIYcSXzD1Gp3rjpn2sq-rR-JLqeHq080Hl0X9c8lzozw3qCpvcTfA-4ZTdXU9ax3nt7emcPVFo9N_JBjYashC7KvjkIWjMhQoTbIRlO18gjEa-Dvtsmo4_xE1ApYRoylbhxg3_fQJjKLfCBv32V6BM8qWKINL7d1nYP7PtAKnglCdnT_dK0_pCK6ejGfmdWZgVj0DhVZUCXf2kN530Y0hoCb1jBgwJ16a5gUqJMtnWScarIGSVrsYMA_uEt7DwQzGYAGESVIgi8JVATkxoq4EbyOLCHX7MIKmAPGV2WWTjWAWLnE2ITjlXtBkUAwjq2v957BMgIqxSNxEjG75Gd0i1XjZAujCSCXMrywZ72-VdQy4cD6kzH9mv04fZ7_dqn9eKPHBY7_T7V6myRDj_ImkffXcygmd7F-_-kHMExI6ABas1WtfDjkYgbRM0jMtIHYL-W9xT8vCSLSdvHcb5Y8XSXT5GTwI7eIlBHnCvFrgunbCo9bJJlT2Gc1RcgeZ4rg3yu1MNF4Mfk6DBwDhduGh5l7nypL00B0HokLfF4H7N1oSKL9jdVE3oMyJuA9NV1SJiyqcneSVHBzqjFZ5sszFXPlBKEz31izxFToAF4tDYMR507zYLcxyLA4AmHlkmn0qeozPb5qk-qZB6yLywtewv8O1KqxieAEJnEmyiqGnoKBdkjpeYmplErSxJyzoRX4fXb9mOdb-izJgvqSQ5uLm0_EXf3OklpMoAJ2CPJ6cxpg_5bhiwbgEtafCuDyXVL9Zzi872yUb_Z-ATlqU1M2Ai67f1piHIUDzMn4kulXQhPMb_R4eznMbX_aRigQh92-t1USoFtX2vmyCUQageFcrHHK524gPQ7rkOfDvZ8GHPXi3YZFnzGkSKjQD0jumFtzDiRqYkkMECep0zgY8MjuWLMjlMuigpkzSm8GPZ2aFeIMxJCYR8iMf7fH9NyHAJnicx4wdW_UqqgY5jfVtDvPSL0SRScyyK0jNdmofDHjWp4Tehih5OOw81E3BYLCiIv2p2zTMRSTZxFyG3HI91tIEaJu_RnIMFxzCilaP1LcbMrjyFzBtLJiXXJ--eFYyIMYKgfbWIOTsjLLJGC8NS2wuzaXB-C6GLsuSEHci5a8f5KGqaXK99LvHtY4k36WhEoVUjQEGY7HNJrtdjBUlqDgnT9iO06gGd2QCGvlP4us-Z3q2Bcyhf0LzOgn_Bpjc6YBk4TNF1AMxG15qmhnsrWcGElkpk3Vh9npC8--vkvhh7pOPfTZlNjQQh900OemY9gwxHyCBbt1096g4qkYOntGJmw-wVjyWxOt9K9CaM9WxY3wFyOmwqiFL1PK4CMeKMZk54iZ1ZiyXDOvsO7qUa9XQGDy3W8ic6R3DUzdkxEEBFYHAjDy3qE\",\"type\":\"redacted_thinking\"},\"index\":0,\"type\":\"content_block_start\"}\n\nevent: content_block_stop\ndata: {\"index\":0,\"type\":\"content_block_stop\"}\n\nevent: content_block_start\ndata: {\"content_block\":{\"text\":\"\",\"type\":\"text\"},\"index\":1,\"type\":\"content_block_start\"}\n\nevent: content_block_delta\ndata: {\"delta\":{\"text\":\"37887\",\"type\":\"text_delta\"},\"index\":1,\"type\":\"content_block_delta\"}\n\nevent: content_block_stop\ndata: {\"index\":1,\"type\":\"content_block_stop\"}\n\nevent: message_delta\ndata: {\"delta\":{\"stop_reason\":\"end_turn\",\"stop_sequence\":null},\"type\":\"message_delta\",\"usage\":{\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"input_tokens\":23,\"output_tokens\":266,\"output_tokens_details\":{\"thinking_tokens\":254}}}\n\nevent: message_stop\ndata: {\"type\":\"message_stop\"}\n\n"
"body": "event: message_start\ndata: {\"message\":{\"content\":[],\"id\":\"msg_6abb4cc8199fc8fa6763470e\",\"model\":\"muse-spark-1.3\",\"role\":\"assistant\",\"stop_reason\":null,\"stop_sequence\":null,\"type\":\"message\",\"usage\":{\"input_tokens\":0,\"output_tokens\":0}},\"type\":\"message_start\"}\n\nevent: content_block_start\ndata: {\"content_block\":{\"data\":\"Q-PaDgEdq_xGJ7Hl5i3AUq-IGolHfh7VsPu8914OD3BSVMiPCf1QVZ4O553OKpiejFGIkjBEfR2gGxcXlTsKuK78wfAiGAft2yQIHPeCJt4Csoxc3UUX405gUHqO9M06WeU118pTrlITW4g4dElALKmE2zi_pkQzEvU07jPqsF4n-3-9FkEnJ3D5Ag0UsU4LI1Mn5-EwPyPkitspoB-ytXsugMGQTMMfInzNq0OKDDMAO88aRQv24EslYBviairfM5c-k6dqOdGfsg_SntRiZJVNoo1yowGhQuiMKXreeQiQj_hrSVEXo8PVybmaCXtfW0I_2rI7JKR1uO-fxUHZckcCvLKMS3Wa2cwPUM9MPAnq05l3lbD1LpqZ_UPlf534p8_0BmF4hVGI2pamDWkL--7UaWrGFYtwBc3r9GVW0aIZC1ArnGjXsInKCnJuJ-elKu8H0Wp8-lxrsEfqtmxCamrsil8jOkIMezwSMUGtawhcOSdOnyQRje2b3OXiRx7Y-XZjrBlErbS13tdMJhXGlVqp9tazXlKdIt9CpMfMxjYW-Z5VkrJNymsBcjW1r2x5DC2VzGcqcha91rBkskrhKGWix95cdaEqUkP66DW9jThk4RS__E_oyt8DMTpvCxoZ5pe_mLkuS-ZpYKFeZbRegegIQErP_yuhvPWCwTYZymW4uUTVEmxdJ1otAST4ICF4ChsbHLLzJc4XHO7TA5GcwEc69m-poLTFrJvuYRErYhHHONOGWZGoyaPXApsmytUW6-lH2OiVpViVrLg_qvDw1NwyTgguQfP1d6JJYTmnjgN-Pa9170lqGzMI1fVjsC9zY5e6qe6NiyOwpxbFoV-jfdzyfeKX5LMVacpj2xV6dk6dYW7zcIZ3rJJvCpnwJROCjKbSRa4RvBFWNW2xjiWi1tYltzsz1YR1RHZJEN8szlfJjdMrfJdR5GbOiiDMcwGgGNOF5kbQqLgkggzR-B4lUNGLvSyBjAd_AdLpab0r9j1XdKBPWLFiXfAtX8lPSfeCSV41LFxKopB-MIVOpgtrwW6dsEayU7_w4Tc1nBTtYOGf2cGUV2XYzEpt5vxzCDC-og8WZ2lr3hOczp543-_I8n2ZOQEEk1k3suykPiQen_wgcpRccm18Aqsjy2t3uitZ-p1p4g0s6tG6Y_DCS4ku713v9Uw2ezHosl4Qk5JVk-KNvi9ZCYb9Jmc7PqHubP6Ua_UTyIeTiBB60zK5EgatIglUv3_NudfsplEH8YJkMvoAPJk-HW7c2WwzoymfA4TFoCg8vt3YHGAtuiyqF17-S3sfhl9J6oX-7FFpGn6bRqgwguC6sHUQw3bqWtyG4LjUy3_qzWfCI5zSm1hxjd6YRaxkdlv37Ol3Ua-65b3gRmQoUlA7omPbHuC8r2TNnG_ltsp9EMJlz7HN-Gyggw\",\"type\":\"redacted_thinking\"},\"index\":0,\"type\":\"content_block_start\"}\n\nevent: content_block_stop\ndata: {\"index\":0,\"type\":\"content_block_stop\"}\n\nevent: content_block_start\ndata: {\"content_block\":{\"text\":\"\",\"type\":\"text\"},\"index\":1,\"type\":\"content_block_start\"}\n\nevent: content_block_delta\ndata: {\"delta\":{\"text\":\"37887\",\"type\":\"text_delta\"},\"index\":1,\"type\":\"content_block_delta\"}\n\nevent: content_block_stop\ndata: {\"index\":1,\"type\":\"content_block_stop\"}\n\nevent: message_delta\ndata: {\"delta\":{\"stop_reason\":\"end_turn\",\"stop_sequence\":null},\"type\":\"message_delta\",\"usage\":{\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"input_tokens\":23,\"output_tokens\":161,\"output_tokens_details\":{\"thinking_tokens\":149}}}\n\nevent: message_stop\ndata: {\"type\":\"message_stop\"}\n\n"
}
}
]
@@ -2,9 +2,16 @@
"version": 1,
"metadata": {
"model": "muse-spark-1.3",
"tags": ["prefix:meta-messages", "provider:meta", "protocol:meta-messages", "text", "reasoning", "enabled"],
"tags": [
"prefix:meta-messages",
"provider:meta",
"protocol:meta-messages",
"text",
"reasoning",
"enabled"
],
"name": "meta-messages/streams-text-with-enabled-thinking",
"recordedAt": "2026-09-07T17:26:20.464Z"
"recordedAt": "2026-09-29T05:29:50.023Z"
},
"interactions": [
{
@@ -15,14 +22,14 @@
"headers": {
"content-type": "application/json"
},
"body": "{\"model\":\"muse-spark-1.3\",\"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"What is 173 multiplied by 219? Reply with only the final integer.\"}]}],\"stream\":true,\"max_tokens\":2048,\"thinking\":{\"type\":\"enabled\",\"budget_tokens\":1024,\"display\":\"omitted\"},\"output_config\":{\"effort\":\"low\"}}"
"body": "{\"model\":\"muse-spark-1.3\",\"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"What is 173 multiplied by 219? Reply with only the final integer.\",\"cache_control\":{\"type\":\"ephemeral\"}}]}],\"stream\":true,\"max_tokens\":2048,\"thinking\":{\"type\":\"enabled\",\"budget_tokens\":1024,\"display\":\"omitted\"},\"output_config\":{\"effort\":\"low\"}}"
},
"response": {
"status": 200,
"headers": {
"content-type": "text/event-stream"
},
"body": "event: message_start\ndata: {\"message\":{\"content\":[],\"id\":\"msg_6a9ef3bbb7e691b3ea26427b\",\"model\":\"muse-spark-1.3\",\"role\":\"assistant\",\"stop_reason\":null,\"stop_sequence\":null,\"type\":\"message\",\"usage\":{\"input_tokens\":0,\"output_tokens\":0}},\"type\":\"message_start\"}\n\nevent: content_block_start\ndata: {\"content_block\":{\"data\":\"Q-PaDgEpowvGua1C8z2RSICyyeSLruZ1JBUhrhLhgqf_XauiaESRHyHgIPVF0Cfxp7eLB_ESNObq7vJNDLdffE-RaN5nEauXhRxH6fJErC-4H_KELZFGF-qNnGu2DWl-gEojkwwkq1HtTtT3rOEk5isGjdribnDvR7a1zkpG3I0BxE_UyseISDSbkev5tyV7DkiH4xyv2ZmwuO3HHGgmAztEek9vm0ZTElZ8PIPvHG6P38aYGTGKrTjyOhmN3LBa_NFvtQQkUMKRB3n7L_N5KB1NbYkPzLEJ6L5dL9DJZBSsZms0O62QZ2zs2oWZ-ekQWL4oIuiRozWVpmwzjtkJl9WXtyvdL-4MACucXMSN9V7hWzC50g-DgdeFPSslsA9gy619wTNPPIxPymGWeD9LYKR3iwTRLNYzuycHakNqikhiwz9MweMod5GLTdbtypZJcV7ourz58jmK4YKsqleVPVvXM6bIoNuT2yGj23-tAYxQszGXQMHxSTnaLAHl6sUdiwTqX4wbFYfX-ogkIZsrca9mv5E2gkH1wJl19GPCxFaSDCHxh3QAW0RIExKUJly9Ydpoegmojfo0F4Hjt13zDm03fLEQ0eY8GjxIIHDt_zVRQGTW8d4tgZpTt4K9H1TTefG34EerN7jl7L-WNUe_gKorN4aSYBS6S6ma4Vb91NlUQ4cEGBKVQvumYe16p9Xj6Te-tCuiBmTOCq6iDsTH-eMlip_Bc18MD2BCGr2NdF4PB2vqHGSPIvSHDRXBjoPNtm18opAsUnOBu7-tysb8-EKFd5z7JbWgegQWNs11FdX0uFtC-U9ROwbHLJ3IHk4m2UHc2USwi7v4Xe6FtnkJ0VCZTajFUTXW33lwO3UsHuqEbxVnU_Rzuj6Ryjb08bpXpJBjIEGf2wBS3_I78cuXKOChHu-evtnjezc77joRUP8_I8pWsQbQ6bHABD95p8q6ZdQpDr9XQALbVg3ZvD5_QgF59rIxkW9a2L78oQpu0O3UT7-jxNFe38-OXuEr3VTv7RzISu1zjvBZD0k9-2q-T9BAJmsK6oPajJQWKwHBYF3w6vbsQ8tRDdh2uerhlAIWU0YL92gPCmT3mR6HxSGjAx08t-gib_YrzvWn0oTfLNk_BNfAMpG7Mj2GYNbyTkd_tjNZ2fOwVKbygsoFIUEvh2B-xyHOYuhXfxzI8-Gt55FMGDLLKcHfgmOZAUz1xVkIdm6bcuRFuQppgTzYIr66GN6tsr9KxTN2GZ2NHCwexe0hSAGfKVyLr8QBxTDRlzubBqe-osgQJrJtS7uM1MuWK_8grkZrNsn6g3ami0JboEmM_Ct3gVhkXo6lhCCcjn1gnGX-HOLczT3Xty1wYkeayUpi9x326nrSmypxzbzCRS3bOaPvSoBCszri0uTfUnVQK5kTVuRD30fGxauPhA\",\"type\":\"redacted_thinking\"},\"index\":0,\"type\":\"content_block_start\"}\n\nevent: content_block_stop\ndata: {\"index\":0,\"type\":\"content_block_stop\"}\n\nevent: content_block_start\ndata: {\"content_block\":{\"text\":\"\",\"type\":\"text\"},\"index\":1,\"type\":\"content_block_start\"}\n\nevent: content_block_delta\ndata: {\"delta\":{\"text\":\"37887\",\"type\":\"text_delta\"},\"index\":1,\"type\":\"content_block_delta\"}\n\nevent: content_block_stop\ndata: {\"index\":1,\"type\":\"content_block_stop\"}\n\nevent: message_delta\ndata: {\"delta\":{\"stop_reason\":\"end_turn\",\"stop_sequence\":null},\"type\":\"message_delta\",\"usage\":{\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"input_tokens\":23,\"output_tokens\":143,\"output_tokens_details\":{\"thinking_tokens\":131}}}\n\nevent: message_stop\ndata: {\"type\":\"message_stop\"}\n\n"
"body": "event: message_start\ndata: {\"message\":{\"content\":[],\"id\":\"msg_6abb4ccc0beea22bb95a41de\",\"model\":\"muse-spark-1.3\",\"role\":\"assistant\",\"stop_reason\":null,\"stop_sequence\":null,\"type\":\"message\",\"usage\":{\"input_tokens\":0,\"output_tokens\":0}},\"type\":\"message_start\"}\n\nevent: content_block_start\ndata: {\"content_block\":{\"data\":\"Q-PaDgGoJiKtl-javJ6dY04Ws79OK_mVBNQgu19iuCSru9wUy0jS0-87v1Lr1jC3I6cI8gnBvTl3HcRgR8MqmTOddFg5f_QSqhUweOVsCzaNL18Bjkye_cK3jGmlc9IsGLJ8dGFvFMPwflkMcofmwZ1P0EsMivXgwsDsWuiHP3PAP6_DfKgpVnNoz1oxNXgoVMTaNylNHtKHXpeC4_doHgWONMjhca771bIh_x0sRT-mQlONkr6HHgzzpCftEGOgnELEC7DlmJ46CDe_VSuaiUk8VLeNV4xyNm4XBHoyFuEn2N4tmG6mxo3cQlN2er8lY39XmY5gqy4ZIJddoY4sVFxBfL-axfDDv89hAhTYgXxH4dj4MyAj5TPWdyVZwunw2dRwf9JobQW-HCGcPxW6rqE7znOAegWSLD-mKRGZXNp7-kTY5Wicv9toV-VsBIFc1YpHFRpw36pxZftd2JnbW38dNYH0fO_QYnCknHxE9RA68uFpu_JvvpHOO0sFAuqwOCMynQyqFZRY9vVJBMYnuB_RIY_WCEuZwt7hV9es1aPk2QJyAWFh-LnNDzp9ktLaNwnnJxRpEK1r0PMbUsIQJMuxcNH7lCstE2dGBf02O4lH4HlZSHNsLcZuUjb01HCISf7cQXzIjG6rO_47m52h6bMpYZITElM66XVlKRE5V3wF_7sQ2BaT4g8GLxxTkgh-C0x8ISPVaU773cDvJSuOI-5FlUbviG9oQG_0PxU5ulQDCMSEM4wZENu6ZRlngTloFSxHJfeQvShiy9pC9kv-DuOT1stUT4HqpV0bV7WGrJjlX9l17JbMgjMBwzhNWTJwGiuw2Ya2cCveYgSzxMZtcI_YH9mv0n53c2h7_1Lq2-kVDx_R5UngrMLu9YxjIwIgK_UtbtnTXJ-q5X4DexgovoBPjzc2GOQeK0DoOnfOczVLMALAkQMnyxCQ65ZuawVuJFmIKY9ooznuqWjl5mLxVCn5Fyhk2uTKjFlbW1JO-RdvNF9CMRD2y2bY2Igy4w9Gh1R_xAhM7rwF0dt_p2g8CUjqsEyIn9bSGuRuzXfkxNz7KYfyCD2rkZc1gw8oU7jiLEpNl5gCkcj7bNql7lcDYgJtGSDRwZlIzZcb7MaxFHyFw7dXGtrOIvLL5eF8TOidjaRaY27xkCCBkQXWeIiX0qV5AuojjrpRAJsVVvdT5s2dHd1IYHU0nnsePU1Dd7gyB59upU9W1AaFrXjx0pn4MsNuYDcyLyzihq1lCeK23OSoXkwH9UI-zaLkV6MGUf_K0jMJPr6FrMgYF1x_SpXYOKSXPHhY5UE3pGLIL5_vFCRkAkMcvpw-H3tyljgnCZwt3g-Yuy4Uj05MS3PseQxuUor-0trgRBaBW4AdB925t2D41RJ4xp3HnEpjEkNFJ8Hsqbs3QxyVoTAvElXOrQ\",\"type\":\"redacted_thinking\"},\"index\":0,\"type\":\"content_block_start\"}\n\nevent: content_block_stop\ndata: {\"index\":0,\"type\":\"content_block_stop\"}\n\nevent: content_block_start\ndata: {\"content_block\":{\"text\":\"\",\"type\":\"text\"},\"index\":1,\"type\":\"content_block_start\"}\n\nevent: content_block_delta\ndata: {\"delta\":{\"text\":\"37887\",\"type\":\"text_delta\"},\"index\":1,\"type\":\"content_block_delta\"}\n\nevent: content_block_stop\ndata: {\"index\":1,\"type\":\"content_block_stop\"}\n\nevent: message_delta\ndata: {\"delta\":{\"stop_reason\":\"end_turn\",\"stop_sequence\":null},\"type\":\"message_delta\",\"usage\":{\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"input_tokens\":23,\"output_tokens\":198,\"output_tokens_details\":{\"thinking_tokens\":186}}}\n\nevent: message_stop\ndata: {\"type\":\"message_stop\"}\n\n"
}
}
]
File diff suppressed because one or more lines are too long
@@ -11,7 +11,7 @@
"reasoning"
],
"name": "minimax-messages/m2-7-streams-default-thinking",
"recordedAt": "2026-09-07T16:53:20.721Z"
"recordedAt": "2026-09-29T05:27:50.902Z"
},
"interactions": [
{
@@ -22,14 +22,14 @@
"headers": {
"content-type": "application/json"
},
"body": "{\"model\":\"MiniMax-M2.7\",\"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"What is 173 multiplied by 219? Reply with only the final integer.\"}]}],\"stream\":true,\"max_tokens\":1536}"
"body": "{\"model\":\"MiniMax-M2.7\",\"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"What is 173 multiplied by 219? Reply with only the final integer.\",\"cache_control\":{\"type\":\"ephemeral\"}}]}],\"stream\":true,\"max_tokens\":1536}"
},
"response": {
"status": 200,
"headers": {
"content-type": "text/event-stream; charset=utf-8"
},
"body": "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"id\":\"bd0c49bb4f5e6f26b77bab9b5a917484\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"model\":\"MiniMax-M2.7\",\"stop_reason\":null,\"stop_sequence\":null,\"usage\":{\"input_tokens\":57,\"output_tokens\":0}}}\n\nevent: ping\ndata: {\"type\":\"ping\"}\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"thinking\",\"thinking\":\"\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"The user\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" asks: \\\"What is 173 multiplied by 219? Reply with only the final integer.\\\" So we compute 173 * 219. Compute:\\n\\n173 * 219 = \"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"173 * (200 + 19) = 173*200 + 173*19 = 34600 + (173*19). 173*19 = 173*20\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" - 173 = 3460 - 173 = 3287. So total = 34600 + 3287 = 37887.\\n\\nAlternatively compute directly: 219 * \"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"173 = 219 * 173 = (219 * 100) + (219 * 70) + (219 * 3) = 21900 + 15330 + 657\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" = 37887.\\n\\nThus answer is 37887. We must reply with only the final integer: \\\"37887\\\". No extra text.\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"signature_delta\",\"signature\":\"1625da2f905565076ee90b9c2919db39b1719731c0820eca2f2a1c9c28324e87\"}}\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\":\"37887\"}}\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\"},\"usage\":{\"input_tokens\":57,\"output_tokens\":187,\"output_tokens_details\":{\"thinking_tokens\":184}}}\n\nevent: message_stop\ndata: {\"type\":\"message_stop\"}\n\n"
"body": "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"id\":\"419c5ecf093decc95250bc4cc3afce52\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"model\":\"MiniMax-M2.7\",\"stop_reason\":null,\"stop_sequence\":null,\"usage\":{\"input_tokens\":0,\"output_tokens\":0,\"cache_creation_input_tokens\":21,\"cache_read_input_tokens\":36}}}\n\nevent: ping\ndata: {\"type\":\"ping\"}\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"thinking\",\"thinking\":\"\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"The user\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" asks: \\\"What is 173 multiplied by 219? Reply with only the final integer.\\\"\\n\\nWe need to calculate 173 * 219 = ?\\n\\nLet's compute.\\n\\n173 *\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" 219:\\n\\nOption 1: compute 173 * 200 = 34,600 (actually 173*200 = 34,600\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"? Wait 173 * 200 = 34,600? Let's check: 173 * 2 = 346, then times 100 = 34,600. Yes\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\".)\\n\\n173 * 19 = 173 * 20 - 173 = 3,460 - 173 = 3\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\",287.\\n\\nAdd them: 34,600 + 3,287 = 37,887.\\n\\nThus product is 37,887.\\n\\nAlternatively, compute \"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"219 * 173: 219*100 = 21,900; 219*70 = 15,330; 219*3 = 657\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"; sum = 21,900 + 15,330 = 37,230; +657 = 37,887. Same.\\n\\nThus answer: 378\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"87.\\n\\nThe user wants only the final integer. So reply \\\"37887\\\". No other text.\\n\\nThus final answer: 37887.\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"signature_delta\",\"signature\":\"48b0f683a3fb6cfc2e060ff709c12a905c76d2e5bdddc709fb5b1cfc5aa600f7\"}}\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\":\"37887\"}}\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\"},\"usage\":{\"input_tokens\":0,\"output_tokens\":259,\"cache_creation_input_tokens\":21,\"cache_read_input_tokens\":36,\"output_tokens_details\":{\"thinking_tokens\":256}}}\n\nevent: message_stop\ndata: {\"type\":\"message_stop\"}\n\n"
}
}
]
@@ -13,7 +13,7 @@
"usage"
],
"name": "minimax-messages/m3-continues-a-tool-loop-with-adaptive-thinking",
"recordedAt": "2026-09-07T16:53:25.185Z"
"recordedAt": "2026-09-29T05:27:53.421Z"
},
"interactions": [
{
@@ -24,14 +24,14 @@
"headers": {
"content-type": "application/json"
},
"body": "{\"model\":\"MiniMax-M3\",\"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"Look up the current weather in Paris using get_weather before answering. After receiving the result, report the weather in one short sentence.\"}]}],\"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}}],\"tool_choice\":{\"type\":\"auto\"},\"stream\":true,\"max_tokens\":1536,\"thinking\":{\"type\":\"adaptive\"}}"
"body": "{\"model\":\"MiniMax-M3\",\"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"Look up the current weather in Paris using get_weather before answering. After receiving the result, report the weather 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\"}}],\"tool_choice\":{\"type\":\"auto\"},\"stream\":true,\"max_tokens\":1536,\"thinking\":{\"type\":\"adaptive\"}}"
},
"response": {
"status": 200,
"headers": {
"content-type": "text/event-stream; charset=utf-8"
},
"body": "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"id\":\"6a532037868639a6189e7c0b9e1c9aa2\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"model\":\"MiniMax-M3\",\"stop_reason\":null,\"stop_sequence\":null,\"usage\":{\"input_tokens\":0,\"output_tokens\":0,\"service_tier\":\"standard\"},\"service_tier\":\"standard\"}}\n\nevent: ping\ndata: {\"type\":\"ping\"}\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"thinking\",\"thinking\":\"\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"The user wants me\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" to look up the\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" current weather in Paris\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" using the get_\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"weather tool, then\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" report it\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" in one short sentence\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\". Let\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" me call the tool\"}}\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\":\"f05cb5f4873950f23d194289c41d79b96ebd37c9411a45b306ed44f135a910d4\"}}\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\":\"tool_use\",\"id\":\"call_01a07cc9f2d47d83a6424ff3\",\"name\":\"get_weather\",\"input\":{}}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"{\\\"city\\\":\\\"Paris\\\"}\"}}\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":1}\n\nevent: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"tool_use\"},\"usage\":{\"input_tokens\":239,\"output_tokens\":63,\"cache_read_input_tokens\":203,\"service_tier\":\"standard\",\"output_tokens_details\":{\"thinking_tokens\":33}}}\n\nevent: message_stop\ndata: {\"type\":\"message_stop\"}\n\n"
"body": "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"id\":\"6e8c4645c02dd05c2f08c22e5eba9a89\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"model\":\"MiniMax-M3\",\"stop_reason\":null,\"stop_sequence\":null,\"usage\":{\"input_tokens\":0,\"output_tokens\":0,\"service_tier\":\"standard\"},\"service_tier\":\"standard\"}}\n\nevent: ping\ndata: {\"type\":\"ping\"}\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"thinking\",\"thinking\":\"\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"The user wants me\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" to look up the\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" current weather in Paris\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" using the get_\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"weather tool, then\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" report the weather in\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" one short sentence after\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" receiving the result.\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" Let\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" me call the tool\"}}\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\":\"61f13458806572210eb8e1809b12f191297eea1a7f0104aa12f86595ef5eb1dc\"}}\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\":\"tool_use\",\"id\":\"call_01a0eba239b7718ba67e5aa3\",\"name\":\"get_weather\",\"input\":{}}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"{\\\"city\\\":\\\"Paris\\\"}\"}}\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":1}\n\nevent: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"tool_use\"},\"usage\":{\"input_tokens\":239,\"output_tokens\":68,\"cache_read_input_tokens\":203,\"service_tier\":\"standard\",\"output_tokens_details\":{\"thinking_tokens\":38}}}\n\nevent: message_stop\ndata: {\"type\":\"message_stop\"}\n\n"
}
},
{
@@ -42,14 +42,14 @@
"headers": {
"content-type": "application/json"
},
"body": "{\"model\":\"MiniMax-M3\",\"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"Look up the current weather in Paris using get_weather before answering. After receiving the result, report the weather in one short sentence.\"}]},{\"role\":\"assistant\",\"content\":[{\"type\":\"thinking\",\"thinking\":\"The user wants me to look up the current weather in Paris using the get_weather tool, then report it in one short sentence. Let me call the tool.\",\"signature\":\"f05cb5f4873950f23d194289c41d79b96ebd37c9411a45b306ed44f135a910d4\"},{\"type\":\"tool_use\",\"id\":\"call_01a07cc9f2d47d83a6424ff3\",\"name\":\"get_weather\",\"input\":{\"city\":\"Paris\"}}]},{\"role\":\"user\",\"content\":[{\"type\":\"tool_result\",\"tool_use_id\":\"call_01a07cc9f2d47d83a6424ff3\",\"content\":\"{\\\"condition\\\":\\\"sunny\\\",\\\"temperature\\\":\\\"18C\\\"}\"}]}],\"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}}],\"tool_choice\":{\"type\":\"none\"},\"stream\":true,\"max_tokens\":1536,\"thinking\":{\"type\":\"adaptive\"}}"
"body": "{\"model\":\"MiniMax-M3\",\"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"Look up the current weather in Paris using get_weather before answering. After receiving the result, report the weather in one short sentence.\"}]},{\"role\":\"assistant\",\"content\":[{\"type\":\"thinking\",\"thinking\":\"The user wants me to look up the current weather in Paris using the get_weather tool, then report the weather in one short sentence after receiving the result. Let me call the tool.\",\"signature\":\"61f13458806572210eb8e1809b12f191297eea1a7f0104aa12f86595ef5eb1dc\"},{\"type\":\"tool_use\",\"id\":\"call_01a0eba239b7718ba67e5aa3\",\"name\":\"get_weather\",\"input\":{\"city\":\"Paris\"}}]},{\"role\":\"user\",\"content\":[{\"type\":\"tool_result\",\"tool_use_id\":\"call_01a0eba239b7718ba67e5aa3\",\"content\":\"{\\\"condition\\\":\\\"sunny\\\",\\\"temperature\\\":\\\"18C\\\"}\",\"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\"}}],\"tool_choice\":{\"type\":\"none\"},\"stream\":true,\"max_tokens\":1536,\"thinking\":{\"type\":\"adaptive\"}}"
},
"response": {
"status": 200,
"headers": {
"content-type": "text/event-stream; charset=utf-8"
},
"body": "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"id\":\"e9e7ea89f63eca1cd17a037aef28eed1\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"model\":\"MiniMax-M3\",\"stop_reason\":null,\"stop_sequence\":null,\"usage\":{\"input_tokens\":0,\"output_tokens\":0,\"service_tier\":\"standard\"},\"service_tier\":\"standard\"}}\n\nevent: ping\ndata: {\"type\":\"ping\"}\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\":\"The weather\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\" in Paris is sunny\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\" with\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\" a\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\" temperature of 18\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"°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\"},\"usage\":{\"input_tokens\":164,\"output_tokens\":16,\"cache_read_input_tokens\":128,\"service_tier\":\"standard\"}}\n\nevent: message_stop\ndata: {\"type\":\"message_stop\"}\n\n"
"body": "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"id\":\"0f6a842fc54eecab0d6df9ffebd8dd3b\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"model\":\"MiniMax-M3\",\"stop_reason\":null,\"stop_sequence\":null,\"usage\":{\"input_tokens\":0,\"output_tokens\":0,\"service_tier\":\"standard\"},\"service_tier\":\"standard\"}}\n\nevent: ping\ndata: {\"type\":\"ping\"}\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"thinking\",\"thinking\":\"\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"The weather\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" in Paris is sunny\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" with a\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" temperature of 18\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"°C. I'll\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" report this in one\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"short sentence.\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"signature_delta\",\"signature\":\"fefe2c0e5f3f0a0edc24b21d37c8b7fb12ed0755bd1898f71acc2325f1b35958\"}}\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\":\"It's\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"text_delta\",\"text\":\" currently\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"text_delta\",\"text\":\" sunny in\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"text_delta\",\"text\":\" Paris with a temperature\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"text_delta\",\"text\":\" of 18°C\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"text_delta\",\"text\":\".\"}}\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\"},\"usage\":{\"input_tokens\":169,\"output_tokens\":38,\"cache_read_input_tokens\":128,\"service_tier\":\"standard\",\"output_tokens_details\":{\"thinking_tokens\":22}}}\n\nevent: message_stop\ndata: {\"type\":\"message_stop\"}\n\n"
}
}
]
@@ -11,7 +11,7 @@
"usage"
],
"name": "minimax-messages/m3-generates-a-named-tool-call-with-default-thinking-off",
"recordedAt": "2026-09-07T16:53:24.366Z"
"recordedAt": "2026-09-29T05:27:51.660Z"
},
"interactions": [
{
@@ -22,14 +22,14 @@
"headers": {
"content-type": "application/json"
},
"body": "{\"model\":\"MiniMax-M3\",\"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"Use get_weather to look up the current weather in Paris.\"}]}],\"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}}],\"tool_choice\":{\"type\":\"tool\",\"name\":\"get_weather\"},\"stream\":true,\"max_tokens\":512}"
"body": "{\"model\":\"MiniMax-M3\",\"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"Use get_weather to look up the current weather in Paris.\",\"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\"}}],\"tool_choice\":{\"type\":\"tool\",\"name\":\"get_weather\"},\"stream\":true,\"max_tokens\":512}"
},
"response": {
"status": 200,
"headers": {
"content-type": "text/event-stream; charset=utf-8"
},
"body": "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"id\":\"a60a8adb2eff7545f809b9df2689f1b8\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"model\":\"MiniMax-M3\",\"stop_reason\":null,\"stop_sequence\":null,\"usage\":{\"input_tokens\":0,\"output_tokens\":0,\"service_tier\":\"standard\"},\"service_tier\":\"standard\"}}\n\nevent: ping\ndata: {\"type\":\"ping\"}\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\":\"I'll\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\" look\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\" up the current weather\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\" in Paris for you\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\".\"}}\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\":\"tool_use\",\"id\":\"call_b0246853f3c4432ea455cc99\",\"name\":\"get_weather\",\"input\":{}}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"{\\\"city\\\":\\\"Paris\\\"}\"}}\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":1}\n\nevent: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"tool_use\"},\"usage\":{\"input_tokens\":401,\"output_tokens\":39,\"service_tier\":\"standard\"}}\n\nevent: message_stop\ndata: {\"type\":\"message_stop\"}\n\n"
"body": "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"id\":\"5f4a7ce22c5a82497fedc65028aa4825\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"model\":\"MiniMax-M3\",\"stop_reason\":null,\"stop_sequence\":null,\"usage\":{\"input_tokens\":0,\"output_tokens\":0,\"service_tier\":\"standard\"},\"service_tier\":\"standard\"}}\n\nevent: ping\ndata: {\"type\":\"ping\"}\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\":\"I'll\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\" look\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\" up the current weather\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\" in Paris for you\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\".\"}}\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\":\"tool_use\",\"id\":\"call_01a0eba235fb7281a0f2ea2c\",\"name\":\"get_weather\",\"input\":{}}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"{\\\"city\\\":\\\"Paris\\\"}\"}}\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":1}\n\nevent: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"tool_use\"},\"usage\":{\"input_tokens\":287,\"output_tokens\":39,\"cache_read_input_tokens\":128,\"service_tier\":\"standard\"}}\n\nevent: message_stop\ndata: {\"type\":\"message_stop\"}\n\n"
}
}
]
File diff suppressed because one or more lines are too long
@@ -11,7 +11,7 @@
"thinking-off"
],
"name": "minimax-messages/m3-streams-text-with-thinking-disabled",
"recordedAt": "2026-09-07T16:53:16.028Z"
"recordedAt": "2026-09-29T05:27:36.888Z"
},
"interactions": [
{
@@ -22,14 +22,14 @@
"headers": {
"content-type": "application/json"
},
"body": "{\"model\":\"MiniMax-M3\",\"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"What is 173 multiplied by 219? Reply with only the final integer.\"}]}],\"stream\":true,\"max_tokens\":1536,\"thinking\":{\"type\":\"disabled\"}}"
"body": "{\"model\":\"MiniMax-M3\",\"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"What is 173 multiplied by 219? Reply with only the final integer.\",\"cache_control\":{\"type\":\"ephemeral\"}}]}],\"stream\":true,\"max_tokens\":1536,\"thinking\":{\"type\":\"disabled\"}}"
},
"response": {
"status": 200,
"headers": {
"content-type": "text/event-stream; charset=utf-8"
},
"body": "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"id\":\"01664c047875a0f573c966467c8cccc1\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"model\":\"MiniMax-M3\",\"stop_reason\":null,\"stop_sequence\":null,\"usage\":{\"input_tokens\":0,\"output_tokens\":0,\"service_tier\":\"standard\"},\"service_tier\":\"standard\"}}\n\nevent: ping\ndata: {\"type\":\"ping\"}\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\":\"378\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"87\"}}\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\"},\"usage\":{\"input_tokens\":51,\"output_tokens\":3,\"cache_read_input_tokens\":128,\"service_tier\":\"standard\"}}\n\nevent: message_stop\ndata: {\"type\":\"message_stop\"}\n\n"
"body": "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"id\":\"444066751666f2071c999bfd0b4c16b0\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"model\":\"MiniMax-M3\",\"stop_reason\":null,\"stop_sequence\":null,\"usage\":{\"input_tokens\":0,\"output_tokens\":0,\"service_tier\":\"standard\"},\"service_tier\":\"standard\"}}\n\nevent: ping\ndata: {\"type\":\"ping\"}\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\":\"378\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"87\"}}\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\"},\"usage\":{\"input_tokens\":179,\"output_tokens\":3,\"service_tier\":\"standard\"}}\n\nevent: message_stop\ndata: {\"type\":\"message_stop\"}\n\n"
}
}
]
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -2,9 +2,16 @@
"version": 1,
"metadata": {
"model": "kimi-k3",
"tags": ["prefix:moonshot-messages", "provider:moonshot", "protocol:messages", "tool", "tool-choice", "usage"],
"tags": [
"prefix:moonshot-messages",
"provider:moonshot",
"protocol:messages",
"tool",
"tool-choice",
"usage"
],
"name": "moonshot-messages/k3-respects-tool-choice-required",
"recordedAt": "2026-09-07T20:37:07.770Z"
"recordedAt": "2026-09-29T05:28:21.798Z"
},
"interactions": [
{
@@ -15,14 +22,14 @@
"headers": {
"content-type": "application/json"
},
"body": "{\"model\":\"kimi-k3\",\"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"Use get_weather to look up the current weather in Paris.\"}]}],\"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}}],\"tool_choice\":{\"type\":\"any\"},\"stream\":true,\"max_tokens\":4096,\"output_config\":{\"effort\":\"low\"}}"
"body": "{\"model\":\"kimi-k3\",\"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"Use get_weather to look up the current weather in Paris.\",\"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\"}}],\"tool_choice\":{\"type\":\"any\"},\"stream\":true,\"max_tokens\":4096,\"output_config\":{\"effort\":\"low\"}}"
},
"response": {
"status": 200,
"headers": {
"content-type": "text/event-stream"
},
"body": "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"id\":\"chatcmpl-2f2e02780e8a031b1b1307171\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"model\":\"kimi-k3\",\"stop_reason\":null,\"stop_sequence\":null,\"usage\":{\"input_tokens\":223,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"output_tokens\":0,\"service_tier\":\"standard\",\"inference_geo\":\"not_available\",\"prompt_tokens\":223,\"cached_tokens\":0}}}\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\":\"Need\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" use\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" get\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"_we\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"ather\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" for\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" Paris\"}}\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\":\" Call\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" function\"}}\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\":\" Then\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" final\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" summarize\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" result\"}}\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\":\" Use\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" tool\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\".\"}}\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\":\"tool_use\",\"id\":\"get_weather_0\",\"name\":\"get_weather\",\"input\":{}}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"{\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"\\\"city\\\": \\\"\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"Paris\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"\\\"\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"}\"}}\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":1}\n\nevent: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"tool_use\",\"stop_sequence\":null},\"usage\":{\"input_tokens\":0,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":223,\"output_tokens\":69,\"output_tokens_details\":{\"thinking_tokens\":19},\"prompt_tokens\":223,\"completion_tokens\":69,\"total_tokens\":292,\"cached_tokens\":223}}\n\nevent: message_stop\ndata: {\"type\":\"message_stop\"}\n\n"
"body": "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"id\":\"chatcmpl-c877bb3f-cb6b-9d0f-b91e-37849d7bff39\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"model\":\"kimi-k3\",\"stop_reason\":null,\"stop_sequence\":null,\"usage\":{\"input_tokens\":223,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"output_tokens\":0,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":0},\"service_tier\":\"standard\",\"inference_geo\":\"not_available\",\"prompt_tokens\":223,\"cached_tokens\":0}}}\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\":\"The\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" user explicitly\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" asks to use\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" get_weather for\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" Paris. Need call\"}}\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\":\" tool. Do\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" that.\"}}\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\":\"tool_use\",\"id\":\"get_weather_0_7e180ea4\",\"name\":\"get_weather\",\"input\":{}}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"{\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"\\\"city\\\":\\\"Paris\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"\\\"\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"}\"}}\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":1}\n\nevent: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"tool_use\",\"stop_sequence\":null},\"usage\":{\"input_tokens\":223,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"output_tokens\":73,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":0},\"output_tokens_details\":{\"thinking_tokens\":20},\"prompt_tokens\":223,\"completion_tokens\":73,\"total_tokens\":296,\"cached_tokens\":0}}\n\nevent: message_stop\ndata: {\"type\":\"message_stop\"}\n\n"
}
}
]
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -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"
}
}
]
}
File diff suppressed because one or more lines are too long
@@ -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"
}
}
]
}
@@ -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"
}
}
]
}
@@ -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"
}
}
]
}
@@ -0,0 +1,57 @@
{
"version": 1,
"metadata": {
"provider": "openrouter",
"route": "openrouter-messages",
"transport": "http",
"model": "anthropic/claude-opus-4.7",
"tags": [
"prefix:openrouter-messages",
"provider:openrouter",
"flagship",
"tool",
"tool-loop",
"golden"
],
"name": "openrouter-messages/openrouter-claude-opus-4-7-tool-loop",
"recordedAt": "2026-09-30T03:26:16.603Z"
},
"interactions": [
{
"transport": "http",
"request": {
"method": "POST",
"url": "https://openrouter.ai/api/v1/messages",
"headers": {
"content-type": "application/json"
},
"body": "{\"model\":\"anthropic/claude-opus-4.7\",\"system\":[{\"type\":\"text\",\"text\":\"Use the get_weather tool exactly once. After the tool result, reply exactly: Paris is sunny.\"}],\"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,\"temperature\":0}"
},
"response": {
"status": 200,
"headers": {
"content-type": "text/event-stream"
},
"body": "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"id\":\"gen-1790738774-LwBI9xYpi0PJ3zyXvhBe\",\"type\":\"message\",\"role\":\"assistant\",\"container\":null,\"content\":[],\"model\":\"anthropic/claude-opus-4.7\",\"stop_reason\":null,\"stop_details\":null,\"stop_sequence\":null,\"usage\":{\"input_tokens\":812,\"output_tokens\":1,\"output_tokens_details\":null,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":0},\"inference_geo\":\"global\",\"server_tool_use\":null,\"service_tier\":\"standard\",\"speed\":\"standard\"},\"provider\":\"Claude Platform on AWS\"}}\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"tool_use\",\"id\":\"toolu_01SvA3dpQ3bwgAKjQB1d9rL5\",\"caller\":{\"type\":\"direct\"},\"name\":\"get_weather\",\"input\":{}}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"{\\\"city\\\": \\\"Paris\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"\\\"}\"}}\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0}\n\nevent: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"container\":null,\"stop_details\":null,\"stop_reason\":\"tool_use\",\"stop_sequence\":null},\"usage\":{\"input_tokens\":812,\"output_tokens\":66,\"output_tokens_details\":{\"thinking_tokens\":0},\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":0},\"server_tool_use\":null,\"service_tier\":\"standard\",\"speed\":\"standard\",\"cost\":0.00571,\"is_byok\":false,\"cost_details\":{\"upstream_inference_cost\":0.00571,\"upstream_inference_prompt_cost\":0.00406,\"upstream_inference_completions_cost\":0.00165}},\"context_management\":null}\n\nevent: message_stop\ndata: {\"type\":\"message_stop\"}\n\nevent: data\ndata: [DONE]\n\n"
}
},
{
"transport": "http",
"request": {
"method": "POST",
"url": "https://openrouter.ai/api/v1/messages",
"headers": {
"content-type": "application/json"
},
"body": "{\"model\":\"anthropic/claude-opus-4.7\",\"system\":[{\"type\":\"text\",\"text\":\"Use the get_weather tool exactly once. After the tool result, reply exactly: Paris is sunny.\"}],\"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"What is the weather in Paris?\"}]},{\"role\":\"assistant\",\"content\":[{\"type\":\"tool_use\",\"id\":\"toolu_01SvA3dpQ3bwgAKjQB1d9rL5\",\"name\":\"get_weather\",\"input\":{\"city\":\"Paris\"}}]},{\"role\":\"user\",\"content\":[{\"type\":\"tool_result\",\"tool_use_id\":\"toolu_01SvA3dpQ3bwgAKjQB1d9rL5\",\"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,\"temperature\":0}"
},
"response": {
"status": 200,
"headers": {
"content-type": "text/event-stream"
},
"body": "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"id\":\"gen-1790738775-mOcJ0PIOdQqPcuWboQoJ\",\"type\":\"message\",\"role\":\"assistant\",\"container\":null,\"content\":[],\"model\":\"anthropic/claude-opus-4.7\",\"stop_reason\":null,\"stop_details\":null,\"stop_sequence\":null,\"usage\":{\"input_tokens\":909,\"output_tokens\":9,\"output_tokens_details\":null,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":0},\"inference_geo\":\"global\",\"server_tool_use\":null,\"service_tier\":\"standard\",\"speed\":\"standard\"},\"provider\":\"Claude Platform on AWS\"}}\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"\",\"citations\":[]}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"Paris is sunny.\"}}\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0}\n\nevent: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"container\":null,\"stop_details\":null,\"stop_reason\":\"end_turn\",\"stop_sequence\":null},\"usage\":{\"input_tokens\":909,\"output_tokens\":12,\"output_tokens_details\":{\"thinking_tokens\":0},\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":0},\"server_tool_use\":null,\"service_tier\":\"standard\",\"speed\":\"standard\",\"cost\":0.004845,\"is_byok\":false,\"cost_details\":{\"upstream_inference_cost\":0.004845,\"upstream_inference_prompt_cost\":0.004545,\"upstream_inference_completions_cost\":0.0003}},\"context_management\":null}\n\nevent: message_stop\ndata: {\"type\":\"message_stop\"}\n\nevent: data\ndata: [DONE]\n\n"
}
}
]
}
File diff suppressed because one or more lines are too long
@@ -2,9 +2,14 @@
"version": 1,
"metadata": {
"model": "anthropic/claude-sonnet-4.6",
"tags": ["prefix:openai-compatible-chat", "provider:openrouter", "protocol:openai-chat", "reasoning"],
"tags": [
"prefix:openai-compatible-chat",
"provider:openrouter",
"protocol:openai-chat",
"reasoning"
],
"name": "openrouter-reasoning",
"recordedAt": "2026-07-18T11:28:39.267Z"
"recordedAt": "2026-09-30T03:26:02.917Z"
},
"interactions": [
{
@@ -15,14 +20,14 @@
"headers": {
"content-type": "application/json"
},
"body": "{\"model\":\"anthropic/claude-sonnet-4.6\",\"messages\":[{\"role\":\"system\",\"content\":\"Think through the arithmetic, then reply with only the final integer.\"},{\"role\":\"user\",\"content\":\"What is 173 multiplied by 219?\"}],\"stream\":true,\"stream_options\":{\"include_usage\":true},\"temperature\":0,\"reasoning\":{\"max_tokens\":1024},\"max_completion_tokens\":1536,\"store\":false,\"usage\":{\"include\":true}}"
"body": "{\"model\":\"anthropic/claude-sonnet-4.6\",\"messages\":[{\"role\":\"system\",\"content\":[{\"type\":\"text\",\"text\":\"Think through the arithmetic, then reply with only the final integer.\",\"cache_control\":{\"type\":\"ephemeral\"}}]},{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"What is 173 multiplied by 219?\",\"cache_control\":{\"type\":\"ephemeral\"}}]}],\"stream\":true,\"stream_options\":{\"include_usage\":true},\"store\":false,\"max_completion_tokens\":1536,\"temperature\":0,\"usage\":{\"include\":true},\"reasoning\":{\"max_tokens\":1024}}"
},
"response": {
"status": 200,
"headers": {
"content-type": "text/event-stream"
},
"body": ": OPENROUTER PROCESSING\n\n: OPENROUTER PROCESSING\n\ndata: {\"id\":\"gen-1784374117-AXXPsQRoclZeQGx2uHeK\",\"object\":\"chat.completion.chunk\",\"created\":1784374117,\"model\":\"anthropic/claude-sonnet-4.6\",\"provider\":\"Anthropic\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"\",\"role\":\"assistant\",\"reasoning\":\"173\",\"reasoning_details\":[{\"type\":\"reasoning.text\",\"text\":\"173\",\"format\":\"anthropic-claude-v1\",\"index\":0}]},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\n: OPENROUTER PROCESSING\n\ndata: {\"id\":\"gen-1784374117-AXXPsQRoclZeQGx2uHeK\",\"object\":\"chat.completion.chunk\",\"created\":1784374117,\"model\":\"anthropic/claude-sonnet-4.6\",\"provider\":\"Anthropic\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"\",\"role\":\"assistant\",\"reasoning\":\" × 219\\n\\n173 × 200 = 34,600\\n173 × 19 = 173 × 20 - 173 = 3,460 - 173 = 3,287\",\"reasoning_details\":[{\"type\":\"reasoning.text\",\"text\":\" × 219\\n\\n173 × 200 = 34,600\\n173 × 19 = 173 × 20 - 173 = 3,460 - 173 = 3,287\",\"format\":\"anthropic-claude-v1\",\"index\":0}]},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"gen-1784374117-AXXPsQRoclZeQGx2uHeK\",\"object\":\"chat.completion.chunk\",\"created\":1784374117,\"model\":\"anthropic/claude-sonnet-4.6\",\"provider\":\"Anthropic\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"\",\"role\":\"assistant\",\"reasoning\":\"\\n\\n34,600 + 3,287 = 37,887\",\"reasoning_details\":[{\"type\":\"reasoning.text\",\"text\":\"\\n\\n34,600 + 3,287 = 37,887\",\"format\":\"anthropic-claude-v1\",\"index\":0}]},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"gen-1784374117-AXXPsQRoclZeQGx2uHeK\",\"object\":\"chat.completion.chunk\",\"created\":1784374117,\"model\":\"anthropic/claude-sonnet-4.6\",\"provider\":\"Anthropic\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"\",\"role\":\"assistant\",\"reasoning_details\":[{\"type\":\"reasoning.text\",\"signature\":\"EtgCCosBCA8YAipA0W4viH3kgBs43Cl5ewwVBPXTQElvzfbA2TLF4iSbKy9ZZDCSDjjAlF3Bs4ELEnP3vrrTuTioC6OB380lXQdyIDIRY2xhdWRlLXNvbm5ldC00LTY4AEIIdGhpbmtpbmdaJDRjMGYwNDZmLTI1ZmQtNDVmYi1iZmIzLWEwOGE4ZTI0OWNhNxIMMiUlJC3x/5p5PuTwGgwlc8eipZyoM94BHwMiMO45uQx/ymeOjbugi7RDVPZ4jZXSIiEbVi2CD7zPjAK5fFQoVGP1HD55v9CER823JCp6Dg5Xb7Lrk6NUd1XN2KTKrttK7mATE+IBrDTFmor/1cNeg+9gjIbxM/jn/6L5HPmh3/esEVu24Q0IGLZVoE7cTgGgxsrceKMD71Jp2XQgIWD8ltsPfWw3gSc4p+z18UuPN6LuR0mHHENTnClHrAPnOrxbDIl4ZwZgMX8YAQ==\",\"format\":\"anthropic-claude-v1\",\"index\":0}]},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"gen-1784374117-AXXPsQRoclZeQGx2uHeK\",\"object\":\"chat.completion.chunk\",\"created\":1784374117,\"model\":\"anthropic/claude-sonnet-4.6\",\"provider\":\"Anthropic\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"37887\",\"role\":\"assistant\"},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"gen-1784374117-AXXPsQRoclZeQGx2uHeK\",\"object\":\"chat.completion.chunk\",\"created\":1784374117,\"model\":\"anthropic/claude-sonnet-4.6\",\"provider\":\"Anthropic\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"\",\"role\":\"assistant\",\"reasoning\":null},\"finish_reason\":\"stop\",\"native_finish_reason\":\"end_turn\"}]}\n\ndata: {\"id\":\"gen-1784374117-AXXPsQRoclZeQGx2uHeK\",\"object\":\"chat.completion.chunk\",\"created\":1784374117,\"model\":\"anthropic/claude-sonnet-4.6\",\"provider\":\"Anthropic\",\"service_tier\":\"default\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"\",\"role\":\"assistant\"},\"finish_reason\":\"stop\",\"native_finish_reason\":\"end_turn\"}],\"usage\":{\"prompt_tokens\":61,\"completion_tokens\":80,\"total_tokens\":141,\"cost\":0.001383,\"is_byok\":false,\"prompt_tokens_details\":{\"cached_tokens\":0,\"cache_write_tokens\":0,\"audio_tokens\":0,\"video_tokens\":0},\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":0},\"cost_details\":{\"upstream_inference_cost\":0.001383,\"upstream_inference_prompt_cost\":0.000183,\"upstream_inference_completions_cost\":0.0012},\"completion_tokens_details\":{\"reasoning_tokens\":29,\"image_tokens\":0,\"audio_tokens\":0}}}\n\ndata: [DONE]\n\n"
"body": "data: {\"id\":\"gen-1790738761-YbCA1TN8srf01M309CrM\",\"object\":\"chat.completion.chunk\",\"created\":1790738761,\"model\":\"anthropic/claude-sonnet-4.6\",\"provider\":\"Claude Platform on AWS\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"\",\"role\":\"assistant\",\"reasoning\":\"173\",\"reasoning_details\":[{\"type\":\"reasoning.text\",\"text\":\"173\",\"format\":\"anthropic-claude-v1\",\"index\":0}]},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"gen-1790738761-YbCA1TN8srf01M309CrM\",\"object\":\"chat.completion.chunk\",\"created\":1790738761,\"model\":\"anthropic/claude-sonnet-4.6\",\"provider\":\"Claude Platform on AWS\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"\",\"role\":\"assistant\",\"reasoning\":\" × 219\",\"reasoning_details\":[{\"type\":\"reasoning.text\",\"text\":\" × 219\",\"format\":\"anthropic-claude-v1\",\"index\":0}]},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\n: OPENROUTER PROCESSING\n\ndata: {\"id\":\"gen-1790738761-YbCA1TN8srf01M309CrM\",\"object\":\"chat.completion.chunk\",\"created\":1790738761,\"model\":\"anthropic/claude-sonnet-4.6\",\"provider\":\"Claude Platform on AWS\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"\",\"role\":\"assistant\",\"reasoning\":\"\\n\\n173 × 200 = 34,600\\n173 × 19 = 173 × 20 - 173 = 3,460 - 173 = 3,287\\n\\n34,600 + 3,287 = 37,887\",\"reasoning_details\":[{\"type\":\"reasoning.text\",\"text\":\"\\n\\n173 × 200 = 34,600\\n173 × 19 = 173 × 20 - 173 = 3,460 - 173 = 3,287\\n\\n34,600 + 3,287 = 37,887\",\"format\":\"anthropic-claude-v1\",\"index\":0}]},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"gen-1790738761-YbCA1TN8srf01M309CrM\",\"object\":\"chat.completion.chunk\",\"created\":1790738761,\"model\":\"anthropic/claude-sonnet-4.6\",\"provider\":\"Claude Platform on AWS\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"\",\"role\":\"assistant\",\"reasoning_details\":[{\"type\":\"reasoning.text\",\"signature\":\"EpUDCsgBCBIYAipA0W4viH3kgBs43Cl5ewwVBPXTQElvzfbA2TLF4iSbKy9ZZDCSDjjAlF3Bs4ELEnP3vrrTuTioC6OB380lXQdyIDIRY2xhdWRlLXNvbm5ldC00LTY4AEIIdGhpbmtpbmdaJDRjMGYwNDZmLTI1ZmQtNDVmYi1iZmIzLWEwOGE4ZTI0OWNhN3oedXByb2ZfMDExQ2VkSE1TN0tEcTM4Z2JkUkp0bXN3mgETChFjbGF1ZGUtc29ubmV0LTQtNqgByoLy1QYSDPcyAL64+muA2ku9uxoM4jS27X6iJtqcbymQIjCyOPIBbqmb8CRfcLBCI42CRACGhf+ApKDRPkGhPEDctIGeeo2QaGy8WGT6kEX2zYsqeuExYES4JaCt7xyC+1GYlUhbEFMRVPeV0gdkE72yBcl9rhbnK3S9Zs3u1pETV9E50s3tGIF+AP2UShvzdbsB7qDfr2xaUyE6/LgyGndMsxZprhW/nZqxt0YtXzJa4n5+pMMxJW+uv9BrleRRHdmyqpGGEduiZW2WQ4WgGAE=.eyJlbmRwb2ludF9zbHVnIjoiYW50aHJvcGljL2NsYXVkZS00LjYtc29ubmV0LTIwMjYwMjE3fGNsYXVkZS1vbi1hd3MifQ\",\"format\":\"anthropic-claude-v1\",\"index\":0}]},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"gen-1790738761-YbCA1TN8srf01M309CrM\",\"object\":\"chat.completion.chunk\",\"created\":1790738761,\"model\":\"anthropic/claude-sonnet-4.6\",\"provider\":\"Claude Platform on AWS\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"37887\",\"role\":\"assistant\"},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"gen-1790738761-YbCA1TN8srf01M309CrM\",\"object\":\"chat.completion.chunk\",\"created\":1790738761,\"model\":\"anthropic/claude-sonnet-4.6\",\"provider\":\"Claude Platform on AWS\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"\",\"role\":\"assistant\",\"reasoning\":null},\"finish_reason\":\"stop\",\"native_finish_reason\":\"end_turn\"}]}\n\ndata: {\"id\":\"gen-1790738761-YbCA1TN8srf01M309CrM\",\"object\":\"chat.completion.chunk\",\"created\":1790738761,\"model\":\"anthropic/claude-sonnet-4.6\",\"provider\":\"Claude Platform on AWS\",\"service_tier\":\"default\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"\",\"role\":\"assistant\"},\"finish_reason\":\"stop\",\"native_finish_reason\":\"end_turn\"}],\"usage\":{\"prompt_tokens\":61,\"completion_tokens\":80,\"total_tokens\":141,\"cost\":0.001383,\"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.001383,\"upstream_inference_prompt_cost\":0.000183,\"upstream_inference_completions_cost\":0.0012},\"completion_tokens_details\":{\"reasoning_tokens\":73,\"image_tokens\":0,\"audio_tokens\":0}}}\n\ndata: [DONE]\n\n"
}
}
]
@@ -0,0 +1,37 @@
{
"version": 1,
"metadata": {
"provider": "openrouter",
"route": "openrouter-responses",
"transport": "http",
"model": "openai/gpt-4o-mini",
"tags": [
"prefix:openrouter-responses",
"provider:openrouter",
"text",
"golden"
],
"name": "openrouter-responses/openrouter-gpt-4o-mini-text",
"recordedAt": "2026-09-30T16:23:26.190Z"
},
"interactions": [
{
"transport": "http",
"request": {
"method": "POST",
"url": "https://openrouter.ai/api/v1/responses",
"headers": {
"content-type": "application/json"
},
"body": "{\"model\":\"openai/gpt-4o-mini\",\"input\":[{\"type\":\"message\",\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"Reply exactly with: Hello!\"}]}],\"instructions\":\"You are concise.\",\"store\":false,\"include\":[\"reasoning.encrypted_content\"],\"max_output_tokens\":40,\"temperature\":0,\"stream\":true}"
},
"response": {
"status": 200,
"headers": {
"content-type": "text/event-stream"
},
"body": "data: {\"type\":\"response.created\",\"response\":{\"id\":\"gen-1790785405-TibUw8IxcUdeh1aIMexG\",\"object\":\"response\",\"created_at\":1790785405,\"model\":\"openai/gpt-4o-mini\",\"status\":\"in_progress\",\"completed_at\":null,\"output\":[],\"error\":null,\"incomplete_details\":null,\"tools\":[],\"tool_choice\":\"auto\",\"parallel_tool_calls\":true,\"max_output_tokens\":40,\"temperature\":0,\"top_p\":1,\"presence_penalty\":0,\"frequency_penalty\":0,\"top_logprobs\":0,\"max_tool_calls\":null,\"metadata\":{},\"background\":false,\"previous_response_id\":null,\"service_tier\":\"auto\",\"truncation\":\"disabled\",\"store\":false,\"instructions\":\"You are concise.\",\"text\":{\"format\":{\"type\":\"text\"}},\"reasoning\":null,\"safety_identifier\":null,\"prompt_cache_key\":null,\"prompt_cache_options\":null,\"usage\":null},\"sequence_number\":0}\n\ndata: {\"type\":\"response.in_progress\",\"response\":{\"id\":\"gen-1790785405-TibUw8IxcUdeh1aIMexG\",\"object\":\"response\",\"created_at\":1790785405,\"model\":\"openai/gpt-4o-mini\",\"status\":\"in_progress\",\"completed_at\":null,\"output\":[],\"error\":null,\"incomplete_details\":null,\"tools\":[],\"tool_choice\":\"auto\",\"parallel_tool_calls\":true,\"max_output_tokens\":40,\"temperature\":0,\"top_p\":1,\"presence_penalty\":0,\"frequency_penalty\":0,\"top_logprobs\":0,\"max_tool_calls\":null,\"metadata\":{},\"background\":false,\"previous_response_id\":null,\"service_tier\":\"auto\",\"truncation\":\"disabled\",\"store\":false,\"instructions\":\"You are concise.\",\"text\":{\"format\":{\"type\":\"text\"}},\"reasoning\":null,\"safety_identifier\":null,\"prompt_cache_key\":null,\"prompt_cache_options\":null,\"usage\":null},\"sequence_number\":1}\n\ndata: {\"type\":\"response.output_item.added\",\"output_index\":0,\"item\":{\"id\":\"msg_tmp_2aq1aouoqhw\",\"type\":\"message\",\"status\":\"in_progress\",\"role\":\"assistant\",\"content\":[]},\"sequence_number\":2}\n\ndata: {\"type\":\"response.content_part.added\",\"output_index\":0,\"item_id\":\"msg_tmp_2aq1aouoqhw\",\"content_index\":0,\"part\":{\"type\":\"output_text\",\"text\":\"\",\"annotations\":[],\"logprobs\":[]},\"sequence_number\":3}\n\ndata: {\"type\":\"response.output_text.delta\",\"output_index\":0,\"item_id\":\"msg_tmp_2aq1aouoqhw\",\"content_index\":0,\"delta\":\"Hello\",\"logprobs\":[],\"sequence_number\":4}\n\ndata: {\"type\":\"response.output_text.delta\",\"output_index\":0,\"item_id\":\"msg_tmp_2aq1aouoqhw\",\"content_index\":0,\"delta\":\"!\",\"logprobs\":[],\"sequence_number\":5}\n\ndata: {\"type\":\"response.output_text.done\",\"output_index\":0,\"item_id\":\"msg_tmp_2aq1aouoqhw\",\"content_index\":0,\"text\":\"Hello!\",\"logprobs\":[],\"sequence_number\":6}\n\ndata: {\"type\":\"response.content_part.done\",\"output_index\":0,\"item_id\":\"msg_tmp_2aq1aouoqhw\",\"content_index\":0,\"part\":{\"type\":\"output_text\",\"text\":\"Hello!\",\"annotations\":[],\"logprobs\":[]},\"sequence_number\":7}\n\ndata: {\"type\":\"response.output_item.done\",\"output_index\":0,\"item\":{\"id\":\"msg_tmp_2aq1aouoqhw\",\"type\":\"message\",\"status\":\"completed\",\"role\":\"assistant\",\"content\":[{\"type\":\"output_text\",\"text\":\"Hello!\",\"annotations\":[],\"logprobs\":[]}]},\"sequence_number\":8}\n\ndata: {\"type\":\"response.completed\",\"response\":{\"id\":\"gen-1790785405-TibUw8IxcUdeh1aIMexG\",\"object\":\"response\",\"created_at\":1790785405,\"model\":\"openai/gpt-4o-mini\",\"status\":\"completed\",\"completed_at\":1790785406,\"output\":[{\"id\":\"msg_tmp_2aq1aouoqhw\",\"type\":\"message\",\"role\":\"assistant\",\"status\":\"completed\",\"content\":[{\"type\":\"output_text\",\"text\":\"Hello!\",\"annotations\":[],\"logprobs\":[]}]}],\"error\":null,\"incomplete_details\":null,\"tools\":[],\"tool_choice\":\"auto\",\"parallel_tool_calls\":true,\"max_output_tokens\":40,\"temperature\":0,\"top_p\":1,\"presence_penalty\":0,\"frequency_penalty\":0,\"top_logprobs\":0,\"max_tool_calls\":null,\"metadata\":{},\"background\":false,\"previous_response_id\":null,\"service_tier\":\"default\",\"truncation\":\"disabled\",\"store\":false,\"instructions\":\"You are concise.\",\"text\":{\"format\":{\"type\":\"text\"}},\"reasoning\":null,\"safety_identifier\":null,\"prompt_cache_key\":null,\"prompt_cache_options\":null,\"usage\":{\"input_tokens\":21,\"input_tokens_details\":{\"cached_tokens\":0},\"output_tokens\":2,\"output_tokens_details\":{\"reasoning_tokens\":0},\"total_tokens\":23,\"cost\":0.00000435,\"is_byok\":false,\"cost_details\":{\"upstream_inference_cost\":0.00000435,\"upstream_inference_input_cost\":0.00000315,\"upstream_inference_output_cost\":0.0000012}}},\"sequence_number\":9}\n\ndata: [DONE]\n\n"
}
}
]
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+28
View File
@@ -355,6 +355,34 @@ describe("provider error rawBody classification", () => {
}
})
test("classifies Workers AI context window rejections as context overflow despite HTTP 413", () => {
const message =
"AiError: Ai: The estimated number of input and maximum output tokens (900047) exceeded this model context window limit (32000). (17851589-f2e6-405d-b67c-5550e52ee1c1)"
const reason = classifyProviderFailure({
message,
status: 413,
rawBody: JSON.stringify({ errors: [{ message, code: 5021 }], success: false, result: {}, messages: [] }),
})
expect(reason._tag === "InvalidRequest" ? reason.classification : reason._tag).toBe("context-overflow")
})
test("classifies Amazon Nova input token rejections as context overflow", () => {
const message =
"The model returned the following errors: Input Tokens Exceeded: Number of input tokens exceeds maximum length. Please update the input to try again."
const reason = classifyProviderFailure({
message,
rawBody: JSON.stringify({
headers: {
":exception-type": { type: "string", value: "validationException" },
":content-type": { type: "string", value: "application/json" },
":message-type": { type: "string", value: "exception" },
},
body: JSON.stringify({ message }),
}),
})
expect(reason._tag === "InvalidRequest" ? reason.classification : reason._tag).toBe("context-overflow")
})
test("classifies Google invalid API keys as authentication failures", () => {
const rawBody = JSON.stringify({
error: {
@@ -242,14 +242,14 @@ describeRecordedGoldenScenarios([
},
{
name: "OpenRouter gpt-4o-mini",
prefix: "openai-compatible-chat",
prefix: "openrouter-responses",
model: openrouter,
requires: ["OPENROUTER_API_KEY"],
scenarios: ["text", "tool-call", "tool-loop"],
},
{
name: "OpenRouter gpt-5.5",
prefix: "openai-compatible-chat",
prefix: "openrouter-responses",
model: openrouterGpt55,
requires: ["OPENROUTER_API_KEY"],
tags: ["flagship"],
@@ -257,10 +257,20 @@ describeRecordedGoldenScenarios([
},
{
name: "OpenRouter Claude Opus 4.7",
prefix: "openai-compatible-chat",
prefix: "openrouter-messages",
model: openrouterOpus,
requires: ["OPENROUTER_API_KEY"],
tags: ["flagship"],
scenarios: ["tool-loop"],
},
{
name: "OpenRouter Grok 4.3",
prefix: "openrouter-responses",
model: OpenRouter.configure({
apiKey: process.env.OPENROUTER_API_KEY ?? "fixture",
providerOptions: { reasoning: { effort: "none" } },
}).model("x-ai/grok-4.3"),
requires: ["OPENROUTER_API_KEY"],
scenarios: ["tool-loop"],
},
])
@@ -33,7 +33,12 @@ it.effect("Meta selects Messages and lowers native search alongside ordinary fun
output_config: { effort: "low" },
tools: [
{ type: "web_search", name: "web_search", user_location: { type: "approximate", country: "US" } },
{ name: "lookup", description: "Lookup", input_schema: { type: "object" } },
{
name: "lookup",
description: "Lookup",
input_schema: { type: "object" },
cache_control: { type: "ephemeral" },
},
],
})
const entrypoint = yield* Effect.promise(() => import("@opencode/ai/providers/meta/messages"))
+19 -3
View File
@@ -303,7 +303,8 @@ describe("Mistral Chat", () => {
fixedResponse(
sseEvents(
chunk({ content: [{ type: "thinking", thinking: [], marker: "empty" }] }),
chunk({ content: [{ type: "thinking", thinking: [{ type: "text", text: "Consider" }] }] }),
chunk({ content: [{ type: "thinking", thinking: [{ type: "text", text: "Con" }] }] }),
chunk({ content: [{ type: "thinking", thinking: [{ type: "text", text: "sider" }], closed: true }] }),
chunk({ content: [{ type: "text", text: "Answer" }] }),
chunk({}, "stop"),
),
@@ -313,6 +314,11 @@ describe("Mistral Chat", () => {
expect(response.reasoning).toBe("Consider")
expect(response.text).toBe("Answer")
expect(response.events.find(LLMEvent.is.reasoningStart)?.providerMetadata).toBeUndefined()
expect(response.events.filter(LLMEvent.is.reasoningDelta).map((event) => [event.text, event.providerMetadata])).toEqual([
["Con", undefined],
["sider", undefined],
])
expect(response.message.content).toEqual([
{
type: "reasoning",
@@ -321,8 +327,12 @@ describe("Mistral Chat", () => {
mistral: {
thinking: {
type: "thinking",
thinking: [{ type: "text", text: "Consider" }],
thinking: [
{ type: "text", text: "Con" },
{ type: "text", text: "sider" },
],
marker: "empty",
closed: true,
},
},
},
@@ -337,8 +347,12 @@ describe("Mistral Chat", () => {
content: [
{
type: "thinking",
thinking: [{ type: "text", text: "Consider" }],
thinking: [
{ type: "text", text: "Con" },
{ type: "text", text: "sider" },
],
marker: "empty",
closed: true,
},
{ type: "text", text: "Answer" },
],
@@ -357,6 +371,8 @@ describe("Mistral Chat", () => {
),
),
)
expect(response.events.find(LLMEvent.is.reasoningStart)?.providerMetadata).toBeUndefined()
expect(response.events.filter(LLMEvent.is.reasoningDelta)).toEqual([])
expect(response.message.content).toEqual([
{
type: "reasoning",
@@ -16,7 +16,7 @@ const cases = [
OpenRouter.configure({
apiKey: process.env.OPENROUTER_API_KEY ?? "fixture",
providerOptions: { reasoning: { max_tokens: 1024 } },
}).model("anthropic/claude-sonnet-4.6"),
}).chat("anthropic/claude-sonnet-4.6"),
{ compatibility: { reasoningField: "reasoning" } },
),
requires: ["OPENROUTER_API_KEY"],
@@ -89,7 +89,14 @@ for (const item of cases) {
const replay = yield* compileRequest(LLM.request({ model: item.model, messages: [response.message] }))
expect(replay.body.messages).toMatchObject([
{ role: "assistant", content: response.text, reasoning: response.reasoning },
{
role: "assistant",
content:
item.model.route.id === "openrouter"
? [{ type: "text", text: response.text, cache_control: { type: "ephemeral" } }]
: response.text,
reasoning: response.reasoning,
},
])
const replayDetails =
replay.body.messages[0]?.role === "assistant" ? replay.body.messages[0].reasoning_details : undefined
@@ -4996,6 +4996,42 @@ describe("OpenAI Responses route", () => {
}),
)
it.effect("retains the token-sharing HTTP 429 body for retry hooks", () =>
Effect.gen(function* () {
const body = JSON.stringify({ error: { code: "subscription_sharing_usage_limit_exceeded", message: "Rate limit exceeded" } })
const error = yield* LLMClient.generate(request).pipe(
Effect.provide(
fixedResponse(body, { status: 429, headers: { "content-type": "application/json" } }),
),
Effect.flip,
)
expect(error).toMatchObject({ reason: { _tag: "RateLimit", http: { status: 429 }, body } })
}),
)
it.effect("retains the token-sharing response.failed event for retry hooks", () =>
Effect.gen(function* () {
const error = yield* LLMClient.generate(request).pipe(
Effect.provide(
fixedResponse(
sseEvents({
type: "response.failed",
response: {
id: "resp_usage_limit",
error: { code: "subscription_sharing_usage_limit_exceeded", message: "Rate limit exceeded" },
},
}),
),
),
Effect.flip,
)
expect(error).toMatchObject({ reason: { _tag: "RateLimit" } })
expect(error.reason.body).toContain("subscription_sharing_usage_limit_exceeded")
}),
)
it.effect("surfaces response.failed code when no nested message is present", () =>
Effect.gen(function* () {
const error = yield* LLMClient.generate(request).pipe(
@@ -0,0 +1,83 @@
import { expect } from "bun:test"
import { Effect } from "effect"
import { LLM, LLMRequest, Message } from "../../src/index.js"
import { OpenRouter } from "../../src/providers/openrouter.js"
import { LLMClient } from "../../src/route.js"
import { recordedTests } from "../recorded-test.js"
const recorded = recordedTests({
prefix: "openrouter-responses",
provider: "openrouter",
protocol: "openrouter-responses",
requires: ["OPENROUTER_API_KEY"],
})
recorded.effect.with(
"continues Meta reasoning through a namespaced tool loop",
{ cassette: "openrouter-responses/openrouter-muse-spark-1-3-tool-loop", tags: ["reasoning", "tool-loop"] },
() =>
Effect.gen(function* () {
const tool = {
name: "lookup",
description: "Look up a reference value",
inputSchema: {
type: "object",
properties: { query: { type: "string" } },
required: ["query"],
},
}
const request = LLM.request({
model: OpenRouter.configure({
apiKey: process.env.OPENROUTER_API_KEY ?? "fixture",
providerOptions: { reasoning: { effort: "minimal" } },
}).model("meta/muse-spark-1.3"),
prompt: "Call only crm.lookup once with query ping. Do not call inventory.lookup. Wait for the result.",
tools: ["crm", "inventory"].map((name) => ({ type: "namespace", name, tools: [tool] })),
generation: { maxTokens: 1536 },
})
const first = yield* LLMClient.generate(request)
expect(first.toolCalls).toMatchObject([{ name: "lookup", namespace: "crm", input: { query: "ping" } }])
const call = first.toolCalls[0]
if (!call) throw new Error("Expected a namespaced tool call")
const second = yield* LLMClient.generate(
LLMRequest.update(request, {
messages: [
...request.messages,
first.message,
Message.tool({ id: call.id, name: call.name, namespace: call.namespace, result: "pong" }),
Message.user("Do not call tools again. Reply exactly pong."),
],
}),
)
expect(second.toolCalls).toHaveLength(0)
expect(second.text.trim()).toBe("pong")
}),
60_000,
)
recorded.effect(
"preserves native hosted-search events",
() =>
Effect.gen(function* () {
const response = yield* LLMClient.generate(
LLM.request({
model: OpenRouter.configure({
apiKey: process.env.OPENROUTER_API_KEY ?? "fixture",
providerOptions: { reasoning: { effort: "low" } },
}).model("x-ai/grok-4.3"),
prompt:
"Use web search once to find the official OpenRouter Responses API documentation. Reply with its URL.",
generation: { maxTokens: 1536 },
http: { body: { tools: [{ type: "web_search_preview", engine: "native", max_uses: 1 }], max_tool_calls: 1 } },
}),
)
expect(response.toolCalls).toContainEqual(expect.objectContaining({ name: "web_search", providerExecuted: true }))
expect(response.message.content).toContainEqual(
expect.objectContaining({ type: "tool-result", name: "web_search", providerExecuted: true }),
)
expect(response.text.trim().length).toBeGreaterThan(0)
}),
60_000,
)
+305 -43
View File
@@ -3,31 +3,261 @@ import { Effect } from "effect"
import { CacheHint, LLM, Message } from "../../src/index.js"
import { LLMClient } from "../../src/route.js"
import { compileRequest } from "../../src/route/client.js"
import * as OpenRouter from "../../src/providers/openrouter.js"
import { OpenRouter } from "../../src/providers/openrouter.js"
import { it } from "../lib/effect.js"
import { fixedResponse } from "../lib/http.js"
import { sseEvents } from "../lib/sse.js"
describe("OpenRouter", () => {
it.effect("prepares OpenRouter models through the OpenAI-compatible Chat route", () =>
it.effect("routes OpenAI, xAI, and Meta models through the same namespace-aware Responses protocol", () =>
Effect.forEach(["openai/gpt-4o-mini", "x-ai/grok-4.3", "meta/muse-spark-1.3", "~x-ai/grok-latest"], (id) =>
Effect.gen(function* () {
const model = OpenRouter.configure({
apiKey: "test-key",
providerOptions: {
reasoning: { effort: "low", exclude: true, summary: "detailed" },
text: { verbosity: "low", format: { type: "json_schema", name: "result", schema: { type: "object" } } },
provider: { allow_fallbacks: false },
},
}).model(id)
const prepared = yield* compileRequest(
LLM.request({
model,
prompt: "Say hello.",
tools: [
{
type: "namespace",
name: "crm",
tools: [
{
type: "namespace",
name: "contacts",
tools: [
{
name: "lookup",
description: "Look up a contact",
inputSchema: { type: "object", properties: {} },
},
],
},
],
},
],
}),
)
expect(model.route.endpoint.baseURL).toBe("https://openrouter.ai/api/v1")
expect(prepared.route).toBe("openrouter-responses")
expect(model.route.protocol).toBe(OpenRouter.responses("openai/gpt-4o-mini").route.protocol)
expect(prepared.body).toMatchObject({
model: id,
input: [{ role: "user", content: [{ type: "input_text", text: "Say hello." }] }],
stream: true,
store: false,
include: ["reasoning.encrypted_content"],
reasoning: { effort: "low", exclude: true, summary: "detailed" },
text: { verbosity: "low", format: { type: "json_schema", name: "result", schema: { type: "object" } } },
tools: [{ type: "namespace", name: "crm", tools: [{ type: "function", name: "contacts_lookup" }] }],
provider: { allow_fallbacks: false },
})
}),
),
)
it.effect("exposes exactly three API routes and leaves other families on Chat", () =>
Effect.gen(function* () {
const model = OpenRouter.configure({ apiKey: "test-key" }).model("openai/gpt-4o-mini")
expect(OpenRouter.routes.map((route) => route.endpoint.path)).toEqual([
"/chat/completions",
"/responses",
"/messages",
])
const openrouter = OpenRouter.configure({ apiKey: "test-key" })
const chat = openrouter.model("meta-llama/llama-3.3-70b-instruct")
expect(chat.route.id).toBe("openrouter")
expect(openrouter.model("qwen/qwen3-coder-plus").route.protocol).toBe(chat.route.protocol)
expect(openrouter.messages("anthropic/claude-sonnet-4.6").route.id).toBe("openrouter-messages")
}),
)
expect(model).toMatchObject({
id: "openai/gpt-4o-mini",
provider: "openrouter",
route: { id: "openrouter" },
it.effect("places default cache breakpoints on tools, system boundaries, and the conversation tail", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
LLM.request({
model: OpenRouter.configure({ apiKey: "test-key" }).model("anthropic/claude-sonnet-4.6"),
system: [
{ type: "text", text: "Base agent" },
{ type: "text", text: "Model details" },
{ type: "text", text: "Project instructions" },
],
tools: [
{ name: "read", description: "Read", inputSchema: { type: "object", properties: {} } },
{ name: "lookup", description: "Lookup", inputSchema: { type: "object", properties: {} } },
],
prompt: "Hello",
}),
)
expect(prepared.body.tools?.map((tool) => tool.cache_control)).toEqual([undefined, { type: "ephemeral" }])
expect(prepared.route).toBe("openrouter-messages")
expect(prepared.body.system).toMatchObject([
{ text: "Base agent", cache_control: { type: "ephemeral" } },
{ text: "Model details" },
{ text: "Project instructions", cache_control: { type: "ephemeral" } },
])
expect(prepared.body.messages).toMatchObject([
{ role: "user", content: [{ text: "Hello", cache_control: { type: "ephemeral" } }] },
])
expect(prepared.body.system).not.toContainEqual(
expect.objectContaining({ text: "Model details", cache_control: expect.anything() }),
)
}),
)
it.effect("preserves gateway routing and lowers native effort updates without changing the baseline", () =>
Effect.gen(function* () {
const openrouter = OpenRouter.configure({
apiKey: "test-key",
providerOptions: { provider: { allow_fallbacks: false }, reasoning: { effort: "low", summary: "detailed" } },
})
expect(model.route.endpoint.baseURL).toBe("https://openrouter.ai/api/v1")
const messages = [Message.effort({ previous: "high", effort: "low" }), Message.user("Hello")]
const response = yield* compileRequest(LLM.request({ model: openrouter.model("openai/gpt-6.1-sol"), messages }))
const message = yield* compileRequest(
LLM.request({ model: openrouter.model("anthropic/claude-fable-5.1"), messages }),
)
const prepared = yield* compileRequest(LLM.request({ model, prompt: "Say hello." }))
expect(response.body).toMatchObject({
provider: { allow_fallbacks: false },
reasoning: { effort: "high", summary: "detailed" },
input: [{ type: "configuration_update", reasoning: { effort: "low" } }, { role: "user" }],
})
expect(message.body).toMatchObject({
provider: { allow_fallbacks: false },
output_config: { effort: "high" },
thinking: { type: "adaptive", block_binding: { prefix_mismatch_behavior: "drop_block" } },
messages: [{ role: "system", content: [], output_config: { effort: "low" } }, { role: "user" }],
})
}),
)
expect(prepared.route).toBe("openrouter")
expect(prepared.body).toMatchObject({
model: "openai/gpt-4o-mini",
messages: [{ role: "user", content: "Say hello." }],
stream: true,
usage: { include: true },
it.effect("replays saved Chat reasoning details through the native APIs", () =>
Effect.gen(function* () {
const openrouter = OpenRouter.configure({ apiKey: "test-key" })
const message = yield* compileRequest(
LLM.request({
model: openrouter.model("anthropic/claude-sonnet-4.6"),
messages: [
Message.assistant({
type: "reasoning",
text: "AB",
providerMetadata: {
openrouter: {
reasoningDetails: [
{ type: "reasoning.text", text: "A", signature: "first" },
{ type: "reasoning.text", text: "B", signature: "second" },
],
},
},
}),
],
}),
)
const response = yield* compileRequest(
LLM.request({
model: openrouter.model("meta/muse-spark-1.3"),
messages: [
Message.assistant({
type: "reasoning",
text: "Summary",
providerMetadata: { openrouter: { reasoningDetails: [{ type: "reasoning.encrypted", data: "opaque" }] } },
}),
],
}),
)
expect(message.body.messages[0]?.content).toMatchObject([
{ type: "thinking", thinking: "A", signature: "first" },
{ type: "thinking", thinking: "B", signature: "second" },
])
expect(response.body.input[0]).toMatchObject({ type: "reasoning", encrypted_content: "opaque" })
}),
)
it.effect("places default cache breakpoints on OpenRouter latest-model aliases", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
LLM.request({
model: OpenRouter.configure({ apiKey: "test-key" }).model("~anthropic/claude-sonnet-latest"),
system: "Base agent",
tools: [{ name: "lookup", description: "Lookup", inputSchema: { type: "object", properties: {} } }],
prompt: "Hello",
}),
)
expect(prepared.body.tools?.[0]?.cache_control).toEqual({ type: "ephemeral" })
expect(prepared.body.system).toMatchObject([{ text: "Base agent", cache_control: { type: "ephemeral" } }])
expect(prepared.body.messages).toMatchObject([
{ role: "user", content: [{ text: "Hello", cache_control: { type: "ephemeral" } }] },
])
}),
)
it.effect("skips the tool breakpoint for Qwen, which caches tools with the system prompt", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
LLM.request({
model: OpenRouter.configure({ apiKey: "test-key" }).model("qwen/qwen3-coder-plus"),
system: "Base agent",
tools: [{ name: "lookup", description: "Lookup", inputSchema: { type: "object", properties: {} } }],
prompt: "Hello",
}),
)
expect(prepared.body.tools?.[0]?.cache_control).toBeUndefined()
expect(prepared.body.messages).toMatchObject([
{ role: "system", content: [{ text: "Base agent", cache_control: { type: "ephemeral" } }] },
{ role: "user", content: [{ text: "Hello", cache_control: { type: "ephemeral" } }] },
])
}),
)
it.effect("places the default Qwen conversation-tail breakpoint inside tool-result text", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
LLM.request({
model: OpenRouter.configure({ apiKey: "test-key" }).model("qwen/qwen3-coder-plus"),
messages: [
Message.user("Call the tool"),
Message.assistant([{ type: "tool-call", id: "call_1", name: "lookup", input: {} }]),
Message.tool({ id: "call_1", name: "lookup", result: "Done" }),
],
}),
)
expect(prepared.body.messages.at(-1)).toMatchObject({
role: "tool",
tool_call_id: "call_1",
content: [{ type: "text", text: '"Done"', cache_control: { type: "ephemeral" } }],
})
}),
)
it.effect("sends no default breakpoints to upstreams that cache without them", () =>
Effect.gen(function* () {
const openrouter = OpenRouter.configure({ apiKey: "test-key" })
const bodies = yield* Effect.forEach(["google/gemini-2.5-flash", "openai/gpt-5-mini"], (id) =>
compileRequest(
LLM.request({
model: openrouter.model(id),
system: "Base agent",
tools: [{ name: "lookup", description: "Lookup", inputSchema: { type: "object", properties: {} } }],
prompt: "Hello",
}),
).pipe(Effect.map((prepared) => prepared.body)),
)
bodies.forEach((body) => {
expect(body.tools?.[0]?.cache_control).toBeUndefined()
expect(JSON.stringify(body)).not.toContain("cache_control")
})
}),
)
@@ -49,14 +279,11 @@ describe("OpenRouter", () => {
expect(prepared.body).toMatchObject({
tools: [{ cache_control: { type: "ephemeral" } }],
system: [
{ text: "Base agent", cache_control: { type: "ephemeral", ttl: "1h" } },
{ text: "Project instructions", cache_control: { type: "ephemeral" } },
],
messages: [
{
role: "system",
content: [
{ text: "Base agent", cache_control: { type: "ephemeral", ttl: "1h" } },
{ text: "Project instructions", cache_control: { type: "ephemeral" } },
],
},
{
role: "user",
content: [{ text: "Hello", cache_control: { type: "ephemeral" } }],
@@ -70,10 +297,12 @@ describe("OpenRouter", () => {
Effect.gen(function* () {
const prepared = yield* compileRequest(
LLM.request({
model: OpenRouter.configure({ apiKey: "test-key" }).model("anthropic/claude-sonnet-4.6"),
model: OpenRouter.configure({ apiKey: "test-key" }).chat("anthropic/claude-sonnet-4.6"),
cache: "none",
messages: [
Message.user("Call the tool"),
Message.assistant("Unmarked reply"),
Message.user("Call again"),
Message.assistant([
{ type: "text", text: "Calling", cache: new CacheHint({ type: "ephemeral" }) },
{ type: "tool-call", id: "call_1", name: "lookup", input: {} },
@@ -90,8 +319,17 @@ describe("OpenRouter", () => {
expect(prepared.body.messages).toMatchObject([
{ role: "user", content: "Call the tool" },
{ role: "assistant", content: "Calling", cache_control: { type: "ephemeral" } },
{ role: "tool", content: '"Done"', cache_control: { type: "ephemeral", ttl: "1h" } },
{ role: "assistant", content: "Unmarked reply" },
{ role: "user", content: "Call again" },
{
role: "assistant",
content: [{ type: "text", text: "Calling", cache_control: { type: "ephemeral" } }],
tool_calls: [{ id: "call_1", type: "function", function: { name: "lookup", arguments: "{}" } }],
},
{
role: "tool",
content: [{ type: "text", text: '"Done"', cache_control: { type: "ephemeral", ttl: "1h" } }],
},
])
}),
)
@@ -108,21 +346,15 @@ describe("OpenRouter", () => {
}),
)
const system = prepared.body.messages[0]
expect(system?.role).toBe("system")
expect(
system && Array.isArray(system.content)
? system.content.filter((part) => "cache_control" in part && part.cache_control !== undefined)
: [],
).toHaveLength(4)
expect(prepared.body.system?.filter((part) => part.cache_control !== undefined)).toHaveLength(4)
}),
)
it.effect("preserves cache policy hints on reasoning-only assistant messages", () =>
it.effect("does not emit text cache markers on reasoning-only assistant messages", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
LLM.request({
model: OpenRouter.configure({ apiKey: "test-key" }).model("anthropic/claude-sonnet-4.6"),
model: OpenRouter.configure({ apiKey: "test-key" }).chat("anthropic/claude-sonnet-4.6"),
cache: { messages: "latest-assistant" },
messages: [Message.user("Think"), Message.assistant([{ type: "reasoning", text: "Reasoning" }])],
}),
@@ -130,8 +362,38 @@ describe("OpenRouter", () => {
expect(prepared.body.messages).toMatchObject([
{ role: "user", content: "Think" },
{ role: "assistant", cache_control: { type: "ephemeral" } },
{ role: "assistant", content: "" },
])
expect(prepared.body.messages[1]).not.toHaveProperty("cache_control")
}),
)
it.effect("counts wrapped system-update markers once so all four default breakpoints survive", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
LLM.request({
model: OpenRouter.configure({ apiKey: "test-key" }).chat("anthropic/claude-sonnet-4.6"),
system: [
{ type: "text", text: "Base agent" },
{ type: "text", text: "Project instructions" },
],
tools: [{ name: "lookup", description: "Lookup", inputSchema: { type: "object", properties: {} } }],
messages: [Message.user("Start"), Message.system("Updated instructions")],
}),
)
expect(prepared.body.tools?.[0]?.cache_control).toEqual({ type: "ephemeral" })
expect(prepared.body.messages.at(-1)).toMatchObject({
role: "user",
content: [
{ type: "text", text: "Start" },
{
type: "text",
text: "<system-update>\nUpdated instructions\n</system-update>",
cache_control: { type: "ephemeral" },
},
],
})
}),
)
@@ -142,7 +404,7 @@ describe("OpenRouter", () => {
model: OpenRouter.configure({
apiKey: "test-key",
providerOptions: { usage: false },
}).model("openai/gpt-4o-mini"),
}).chat("openai/gpt-4o-mini"),
cache: "none",
prompt: "Hello",
}),
@@ -190,7 +452,7 @@ describe("OpenRouter", () => {
user: "user_123",
future_option: { enabled: true },
},
}).model("anthropic/claude-3.7-sonnet:thinking"),
}).chat("anthropic/claude-3.7-sonnet:thinking"),
prompt: "Think briefly.",
promptCacheKey: "session_123",
}),
@@ -265,7 +527,7 @@ describe("OpenRouter", () => {
it.effect("preserves the upstream provider finish reason", () =>
Effect.gen(function* () {
const model = OpenRouter.configure({ apiKey: "test-key" }).model("anthropic/claude-sonnet-4.6")
const model = OpenRouter.configure({ apiKey: "test-key" }).chat("anthropic/claude-sonnet-4.6")
const response = yield* LLMClient.generate(LLM.request({ model, prompt: "Say hello." })).pipe(
Effect.provide(
fixedResponse(
@@ -282,7 +544,7 @@ describe("OpenRouter", () => {
it.effect("fails on a mid-stream provider error", () =>
Effect.gen(function* () {
const model = OpenRouter.configure({ apiKey: "test-key" }).model("openai/gpt-4o-mini")
const model = OpenRouter.configure({ apiKey: "test-key" }).chat("openai/gpt-4o-mini")
const error = yield* LLMClient.generate(LLM.request({ model, prompt: "Say hello." })).pipe(
Effect.provide(
fixedResponse(
@@ -309,7 +571,7 @@ describe("OpenRouter", () => {
]
const prepared = yield* compileRequest(
LLM.request({
model: OpenRouter.configure({ apiKey: "test-key" }).model("anthropic/claude-sonnet-4.6"),
model: OpenRouter.configure({ apiKey: "test-key" }).chat("anthropic/claude-sonnet-4.6"),
cache: "none",
messages: [
Message.assistant([
@@ -344,7 +606,7 @@ describe("OpenRouter", () => {
]
const prepared = yield* compileRequest(
LLM.request({
model: OpenRouter.configure({ apiKey: "test-key" }).model("anthropic/claude-sonnet-4.6"),
model: OpenRouter.configure({ apiKey: "test-key" }).chat("anthropic/claude-sonnet-4.6"),
cache: "none",
messages: [
Message.assistant({
@@ -385,7 +647,7 @@ describe("OpenRouter", () => {
]
const prepared = yield* compileRequest(
LLM.request({
model: OpenRouter.configure({ apiKey: "test-key" }).model("anthropic/claude-sonnet-4.6"),
model: OpenRouter.configure({ apiKey: "test-key" }).chat("anthropic/claude-sonnet-4.6"),
cache: "none",
messages: [
Message.assistant({
@@ -414,7 +676,7 @@ describe("OpenRouter", () => {
Effect.gen(function* () {
const prepared = yield* compileRequest(
LLM.request({
model: OpenRouter.configure({ apiKey: "test-key" }).model("anthropic/claude-sonnet-4.6"),
model: OpenRouter.configure({ apiKey: "test-key" }).chat("anthropic/claude-sonnet-4.6"),
cache: "none",
messages: [Message.assistant({ type: "reasoning", text: "Thinking" })],
}),
@@ -4,9 +4,9 @@ import { For, Show } from "solid-js"
import { createStore } from "solid-js/store"
import { Portal, render } from "solid-js/web"
import { LanguageProvider, UiI18nBridge } from "../src/runtime/i18n/language"
import type { BrowserPaneLayout, BrowserPaneRegistration } from "../src/runtime/platform/browser-pane"
import type { BrowserPaneEvent, BrowserPaneLayout, BrowserPaneRegistration } from "../src/runtime/platform/browser-pane"
import type { createSessionBrowser } from "../src/session/browser/model"
import { SessionBrowserPane } from "../src/session/browser/pane"
import { SessionBrowserPane, type SessionBrowserComment } from "../src/session/browser/pane"
export function mountBrowserPane() {
const host = document.createElement("main")
@@ -30,9 +30,15 @@ export function mountBrowserPane() {
covered: false,
captures: 0,
holdCapture: false,
picker: {} as Record<string, boolean | undefined>,
highlights: [] as string[],
comments: [] as SessionBrowserComment[],
})
// Each capture waits until the fixture releases it, so a spec can observe the pending state.
const held: (() => void)[] = []
const inspectors = new Set<(event: Extract<BrowserPaneEvent, { type: "inspect" }>) => void>()
const emitInspect = (event: Extract<BrowserPaneEvent, { type: "inspect" }>) =>
inspectors.forEach((listener) => listener(event))
const tabs = ["Alpha", "Beta"].map((name) => ({
id: Browser.TabID.make(`tab_${name === "Alpha" ? "11111111" : "22222222"}-1111-1111-1111-111111111111`),
title: name,
@@ -60,6 +66,12 @@ export function mountBrowserPane() {
}
return canvas.convertToBlob()
},
// The desktop confirms each picker change, as the page does once inspect mode is armed.
inspect: (tabID, enabled) => {
setStore("picker", tab.title, enabled)
emitInspect({ type: "inspect", tabID, active: enabled })
},
highlight: (_tabID, ref) => setStore("highlights", (items) => [...items, ref ?? "clear"]),
close: () => undefined,
},
]),
@@ -97,7 +109,26 @@ export function mountBrowserPane() {
}
if (command.type === "stop") setStore("loading", false)
},
onInspect: (listener) => {
inspectors.add(listener)
return () => inspectors.delete(listener)
},
reveal: () => undefined,
}
const pick = () =>
emitInspect({
type: "inspect",
tabID: browser.active().id,
active: false,
element: {
ref: Browser.Ref.make("e7"),
selector: "main > button.primary",
label: "button.primary",
role: "button",
name: "Save changes",
rect: { x: 48, y: 40, width: 160, height: 36 },
},
})
return (
<>
<h1 style={{ "font-size": "24px", "margin-bottom": "16px" }}>Browser pane lifecycle</h1>
@@ -137,13 +168,30 @@ export function mountBrowserPane() {
<button onClick={() => setStore("holdCapture", true)}>Hold capture</button>
<button onClick={() => held.splice(0).forEach((resolve) => resolve())}>Release capture</button>
<button onClick={() => setStore("covered", (covered) => !covered)}>Toggle popover</button>
<button onClick={pick}>Pick element</button>
</nav>
<p>Captures: {store.captures}</p>
<p>Picker: {store.picker[store.session] ? "on" : "off"}</p>
<p>Highlights: {store.highlights.join(",")}</p>
<div style={{ position: "relative", width: "640px", height: "360px", border: "1px solid #555" }}>
<Show when={store.mounted}>
<SessionBrowserPane browser={browser} visible={store.visible} />
<SessionBrowserPane
browser={browser}
visible={store.visible}
onComment={(comment) => setStore("comments", (items) => [...items, comment])}
/>
</Show>
</div>
<ul data-testid="fixture-comments">
<For each={store.comments}>
{(comment) => (
<li>
{comment.element.label} {comment.element.ref ? `@${comment.element.ref}` : "(no ref)"}:{" "}
{comment.comment}
</li>
)}
</For>
</ul>
<Show when={store.covered}>
{/* Floating content portals into <body> like a menu or hover card over the page. */}
<Portal mount={document.body}>
@@ -79,6 +79,81 @@ story("keeps a still of the page under floating content that covers it", async (
await expect(root.getByText("Captures: 1", { exact: true })).toBeVisible()
})
story("comments on a picked element over a still of the page", async ({ page }, testInfo) => {
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(picker).toHaveAttribute("aria-pressed", "true")
await expect(root.getByText("Picker: on", { exact: true })).toBeVisible()
await root.getByRole("button", { name: "Pick element", exact: true }).click()
await expect(picker).toHaveAttribute("aria-pressed", "false")
const editor = root.locator('[data-slot="browser-comment-editor"] textarea')
await expect(editor).toBeFocused()
await expect(root.locator('[data-slot="browser-comment-editor"]')).toContainText("button.primary")
// The native page hides behind its still so the editor can float over it.
await expect(root.getByTestId("native-Alpha")).toHaveAttribute("data-visible", "false")
await expect(root.locator("#browser-panel img")).toBeVisible()
await page.screenshot({ path: testInfo.outputPath("comment.png") })
await editor.fill("Make this the primary colour")
await editor.press("Enter")
await expect(root.getByTestId("fixture-comments")).toHaveText("button.primary @e7: Make this the primary colour")
await expect(root.locator('[data-component="browser-comment"]')).toHaveCount(0)
await expect(root.getByText("Highlights: clear", { exact: true })).toBeVisible()
await expect(root.getByTestId("native-Alpha")).toHaveAttribute("data-visible", "true")
})
story("cancels the picker and a comment with Escape", 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(picker).toHaveAttribute("aria-pressed", "true")
await page.keyboard.press("Escape")
await expect(picker).toHaveAttribute("aria-pressed", "false")
await expect(root.getByText("Picker: off", { exact: true })).toBeVisible()
await picker.click()
await root.getByRole("button", { name: "Pick element", exact: true }).click()
const editor = root.locator('[data-slot="browser-comment-editor"] textarea')
await expect(editor).toBeFocused()
await editor.press("Escape")
await expect(root.locator('[data-component="browser-comment"]')).toHaveCount(0)
await expect(root.getByTestId("fixture-comments")).toHaveText("")
await expect(root.getByTestId("native-Alpha")).toHaveAttribute("data-visible", "true")
})
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()
await root.getByRole("button", { name: "Pick element", exact: true }).click()
const editor = root.locator('[data-slot="browser-comment-editor"] textarea')
await editor.fill("Still misaligned after the reload")
await root.getByRole("button", { name: "Load current page", exact: true }).click()
await expect(editor).toHaveValue("Still misaligned after the reload")
await editor.press("Enter")
await expect(root.getByTestId("fixture-comments")).toHaveText(
"button.primary (no ref): Still misaligned after the reload",
)
})
story("keeps the comment editor and its actions inside the page", async ({ page }) => {
const root = page.getByTestId("browser-pane-fixture")
await root.getByRole("button", { name: "Select an element to comment on", exact: true }).click()
await root.getByRole("button", { name: "Pick element", exact: true }).click()
const editor = root.locator('[data-slot="browser-comment-editor"]')
// As if the user dragged the textarea's resize handle far past the page.
await editor.locator("textarea").evaluate((element) => {
element.style.height = "900px"
})
const surface = await root.locator('[data-component="browser-comment"]').boundingBox()
const box = await editor.boundingBox()
expect(surface && box && box.y + box.height <= surface.y + surface.height).toBe(true)
const submit = editor.getByRole("button", { name: "Comment", exact: true })
await submit.scrollIntoViewIfNeeded()
await expect(submit).toBeInViewport()
})
story("shows the empty state over a blank native page and restores navigation", async ({ page }) => {
const root = page.getByTestId("browser-pane-fixture")
await root.getByRole("button", { name: "Blank page", exact: true }).click()
@@ -186,11 +186,17 @@ for (const scenario of scenarios) {
const toolTitle = element
.closest('[data-component="edit-tool"]')
?.querySelector('[data-slot="collapsible-trigger"][data-locked]')
const top = viewport.getBoundingClientRect().top + (title?.getBoundingClientRect().height ?? 0)
// Grouped file headers stack below the stuck Used header.
const group = element
.closest('[data-component="collapsed-tool-group"]')
?.querySelector(':scope > [data-component="collapsible"] > [data-slot="collapsible-trigger"]')
const sessionTop = viewport.getBoundingClientRect().top + (title?.getBoundingClientRect().height ?? 0)
const top = sessionTop + (group?.getBoundingClientRect().height ?? 0)
const rect = element.getBoundingClientRect()
const trigger = element.querySelector("button")!
return {
gap: Math.abs(rect.top - top - (toolTitle?.getBoundingClientRect().height ?? 0)),
groupGap: group ? Math.abs(group.getBoundingClientRect().top - sessionTop) : 0,
titleGap: toolTitle ? Math.abs(toolTitle.getBoundingClientRect().top - top) : 0,
clickable: trigger.contains(
document.elementFromPoint(rect.left + rect.width / 2, rect.top + rect.height / 2),
@@ -198,7 +204,7 @@ for (const scenario of scenarios) {
}
}),
)
.toEqual({ gap: 0, titleGap: 0, clickable: true })
.toEqual({ gap: 0, groupGap: 0, titleGap: 0, clickable: true })
await page.screenshot({ path: info.outputPath(`${file}.png`) })
}
@@ -0,0 +1,43 @@
import { expect, test } from "@playwright/test"
import { base64Encode } from "@opencode/util/encode"
import {
assistantMessage,
session,
setupTimeline,
textPart,
userMessage,
} from "../performance/timeline-stability/fixture"
const target = "ses_0123456789abcdefghijklmnop"
const missing = "ses_abcdefghijklmnopqrstuvwxyz"
const server = `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}`
test("opens a verified session from agent prose or inline code with the keyboard", async ({ page }) => {
await setupTimeline(page, {
sessions: [session(), session({ id: target, title: "Linked session" })],
messages: [
userMessage(),
assistantMessage([textPart("prt_session_links", `Visit ${target} or \`${target}\` to see the result.`)]),
],
})
const markdown = page.locator('[data-component="markdown"]').filter({ hasText: `Visit ${target}` })
await expect(markdown).toHaveAttribute("data-markdown-ready", "")
await expect(markdown.getByRole("button", { name: target })).toHaveCount(2)
await markdown
.getByRole("button", { name: target })
.filter({ has: page.locator("code") })
.press("Enter")
await expect(page).toHaveURL(`/server/${base64Encode(server)}/session/${target}`)
await expect(page.locator(`[data-titlebar-tab-link][href$="/session/${target}"]`)).toContainText("Linked session")
})
test("does not navigate to an ID that is absent from the current server", async ({ page }) => {
await setupTimeline(page, {
messages: [userMessage(), assistantMessage([textPart("prt_session_missing", `See ${missing}.`)])],
})
const markdown = page.locator('[data-component="markdown"]').filter({ hasText: `See ${missing}.` })
await expect(markdown).toHaveAttribute("data-markdown-ready", "")
await markdown.getByRole("button", { name: missing }).click()
await expect(page.getByText("This session cannot be found")).toBeVisible()
await expect(page).toHaveURL(/\/session\/ses_timeline_stability$/)
})
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode/app",
"version": "2.0.19",
"version": "2.0.20",
"description": "",
"type": "module",
"exports": {
+4
View File
@@ -30,6 +30,10 @@ export type ComposerControls = {
opened: () => boolean
open: () => void
}
/** Present in sessions with a desktop browser pane. */
browser?: {
reveal: (tabID: string, ref?: string) => void
}
}
}
@@ -0,0 +1,41 @@
import { describe, expect, test } from "bun:test"
import { commentContextItem, formatBrowserCommentNote, readPromptPresentation } from "./comment-note"
const browser = {
type: "browser" as const,
tabID: "tab_00000000-0000-4000-8000-000000000000",
url: "http://localhost:5173/",
element: { ref: "e42", selector: "#save", label: "button#save" },
comment: "Rename this",
}
describe("browser element comments", () => {
test("read from message metadata beside file comments and skip malformed entries", () => {
const value = readPromptPresentation({
displayText: "hi",
comments: [browser, { ...browser, element: { label: "button" } }, { path: "src/app.ts", comment: "Keep" }],
})
expect(value?.comments).toEqual([browser, { path: "src/app.ts", comment: "Keep" }])
})
test("explain a selector that crosses into a shadow root", () => {
expect(
formatBrowserCommentNote({ ...browser, element: { ...browser.element, selector: "#card >>> div > button" } }),
).toContain('selector "#card >>> div > button" (">>>" enters a shadow root)')
expect(formatBrowserCommentNote(browser)).not.toContain("shadow root")
})
test("leave out a selector that was too long to keep", () => {
expect(formatBrowserCommentNote({ ...browser, element: { ...browser.element, selector: "" } })).toBe(
'The user made the following comment regarding the "button#save" element in browser tab tab_00000000-0000-4000-8000-000000000000 at http://localhost:5173/ (browser ref @e42, usable as ref in any browser tool including browser.evaluate until the page navigates): Rename this',
)
})
test("return to the composer without their element ref", () => {
expect(commentContextItem(browser)).toEqual({
...browser,
element: { selector: "#save", label: "button#save" },
commentID: expect.any(String),
})
})
})
+42 -2
View File
@@ -1,12 +1,18 @@
import { Option, Schema } from "effect"
import type { FileSelection } from "@/workspaces/files/model"
import { BrowserComment, durableBrowserElement, type ContextItem } from "./schema"
export type PromptComment = {
export type PromptFileComment = {
type?: "file"
path: string
selection?: FileSelection
comment: string
preview?: string
origin?: "review" | "file"
}
export type PromptComment = PromptFileComment | BrowserComment
const decodeBrowserComment = Schema.decodeUnknownOption(BrowserComment)
/** An attachment the model receives as a path on the server rather than inline bytes. */
export type PromptAttachmentReference = {
@@ -30,7 +36,7 @@ function selection(selection: unknown) {
} satisfies FileSelection
}
export function createCommentMetadata(input: PromptComment) {
export function createCommentMetadata(input: PromptFileComment) {
return {
opencodeComment: {
path: input.path,
@@ -78,6 +84,7 @@ export function readPromptPresentation(value: unknown) {
}),
comments: comments.flatMap((item): PromptComment[] => {
if (!item || typeof item !== "object") return []
if ((item as { type?: unknown }).type === "browser") return Option.toArray(decodeBrowserComment(item))
const path = (item as { path?: unknown }).path
const comment = (item as { comment?: unknown }).comment
if (typeof path !== "string" || typeof comment !== "string") return []
@@ -100,6 +107,39 @@ export function formatAttachmentReference(input: PromptAttachmentReference) {
return `Attached file: \`${input.path}\``
}
export function formatBrowserCommentNote(input: BrowserComment) {
const element = input.element
// Page-provided strings are quoted so they read as data, not as part of the user's request.
const details = [
element.role ? `role ${element.role}` : undefined,
element.name ? `accessible name ${JSON.stringify(element.name)}` : undefined,
element.text && element.text !== element.name ? `text ${JSON.stringify(element.text.slice(0, 80))}` : undefined,
// A selector too long to keep is empty rather than cut into invalid syntax.
element.selector
? `selector ${JSON.stringify(element.selector)}${element.selector.includes(" >>> ") ? ' (">>>" enters a shadow root)' : ""}`
: undefined,
element.ref
? `browser ref @${element.ref}, usable as ref in any browser tool including browser.evaluate until the page navigates`
: undefined,
].filter((detail) => detail !== undefined)
return `The user made the following comment regarding the ${JSON.stringify(element.label)} element in browser tab ${input.tabID} at ${input.url}${details.length ? ` (${details.join("; ")})` : ""}: ${input.comment}`
}
/** Restores a sent comment to the composer, for example after a revert or fork. */
export function commentContextItem(comment: PromptComment): ContextItem {
// The message may predate the desktop process, so its element ref can no longer be trusted.
if (comment.type === "browser")
return { ...comment, element: durableBrowserElement(comment.element), commentID: crypto.randomUUID() }
return {
type: "file",
path: comment.path,
selection: comment.selection,
comment: comment.comment,
preview: comment.preview,
commentOrigin: comment.origin,
}
}
export function formatCommentNote(input: { path: string; selection?: FileSelection; comment: string }) {
const start = input.selection ? Math.min(input.selection.startLine, input.selection.endLine) : undefined
const end = input.selection ? Math.max(input.selection.startLine, input.selection.endLine) : undefined
@@ -279,6 +279,39 @@ export const MixedAttachments = {
),
}
export const BrowserElementComment = {
render: () => (
<ComposerStory
prompt={text("Tidy up the settings page")}
comments={[
{
type: "browser",
key: "browser:tab_story:c=save",
tabID: "tab_00000000-0000-4000-8000-000000000000",
url: "http://localhost:5173/settings",
title: "Settings",
element: {
ref: "e42",
selector: "#settings > form > button.btn.primary",
label: "button.btn.primary",
role: "button",
name: "Save changes",
},
comment: "Match the secondary button height",
commentID: "save",
},
{
type: "file",
key: "comment:src/app.tsx",
path: "src/app.tsx",
selection: { startLine: 12, startChar: 0, endLine: 14, endChar: 0 },
comment: "Keep the normal flow flat",
},
]}
/>
),
}
export const ModelAndVariant = { render: () => <ComposerStory prompt={text("Compare both variants")} /> }
export const SlashSuggestions = { render: () => <ComposerStory suggestions="command" /> }
+5 -2
View File
@@ -533,8 +533,11 @@ export function ComposerAttachments(props: {
>
<CommentCard
comment={comment.comment ?? ""}
path={comment.path}
selection={comment.selection}
target={
comment.type === "browser"
? { type: "browser", element: comment.element.label }
: { type: "file", path: comment.path, selection: comment.selection }
}
active={comment.key === props.activeCommentID}
onClick={() => props.onCommentClick?.(comment)}
/>
+12 -6
View File
@@ -98,7 +98,7 @@ export function createComposerModel(adapter: ComposerAdapter, options?: { queue?
const byID = new Map(comments.all().map((item) => [`${item.file}\n${item.id}`, item] as const))
return prompt.context.items().flatMap((item) => {
const comment = item.comment?.trim()
if (!comment) return []
if (!comment || item.type !== "file") return []
const selection = item.commentID ? byID.get(`${item.path}\n${item.commentID}`)?.selection : undefined
const nextSelection =
selection ??
@@ -129,9 +129,11 @@ export function createComposerModel(adapter: ComposerAdapter, options?: { queue?
time: item.time,
})),
)
prompt.context.replaceComments(
items.map((item) => ({
type: "file",
// History records file comments only; browser comments stay with the draft while it is browsed.
prompt.context.replaceComments([
...prompt.context.items().filter((item) => item.type === "browser"),
...items.map((item) => ({
type: "file" as const,
path: item.path,
selection: selectionFromLines(item.selection),
comment: item.comment,
@@ -139,7 +141,7 @@ export function createComposerModel(adapter: ComposerAdapter, options?: { queue?
commentOrigin: item.origin,
preview: item.preview,
})),
)
])
}
const referenceDescription = (reference: ReferenceInfo) =>
@@ -298,7 +300,7 @@ export function createComposerModel(adapter: ComposerAdapter, options?: { queue?
mention: { type: "file", path, content: `@${path}`, start: 0, end: 0 },
})),
onContextRemove(item) {
if (item?.commentID) comments.remove(item.path, item.commentID)
if (item.type === "file" && item.commentID) comments.remove(item.path, item.commentID)
},
openAttachment: (attachment) => {
if (attachment.type !== "image") return
@@ -308,6 +310,10 @@ export function createComposerModel(adapter: ComposerAdapter, options?: { queue?
},
openContext(key) {
const item = controller.contextItem(key)
if (item?.type === "browser") {
adapter.controls().session.browser?.reveal(item.tabID, item.element.ref)
return
}
if (item) openComment(item, adapter.controls(), layout, files, comments)
},
onEditor(element) {
+2 -1
View File
@@ -28,6 +28,7 @@ export {
} from "./state"
export type {
AgentPart,
BrowserContextItem,
ContentPart,
ContextItem,
FileAttachmentPart,
@@ -160,7 +161,7 @@ export const { use: useComposerState, provider: ComposerPersistenceProvider } =
removeComment: (path: string, commentID: string) => session().context.removeComment(path, commentID),
updateComment: (path: string, commentID: string, next: Partial<FileContextItem> & { comment?: string }) =>
session().context.updateComment(path, commentID, next),
replaceComments: (items: FileContextItem[]) => session().context.replaceComments(items),
replaceComments: (items: ContextItem[]) => session().context.replaceComments(items),
},
set: (prompt: Prompt, cursorPosition?: number, scope?: PromptScope) => pick(scope).set(prompt, cursorPosition),
reset: (scope?: PromptScope) => pick(scope).reset(),
+51
View File
@@ -144,6 +144,57 @@ describe("buildPromptRequest", () => {
expect(result.files.some((file) => file.uri === "file:///repo/src/shared.ts")).toBe(true)
})
test("sends a browser element comment as a note the browser tools can act on", () => {
const result = buildPromptRequest({
prompt: [{ type: "text", content: "tidy up", start: 0, end: 7 }],
context: [
{
key: "browser:tab:c=1",
type: "browser",
tabID: "tab_00000000-0000-4000-8000-000000000000",
url: "http://localhost:5173/settings",
title: "Settings",
element: {
ref: "e42",
selector: "#settings > button.primary",
label: "button.primary",
role: "button",
name: "Save",
text: "Save",
},
comment: "Match @src/button.css",
commentID: "1",
},
],
images: [],
text: "tidy up",
sessionDirectory: "/repo",
})
expect(result.text).toBe(
'tidy up\nThe user made the following comment regarding the "button.primary" element in browser tab tab_00000000-0000-4000-8000-000000000000 at http://localhost:5173/settings (role button; accessible name "Save"; selector "#settings > button.primary"; browser ref @e42, usable as ref in any browser tool including browser.evaluate until the page navigates): Match @src/button.css',
)
expect(result.comments).toEqual([
{
type: "browser",
tabID: "tab_00000000-0000-4000-8000-000000000000",
url: "http://localhost:5173/settings",
title: "Settings",
element: {
ref: "e42",
selector: "#settings > button.primary",
label: "button.primary",
role: "button",
name: "Save",
text: "Save",
},
comment: "Match @src/button.css",
},
])
// A browser comment has no file of its own; only files it mentions are attached.
expect(result.files).toEqual([{ uri: "file:///repo/src/button.css", mime: "text/plain", name: "button.css" }])
})
test("handles Windows paths correctly (simulated on macOS)", () => {
const prompt: Prompt = [{ type: "file", path: "src\\foo.ts", content: "@src\\foo.ts", start: 0, end: 11 }]
+35 -21
View File
@@ -1,9 +1,18 @@
import { getFilename } from "@opencode/util/path"
import type { FileSelection } from "@/workspaces/files/model"
import { encodeFilePath } from "@/workspaces/files/path"
import type { AgentPart, FileAttachmentPart, ImageAttachmentPart, PathAttachmentPart, Prompt, SkillPart } from "@/composer/state"
import type {
AgentPart,
ContextItem,
FileAttachmentPart,
ImageAttachmentPart,
PathAttachmentPart,
Prompt,
SkillPart,
} from "@/composer/state"
import {
formatAttachmentReference,
formatBrowserCommentNote,
formatCommentNote,
type PromptAttachmentReference,
type PromptComment,
@@ -20,20 +29,9 @@ type PromptRequest = {
attachments: PromptAttachmentReference[]
}
type ContextFile = {
key: string
type: "file"
path: string
selection?: FileSelection
comment?: string
commentID?: string
commentOrigin?: "review" | "file"
preview?: string
}
type BuildPromptRequestInput = {
prompt: Prompt
context: ContextFile[]
context: (ContextItem & { key: string })[]
images: (Omit<ImageAttachmentPart, "blob"> & { dataUrl: string })[]
text: string
sessionDirectory: string
@@ -87,7 +85,27 @@ export function buildPromptRequest(input: BuildPromptRequestInput): PromptReques
const used = new Set(files.map((file) => file.uri))
const comments: PromptComment[] = []
const mentioned = (comment: string) =>
parseCommentMentions(comment).flatMap((path) => {
const uri = `file://${encodeFilePath(absolute(input.sessionDirectory, path))}`
if (used.has(uri)) return []
used.add(uri)
return [{ uri, mime: "text/plain", name: getFilename(path) }]
})
const context = input.context.flatMap((item) => {
if (item.type === "browser") {
const comment = item.comment.trim()
if (!comment) return []
comments.push({
type: "browser",
tabID: item.tabID,
url: item.url,
...(item.title ? { title: item.title } : {}),
element: { ...item.element },
comment,
})
return mentioned(comment)
}
const path = absolute(input.sessionDirectory, item.path)
const uri = `file://${encodeFilePath(path)}${fileQuery(item.selection)}`
const comment = item.comment?.trim()
@@ -104,13 +122,7 @@ export function buildPromptRequest(input: BuildPromptRequestInput): PromptReques
preview: item.preview,
origin: item.commentOrigin,
})
const mentions = parseCommentMentions(comment).flatMap((path) => {
const uri = `file://${encodeFilePath(absolute(input.sessionDirectory, path))}`
if (used.has(uri)) return []
used.add(uri)
return [{ uri, mime: "text/plain", name: getFilename(path) }]
})
return [file, ...mentions]
return [file, ...mentioned(comment)]
})
const inline = input.images.map((attachment) => ({
@@ -127,7 +139,9 @@ export function buildPromptRequest(input: BuildPromptRequestInput): PromptReques
text: [
...(input.text.trim() ? [input.text] : []),
...attachments.map(formatAttachmentReference),
...comments.map(formatCommentNote),
...comments.map((comment) =>
comment.type === "browser" ? formatBrowserCommentNote(comment) : formatCommentNote(comment),
),
].join("\n"),
displayText: input.text,
files: [...files, ...context, ...inline],
+22
View File
@@ -53,6 +53,28 @@ describe("composer persistence schemas", () => {
expect(decode({}).prompt).toEqual(DEFAULT_PROMPT)
})
test("restores browser element comments without the process-bound element ref", () => {
const decode = Schema.decodeUnknownSync(
Persistence.withInitial(ComposerStore, { prompt: DEFAULT_PROMPT, context: { items: [] } }),
)
const browser = {
type: "browser" as const,
tabID: "tab_00000000-0000-4000-8000-000000000000",
url: "http://localhost:5173/",
element: { ref: "e42", selector: "#save", label: "button#save", role: 42 },
comment: "Rename this",
commentID: "note",
}
const value = decode({ context: { items: [browser, { ...browser, url: null }] } })
expect(value.context.items).toEqual([
{
...browser,
element: { selector: "#save", label: "button#save" },
key: "browser:tab_00000000-0000-4000-8000-000000000000:c=note",
},
])
})
test("drops invalid parts without losing valid mentions or optional field recovery", () => {
const value = Schema.decodeUnknownSync(
Persistence.withInitial(ComposerStore, { prompt: DEFAULT_PROMPT, context: { items: [] } }),
+57 -2
View File
@@ -134,9 +134,49 @@ export const FileContextItem = Persistence.struct({
preview: Persistence.optional(Schema.String),
})
export type FileContextItem = typeof FileContextItem.Type
export type ContextItem = FileContextItem
/** An element the user picked in a desktop browser tab. The ref is valid until the page navigates. */
export const BrowserElement = Persistence.struct({
ref: Persistence.optional(Schema.String),
selector: Schema.String,
label: Schema.String,
role: Persistence.optional(Schema.String),
name: Persistence.optional(Schema.String),
text: Persistence.optional(Schema.String),
})
export type BrowserElement = typeof BrowserElement.Type
/**
* A ref names an element only inside the desktop process that picked it; a later process can hand
* the same ref to another element. Anything that may outlive the pick keeps the description only.
*/
export function durableBrowserElement(element: BrowserElement): BrowserElement {
return {
selector: element.selector,
label: element.label,
...(element.role ? { role: element.role } : {}),
...(element.name ? { name: element.name } : {}),
...(element.text ? { text: element.text } : {}),
}
}
const BrowserCommentFields = {
type: Schema.Literal("browser"),
tabID: Schema.String,
url: Schema.String,
title: Persistence.optional(Schema.String),
element: BrowserElement,
comment: Schema.String,
}
/** A comment on a browser element as sent in message metadata. */
export const BrowserComment = Persistence.struct(BrowserCommentFields)
export type BrowserComment = typeof BrowserComment.Type
export const BrowserContextItem = Persistence.struct({ ...BrowserCommentFields, commentID: Schema.String })
export type BrowserContextItem = typeof BrowserContextItem.Type
export type ContextItem = FileContextItem | BrowserContextItem
export function contextItemKey(item: ContextItem) {
if (item.type === "browser") return `browser:${item.tabID}:c=${item.commentID}`
const key = `${item.type}:${item.path}:${item.selection?.startLine}:${item.selection?.endLine}`
if (item.commentID) return `${key}:c=${item.commentID}`
const comment = item.comment?.trim()
@@ -145,12 +185,27 @@ export function contextItemKey(item: ContextItem) {
return `${key}:c=${digest.slice(0, 8)}`
}
const ContextEntry = Schema.Struct({ ...FileContextItem.fields, key: Persistence.optional(Schema.String) }).pipe(
const FileContextEntry = Schema.Struct({ ...FileContextItem.fields, key: Persistence.optional(Schema.String) }).pipe(
Schema.decodeTo(Persistence.struct({ ...FileContextItem.fields, key: Schema.String }).pipe(Schema.toType), {
decode: SchemaGetter.transform((item) => ({ ...item, key: contextItemKey(item) })),
encode: SchemaGetter.transform((item) => item),
}),
)
const BrowserContextEntry = Schema.Struct({
...BrowserContextItem.fields,
key: Persistence.optional(Schema.String),
}).pipe(
Schema.decodeTo(Persistence.struct({ ...BrowserContextItem.fields, key: Schema.String }).pipe(Schema.toType), {
// A stored draft can outlive the desktop process that picked the element.
decode: SchemaGetter.transform((item) => ({
...item,
element: durableBrowserElement(item.element),
key: contextItemKey(item),
})),
encode: SchemaGetter.transform((item) => item),
}),
)
const ContextEntry = Schema.Union([FileContextEntry, BrowserContextEntry])
export const DEFAULT_PROMPT: Prompt = [{ type: "text", content: "", start: 0, end: 0 }]
+6 -1
View File
@@ -12,7 +12,11 @@ import { cycleModelVariant, getConfiguredAgentVariant, resolveModelVariant } fro
import { useComposerState } from "./persistence"
import { useConfiguredModel } from "@/providers/models/configured"
export function createComposerControls(input: { sessionKey: Accessor<string>; model?: ModelSelection }) {
export function createComposerControls(input: {
sessionKey: Accessor<string>
model?: ModelSelection
browser?: ComposerControls["session"]["browser"]
}) {
const layout = useLayout()
const local = useLocal()
const sdk = useWorkspaceLocation()
@@ -40,6 +44,7 @@ export function createComposerControls(input: { sessionKey: Accessor<string>; mo
session: {
tabs: layout.tabs(input.sessionKey),
reviewPanel: view.reviewPanel,
browser: input.browser,
},
}
})
+5 -2
View File
@@ -17,6 +17,9 @@ import {
export { DEFAULT_PROMPT } from "./schema"
export type {
AgentPart,
BrowserComment,
BrowserContextItem,
BrowserElement,
ComposerStore,
ContentPart,
ContextItem,
@@ -38,7 +41,7 @@ type InitialPrompt = {
}
export function isCommentItem(item: ContextItem | (ContextItem & { key: string })) {
return item.type === "file" && !!item.comment?.trim()
return !!item.comment?.trim()
}
function createComposerActions(setStore: SetStoreFunction<ComposerStore>) {
@@ -137,7 +140,7 @@ function createComposerStateValue(store: ComposerStore, setStore: SetStoreFuncti
)
clearRetry()
},
replaceComments(items: FileContextItem[]) {
replaceComments(items: ContextItem[]) {
setStore("context", "items", (current) => [
...current.filter((item) => !isCommentItem(item)),
...items.map((item) => ({ ...item, key: contextItemKey(item) })),
+53 -26
View File
@@ -1,6 +1,7 @@
import { SessionMessage } from "@opencode/schema/session-message"
import type { SessionMessageUser } from "@opencode/client/promise"
import type { Accessor } from "solid-js"
import type { PromptComment } from "./comment-note"
import type { PromptHistoryComment } from "./history/entry"
import type { ImageAttachmentPart, Prompt } from "./state"
import { clonePrompt, promptLength } from "./prompt-parts"
@@ -73,10 +74,13 @@ export function createComposerSubmit(input: ComposerSubmitInput) {
const submission = createComposerSubmission({
target: input.adapter.state,
prompt,
context: input.adapter.state.context.items().map((item) => ({
...item,
selection: item.selection ? { ...item.selection } : undefined,
})),
context: input.adapter.state.context
.items()
.map((item) =>
item.type === "browser"
? { ...item, element: { ...item.element } }
: { ...item, selection: item.selection ? { ...item.selection } : undefined },
),
})
const read = readSubmission(input, submission.prompt, submission.context, text, options?.alternate ?? false)
if (!read) {
@@ -195,19 +199,30 @@ function handoffMessage(value: ComposerSubmission): SessionMessageUser {
attachments: value.prompt.flatMap((part) =>
part.type === "path" ? [{ name: part.filename, mime: part.mime, path: part.path }] : [],
),
comments: value.context.flatMap((item) =>
item.comment?.trim()
? [
{
path: item.path,
comment: item.comment.trim(),
...(item.selection ? { selection: { ...item.selection } } : {}),
...(item.preview !== undefined ? { preview: item.preview } : {}),
...(item.commentOrigin ? { origin: item.commentOrigin } : {}),
},
]
: [],
),
comments: value.context.flatMap((item): PromptComment[] => {
const comment = item.comment?.trim()
if (!comment) return []
if (item.type === "browser")
return [
{
type: "browser",
tabID: item.tabID,
url: item.url,
...(item.title ? { title: item.title } : {}),
element: { ...item.element },
comment,
},
]
return [
{
path: item.path,
comment,
...(item.selection ? { selection: { ...item.selection } } : {}),
...(item.preview !== undefined ? { preview: item.preview } : {}),
...(item.commentOrigin ? { origin: item.commentOrigin } : {}),
},
]
}),
agent: value.selection.agent,
model: {
...value.selection.model,
@@ -287,15 +302,27 @@ function restoreSubmission(
restored.target.context.replaceComments(
restored.context
.filter((item) => !!item.comment?.trim())
.map((item) => ({
type: "file",
path: item.path,
selection: item.selection,
comment: item.comment,
commentID: item.commentID,
commentOrigin: item.commentOrigin,
preview: item.preview,
})),
.map((item) =>
item.type === "browser"
? {
type: "browser",
tabID: item.tabID,
url: item.url,
title: item.title,
element: item.element,
comment: item.comment,
commentID: item.commentID,
}
: {
type: "file",
path: item.path,
selection: item.selection,
comment: item.comment,
commentID: item.commentID,
commentOrigin: item.commentOrigin,
preview: item.preview,
},
),
)
// A recovered follow-up changes the payload, so it must use a new admission ID.
if (value.mode === "normal" && restored.prompt === submission.prompt) {
+1
View File
@@ -5,6 +5,7 @@ export { loadLocaleDict, normalizeLocale, type Locale, useLanguage } from "./run
export { type FatalRendererErrorLog, type Platform, PlatformProvider } from "./runtime/platform/platform"
export type {
BrowserPaneCommand,
BrowserPaneElement,
BrowserPaneEndpoint,
BrowserPaneEvent,
BrowserPaneLayout,
+36
View File
@@ -445,6 +445,42 @@
}
}
@keyframes browser-comment-enter {
from {
opacity: 0;
}
}
@keyframes browser-comment-editor-enter {
from {
opacity: 0;
transform: translateY(4px) scale(0.98);
}
}
[data-component="browser-comment"] {
animation: browser-comment-enter 160ms cubic-bezier(0.215, 0.61, 0.355, 1);
}
/* Dim the frozen page around the picked element, whose DevTools highlight stays in the still. */
[data-component="browser-comment"] [data-slot="browser-comment-spotlight"] {
box-shadow:
0 0 0 1px var(--v2-border-border-focus),
0 0 0 100vmax color-mix(in srgb, var(--v2-overlay-simple-overlay-scrim) 50%, transparent);
}
[data-component="browser-comment"] [data-slot="browser-comment-editor"] {
animation: browser-comment-editor-enter 200ms cubic-bezier(0.215, 0.61, 0.355, 1);
transform-origin: top left;
}
@media (prefers-reduced-motion: reduce) {
[data-component="browser-comment"],
[data-component="browser-comment"] [data-slot="browser-comment-editor"] {
animation: none;
}
}
[data-slot="home-projects-scroll"] {
timeline-scope: --home-projects-scroll;
}
@@ -62,6 +62,7 @@ export function createComposerProjectControls(props: { draftId: string; worktree
return createMemo<PromptProjectControls>(() => ({
available: projects(),
directory: location().directory,
projectID: location().current?.project.id,
server: servers.list.length > 1 ? ServerConnection.key(projectServer()) : undefined,
select: selectProject,
add: addProject,
@@ -0,0 +1,51 @@
import { describe, expect, mock, test } from "bun:test"
import { createRoot } from "solid-js"
import type { PromptProjectControls } from "./selector"
mock.module("@/runtime/i18n/language", () => ({ useLanguage: () => ({ t: (key: string) => key }) }))
const { createPromptProjectController } = await import("./selector")
describe("new session project selection", () => {
test("shows the current project's appearance before its worktree inventory loads", () => {
const generic = { id: "other", worktree: "/other" }
const current = {
id: "current",
worktree: "/repo",
name: "My custom name",
icon: { override: "data:image/png;base64,AAAA" },
}
const controls: PromptProjectControls = {
available: [generic, current],
directory: "/repo/.opencode/worktree/feature",
projectID: "current",
select() {},
add() {},
}
createRoot((dispose) => {
const controller = createPromptProjectController({ controls: () => controls, onDone() {} })
expect(controller.selected()).toEqual(current)
dispose()
})
})
test("does not select a matching project ID from another server", () => {
const remote = { id: "shared", worktree: "/remote", server: { key: "remote", name: "Remote" } }
const local = { id: "shared", worktree: "/local", server: { key: "local", name: "Local" } }
const controls: PromptProjectControls = {
available: [remote, local],
directory: "/local/feature",
projectID: "shared",
server: "local",
select() {},
add() {},
}
createRoot((dispose) => {
const controller = createPromptProjectController({ controls: () => controls, onDone() {} })
expect(controller.selected()).toEqual(local)
dispose()
})
})
})
@@ -31,6 +31,7 @@ export type PromptProject = {
export type PromptProjectControls = {
available: PromptProject[]
directory: string
projectID?: string
server?: string
select: (worktree: string, server?: string) => void
add: (title: string, server?: string) => void
@@ -56,14 +57,15 @@ export function createPromptProjectController(input: {
let searchRef: HTMLInputElement | undefined
const current = () => {
const key = pathKey(input.controls().directory)
return input
.controls()
.available.find(
const controls = input.controls()
const key = pathKey(controls.directory)
const projects = controls.available.filter((project) => !project.server || project.server.key === controls.server)
return (
projects.find(
(project) =>
(!project.server || project.server.key === input.controls().server) &&
(pathKey(project.worktree) === key || project.sandboxes?.some((sandbox) => pathKey(sandbox) === key)),
)
pathKey(project.worktree) === key || project.sandboxes?.some((sandbox) => pathKey(sandbox) === key),
) ?? projects.find((project) => controls.projectID && project.id === controls.projectID)
)
}
const selected = () => current() ?? input.controls().available[0]
const projects = () => {
@@ -0,0 +1,23 @@
import { Button } from "@opencode/ui/button"
import { useDialog } from "@opencode/ui/context/dialog"
import { onMount } from "solid-js"
import { DialogChatGPTUsageLimit } from "./chatgpt-usage-limit"
function UsageLimitStory() {
const dialog = useDialog()
const open = () => dialog.show(() => <DialogChatGPTUsageLimit />)
onMount(open)
return (
<Button variant="neutral" onClick={open}>
Open ChatGPT usage limit
</Button>
)
}
export default {
title: "App/Dialogs/Connect Provider",
id: "app-dialog-chatgpt-usage-limit",
}
export const ChatGPTUsageLimit = { render: () => <UsageLimitStory /> }
@@ -0,0 +1,48 @@
import { Button } from "@opencode/ui/button"
import { useDialog } from "@opencode/ui/context/dialog"
import { Dialog, DialogTitle } from "@opencode/ui/dialog"
import { ProviderIcon } from "@opencode/ui/provider-icon"
import { useLanguage } from "@/runtime/i18n/language"
import { usePlatform } from "@/runtime/platform/platform"
export function DialogChatGPTUsageLimit() {
const dialog = useDialog()
const language = useLanguage()
const platform = usePlatform()
return (
<Dialog fit containerClass="!w-[min(calc(100vw_-_32px),390px)] !rounded-xl">
<div class="flex w-full flex-col items-center px-8 pb-8 pt-9 text-center [font-family:var(--v2-font-family-sans)]">
<ProviderIcon id="openai" class="!size-12 text-v2-icon-icon-base" aria-hidden="true" />
<div class="mt-6 max-w-[270px] text-[20px] font-[530] leading-7 tracking-[-0.3px] text-v2-text-text-base">
<DialogTitle>
<bdi dir="auto">{language.t("provider.connect.chatgptUsageLimit.title")}</bdi>
</DialogTitle>
</div>
<p class="mt-3 text-[13px] leading-5 text-v2-text-text-muted">
<bdi dir="auto">{language.t("provider.connect.chatgptUsageLimit.description")}</bdi>
</p>
<Button
variant="contrast"
size="large"
class="mt-8 w-full"
autofocus
onClick={() => {
platform.openExternal("https://chatgpt.com/settings/usage")
dialog.close()
}}
>
{language.t("provider.connect.chatgptUsageLimit.manage")}
</Button>
<Button
variant="ghost"
size="large"
class="mt-2 w-full"
onClick={() => dialog.close()}
>
{language.t("provider.connect.chatgptUsageLimit.close")}
</Button>
</div>
</Dialog>
)
}
@@ -0,0 +1,23 @@
import { Button } from "@opencode/ui/button"
import { useDialog } from "@opencode/ui/context/dialog"
import { onMount } from "solid-js"
import { DialogChatGPTPlanWelcome } from "./chatgpt-welcome"
function WelcomeStory() {
const dialog = useDialog()
const open = () => dialog.show(() => <DialogChatGPTPlanWelcome />)
onMount(open)
return (
<Button variant="neutral" onClick={open}>
Open ChatGPT plan welcome
</Button>
)
}
export default {
title: "App/Dialogs/Connect Provider",
id: "app-dialog-chatgpt-welcome",
}
export const ChatGPTPlanWelcome = { render: () => <WelcomeStory /> }
@@ -0,0 +1,37 @@
import { Button } from "@opencode/ui/button"
import { useDialog } from "@opencode/ui/context/dialog"
import { Dialog, DialogTitle } from "@opencode/ui/dialog"
import { ProviderIcon } from "@opencode/ui/provider-icon"
import { useLanguage } from "@/runtime/i18n/language"
import { ExternalLink } from "@/runtime/platform/external-link"
export function DialogChatGPTPlanWelcome() {
const dialog = useDialog()
const language = useLanguage()
return (
<Dialog fit containerClass="!w-[min(calc(100vw_-_32px),390px)] !rounded-xl">
<div class="flex w-full flex-col items-center px-8 pb-8 pt-9 text-center [font-family:var(--v2-font-family-sans)]">
<ProviderIcon id="openai" class="!size-12 text-v2-icon-icon-base" aria-hidden="true" />
<div class="mt-6 max-w-[270px] text-[20px] font-[530] leading-7 tracking-[-0.3px] text-v2-text-text-base">
<DialogTitle>
<bdi dir="auto">{language.t("provider.connect.chatgptWelcome.title")}</bdi>
</DialogTitle>
</div>
<p class="mt-3 text-[13px] leading-5 text-v2-text-text-muted">
<bdi dir="auto">{language.t("provider.connect.chatgptWelcome.description")}</bdi>
</p>
<ExternalLink
href="https://chatgpt.com/settings/usage"
dir="auto"
class="mt-1 rounded-sm text-[13px] leading-5 text-v2-text-text-muted underline-offset-2 hover:text-v2-text-text-base focus-visible:outline focus-visible:outline-2"
>
{language.t("provider.connect.chatgptWelcome.usage")}
</ExternalLink>
<Button variant="contrast" size="large" class="mt-8 w-full" autofocus onClick={() => dialog.close()}>
{language.t("provider.connect.chatgptWelcome.confirm")}
</Button>
</div>
</Dialog>
)
}
+29 -6
View File
@@ -39,6 +39,9 @@ import { OpenCodeLogo } from "@/providers/opencode-logo"
import { decode64 } from "@/runtime/persistence/base64"
import { SettingsList } from "@/settings/list"
import { useTabs } from "@/shell/tabs/tabs"
import { Persist, persisted } from "@/runtime/persistence/storage"
import { Persistence } from "@/runtime/persistence/schema"
import { Schema } from "effect"
import {
CONSOLE_INTEGRATION,
CONSOLE_PROVIDERS,
@@ -48,6 +51,7 @@ import {
type ProviderConnectMethod,
} from "./controller"
import { ConsoleAuthorization } from "./console"
import { DialogChatGPTPlanWelcome } from "./chatgpt-welcome"
import { authServerName, RemoteAuthNotice } from "./remote"
import "./models.css"
@@ -81,8 +85,15 @@ export const DialogConnectProvider: Component<{
completed: false,
modelProvider: undefined as { id: string; name: string } | undefined,
authorization: false,
chatgptWelcome: false,
})
const language = useLanguage()
const dialog = useDialog()
const [welcome, setWelcome, , welcomeReady] = persisted(
Persist.global("chatgpt-plan-welcome.v1"),
Persistence.struct({ seen: Schema.Boolean }),
{ seen: false },
)
const reset = controller.reset
const back = { current: reset }
const consoleSelected = () => CONSOLE_PROVIDERS.has(controller.selected() ?? "")
@@ -112,7 +123,11 @@ export const DialogConnectProvider: Component<{
setBack={(handler) => (back.current = handler)}
selection={props.selection}
onDone={props.onDone ? () => setState("completed", true) : undefined}
onConnected={() => props.onConnected?.(provider)}
onConnected={(methodID) => {
props.onConnected?.(provider)
if (provider === "openai" && methodID === "chatgpt-token-sharing")
setState("chatgptWelcome", true)
}}
onFirstConnection={(provider) => setState("modelProvider", provider)}
onAuthorization={(authorization) => setState("authorization", authorization)}
/>
@@ -136,9 +151,16 @@ export const DialogConnectProvider: Component<{
: "!h-[min(calc(100vh_-_16px),512px)] !w-[min(calc(100vw_-_16px),640px)]"
}
onCloseAutoFocus={(event) => {
if (!state.completed || !props.onDone) return
event.preventDefault()
props.onDone()
if (state.completed && props.onDone) {
event.preventDefault()
props.onDone()
}
if (!state.chatgptWelcome) return
void Promise.resolve(welcomeReady.promise).then(() => {
if (welcome.seen) return
setWelcome("seen", true)
void dialog.show(() => <DialogChatGPTPlanWelcome />)
})
}}
class="[font-family:var(--v2-font-family-sans)] [&_[data-slot=dialog-header]]:!px-5 [&_[data-slot=dialog-header-title]]:!text-[15px] [&_[data-slot=dialog-header-title]]:!tracking-[-0.13px]"
classList={{
@@ -354,7 +376,7 @@ function ProviderConnection(props: {
setBack: (handler: () => void) => void
selection?: ModelSelection
onDone?: () => void
onConnected?: () => void
onConnected?: (methodID?: string) => void
onFirstConnection: (provider: { id: string; name: string }) => void
onAuthorization: (authorization: boolean) => void
}) {
@@ -401,7 +423,8 @@ function ProviderConnection(props: {
prepare: isConsole ? prepareConsoleCatalog : undefined,
pollInterval: isConsole ? 500 : undefined,
onComplete: () => {
props.onConnected?.()
const method = controller.currentMethod()
props.onConnected?.(method?.type === "oauth" ? method.id : undefined)
// The picker only lists the newest model per family by default, which hides most of
// what a new connection just unlocked. Show everything the connected integration offers.
global.models.show(
@@ -0,0 +1,78 @@
import { Button } from "@opencode/ui/button"
import { createSignal } from "solid-js"
import type { ModelSelection } from "./selection"
import { ModelSelectorPopoverView } from "./select-dialog"
const chatgpt = {
id: "gpt-5.6-sol",
providerID: "openai",
api: { id: "gpt-5.6-sol", url: "https://api.openai.com/v1", npm: "@opencode/ai/providers/openai" },
name: "GPT-5.6 Sol",
family: "gpt",
capabilities: {
temperature: true,
reasoning: true,
attachment: true,
toolcall: true,
input: { text: true, audio: false, image: true, video: false, pdf: false },
output: { text: true, audio: false, image: false, video: false, pdf: false },
interleaved: true,
},
cost: { input: 0, output: 0, cache: { read: 0, write: 0 } },
limit: { context: 400_000, output: 64_000 },
status: "active",
options: {},
headers: {},
release_date: "2026-09-01",
variants: {},
provider: {
id: "openai",
name: "OpenAI",
source: "custom",
env: [],
options: {},
models: {},
},
latest: true,
} satisfies NonNullable<ReturnType<ModelSelection["current"]>>
const models = [
chatgpt,
{
...chatgpt,
id: "gpt-5.6-terra",
api: { ...chatgpt.api, id: "gpt-5.6-terra" },
name: "GPT-5.6 Terra",
latest: false,
},
]
function SelectorStory(props: { plan: boolean }) {
const [current, setCurrent] = createSignal(models[0].id)
return (
<div class="flex min-h-[280px] items-end justify-center">
<ModelSelectorPopoverView
trigger={(trigger) => (
<Button {...trigger} variant="ghost-muted">
{models.find((model) => model.id === current())?.name}
</Button>
)}
models={(search) => models.filter((model) => model.name.toLowerCase().includes(search.toLowerCase()))}
groups={(items) => [{ category: "openai", items }]}
current={`openai:${current()}`}
chatgptPlan={props.plan}
select={(item) => setCurrent(item.id)}
onManage={() => undefined}
onClose={() => undefined}
/>
</div>
)
}
export default {
title: "App/Dialogs/Model Selector",
id: "app-dialog-model-selector",
}
export const ChatGPTPlan = { render: () => <SelectorStory plan /> }
export const ApiKey = { render: () => <SelectorStory plan={false} /> }
@@ -15,13 +15,16 @@ import { Menu } from "@opencode/ui/menu"
import { TextInput } from "@opencode/ui/text-input"
import { ModelTooltip } from "./tooltip"
import { useLanguage } from "@/runtime/i18n/language"
import { ExternalLink } from "@/runtime/platform/external-link"
import { useData } from "@/runtime/server/current"
import { useWorkspaceLocation } from "@/workspaces/location"
import { decode64 } from "@/runtime/persistence/base64"
import { handleDocumentSearchKeydown } from "@/shell/commands/search-keydown"
import { createMenuDismissController } from "@/shell/commands/menu-dismiss"
import { createEventListener } from "@solid-primitives/event-listener"
import { matchesModelSearch } from "./search"
import { SettingsList } from "@/settings/list"
import { CONSOLE_GROUP_KEY, consoleModelGroup, ProviderModelSections } from "@/providers/models/provider-group"
import { CONSOLE_GROUP_KEY, consoleModelGroup, ProviderModelIcon, ProviderModelSections } from "@/providers/models/provider-group"
import "@/settings/settings.css"
const isFree = (provider: string, cost: { input: number } | undefined) =>
@@ -218,11 +221,21 @@ export function ModelSelectorPopover(props: {
onClose?: () => void
}) {
const dialog = useDialog()
const data = useData()
const location = useWorkspaceLocation()
const controller = createModelSelectorController({
model: props.model,
provider: () => props.provider,
onSelect: () => props.onClose?.(),
})
const chatgptPlan = () => {
if (!controller.current()?.startsWith("openai:")) return false
const connection = data.location.integration
.list(location().ref)
?.find((integration) => integration.id === "openai")
?.connections[0]
return connection?.type === "credential" && connection.method === "oauth"
}
return (
<ModelSelectorPopoverView
@@ -230,6 +243,7 @@ export function ModelSelectorPopover(props: {
models={controller.models}
groups={controller.groups}
current={controller.current()}
chatgptPlan={chatgptPlan()}
select={controller.select}
onManage={() => {
void import("./manage").then((module) => {
@@ -281,11 +295,12 @@ function createModelSelectorController(input: {
}
}
function ModelSelectorPopoverView(props: {
export function ModelSelectorPopoverView(props: {
trigger: ModelSelectorTrigger
models: (search: string) => ModelItem[]
groups: (models: ModelItem[]) => { category: string; items: ModelItem[] }[]
current: string | undefined
chatgptPlan?: boolean
select: (item: ModelItem) => void
onManage: () => void
onClose: () => void
@@ -368,7 +383,8 @@ function ModelSelectorPopoverView(props: {
<Menu.Portal>
<Menu.Content
ref={(element: HTMLDivElement) => (contentRef = element)}
class="w-[284px] overflow-hidden rounded-md border-0 bg-v2-background-bg-layer-01 !p-0 shadow-[var(--v2-elevation-floating)] focus:outline-none"
class="w-[284px] max-w-[calc(100vw-16px)] overflow-hidden rounded-md border-0 bg-v2-background-bg-layer-01 !p-0 shadow-[var(--v2-elevation-floating)] focus:outline-none"
classList={{ "!w-[320px]": props.chatgptPlan }}
onPointerDownOutside={dismiss.preventTriggerRestore}
onFocusOutside={dismiss.preventTriggerRestore}
onCloseAutoFocus={dismiss.onCloseAutoFocus}
@@ -505,6 +521,20 @@ function ModelSelectorPopoverView(props: {
<span class="min-w-0 flex-1 truncate leading-5">{language.t("dialog.model.manage")}</span>
</Menu.Item>
</div>
<Show when={props.chatgptPlan}>
<div class="h-px bg-v2-border-border-muted" />
<div class="flex min-h-10 items-center gap-2 px-3 py-2 text-[13px] leading-5 text-v2-text-text-base">
<ProviderModelIcon provider={{ id: "openai", name: "OpenAI" }} class="shrink-0" />
<span class="min-w-0 flex-1 truncate">{language.t("dialog.model.chatgptPlan")}</span>
<ExternalLink
href="https://chatgpt.com/settings/usage"
class="flex shrink-0 items-center gap-1 rounded-sm text-v2-text-text-muted no-underline hover:text-v2-text-text-base focus-visible:outline focus-visible:outline-2"
>
{language.t("dialog.model.chatgptManageUsage")}
<Icon name="arrow-up-right" size="small" />
</ExternalLink>
</div>
</Show>
</Menu.Content>
</Menu.Portal>
</Menu>
+14
View File
@@ -111,6 +111,7 @@ export const dict = {
"command.file.open": "Open file",
"command.browser.open": "Open browser",
"command.browser.reload": "Reload browser page",
"command.browser.inspect": "Select an element in the browser page",
"command.tab.close": "Close tab",
"command.tab.reopenClosed": "Reopen closed tab",
"command.context.addSelection": "Add selection to context",
@@ -186,6 +187,8 @@ export const dict = {
"dialog.model.empty": "No model results",
"dialog.model.manage": "Manage models",
"dialog.model.manage.description": "Customize which models appear in the model selector.",
"dialog.model.chatgptPlan": "Using ChatGPT plan",
"dialog.model.chatgptManageUsage": "Manage usage",
"dialog.model.manage.provider.toggle": "Toggle all {{provider}} models",
"dialog.model.unpaid.freeModels.title": "Free models provided by OpenCode",
@@ -217,7 +220,15 @@ export const dict = {
"provider.connect.models.title": "Connected to {{provider}}",
"provider.connect.models.description": "Choose a model to start with. You can switch models anytime.",
"provider.connect.models.available": "Available models",
"provider.connect.chatgptWelcome.title": "ChatGPT connected",
"provider.connect.chatgptWelcome.description": "Eligible requests in OpenCode can use your ChatGPT plan.",
"provider.connect.chatgptWelcome.usage": "Manage usage in ChatGPT settings",
"provider.connect.chatgptWelcome.confirm": "Got it",
"provider.connect.models.list": "Models available from {{provider}}",
"provider.connect.chatgptUsageLimit.title": "ChatGPT usage limit reached",
"provider.connect.chatgptUsageLimit.description": "Check your usage and limits in ChatGPT settings.",
"provider.connect.chatgptUsageLimit.manage": "Manage usage",
"provider.connect.chatgptUsageLimit.close": "Close",
"provider.connect.console.refreshFailed":
"Your account is connected, but we couldn't load your models. Try again to refresh them.",
"provider.connect.console.connected": "OpenCode connected",
@@ -1012,6 +1023,9 @@ export const dict = {
"session.browser.unsupported": "This desktop app does not support the browser pane.",
"session.browser.suspended": "Browser suspended. Interact with this session to reconnect.",
"session.browser.address.placeholder": "Enter URL",
"session.browser.inspect": "Select an element to comment on",
"session.browser.inspect.active": "Click an element in the page to comment on it. Press Escape to cancel.",
"session.browser.inspect.pageShortcut": "While the page has focus",
"session.browser.empty.title": "Enter URL",
"session.browser.empty.description": 'Or prompt "Open in the app browser"',
"session.browser.failed.title": "URL can't be reached",

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