Compare commits

..
Author SHA1 Message Date
Aiden Cline 1e4a3f8062 fix(ai): classify xAI invalid API keys as authentication errors 2026-09-29 23:53:14 -05: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]andBrendonovich 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]andrekram1-node 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
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]andjlongster 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]andthdxr 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]andrekram1-node 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
Aiden Cline 2aaa265cdc fix(ai): read provider error messages from common body layouts (#52014) 2026-09-29 00:19:20 -05:00
Luke Parker ff72659a47 fix(desktop): give isolated dev service a fixed port (#52013) 2026-09-29 15:09:50 +10:00
Aiden Cline 14af33d53d fix(ai): show provider error bodies when no message is recognized (#51978) 2026-09-29 00:06:52 -05:00
Luke Parker a604b7f773 fix(ui): calm diff word highlights with @pierre/diffs 1.5.1 (#51236) 2026-09-29 14:58:24 +10:00
Luke Parker 41d4a9c45c fix(session-ui): render moved notice as a timeline divider (#52009) 2026-09-29 14:58:13 +10:00
Luke Parker 293d60a89f chore(desktop): bump electron to 44.4.5 (#52005) 2026-09-29 14:50:04 +10:00
Aiden Cline 34a8938dc7 fix(ai): bind Bedrock Claude thinking blocks by default (#51959) 2026-09-28 23:38:20 -05:00
Aiden Cline 36ef6cc216 fix(ai): finalize Bedrock redacted reasoning at block end (#51999) 2026-09-28 23:37:51 -05:00
opencode f02c30eb55 sync release versions for v2.0.19 2026-09-29 04:31:02 +00:00
Aiden Cline ca084b2430 fix(ai): give provider routes distinct IDs (#51976) 2026-09-28 19:55:06 -05:00
Aiden Cline 3740ec311b feat(core): align shell tool environment with agent conventions (#51975) 2026-09-28 19:53:53 -05:00
opencode-agent[bot]andrekram1-node 35bd8ac442 test(core): update xAI variant expectations for Responses (#51977)
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
2026-09-28 19:48:35 -05:00
Aiden Cline 1fc05ca590 fix(core): share affinity in provider session headers (#51931) 2026-09-28 19:30:24 -05:00
Aiden Cline bda798b167 fix(core): drop session ID and order instructions for prompt-cache reuse (#51960) 2026-09-28 19:29:28 -05:00
Aiden Cline 3babae35c0 fix(core): fall back to Cloudflare environment IDs when not configured (#51963) 2026-09-28 19:28:03 -05:00
Aiden Cline 3ab5c1433c fix(core): request xAI reasoning summaries on Responses variants (#51964) 2026-09-28 19:11:48 -05:00
Aiden Cline 49437a25b1 fix(ai): classify invalid Google API keys as authentication errors (#51950) 2026-09-28 18:49:29 -05:00
Aiden Cline 49403a554f feat(core): cap requested output tokens at 256k (#51962) 2026-09-28 18:49:05 -05:00
opencode-agent[bot]andrekram1-node 87d6f93409 test(cli): isolate run exit codes between tests (#51955)
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
2026-09-28 19:16:01 -04:00
Frank 97d4eaa2ba zen: jev privacy policy 2026-09-28 19:01:46 -04:00
241 changed files with 6202 additions and 936 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.
+43 -48
View File
@@ -32,7 +32,7 @@
},
"packages/ai": {
"name": "@opencode/ai",
"version": "2.0.18",
"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.18",
"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.18",
"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.18",
"version": "2.0.20",
"dependencies": {
"@opencode/protocol": "workspace:*",
"@opencode/schema": "workspace:*",
@@ -204,7 +204,7 @@
},
"packages/codemode": {
"name": "@opencode/codemode",
"version": "2.0.18",
"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.18",
"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.18",
"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.18",
"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.18",
"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.18",
"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.18",
"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.18",
"version": "2.0.20",
"dependencies": {
"@zip.js/zip.js": "2.7.62",
"electron-context-menu": "5.0.0",
@@ -439,7 +440,7 @@
"drizzle-kit": "catalog:",
"drizzle-orm": "catalog:",
"effect": "catalog:",
"electron": "44.4.3",
"electron": "44.4.5",
"electron-builder": "26.15.7",
"electron-vite": "6.0.0-beta.1",
"puppeteer-core": "25.9.0",
@@ -458,7 +459,7 @@
},
"packages/enterprise": {
"name": "@opencode/enterprise",
"version": "2.0.18",
"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.18",
"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.18",
"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.18",
"version": "2.0.20",
"dependencies": {
"effect": "catalog:",
"prettier": "3.6.2",
@@ -543,7 +544,7 @@
},
"packages/latex": {
"name": "@opencode/latex",
"version": "2.0.18",
"version": "2.0.20",
"dependencies": {
"@opencode/plugin": "workspace:*",
"@opentui/core": "catalog:",
@@ -557,7 +558,7 @@
},
"packages/merman": {
"name": "@opencode/merman",
"version": "2.0.18",
"version": "2.0.20",
"dependencies": {
"@opencode/plugin": "workspace:*",
"@opentui/core": "catalog:",
@@ -572,7 +573,7 @@
},
"packages/plugin": {
"name": "@opencode/plugin",
"version": "2.0.18",
"version": "2.0.20",
"dependencies": {
"@ai-sdk/provider": "3.0.8",
"@opencode/ai": "workspace:*",
@@ -611,7 +612,7 @@
},
"packages/plugin-browser": {
"name": "@opencode/plugin-browser",
"version": "2.0.18",
"version": "2.0.20",
"dependencies": {
"@opencode/plugin": "workspace:*",
"@opencode/schema": "workspace:*",
@@ -641,7 +642,7 @@
},
"packages/protocol": {
"name": "@opencode/protocol",
"version": "2.0.18",
"version": "2.0.20",
"dependencies": {
"@opencode/schema": "workspace:*",
"effect": "catalog:",
@@ -656,7 +657,7 @@
},
"packages/schema": {
"name": "@opencode/schema",
"version": "2.0.18",
"version": "2.0.20",
"dependencies": {
"@standard-schema/spec": "catalog:",
"effect": "catalog:",
@@ -680,7 +681,7 @@
},
"packages/sdk": {
"name": "@opencode/sdk",
"version": "2.0.18",
"version": "2.0.20",
"dependencies": {
"@opencode/client": "workspace:*",
"@opencode/core": "workspace:*",
@@ -701,7 +702,7 @@
},
"packages/server": {
"name": "@opencode/server",
"version": "2.0.18",
"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.18",
"version": "2.0.20",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode/client": "workspace:*",
@@ -758,7 +759,7 @@
},
"packages/simulation": {
"name": "@opencode/simulation",
"version": "2.0.18",
"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.18",
"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.18",
"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.18",
"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.18",
"version": "2.0.20",
"dependencies": {
"@opentui/core": "catalog:",
"effect": "catalog:",
@@ -891,7 +892,7 @@
},
"packages/tui": {
"name": "@opencode/tui",
"version": "2.0.18",
"version": "2.0.20",
"dependencies": {
"@opencode/client": "workspace:*",
"@opencode/core": "workspace:*",
@@ -925,7 +926,7 @@
},
"packages/ui": {
"name": "@opencode/ui",
"version": "2.0.18",
"version": "2.0.20",
"dependencies": {
"@kobalte/core": "catalog:",
"@pierre/diffs": "catalog:",
@@ -960,7 +961,7 @@
},
"packages/util": {
"name": "@opencode/util",
"version": "2.0.18",
"version": "2.0.20",
"dependencies": {
"@effect/opentelemetry": "catalog:",
"@effect/platform-node": "catalog:",
@@ -998,7 +999,7 @@
},
"packages/web": {
"name": "@opencode/web",
"version": "2.0.18",
"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.18",
"version": "2.0.20",
"dependencies": {
"jose": "6.0.11",
"semver": "catalog:",
@@ -1119,7 +1120,7 @@
"@opentui/core": "0.5.12",
"@opentui/keymap": "0.5.12",
"@opentui/solid": "0.5.12",
"@pierre/diffs": "1.2.10",
"@pierre/diffs": "1.5.1",
"@playwright/test": "1.59.1",
"@sentry/solid": "10.71.0",
"@sentry/vite-plugin": "5.4.0",
@@ -2530,11 +2531,11 @@
"@peculiar/webcrypto": ["@peculiar/webcrypto@1.7.1", "", { "dependencies": { "@peculiar/asn1-schema": "^2.7.0", "@peculiar/json-schema": "^1.1.12", "@peculiar/utils": "^2.0.2", "tslib": "^2.8.1", "webcrypto-core": "^1.9.2" } }, "sha512-ODOov0sGMJMf3jPonOkgGqPknTsu+DdQ7kD++gz8aI+aFMOMHFbWAA2taqXXVTdP+OTOQR/znGvSpmkeI0WTYQ=="],
"@pierre/diffs": ["@pierre/diffs@1.2.10", "", { "dependencies": { "@pierre/theme": "1.0.3", "@pierre/theming": "0.0.1", "@shikijs/transformers": "^3.0.0 || ^4.0.0", "diff": "8.0.3", "hast-util-to-html": "9.0.5", "lru_map": "0.4.1", "shiki": "^3.0.0 || ^4.0.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-rPeAmDWarxFVTQpaf4y6wTxjZxU44xKJKoJti2zU21P06DVd9nRHZX+xSIObLB307Qjpaesyb1x/j0z94t7vLw=="],
"@pierre/diffs": ["@pierre/diffs@1.5.1", "", { "dependencies": { "@pierre/theme": "2.0.0", "@pierre/theming": "1.0.1", "@shikijs/transformers": "^3.0.0 || ^4.0.0", "diff": "9.0.0", "hast-util-to-html": "9.0.5", "lru_map": "0.4.1", "shiki": "^3.0.0 || ^4.0.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" }, "optionalPeers": ["react", "react-dom"] }, "sha512-+EXNfz4ZXI6FHr7P6ToWEORwyYiPVfAmzhA70xF+0y/oNvj7gbNTLJJ6jNzkBjuMbRlu564TAmgYZLc5OyHURA=="],
"@pierre/theme": ["@pierre/theme@1.0.3", "", {}, "sha512-sWHv11TMoqKxKDgTIk5VbhQjdPhs8DCcBxbjh3mRlS3YOM/OcrWoGX6MM8eBGn9cUu3M46Py0JnxsG2nJaFTuA=="],
"@pierre/theme": ["@pierre/theme@2.0.0", "", {}, "sha512-yNDd9GYLQl1mEUJR8AneJ5e4ohLIHQd/wZLWr4fagt78vS2RwwZNW530vVgHqXFAyFVcFlRmGUD5ramXH46OXw=="],
"@pierre/theming": ["@pierre/theming@0.0.1", "", { "peerDependencies": { "@pierre/theme": "^1.0.0", "@shikijs/themes": "^3.0.0 || ^4.0.0", "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0", "shiki": "^3.0.0 || ^4.0.0" }, "optionalPeers": ["@pierre/theme", "@shikijs/themes", "react", "react-dom", "shiki"] }, "sha512-1thlEtJbqdyLzc1ZS2KQa1q7FzDGHT4dTEdKHoyQjOMeWWOmbVG5/ndEfOKfAb5Fzkz8cNJrOjFLiZoDH/A03A=="],
"@pierre/theming": ["@pierre/theming@1.0.1", "", { "peerDependencies": { "@pierre/theme": "^1.1.0 || ^2.0.0", "@shikijs/themes": "^3.0.0 || ^4.0.0", "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0", "shiki": "^3.0.0 || ^4.0.0" }, "optionalPeers": ["@pierre/theme", "@shikijs/themes", "react", "react-dom", "shiki"] }, "sha512-WCI5Qd7iprDpISL9fBYOLe8RV53+b7mFNA3bPzl60/2CKCSrsKN8zEcep6Y3BAzvARlmca50zGjDodqPGiTUKA=="],
"@pierre/trees": ["@pierre/trees@1.0.0-beta.4", "", { "dependencies": { "preact": "11.0.0-beta.0", "preact-render-to-string": "6.6.5" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-OfT1yk9ne8Te5+GB5zUY8yqE6B8BqjBHQJleH4lu8ltwNpoocZl4vXt1AzlEExpxI/pp+AFX5QG+lR3JjtTEag=="],
@@ -3870,7 +3871,7 @@
"ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="],
"electron": ["electron@44.4.3", "", { "dependencies": { "@electron-internal/extract-zip": "^1.0.1", "@electron/get": "^5.0.0", "@types/node": "^24.9.0" }, "bin": { "electron": "cli.js", "install-electron": "install.js" } }, "sha512-LTpSFTB40qVCXIX5xMo+cgHI/Jjkbjw7VpB26PccEbroqOn72LBukeaDwPVo1fBYzSzs0c9iPuAucFCO7Tw81Q=="],
"electron": ["electron@44.4.5", "", { "dependencies": { "@electron-internal/extract-zip": "^1.0.1", "@electron/get": "^5.0.0", "@types/node": "^24.9.0" }, "bin": { "electron": "cli.js", "install-electron": "install.js" } }, "sha512-SjgoaeYsSWZfJzubgQU7juvuXMTvn6/e1gAHdGFA/yuMbpF+I+skYqIJ6DdXBXHeWbkbpNpmwZCTpiivtlWZSw=="],
"electron-builder": ["electron-builder@26.15.7", "", { "dependencies": { "app-builder-lib": "26.15.7", "builder-util": "26.15.3", "builder-util-runtime": "9.7.0", "chalk": "^4.1.2", "ci-info": "^4.2.0", "dmg-builder": "26.15.7", "fs-extra": "^10.1.0", "lazy-val": "^1.0.5", "simple-update-notifier": "2.0.0", "yargs": "^17.6.2" }, "bin": { "electron-builder": "./cli.js", "install-app-deps": "./install-app-deps.js" } }, "sha512-DBpaNzxsPs1BvEblzFoNriSbzsBqDCy/gseIngeEhYzQG1IxfB7Hvc2tBBVmpWE2BTQGP9J1RrAvDT+Vc/uAxg=="],
@@ -6258,11 +6259,7 @@
"@parcel/watcher/detect-libc": ["detect-libc@1.0.3", "", { "bin": { "detect-libc": "./bin/detect-libc.js" } }, "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg=="],
"@pierre/diffs/diff": ["diff@8.0.3", "", {}, "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ=="],
"@pierre/diffs/react": ["react@19.2.8", "", {}, "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw=="],
"@pierre/diffs/react-dom": ["react-dom@19.2.8", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.8" } }, "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ=="],
"@pierre/diffs/diff": ["diff@9.0.0", "", {}, "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw=="],
"@pierre/trees/react": ["react@19.2.8", "", {}, "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw=="],
@@ -7160,8 +7157,6 @@
"@oxc-resolver/binding-wasm32-wasi/@emnapi/core/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="],
"@pierre/diffs/react-dom/scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
"@pierre/trees/react-dom/scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
"@puppeteer/browsers/yargs/cliui": ["cliui@9.0.1", "", { "dependencies": { "string-width": "^7.2.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w=="],
+2 -2
View File
@@ -2,7 +2,7 @@
"$schema": "https://json.schemastore.org/package.json",
"name": "opencode",
"description": "AI-powered development tool",
"version": "2.0.18",
"version": "2.0.20",
"private": true,
"type": "module",
"packageManager": "bun@1.4.2",
@@ -68,7 +68,7 @@
"@tsconfig/bun": "1.0.9",
"@cloudflare/workers-types": "4.20251008.0",
"@openauthjs/openauth": "0.0.0-20250322224806",
"@pierre/diffs": "1.2.10",
"@pierre/diffs": "1.5.1",
"opentui-spinner": "0.0.7",
"@solid-primitives/event-listener": "2.4.6",
"@solid-primitives/media": "2.3.6",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "2.0.18",
"version": "2.0.20",
"name": "@opencode/ai",
"type": "module",
"license": "MIT",
+22 -3
View File
@@ -38,12 +38,30 @@ 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 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" })
@@ -149,9 +167,10 @@ 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.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)
@@ -29,6 +29,7 @@ import { JsonObject, knownString, optionalArray, optionalNull, ProviderShared }
import { classifyProviderFailure } from "../provider-error.js"
import { effortUpdate, resolveEffortUpdates } from "../effort-updates.js"
import * as Cache from "./utils/cache.js"
import { claudeVersion, supportsThinkingBlockBinding, THINKING_BINDING_BETA } from "./utils/claude-model.js"
import { Lifecycle } from "./utils/lifecycle.js"
import { ToolStream } from "./utils/tool-stream.js"
@@ -988,22 +989,6 @@ const lowerMessages = Effect.fn("AnthropicMessages.lowerMessages")(function* (
return messages
})
// Accept gateway namespaces and Vertex suffixes without treating a snapshot date as a minor version.
const claudeVersion = (id: string) => {
const match = /(?:^|[./])claude-(?<family>[a-z]+)-(?<major>\d+)(?:[.-](?<minor>\d{1,2}))?(?:$|[-:@])/.exec(
id.toLowerCase(),
)?.groups
if (!match) return undefined
return { family: match.family, major: Number(match.major), minor: Number(match.minor ?? 0) }
}
const supportsThinkingBlockBinding = (model: LLMRequest["model"]) => {
const override = model.compatibility?.supportsThinkingBlockBinding
if (override !== undefined) return override
const version = claudeVersion(model.id)
return version !== undefined && (version.major > 5 || (version.major === 5 && version.minor >= 1))
}
const supportsEffortUpdates = (model: LLMRequest["model"]) => {
const override = model.compatibility?.supportsEffortUpdates
if (override !== undefined) return override
@@ -1652,8 +1637,7 @@ function requiredBetaHeaders(body: Pick<AnthropicMessagesBody, "messages" | "con
betas.push("mid-conversation-output-config-2026-07-01")
const thinking = body.thinking
if (thinking && thinking.type !== "disabled" && thinking.block_binding)
betas.push("thinking-binding-controls-2026-08-01")
if (thinking && thinking.type !== "disabled" && thinking.block_binding) betas.push(THINKING_BINDING_BETA)
return betas
}
+48 -11
View File
@@ -23,6 +23,7 @@ import { JsonObject, optionalArray, ProviderShared } from "./shared.js"
import { BedrockAuth } from "./utils/bedrock-auth.js"
import { BedrockCache } from "./utils/bedrock-cache.js"
import { BedrockMedia } from "./utils/bedrock-media.js"
import { supportsThinkingBlockBinding, THINKING_BINDING_BETA } from "./utils/claude-model.js"
import { Lifecycle } from "./utils/lifecycle.js"
import { MistralToolID } from "./utils/mistral-tool-id.js"
import { ToolStream } from "./utils/tool-stream.js"
@@ -443,6 +444,23 @@ const decodeOptions = ProviderShared.validateWith(Schema.decodeUnknownEffect(Opt
// Claude on Bedrock requires the thinking budget below `maxTokens`, with a minimum of 1,024.
const MIN_THINKING_BUDGET = 1_024
const isThinkingDisabled = Schema.is(
Schema.Struct({
additionalModelRequestFields: Schema.Struct({ thinking: Schema.Struct({ type: Schema.Literal("disabled") }) }),
}),
)
// Claude 5.1+ binds each thinking signature to the prefix above it. Ask Bedrock to drop the affected blocks instead of
// failing when that prefix changes. `http.body` overlays this field by field, so callers can still override it.
const applyThinkingBindingDefault = (request: LLMRequest, thinking: Readonly<Record<string, unknown>> | undefined) => {
if (isThinkingDisabled(request.http?.body)) return thinking
if (!supportsThinkingBlockBinding(request.model)) return thinking
return {
...(thinking ?? { type: "adaptive" as const }),
block_binding: { prefix_mismatch_behavior: "drop_block" },
}
}
const fromRequest = Effect.fn("BedrockConverse.fromRequest")(function* (request: LLMRequest) {
const toolChoice = request.toolChoice ? yield* lowerToolChoice(request.toolChoice) : undefined
const flattened = ProviderShared.flattenToolRequest(request)
@@ -450,7 +468,8 @@ const fromRequest = Effect.fn("BedrockConverse.fromRequest")(function* (request:
const options = yield* decodeOptions(request.providerOptions ?? {})
const maxTokens =
isNova2(request.model) && isHighReasoningEffort(request.http?.body) ? undefined : generation?.maxTokens
const thinking =
const thinking = applyThinkingBindingDefault(
request,
options.thinking === undefined
? undefined
: {
@@ -460,7 +479,8 @@ const fromRequest = Effect.fn("BedrockConverse.fromRequest")(function* (request:
maxTokens,
MIN_THINKING_BUDGET,
),
}
},
)
// Bedrock-Claude shares Anthropic's 4-breakpoint cap. Spend the budget in
// tools → system → messages order to favour the highest-impact prefixes.
const breakpoints = BedrockCache.breakpoints(request.model.id)
@@ -509,6 +529,8 @@ const fromRequest = Effect.fn("BedrockConverse.fromRequest")(function* (request:
: {
...(generation?.topK === undefined ? {} : { top_k: generation.topK }),
...(thinking === undefined ? {} : { thinking }),
// Converse takes Anthropic betas in the body, and Bedrock rejects `block_binding` without this one.
...(thinking?.block_binding === undefined ? {} : { anthropic_beta: [THINKING_BINDING_BETA] }),
},
}
})
@@ -555,7 +577,7 @@ interface ParserState {
readonly hasToolCalls: boolean
readonly lifecycle: Lifecycle.State
readonly reasoningSignatures: Readonly<Record<number, string>>
readonly reasoningRedactedContent: Readonly<Record<number, ReadonlyArray<Uint8Array>>>
readonly reasoningRedactedContent: Readonly<Record<number, Uint8Array[]>>
}
const encodeRedactedContent = (chunks: ReadonlyArray<Uint8Array>) => Encoding.encodeBase64(concatBytes(chunks))
@@ -605,10 +627,9 @@ const step = (state: ParserState, event: BedrockEvent) =>
const index = event.contentBlockDelta.contentBlockIndex
const reasoning = event.contentBlockDelta.delta.reasoningContent
const events: LLMEvent[] = []
const redactedChunks = yield* (() => {
const redactedChunk = yield* (() => {
if (reasoning.redactedContent === undefined) return Effect.succeed(undefined)
return Effect.fromResult(Encoding.decodeBase64(reasoning.redactedContent)).pipe(
Effect.map((chunk) => [...(state.reasoningRedactedContent[index] ?? []), chunk]),
Effect.mapError((cause) =>
ProviderShared.eventError(
ADAPTER,
@@ -619,17 +640,21 @@ const step = (state: ParserState, event: BedrockEvent) =>
),
)
})()
const redactedData = redactedChunks === undefined ? reasoning.data : encodeRedactedContent(redactedChunks)
const redactedChunks = state.reasoningRedactedContent[index] ?? []
if (redactedChunk !== undefined) redactedChunks.push(redactedChunk)
const metadata = (() => {
if (reasoning.signature) return providerMetadata(state.providerMetadataKey, { signature: reasoning.signature })
if (redactedData !== undefined) return providerMetadata(state.providerMetadataKey, { redactedData })
if (redactedChunk === undefined && reasoning.data !== undefined)
return providerMetadata(state.providerMetadataKey, { redactedData: reasoning.data })
})()
const lifecycle = (() => {
if (reasoning.text === undefined && metadata === undefined) return state.lifecycle
return Lifecycle.reasoningDelta(state.lifecycle, events, `reasoning-${index}`, reasoning.text ?? "", metadata)
if (reasoning.text !== undefined || metadata !== undefined)
return Lifecycle.reasoningDelta(state.lifecycle, events, `reasoning-${index}`, reasoning.text ?? "", metadata)
if (redactedChunk !== undefined) return Lifecycle.reasoningStart(state.lifecycle, events, `reasoning-${index}`)
return state.lifecycle
})()
const reasoningRedactedContent = (() => {
if (redactedChunks !== undefined) return { ...state.reasoningRedactedContent, [index]: redactedChunks }
if (redactedChunk !== undefined) return { ...state.reasoningRedactedContent, [index]: redactedChunks }
if (reasoning.data === undefined) return state.reasoningRedactedContent
return Object.fromEntries(
Object.entries(state.reasoningRedactedContent).filter(([key]) => key !== String(index)),
@@ -765,7 +790,19 @@ const onHalt = (state: ParserState): ReadonlyArray<LLMEvent> => {
return state.finishReason.normalized
})()
const events: LLMEvent[] = []
Lifecycle.finish(state.lifecycle, events, {
const lifecycle = Object.entries(state.reasoningRedactedContent).reduce((current, [index, chunks]) => {
const signature = state.reasoningSignatures[Number(index)]
return Lifecycle.reasoningEnd(
current,
events,
`reasoning-${index}`,
providerMetadata(
state.providerMetadataKey,
signature ? { signature } : { redactedData: encodeRedactedContent(chunks) },
),
)
}, state.lifecycle)
Lifecycle.finish(lifecycle, events, {
reason: {
...state.finishReason,
normalized,
+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,
}
}
+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 },
)
@@ -0,0 +1,19 @@
import type { LLMRequest } from "../../schema/index.js"
export const THINKING_BINDING_BETA = "thinking-binding-controls-2026-08-01"
// Accept gateway namespaces and Vertex suffixes without treating a snapshot date as a minor version.
export const claudeVersion = (id: string) => {
const match = /(?:^|[./])claude-(?<family>[a-z]+)-(?<major>\d+)(?:[.-](?<minor>\d{1,2}))?(?:$|[-:@])/.exec(
id.toLowerCase(),
)?.groups
if (!match) return undefined
return { family: match.family, major: Number(match.major), minor: Number(match.minor ?? 0) }
}
export const supportsThinkingBlockBinding = (model: LLMRequest["model"]) => {
const override = model.compatibility?.supportsThinkingBlockBinding
if (override !== undefined) return override
const version = claudeVersion(model.id)
return version !== undefined && (version.major > 5 || (version.major === 5 && version.minor >= 1))
}
+52 -3
View File
@@ -1,4 +1,4 @@
import { Option, Schema } from "effect"
import { Option, Schema, SchemaGetter } from "effect"
import {
AuthenticationError,
ContentPolicyError,
@@ -16,6 +16,8 @@ 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,
@@ -109,7 +111,10 @@ const QUOTA_CODES = new Set([
"freeusagelimiterror",
"creditlimitexceeded",
])
const AUTH_CODES = new Set(["authentication_error", "permission_error"])
// Google reports an invalid API key as HTTP 400 INVALID_ARGUMENT with this `details[].reason`.
const AUTH_CODES = new Set(["authentication_error", "permission_error", "api_key_invalid"])
// xAI reports an invalid API key as HTTP 400 with the generic `invalid-argument` code.
const AUTH_TEXT = /incorrect api key provided/i
const SERVER_CODES = new Set([
"api_error",
"internal_error",
@@ -154,6 +159,41 @@ const CONTENT_POLICY_TEXT =
const SERVER_ERROR_TEXT =
/\b(?:try again|(?:please |you can )?retry (?:the |this |your )?request|try (?:the |this |your )?request again|(?:currently |temporarily )?at capacity|overloaded|temporarily unavailable|service[-_\s]?unavailable|(?:server|internal)[-_\s]?error|server (?:is )?busy|provider returned (?:an )?error|resource[-_\s]?exhausted|upstream (?:connect|connection|request)|request buffer limit while retrying upstream)\b/i
const Message = Schema.String.check(Schema.isPattern(/\S/))
const messageAt = <Fields extends Schema.Struct.Fields>(
fields: Fields,
message: (body: Schema.Struct<Fields>["Type"]) => string,
) =>
Schema.Struct(fields).pipe(
Schema.decodeTo(Schema.String, {
decode: SchemaGetter.transform(message),
encode: SchemaGetter.forbidden(() => "Provider error messages are decode-only"),
}),
)
// Common error body layouts that carry a human-readable message, in priority order.
// Provider-specific layouts belong in their protocol.
const decodeMessage = Schema.decodeUnknownOption(
Schema.fromJsonString(
Schema.Union([
messageAt({ error: Schema.Struct({ message: Message }) }, (body) => body.error.message),
messageAt({ error: Message }, (body) => body.error),
messageAt({ message: Message }, (body) => body.message),
// AWS services
messageAt({ Message: Message }, (body) => body.Message),
// RFC 9457 problem details
messageAt({ detail: Message }, (body) => body.detail),
messageAt(
{ errors: Schema.NonEmptyArray(Schema.Struct({ message: Message })) },
(body) => body.errors[0].message,
),
]),
),
)
export const providerErrorMessage = (body: string) => Option.getOrUndefined(decodeMessage(body))
export interface ProviderFailure {
readonly message: string
readonly status?: number | undefined
@@ -206,7 +246,12 @@ export function classifyProviderFailure(input: ProviderFailure): AIError["reason
(input.status === 429 && QUOTA_TEXT.test(text))
)
return new QuotaExceededError(details)
if (input.status === 401 || input.status === 403 || codes.some((code) => AUTH_CODES.has(code)))
if (
input.status === 401 ||
input.status === 403 ||
codes.some((code) => AUTH_CODES.has(code)) ||
(input.status === 400 && AUTH_TEXT.test(text))
)
return new AuthenticationError(details)
if (
input.status === 429 ||
@@ -259,6 +304,10 @@ function providerCodes(value: unknown) {
error?.type,
error?.status,
error?.error_type,
// Google `google.rpc.ErrorInfo` details carry the specific reason.
...(Array.isArray(error?.details)
? error.details.map((detail) => (isRecord(detail) ? detail.reason : undefined))
: []),
inner?.code,
metadata?.error_type,
responseError?.code,
@@ -28,7 +28,8 @@ export type Settings = ProviderPackage.Settings &
readonly provider?: string
}
export const routes = [AnthropicMessages.route]
const compatibleRoute = AnthropicMessages.route.with({ id: "anthropic-compatible-messages", provider: id })
export const routes = [compatibleRoute]
const auth = (input: ProviderAuthOption<"optional">) => {
if ("auth" in input && input.auth) return input.auth
@@ -43,7 +44,7 @@ export const configure = (input: Config) => {
message: "Anthropic-compatible providers require a baseURL",
})
const { provider: _, baseURL, apiKey: _apiKey, auth: _auth, ...rest } = input
const route = AnthropicMessages.route.with({
const route = (provider === "anthropic" ? AnthropicMessages.route : compatibleRoute).with({
...rest,
provider,
endpoint: { baseURL },
+3 -3
View File
@@ -35,13 +35,13 @@ const RESPONSES_WEBSOCKET_ROTATE_AFTER_MS = 24 * 60 * 1000
const responsesRoute = Route.make({
compact: { endpoint: XAIResponses.compact },
id: "openai-responses",
id: "xai-responses",
provider: id,
providerMetadataKey: "xai",
protocol: XAIResponses.protocol,
endpoint: Endpoint.path("/responses", { baseURL }),
transport: OpenResponsesChannel.transport({
id: "openai-responses",
id: "xai-responses",
name: "xAI Responses",
rotateAfterMs: RESPONSES_WEBSOCKET_ROTATE_AFTER_MS,
// xAI continues a chain only from stored responses: with `store: false` (the route default) `previous_response_id`
@@ -53,7 +53,7 @@ const responsesRoute = Route.make({
})
const chatRoute = Route.make({
id: "openai-compatible-chat",
id: "xai-chat",
provider: id,
providerMetadataKey: "xai",
protocol: OpenAIChat.protocol,
+10 -14
View File
@@ -8,7 +8,7 @@ import {
HttpClientResponse,
} from "effect/unstable/http"
import { HttpContext, HttpRateLimitDetails, AIError, TransportError } from "../schema/index.js"
import { classifyProviderFailure } from "../provider-error.js"
import { classifyProviderFailure, providerErrorMessage } from "../provider-error.js"
import { Service, type HttpMiddleware, type Interface } from "./executor-service.js"
export { Service } from "./executor-service.js"
@@ -84,21 +84,17 @@ export const responseHttp = (response: HttpClientResponse.HttpClientResponse) =>
headers: headerDetails(response.headers),
})
const decodeProviderBody = Schema.decodeUnknownOption(
Schema.fromJsonString(
Schema.Struct({
message: Schema.optionalKey(Schema.String),
error: Schema.optionalKey(Schema.Struct({ message: Schema.optionalKey(Schema.String) })),
}),
),
)
const MAX_BODY_CHARS = 2000
// Without a recognized message, show the raw body so the provider's explanation is never dropped.
const providerMessage = (status: number, body: string | void) => {
const decoded = body === undefined ? undefined : Option.getOrUndefined(decodeProviderBody(body))
return (
[decoded?.error?.message, decoded?.message].find((message) => message?.trim()) ??
`Provider request failed with HTTP ${status}`
)
const fallback = `Provider request failed with HTTP ${status}`
const text = body?.trim() ?? ""
const message = providerErrorMessage(text)
if (message) return message
// Gateway and proxy HTML error pages are markup, not an explanation.
if (!text || /^<(?:!doctype|html)/i.test(text)) return fallback
return `${fallback}: ${text.length > MAX_BODY_CHARS ? `${text.slice(0, MAX_BODY_CHARS)}…` : text}`
}
const statusError = (response: HttpClientResponse.HttpClientResponse) =>
+59 -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, 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"
@@ -107,6 +117,54 @@ describe("applyCachePolicy", () => {
}),
)
it.effect("'auto' emits Anthropic cache markers on Anthropic-compatible routes", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
LLM.request({
model: AnthropicCompatible.configure({ apiKey: "test", baseURL: "https://messages.example.test/v1" }).model(
"compatible",
),
system: "You are concise.",
prompt: "hi",
}),
)
expect(prepared.route).toBe("anthropic-compatible-messages")
expect(prepared.body).toMatchObject({
system: [{ type: "text", text: "You are concise.", cache_control: { type: "ephemeral" } }],
messages: [{ role: "user", content: [{ type: "text", text: "hi", cache_control: { type: "ephemeral" } }] }],
})
}),
)
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(
+49 -3
View File
@@ -273,10 +273,56 @@ describe("RequestExecutor", () => {
expectAIError(error)
expect(error.reason).toMatchObject({ _tag: "InvalidRequest" })
expect("classification" in error.reason ? error.reason.classification : undefined).toBeUndefined()
expect(error.message).toBe("Provider request failed with HTTP 400")
expect(error.message).toBe("Provider request failed with HTTP 400: invalid parameter")
}).pipe(Effect.provide(fixedResponse("invalid parameter", { status: 400 }))),
)
it.effect("shows unrecognized provider error bodies", () =>
Effect.gen(function* () {
const executor = yield* RequestExecutor.Service
const error = yield* executor.execute(request).pipe(Effect.flip)
expect(error.message).toBe(
'Provider request failed with HTTP 422: {"object":"error","message":{"detail":[{"msg":"Input should be less than or equal to 1.5"}]}}',
)
}).pipe(
Effect.provide(
fixedResponse('{"object":"error","message":{"detail":[{"msg":"Input should be less than or equal to 1.5"}]}}', {
status: 422,
}),
),
),
)
it.effect("shows messages from common provider error layouts", () =>
Effect.gen(function* () {
const executor = yield* RequestExecutor.Service
const error = yield* executor.execute(request).pipe(Effect.flip)
expect(error.message).toBe("Invalid API Key")
}).pipe(Effect.provide(fixedResponse('{"detail":"Invalid API Key"}', { status: 401 }))),
)
it.effect("truncates long unrecognized provider error bodies", () =>
Effect.gen(function* () {
const executor = yield* RequestExecutor.Service
const error = yield* executor.execute(request).pipe(Effect.flip)
expect(error.message).toBe(`Provider request failed with HTTP 400: ${"x".repeat(2000)}…`)
expect(error.reason.body).toHaveLength(5000)
}).pipe(Effect.provide(fixedResponse("x".repeat(5000), { status: 400 }))),
)
it.effect("does not show HTML error pages", () =>
Effect.gen(function* () {
const executor = yield* RequestExecutor.Service
const error = yield* executor.execute(request).pipe(Effect.flip)
expect(error.message).toBe("Provider request failed with HTTP 502")
expect(error.reason.body).toContain("Bad Gateway")
}).pipe(Effect.provide(fixedResponse("<!DOCTYPE html><html><body>Bad Gateway</body></html>", { status: 502 }))),
)
it.effect("preserves structured provider messages from large error bodies", () =>
Effect.gen(function* () {
const executor = yield* RequestExecutor.Service
@@ -299,7 +345,7 @@ describe("RequestExecutor", () => {
),
)
it.effect("falls back when structured provider messages are empty", () =>
it.effect("shows the body when structured provider messages are empty", () =>
Effect.gen(function* () {
const executor = yield* RequestExecutor.Service
const error = yield* executor.execute(request).pipe(Effect.flip)
@@ -308,7 +354,7 @@ describe("RequestExecutor", () => {
expect(error.reason).toMatchObject({
_tag: "InvalidRequest",
})
expect(error.message).toBe("Provider request failed with HTTP 400")
expect(error.message).toBe('Provider request failed with HTTP 400: {"error":{"message":" "}}')
}).pipe(Effect.provide(fixedResponse('{"error":{"message":" "}}', { status: 400 }))),
)
+2 -2
View File
@@ -154,8 +154,8 @@ describe("public exports", () => {
expect(XAI.model).toBeFunction()
expect(XAI.provider.responses).toBe(XAI.responses)
expect(XAI.provider.chat).toBe(XAI.chat)
expect(XAI.configure({ apiKey: "fixture" }).responses("grok-4.3").route.id).toBe("openai-responses")
expect(XAI.configure({ apiKey: "fixture" }).chat("grok-4.3").route.id).toBe("openai-compatible-chat")
expect(XAI.configure({ apiKey: "fixture" }).responses("grok-4.3").route.id).toBe("xai-responses")
expect(XAI.configure({ apiKey: "fixture" }).chat("grok-4.3").route.id).toBe("xai-chat")
expect(OpenAI.configure({ apiKey: "fixture" }).image("gpt-image-2").route.id).toBe("openai-images")
expect(OpenAI.provider.image).toBe(OpenAI.image)
expect(Google.configure({ apiKey: "fixture" }).image("imagen-4.0-generate-001").route.id).toBe("google-images")
@@ -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
@@ -0,0 +1,34 @@
{
"version": 1,
"metadata": {
"tags": [
"prefix:bedrock-converse-thinking-binding",
"provider:amazon-bedrock",
"protocol:bedrock-converse",
"reasoning"
],
"name": "bedrock-converse-thinking-binding/accepts-the-default-binding-with-no-thinking-configured",
"recordedAt": "2026-09-28T23:09:14.527Z"
},
"interactions": [
{
"transport": "http",
"request": {
"method": "POST",
"url": "https://bedrock-runtime.us-east-1.amazonaws.com/model/global.anthropic.claude-opus-5-5/converse-stream",
"headers": {
"content-type": "application/json"
},
"body": "{\"modelId\":\"global.anthropic.claude-opus-5-5\",\"messages\":[{\"role\":\"user\",\"content\":[{\"text\":\"Say hello.\"}]}],\"system\":[{\"text\":\"Reply with the single word 'Hello'.\"}],\"inferenceConfig\":{\"maxTokens\":1024},\"additionalModelRequestFields\":{\"thinking\":{\"type\":\"adaptive\",\"block_binding\":{\"prefix_mismatch_behavior\":\"drop_block\"}},\"anthropic_beta\":[\"thinking-binding-controls-2026-08-01\"]}}"
},
"response": {
"status": 200,
"headers": {
"content-type": "application/vnd.amazon.eventstream"
},
"body": "AAAAoQAAAFKtAFmXCzpldmVudC10eXBlBwAMbWVzc2FnZVN0YXJ0DTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSiIsInJvbGUiOiJhc3Npc3RhbnQifUMJiXoAAAC8AAAAV0Ua/isLOmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0ZXh0IjoiSGVsbG8ifSwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDRCJ91Ic6iwAAALQAAABWAm2FfAs6ZXZlbnQtdHlwZQcAEGNvbnRlbnRCbG9ja1N0b3ANOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUFFSU1RVViJ9qr7BagAAAIYAAABRR+j7OQs6ZXZlbnQtdHlwZQcAC21lc3NhZ2VTdG9wDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsicCI6ImFiY2RlIiwic3RvcFJlYXNvbiI6ImVuZF90dXJuIn0pJ2GhAAABCAAAAE4PaiuaCzpldmVudC10eXBlBwAIbWV0YWRhdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJtZXRyaWNzIjp7ImxhdGVuY3lNcyI6MTI0OX0sInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFkiLCJ1c2FnZSI6eyJpbnB1dFRva2VucyI6MjgsIm91dHB1dFRva2VucyI6NSwic2VydmVyVG9vbFVzYWdlIjp7fSwidG90YWxUb2tlbnMiOjMzfX2s2SiQ",
"bodyEncoding": "base64"
}
}
]
}
@@ -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
@@ -4,7 +4,7 @@
"model": "anthropic/claude-sonnet-4.6",
"tags": ["prefix:openai-compatible-chat", "provider:openrouter", "protocol:openai-chat", "reasoning"],
"name": "openrouter-reasoning",
"recordedAt": "2026-07-18T11:28:39.267Z"
"recordedAt": "2026-09-29T16:19:02.835Z"
},
"interactions": [
{
@@ -15,14 +15,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-1790698739-Acm5E9n2uVTO14sqrick\",\"object\":\"chat.completion.chunk\",\"created\":1790698739,\"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\n: OPENROUTER PROCESSING\n\ndata: {\"id\":\"gen-1790698739-Acm5E9n2uVTO14sqrick\",\"object\":\"chat.completion.chunk\",\"created\":1790698739,\"model\":\"anthropic/claude-sonnet-4.6\",\"provider\":\"Claude Platform on AWS\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"\",\"role\":\"assistant\",\"reasoning\":\" × 219\\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\":\" × 219\\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-1790698739-Acm5E9n2uVTO14sqrick\",\"object\":\"chat.completion.chunk\",\"created\":1790698739,\"model\":\"anthropic/claude-sonnet-4.6\",\"provider\":\"Claude Platform on AWS\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"\",\"role\":\"assistant\",\"reasoning_details\":[{\"type\":\"reasoning.text\",\"signature\":\"Ev8CCrIBCBIYAipA0W4viH3kgBs43Cl5ewwVBPXTQElvzfbA2TLF4iSbKy9ZZDCSDjjAlF3Bs4ELEnP3vrrTuTioC6OB380lXQdyIDIRY2xhdWRlLXNvbm5ldC00LTY4AEIIdGhpbmtpbmdaJDRjMGYwNDZmLTI1ZmQtNDVmYi1iZmIzLWEwOGE4ZTI0OWNhN3oedXByb2ZfMDExQ2VkSE1TN0tEcTM4Z2JkUkp0bXN3qAH2ye/VBhIMhjcPOnE2wLhs0P05GgwA3z9luwbhnSyRMSQiMDYeIIThirdkJb/n2s2TxUk5EOMrSc1xejAb0RN7BIl66P5KhtwXvcZTiyCkBS8+7Cp6MOinWi+I8U7DhWskSA0pHZqaAncbV7qCIHkoZ5aT8jiPWplAOnlciDo2tLkGsLnzXrnfhI11GTF0kcfyhbrt3tjEDSiPstBbT2u19LJ5IqwV2VKl0pWVdl4PTsDwC09ClbnJj1y0A3AM9gEh4B9mEo4BvT0XVEncux4YAQ==.eyJlbmRwb2ludF9zbHVnIjoiYW50aHJvcGljL2NsYXVkZS00LjYtc29ubmV0LTIwMjYwMjE3fGNsYXVkZS1vbi1hd3MifQ\",\"format\":\"anthropic-claude-v1\",\"index\":0}]},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"gen-1790698739-Acm5E9n2uVTO14sqrick\",\"object\":\"chat.completion.chunk\",\"created\":1790698739,\"model\":\"anthropic/claude-sonnet-4.6\",\"provider\":\"Claude Platform on AWS\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"37\",\"role\":\"assistant\"},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"gen-1790698739-Acm5E9n2uVTO14sqrick\",\"object\":\"chat.completion.chunk\",\"created\":1790698739,\"model\":\"anthropic/claude-sonnet-4.6\",\"provider\":\"Claude Platform on AWS\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"887\",\"role\":\"assistant\"},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"gen-1790698739-Acm5E9n2uVTO14sqrick\",\"object\":\"chat.completion.chunk\",\"created\":1790698739,\"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-1790698739-Acm5E9n2uVTO14sqrick\",\"object\":\"chat.completion.chunk\",\"created\":1790698739,\"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"
}
}
]
+99 -1
View File
@@ -1,6 +1,6 @@
import { describe, expect, test } from "bun:test"
import { isContextOverflow } from "../src/index.js"
import { classifyProviderFailure } from "../src/provider-error.js"
import { classifyProviderFailure, providerErrorMessage } from "../src/provider-error.js"
describe("provider error classification", () => {
test("classifies provider token limit messages as context overflow", () => {
@@ -355,6 +355,65 @@ 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 xAI invalid API keys as authentication failures", () => {
const message = "Incorrect API key provided. You can obtain an API key from https://console.x.ai."
expect(
classifyProviderFailure({
message,
status: 400,
rawBody: JSON.stringify({ code: "invalid-argument", error: message }),
})._tag,
).toBe("Authentication")
})
test("keeps other xAI invalid arguments as invalid requests", () => {
const message = "Temperature must be less than 2 but temperature = 99"
expect(
classifyProviderFailure({
message,
status: 400,
rawBody: JSON.stringify({ code: "invalid-argument", error: message }),
})._tag,
).toBe("InvalidRequest")
})
test("classifies Google invalid API keys as authentication failures", () => {
const rawBody = JSON.stringify({
error: {
code: 400,
message: "API key not valid. Please pass a valid API key.",
status: "INVALID_ARGUMENT",
details: [
{
"@type": "type.googleapis.com/google.rpc.ErrorInfo",
reason: "API_KEY_INVALID",
domain: "googleapis.com",
},
{
"@type": "type.googleapis.com/google.rpc.LocalizedMessage",
locale: "en-US",
message: "API key not valid. Please pass a valid API key.",
},
],
},
})
expect(
classifyProviderFailure({ message: "API key not valid. Please pass a valid API key.", status: 400, rawBody })
._tag,
).toBe("Authentication")
})
test("classifies overflow signals buried in the raw payload when the summary is vague", () => {
const reason = classifyProviderFailure({
message: "Request failed",
@@ -370,3 +429,42 @@ describe("provider error rawBody classification", () => {
).toBe("QuotaExceeded")
})
})
describe("provider error messages", () => {
test("reads messages from common error body layouts", () => {
expect(
[
'{"error":{"message":"Invalid API Key","type":"invalid_request_error"}}',
'{"code":"invalid-argument","error":"Incorrect API key provided."}',
'{"message":"1 validation error detected"}',
'{"Message":"Invalid API Key format: Must start with pre-defined prefix"}',
'{"type":"about:blank","title":"Gone","status":410,"detail":"The model has reached its end of life"}',
'{"result":null,"success":false,"errors":[{"code":10000,"message":"Authentication error"}]}',
].map(providerErrorMessage),
).toEqual([
"Invalid API Key",
"Incorrect API key provided.",
"1 validation error detected",
"Invalid API Key format: Must start with pre-defined prefix",
"The model has reached its end of life",
"Authentication error",
])
})
test("prefers the nested error message over a top-level message", () => {
expect(providerErrorMessage('{"message":"Bad Request","error":{"message":"model not found"}}')).toBe(
"model not found",
)
})
test("ignores blank, non-string, and non-JSON messages", () => {
expect(
[
'{"error":{"message":" "}}',
'{"message":{"detail":[{"msg":"too high"}]}}',
'{"errors":[]}',
"invalid parameter",
].map(providerErrorMessage),
).toEqual([undefined, undefined, undefined, undefined])
})
})
+4 -1
View File
@@ -300,7 +300,9 @@ describe("provider package entrypoints", () => {
})
expect(String(selected.provider)).toBe("example")
expect(selected.route.id).toBe("anthropic-messages")
expect(selected.route.id).toBe("anthropic-compatible-messages")
expect(selected.route.protocol).toBe("anthropic-messages")
expect(selected.route.providerMetadataKey).toBe("example")
expect(selected.route.endpoint).toMatchObject({
baseURL: "https://messages.example.test/v1",
})
@@ -319,6 +321,7 @@ describe("provider package entrypoints", () => {
thinking: { type: "adaptive" },
})
expect(selected.route.id).toBe("anthropic-messages")
expect(selected.route.defaults.providerOptions).toEqual({ thinking: { type: "adaptive" } })
})
@@ -0,0 +1,103 @@
import { describe, expect } from "bun:test"
import { Effect } from "effect"
import { LLM, LLMRequest, Message } from "../../src/index.js"
import { LLMClient } from "../../src/route.js"
import { AmazonBedrock } from "../../src/providers.js"
import { recordedTests } from "../recorded-test.js"
const RECORDING_REGION = process.env.BEDROCK_RECORDING_REGION ?? "us-east-1"
// Claude Opus 5.5 enforces the conversation-prefix check on Bedrock, so a changed system prompt is a real
// mismatch. Override with BEDROCK_BINDING_MODEL_ID for another model that enforces it.
const modelID = process.env.BEDROCK_BINDING_MODEL_ID ?? "global.anthropic.claude-opus-5-5"
// Mirrors the "high" variant, so the recording covers the real overlay and default merge.
const model = (blockBinding?: { readonly prefix_mismatch_behavior: string }) =>
AmazonBedrock.model(modelID, {
apiKey: process.env.AWS_BEARER_TOKEN_BEDROCK ?? "fixture",
region: RECORDING_REGION,
body: {
additionalModelRequestFields: {
thinking: { type: "adaptive", display: "summarized", ...(blockBinding ? { block_binding: blockBinding } : {}) },
output_config: { effort: "high" },
},
},
})
const question = "How many positive integers below 5000 have exactly 12 positive divisors? Reply with the number only."
const first = LLM.request({
id: "recorded_bedrock_thinking_binding_first",
model: model(),
system: "You are a concise assistant.",
prompt: question,
cache: "none",
generation: { maxTokens: 12_000 },
})
const recorded = recordedTests({
prefix: "bedrock-converse-thinking-binding",
provider: "amazon-bedrock",
protocol: "bedrock-converse",
requires: ["AWS_BEARER_TOKEN_BEDROCK"],
})
describe("Bedrock Converse thinking binding recorded", () => {
recorded.effect.with("accepts the default binding with no thinking configured", { tags: ["reasoning"] }, () =>
Effect.gen(function* () {
const response = yield* LLMClient.generate(
LLM.request({
id: "recorded_bedrock_thinking_binding_default",
model: AmazonBedrock.model(modelID, {
apiKey: process.env.AWS_BEARER_TOKEN_BEDROCK ?? "fixture",
region: RECORDING_REGION,
}),
system: "Reply with the single word 'Hello'.",
prompt: "Say hello.",
cache: "none",
generation: { maxTokens: 1_024 },
}),
)
expect(response.finishReason?.normalized).toBe("stop")
expect(response.text).toMatch(/hello/i)
}),
)
recorded.effect.with(
"keeps a session working after the system prompt changes",
{ tags: ["reasoning"] },
() =>
Effect.gen(function* () {
const turn = yield* LLMClient.generate(first)
// The signed thinking block is what Bedrock binds to the first system prompt.
expect(turn.message.content.some((part) => part.type === "reasoning")).toBe(true)
// Replay the assistant turn under a different system prompt, as a prompt rebuild or compaction would.
const next = LLMRequest.update(first, {
id: "recorded_bedrock_thinking_binding_next",
system: [{ type: "text", text: "You are a concise assistant. Today is Monday." }],
messages: [
...first.messages,
Message.assistant(turn.message.content),
Message.user("Now double it. Reply with the number only."),
],
})
const bound = yield* LLMClient.generate(next)
expect(bound.finishReason?.normalized).toBe("stop")
expect(bound.text).toMatch(/\d/)
// The same replay is rejected when the caller asks for the default `error` behavior, proving the check is
// enforced and that the default `drop_block` is what let the request above through.
const rejected = yield* LLMClient.generate(
LLMRequest.update(next, {
id: "recorded_bedrock_thinking_binding_error",
model: model({ prefix_mismatch_behavior: "error" }),
}),
).pipe(Effect.flip)
expect(rejected.message).toContain("bound to a different conversation")
}),
60_000,
)
})
@@ -0,0 +1,100 @@
import { describe, expect } from "bun:test"
import { Effect } from "effect"
import { GenerationOptions, LanguageModel, LLM } from "../../src/index.js"
import { compileRequest } from "../../src/route/client.js"
import { AmazonBedrock } from "../../src/providers.js"
import { it } from "../lib/effect.js"
const binding = { prefix_mismatch_behavior: "drop_block" }
const beta = ["thinking-binding-controls-2026-08-01"]
const bedrock = (id: string, settings: Parameters<typeof AmazonBedrock.model>[1] = {}) =>
AmazonBedrock.model(id, { baseURL: "https://bedrock-runtime.test", apiKey: "test-bearer", ...settings })
const fields = (model: LanguageModel, generation?: GenerationOptions) =>
compileRequest(
LLM.request({ id: "req_1", model, system: "You are concise.", prompt: "Say hello.", cache: "none", generation }),
).pipe(Effect.map((prepared) => prepared.body.additionalModelRequestFields))
describe("Bedrock Converse thinking block binding", () => {
for (const id of [
"global.anthropic.claude-fable-5-1",
"us.anthropic.claude-fable-5-1-v1:0",
"anthropic.claude-fable-5.1",
"anthropic.claude-mythos-5-1",
"us.anthropic.claude-opus-5-5",
"global.anthropic.claude-sonnet-5-5",
"anthropic.claude-opus-6",
]) {
it.effect(`defaults adaptive thinking to drop_block with the beta for ${id}`, () =>
Effect.gen(function* () {
expect(yield* fields(bedrock(id))).toEqual({
thinking: { type: "adaptive", block_binding: binding },
anthropic_beta: beta,
})
}),
)
}
for (const id of [
"global.anthropic.claude-opus-5",
"us.anthropic.claude-sonnet-5",
"global.anthropic.claude-fable-5",
"anthropic.claude-fable-5-v1:0",
"global.anthropic.claude-opus-4-8",
"us.anthropic.claude-opus-4-5-20251101-v1:0",
"us.anthropic.claude-haiku-4-5-20251001-v1:0",
"anthropic.claude-3-5-sonnet-20241022-v2:0",
"us.amazon.nova-2-lite-v1:0",
]) {
it.effect(`leaves ${id} untouched`, () =>
Effect.gen(function* () {
expect(yield* fields(bedrock(id))).toBeUndefined()
}),
)
}
it.effect("binds a manual thinking budget and keeps top_k", () =>
Effect.gen(function* () {
expect(
yield* fields(
bedrock("global.anthropic.claude-fable-5-1", { thinking: { type: "enabled", budgetTokens: 4_000 } }),
GenerationOptions.make({ maxTokens: 64_000, topK: 40 }),
),
).toEqual({
top_k: 40,
thinking: { type: "enabled", budget_tokens: 4_000, block_binding: binding },
anthropic_beta: beta,
})
}),
)
it.effect("does not bind disabled thinking", () =>
Effect.gen(function* () {
expect(
yield* fields(
bedrock("global.anthropic.claude-fable-5-1", {
body: { additionalModelRequestFields: { thinking: { type: "disabled" } } },
}),
),
).toBeUndefined()
}),
)
it.effect("honors the compatibility override in both directions", () =>
Effect.gen(function* () {
const arn = "arn:aws:bedrock:us-east-1:123456789012:application-inference-profile/abc123"
expect(yield* fields(bedrock(arn))).toBeUndefined()
expect(
yield* fields(LanguageModel.update(bedrock(arn), { compatibility: { supportsThinkingBlockBinding: true } })),
).toEqual({ thinking: { type: "adaptive", block_binding: binding }, anthropic_beta: beta })
expect(
yield* fields(
LanguageModel.update(bedrock("global.anthropic.claude-fable-5-1"), {
compatibility: { supportsThinkingBlockBinding: false },
}),
),
).toBeUndefined()
}),
)
})
@@ -1301,11 +1301,11 @@ describe("Bedrock Converse route", () => {
),
),
)
expect(response.events.filter((event) => event.type === "reasoning-delta" && event.text === "").at(-1)).toEqual({
type: "reasoning-delta",
expect(response.events.filter((event) => event.type === "reasoning-delta")).toEqual([])
expect(response.events.find((event) => event.type === "reasoning-start")).toEqual({
type: "reasoning-start",
id: "reasoning-0",
text: "",
providerMetadata: { bedrock: { redactedData } },
providerMetadata: undefined,
})
expect(response.events.find((event) => event.type === "reasoning-end")).toEqual({
type: "reasoning-end",
@@ -1383,6 +1383,13 @@ describe("Bedrock Converse route", () => {
),
)
expect(response.events.filter((event) => event.type === "reasoning-delta")).toEqual([])
expect(response.events.find((event) => event.type === "reasoning-end")).toEqual({
type: "reasoning-end",
id: "reasoning-0",
providerMetadata: { bedrock: { redactedData: "AQID" } },
text: undefined,
})
expect(response.message.content).toEqual([
{ type: "reasoning", text: "", providerMetadata: { bedrock: { redactedData: "AQID" } } },
])
@@ -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",
@@ -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(
+164 -4
View File
@@ -32,6 +32,125 @@ describe("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.body.messages).toMatchObject([
{
role: "system",
content: [
{ text: "Base agent", cache_control: { type: "ephemeral" } },
{ text: "Model details" },
{ text: "Project instructions", cache_control: { type: "ephemeral" } },
],
},
{ role: "user", content: [{ text: "Hello", cache_control: { type: "ephemeral" } }] },
])
expect(prepared.body.messages[0]?.content).not.toContainEqual(
expect.objectContaining({ text: "Model details", cache_control: expect.anything() }),
)
}),
)
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.messages).toMatchObject([
{ role: "system", content: [{ text: "Base agent", cache_control: { type: "ephemeral" } }] },
{ 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(body.messages).toMatchObject([
{ role: "system", content: "Base agent" },
{ role: "user", content: "Hello" },
])
})
}),
)
it.effect("lowers the native cache policy to OpenRouter cache controls", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
@@ -74,6 +193,8 @@ describe("OpenRouter", () => {
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 +211,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" } }],
},
])
}),
)
@@ -118,7 +248,7 @@ describe("OpenRouter", () => {
}),
)
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({
@@ -130,8 +260,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" }).model("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" },
},
],
})
}),
)
@@ -56,7 +56,9 @@ describe("xAI Responses route", () => {
expect(XAIResponses.protocol.body).not.toBe(OpenAIResponses.protocol.body)
const prepared = yield* compileRequest(LLM.request({ model, prompt: "Hello" }))
expect(prepared.route).toBe("xai-responses")
expect(prepared.protocol).toBe("xai-responses")
expect(prepared.model.route.providerMetadataKey).toBe("xai")
expect(prepared.body.store).toBe(false)
expect(prepared.body.include).toEqual(["reasoning.encrypted_content"])
}),
@@ -298,3 +300,14 @@ describe("xAI Responses route", () => {
}),
)
})
it.effect("names the xAI Chat route separately from its OpenAI Chat protocol", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
LLM.request({ model: XAI.configure({ apiKey: "test" }).chat("grok-4.6"), prompt: "Hello" }),
)
expect(prepared.route).toBe("xai-chat")
expect(prepared.protocol).toBe("openai-chat")
expect(prepared.model.route.providerMetadataKey).toBe("xai")
}),
)
@@ -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`) })
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode/app",
"version": "2.0.18",
"version": "2.0.20",
"description": "",
"type": "module",
"exports": {
@@ -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>
+10
View File
@@ -186,6 +186,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 +219,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",
@@ -1,6 +1,6 @@
import { describe, expect, test } from "bun:test"
import { Schema } from "effect"
import { GoUpsellState } from "./usage-exceeded-dialogs"
import { GoUpsellState, isChatGPTUsageLimit } from "./usage-exceeded-dialogs"
import { Persistence } from "@/runtime/persistence/schema"
const decode = Schema.decodeUnknownSync(
@@ -38,3 +38,31 @@ describe("usage exceeded preferences", () => {
})
})
})
describe("ChatGPT usage limit", () => {
const message =
"ChatGPT usage limit reached. Try again after your allowance resets; check ChatGPT Settings → Usage for details."
test("detects the mapped message regardless of failure type", () => {
expect(
isChatGPTUsageLimit({
type: "provider.rate-limit",
message,
status: 429,
}),
).toBe(true)
expect(isChatGPTUsageLimit({ type: "provider.unknown", message })).toBe(true)
})
test("ignores other rate limits and similar messages", () => {
expect(isChatGPTUsageLimit({ type: "provider.rate-limit", message: "Rate limit exceeded", status: 429 })).toBe(
false,
)
expect(
isChatGPTUsageLimit({
type: "provider.rate-limit",
message: "ChatGPT usage limit reached",
}),
).toBe(false)
})
})
@@ -1,6 +1,6 @@
import { useWorkspaceLocation } from "@/workspaces/location"
import { Persist, persisted } from "@/runtime/persistence/storage"
import type { SessionStatus } from "@opencode/client/promise"
import type { SessionStatus, SessionStepFailed } from "@opencode/client/promise"
import { onCleanup } from "solid-js"
import { Schema } from "effect"
import { Persistence } from "@/runtime/persistence/schema"
@@ -14,6 +14,14 @@ const GO_UPSELL_ACCOUNT_RATE_LIMIT_LAST_SEEN_AT = "go_upsell_account_rate_limit_
const GO_UPSELL_ACCOUNT_RATE_LIMIT_DONT_SHOW = "go_upsell_account_rate_limit_dont_show"
const GO_UPSELL_WINDOW = 86_400_000 // 24 hrs
const GO_UPSELL_PROVIDERS = new Set(["opencode", "opencode-go"])
const CHATGPT_USAGE_LIMIT_WINDOW = 86_400_000 // 24 hrs
export function isChatGPTUsageLimit(error: SessionStepFailed["data"]["error"]) {
return (
error.message ===
"ChatGPT usage limit reached. Try again after your allowance resets; check ChatGPT Settings → Usage for details."
)
}
export const GoUpsellState = Persistence.struct({
[GO_UPSELL_FREE_TIER_LAST_SEEN_AT]: Schema.NullOr(Schema.Finite),
@@ -52,6 +60,25 @@ export function useUsageExceededDialogs() {
[GO_UPSELL_ACCOUNT_RATE_LIMIT_LAST_SEEN_AT]: null,
[GO_UPSELL_ACCOUNT_RATE_LIMIT_DONT_SHOW]: null,
})
const [chatgptUsageLimit, setChatGPTUsageLimit] = persisted(
Persist.global("chatgpt-usage-limit"),
Persistence.struct({ lastSeenAt: Schema.NullOr(Schema.Finite) }),
{ lastSeenAt: null },
)
onCleanup(
sdk().event.on("session.step.failed", (evt) => {
if (evt.data.sessionID !== params.id) return
if (!isChatGPTUsageLimit(evt.data.error) || dialog.active) return
if (chatgptUsageLimit.lastSeenAt && Date.now() - chatgptUsageLimit.lastSeenAt < CHATGPT_USAGE_LIMIT_WINDOW) return
void import("@/providers/connect/chatgpt-usage-limit").then((usage) => {
if (dialog.active) return
setChatGPTUsageLimit("lastSeenAt", Date.now())
dialog.show(() => <usage.DialogChatGPTUsageLimit />)
})
}),
)
onCleanup(
sdk().event.on("session.status", (evt) => {
@@ -0,0 +1,94 @@
import { afterAll, beforeAll, expect, mock, test } from "bun:test"
import { createRoot } from "solid-js"
import { createStore } from "solid-js/store"
import type { ServerSDK } from "@/runtime/server/client"
import type { Data } from "@opencode/client/solid"
import type { ServerConnection } from "@/runtime/server/registry"
import { ServerScope } from "@/runtime/server/scope"
import type { Tab } from "@/shell/tabs/tabs"
const server = "local\nhttp://localhost:4096" as ServerConnection.Key
const session = { id: "session-1", title: "Test session", location: { directory: "/project" } }
const alerts: string[] = []
const tabs: { store: Tab[] } = { store: [] }
let createServerNotificationState: typeof import("./notification").createServerNotificationState
let storage: typeof import("@/runtime/persistence/storage")
beforeAll(async () => {
storage = await import("@/runtime/persistence/storage")
const { sessionIDHasOpenTab } = await import("@/shell/tabs/tabs")
mock.module("@/runtime/platform/platform", () => ({
usePlatform: () => ({
platform: "web",
notify: async (title: string) => {
alerts.push(title)
},
}),
}))
mock.module("@/settings/model", () => ({
useSettings: () => ({
sounds: { agentEnabled: () => false, errorsEnabled: () => false },
notifications: { agent: () => true, errors: () => true },
}),
}))
mock.module("@/runtime/i18n/language", () => ({ useLanguage: () => ({ t: (key: string) => key }) }))
mock.module("@/shell/tabs/tabs", () => ({
useTabs: () => tabs,
sessionIDHasOpenTab,
}))
mock.module("@/runtime/persistence/storage", () => ({
...storage,
persisted: () => {
const [store, setStore] = createStore({ list: [] })
return [store, setStore, undefined, () => false]
},
}))
createServerNotificationState = (await import("./notification")).createServerNotificationState
})
afterAll(() => mock.module("@/runtime/persistence/storage", () => storage))
test.each([
["session.execution.succeeded", "notification.session.responseReady.title"],
["session.execution.failed", "notification.session.error.title"],
] as const)("system alert for %s requires an open session tab", async (type, title) => {
alerts.length = 0
tabs.store = [{ type: "session", server, sessionId: "another-session" }]
let listener: ((event: unknown) => void) | undefined
const dispose = createRoot((dispose) => {
const state = createServerNotificationState({
key: server,
sdk: {
scope: ServerScope.local,
event: {
listen: (fn: typeof listener) => {
listener = fn
return () => {}
},
},
} as unknown as ServerSDK,
data: { session: { get: () => session } } as unknown as Data,
coordinator: { system: async (_id: string, fn: () => Promise<void>) => fn() },
} as Parameters<typeof createServerNotificationState>[0])
return { dispose, state }
})
listener?.({
type,
id: "event-1",
data: { sessionID: session.id, error: { type: "api", message: "failed", status: 500 } },
})
await new Promise((resolve) => setTimeout(resolve, 0))
expect(dispose.state.session.all(session.id)).toHaveLength(1)
expect(alerts).toEqual([])
tabs.store = [{ type: "session", server, sessionId: session.id }]
listener?.({
type,
id: "event-2",
data: { sessionID: session.id, error: { type: "api", message: "failed", status: 500 } },
})
await new Promise((resolve) => setTimeout(resolve, 0))
expect(alerts).toEqual([title])
dispose.dispose()
})
@@ -228,7 +228,8 @@ export function createServerNotificationState(input: {
if (!session) return
if (session.parentID) return
if (sessionIDHasOpenTab(tabs.store, input.key, sessionID) && settings.sounds.agentEnabled()) {
const hasOpenTab = sessionIDHasOpenTab(tabs.store, input.key, sessionID)
if (hasOpenTab && settings.sounds.agentEnabled()) {
void input.coordinator.sound(`${input.key}\0${eventID}`, () => playSoundById(settings.sounds.agent()))
}
@@ -240,7 +241,7 @@ export function createServerNotificationState(input: {
session: sessionID,
})
if (settings.notifications.agent()) {
if (hasOpenTab && settings.notifications.agent()) {
void input.coordinator.system(`${input.key}\0${eventID}`, () =>
platform.notify(language.t("notification.session.responseReady.title"), session.title ?? sessionID, () =>
openNotificationSession(tabs, input.key, sessionID),
@@ -255,7 +256,8 @@ export function createServerNotificationState(input: {
if (meta.disposed) return
if (session?.parentID) return
if (sessionIDHasOpenTab(tabs.store, input.key, sessionID) && settings.sounds.errorsEnabled()) {
const hasOpenTab = sessionIDHasOpenTab(tabs.store, input.key, sessionID)
if (hasOpenTab && settings.sounds.errorsEnabled()) {
void input.coordinator.sound(`${input.key}\0${eventID}`, () => playSoundById(settings.sounds.errors()))
}
@@ -270,7 +272,7 @@ export function createServerNotificationState(input: {
const description =
session?.title ??
(typeof error === "string" ? error : language.t("notification.session.error.fallbackDescription"))
if (settings.notifications.errors()) {
if (hasOpenTab && settings.notifications.errors()) {
void input.coordinator.system(`${input.key}\0${eventID}`, () =>
platform.notify(language.t("notification.session.error.title"), description, () =>
openNotificationSession(tabs, input.key, sessionID),
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/cli",
"version": "2.0.18",
"version": "2.0.20",
"type": "module",
"license": "MIT",
"bin": {
+20
View File
@@ -183,6 +183,26 @@ const Root = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME
),
},
}),
Spec.make("export", {
description: "print stored credentials, including secrets, as JSON",
params: {
...ServerParams,
target: Argument.string("target").pipe(
Argument.withDescription("Integration ID or name (exports every integration when omitted)"),
Argument.optional,
),
},
}),
Spec.make("import", {
description: "import credentials exported by auth export",
params: {
...ServerParams,
file: Argument.string("file").pipe(
Argument.withDescription("JSON file to import (reads stdin when omitted)"),
Argument.optional,
),
},
}),
Spec.make("switch", {
description: "switch the active account for an integration",
params: {
@@ -0,0 +1,31 @@
import { EOL } from "node:os"
import { Effect, Option } from "effect"
import { Commands } from "../../commands"
import { Runtime } from "../../../framework/runtime"
import { createClient, loadIntegrations, request, resolveIntegration } from "./shared"
import { errorMessage } from "../../../util/error"
export default Runtime.handler(
Commands.commands.auth.commands.export,
Effect.fn("cli.auth.export")(
function* (input) {
const client = yield* createClient({ server: Option.getOrUndefined(input.server), standalone: input.standalone })
const target = Option.getOrUndefined(input.target)
const integrationID = target ? (yield* resolveIntegration(yield* loadIntegrations(client), target)).id : undefined
const credentials = (yield* request((signal) => client.credential.list({ signal }))).filter(
(credential) => !integrationID || credential.integrationID === integrationID,
)
if (process.stdout.isTTY)
process.stderr.write(
"Warning: the output contains secrets; redirect it to a file or pipe it to auth import" + EOL,
)
process.stdout.write(JSON.stringify(credentials, null, 2) + EOL)
},
Effect.catch((error) =>
Effect.sync(() => {
process.stderr.write(errorMessage(error) + EOL)
process.exitCode = 1
}),
),
),
)
@@ -0,0 +1,58 @@
import { EOL } from "node:os"
import { isConflictError, type CredentialCreateInput } from "@opencode/client"
import { Credential } from "@opencode/schema/credential"
import { Effect, Option, Schema } from "effect"
import { Commands } from "../../commands"
import { Runtime } from "../../../framework/runtime"
import { createClient, request } from "./shared"
import { errorMessage } from "../../../util/error"
import { readStdin } from "../../../util/io"
export default Runtime.handler(
Commands.commands.auth.commands.import,
Effect.fn("cli.auth.import")(
function* (input) {
const file = Option.getOrUndefined(input.file)
if (!file && process.stdin.isTTY)
return yield* Effect.fail(new Error("Pipe auth export output into stdin or pass a file to import"))
const text = yield* request(() => (file ? Bun.file(file).text() : readStdin()))
const credentials = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Array(Credential.Entry)))(text)
const client = yield* createClient({ server: Option.getOrUndefined(input.server), standalone: input.standalone })
const existing = yield* request((signal) => client.credential.list({ signal }))
const ids = new Set(existing.map((credential) => credential.id))
const integrations = new Set(existing.map((credential) => credential.integrationID))
const results = yield* Effect.forEach(credentials, (credential) => {
if (ids.has(credential.id)) return Effect.succeed(false)
return request((signal) =>
client.credential.create(
{
id: credential.id,
integrationID: credential.integrationID,
label: credential.label,
// Stored metadata is JSON, but the credential schema types it as unknown while the generated client expects JSON.
value: credential.value as CredentialCreateInput["value"],
// Keep the destination's current selections; only integrations new to it adopt the exported selection.
activate: credential.active && !integrations.has(credential.integrationID),
},
{ signal },
),
).pipe(
Effect.as(true),
Effect.catchIf(isConflictError, () => Effect.succeed(false)),
)
})
const imported = results.filter(Boolean).length
process.stderr.write(
`Imported ${imported} ${imported === 1 ? "credential" : "credentials"}` +
(results.length > imported ? `, skipped ${results.length - imported} already present` : "") +
EOL,
)
},
Effect.catch((error) =>
Effect.sync(() => {
process.stderr.write(errorMessage(error) + EOL)
process.exitCode = 1
}),
),
),
)
@@ -4,12 +4,12 @@ import { OpenCode } from "@opencode/client"
import { Commands } from "../../commands"
import { Runtime } from "../../../framework/runtime"
import { Service } from "@opencode/client/effect/service"
import { ServiceConfig } from "../../../services/service-config"
import { ServerConnection } from "../../../services/server-connection"
export default Runtime.handler(
Commands.commands.debug.commands.agents,
Effect.fn("cli.debug.agents")(function* () {
const endpoint = yield* Service.ensure(yield* ServiceConfig.options())
const { endpoint } = yield* ServerConnection.resolve()
const client = OpenCode.make({ baseUrl: endpoint.url, headers: Service.headers(endpoint) })
const response = yield* Effect.promise(() => client.agent.list({ location: { directory: process.cwd() } }))
process.stdout.write(
@@ -4,13 +4,13 @@ import { OpenCode } from "@opencode/client"
import { Service } from "@opencode/client/effect/service"
import { Commands } from "../../commands"
import { Runtime } from "../../../framework/runtime"
import { ServiceConfig } from "../../../services/service-config"
import { ServerConnection } from "../../../services/server-connection"
import { redactConfig } from "./redact"
export default Runtime.handler(
Commands.commands.debug.commands.config,
Effect.fn("cli.debug.config")(function* () {
const endpoint = yield* Service.ensure(yield* ServiceConfig.options())
const { endpoint } = yield* ServerConnection.resolve()
const client = OpenCode.make({ baseUrl: endpoint.url, headers: Service.headers(endpoint) })
const entries = yield* Effect.promise(() => client.config.get({ location: { directory: process.cwd() } }))
process.stdout.write(JSON.stringify(redactConfig(entries), null, 2) + EOL)
@@ -4,7 +4,7 @@ import { OpenCode, type IntegrationInfo, type IntegrationOAuthMethod, type McpSe
import { Commands } from "../../commands"
import { Runtime } from "../../../framework/runtime"
import { Service } from "@opencode/client/effect/service"
import { ServiceConfig } from "../../../services/service-config"
import { ServerConnection } from "../../../services/server-connection"
import { selectIntegration, type IntegrationChoice } from "../../../ui/integration-picker"
import { handlePromptErrors, prompt, requireInteractive } from "../../../ui/prompt"
import { answerForm } from "../auth/form"
@@ -21,7 +21,7 @@ export default Runtime.handler(
const authenticate = Effect.fn("cli.mcp.auth.run")(function* (name?: string) {
if (!name) yield* requireInteractive("Pass an MCP server name when running without an interactive terminal")
intro("Authenticate an MCP server")
const endpoint = yield* Service.ensure(yield* ServiceConfig.options())
const { endpoint } = yield* ServerConnection.resolve()
const client = OpenCode.make({ baseUrl: endpoint.url, headers: Service.headers(endpoint) })
const integrations = yield* loadIntegrations(client)
const servers = yield* request((signal) => client.mcp.list({ location }, { signal }))
@@ -4,12 +4,12 @@ import { OpenCode, type McpServer } from "@opencode/client"
import { Commands } from "../../commands"
import { Runtime } from "../../../framework/runtime"
import { Service } from "@opencode/client/effect/service"
import { ServiceConfig } from "../../../services/service-config"
import { ServerConnection } from "../../../services/server-connection"
export default Runtime.handler(
Commands.commands.mcp.commands.list,
Effect.fn("cli.mcp.list")(function* () {
const endpoint = yield* Service.ensure(yield* ServiceConfig.options())
const { endpoint } = yield* ServerConnection.resolve()
const client = OpenCode.make({ baseUrl: endpoint.url, headers: Service.headers(endpoint) })
const response = yield* Effect.promise(() => client.mcp.list({ location: { directory: process.cwd() } }))
const servers = response.data.toSorted((a, b) => a.name.localeCompare(b.name))
@@ -4,7 +4,7 @@ import { OpenCode } from "@opencode/client"
import { Commands } from "../../commands"
import { Runtime } from "../../../framework/runtime"
import { Service } from "@opencode/client/effect/service"
import { ServiceConfig } from "../../../services/service-config"
import { ServerConnection } from "../../../services/server-connection"
import { resolveIntegration } from "./resolve"
const location = { directory: process.cwd() }
@@ -12,7 +12,7 @@ const location = { directory: process.cwd() }
export default Runtime.handler(
Commands.commands.mcp.commands.logout,
Effect.fn("cli.mcp.logout")(function* (input) {
const endpoint = yield* Service.ensure(yield* ServiceConfig.options())
const { endpoint } = yield* ServerConnection.resolve()
const client = OpenCode.make({ baseUrl: endpoint.url, headers: Service.headers(endpoint) })
const integration = yield* resolveIntegration(client, input.name, location)
@@ -10,6 +10,10 @@ import { ServiceConfig } from "../../services/service-config"
export default Runtime.handler(
Commands.commands.pair,
Effect.fn("cli.pair")(function* (input: Runtime.Input<typeof Commands.commands.pair>) {
if ((yield* ServiceConfig.read()).disabled === true)
return yield* Effect.fail(
new Error("Pairing requires the background service; run `opencode service unset disabled` first"),
)
const endpoint = yield* Service.ensure(yield* ServiceConfig.options())
const client = OpenCode.make({ baseUrl: endpoint.url, headers: Service.headers(endpoint) })
const urls = Option.isSome(input.url)
@@ -4,7 +4,7 @@ import { OpenCode, type PluginInfo } from "@opencode/client"
import { Service } from "@opencode/client/effect/service"
import { Npm } from "@opencode/util/npm"
import { Config } from "../../../config"
import { ServiceConfig } from "../../../services/service-config"
import { ServerConnection } from "../../../services/server-connection"
export interface Item {
readonly runtime: "Server" | "TUI"
@@ -16,7 +16,7 @@ export interface Item {
}
export const inspect = Effect.fn("cli.plugin.inspect")(function* (selected?: string) {
const endpoint = yield* Service.ensure(yield* ServiceConfig.options())
const { endpoint } = yield* ServerConnection.resolve()
const client = OpenCode.make({ baseUrl: endpoint.url, headers: Service.headers(endpoint) })
const location = { directory: process.cwd() }
const listed = yield* Effect.promise(() => client.plugin.list({ location }))
@@ -5,7 +5,7 @@ import { OpenCode, type PluginInfo } from "@opencode/client"
import { Service } from "@opencode/client/effect/service"
import { Commands } from "../../commands"
import { Runtime } from "../../../framework/runtime"
import { ServiceConfig } from "../../../services/service-config"
import { ServerConnection } from "../../../services/server-connection"
import { Config } from "../../../config"
import { Global } from "@opencode/util/global"
import { Npm } from "@opencode/util/npm"
@@ -16,7 +16,7 @@ import { discoverPluginTargets, localPluginDirectories, localSource } from "@ope
export default Runtime.handler(
Commands.commands.plugin.commands.list,
Effect.fn("cli.plugin.list")(function* (input) {
const endpoint = yield* Service.ensure(yield* ServiceConfig.options())
const { endpoint } = yield* ServerConnection.resolve()
const client = OpenCode.make({ baseUrl: endpoint.url, headers: Service.headers(endpoint) })
const response = yield* Effect.promise(() => client.plugin.list({ location: { directory: process.cwd() } }))
const config = yield* Config.Service
+2
View File
@@ -31,6 +31,8 @@ const Handlers = Runtime.handlers(Commands, {
list: () => import("./commands/handlers/auth/list"),
login: () => import("./commands/handlers/auth/login"),
logout: () => import("./commands/handlers/auth/logout"),
export: () => import("./commands/handlers/auth/export"),
import: () => import("./commands/handlers/auth/import"),
switch: () => import("./commands/handlers/auth/switch"),
},
debug: {
+36 -7
View File
@@ -67,6 +67,9 @@ type FormRequest = Extract<V2Event, { type: "form.created" }>["data"]["form"]
// attached client must not cancel input that may belong to another session.
const GLOBAL_FORM_SESSION_ID = "global"
const PERMISSION_REJECTED_FEEDBACK =
"This non-interactive run cannot ask the user for permission, so the request was rejected. Continue without this action."
export async function runNonInteractivePrompt(input: Input) {
const controller = new AbortController()
const stream = input.client.event.subscribe({ signal: controller.signal })[Symbol.asyncIterator]()
@@ -132,10 +135,34 @@ export async function runNonInteractivePrompt(input: Input) {
}
}
const replyPermission = async (request: { id: string; action: string; resources: ReadonlyArray<string> }) => {
// Subagents run in child sessions; their asks and questions belong to this run too. Other
// sessions on a shared server (e.g. the TUI's) must be left alone.
const owned = new Map<string, Promise<boolean>>([[input.sessionID, Promise.resolve(true)]])
const ownsSession = (sessionID: string): Promise<boolean> => {
const known = owned.get(sessionID)
if (known) return known
const result =
sessionID === GLOBAL_FORM_SESSION_ID
? Promise.resolve(false)
: input.client.session
.get({ sessionID })
.then((session) => (session.parentID ? ownsSession(session.parentID) : false))
.catch(() => false)
owned.set(sessionID, result)
return result
}
const replyPermission = async (request: {
id: string
sessionID: string
action: string
resources: ReadonlyArray<string>
}) => {
// Nobody can approve here. Outside V1 compatibility, reject with feedback so the tool fails
// as ordinary model-visible output and the model continues without the action.
const continuing = !input.auto && input.compatibility !== "v1"
if (!input.auto) {
permissionRejected = true
if (input.compatibility !== "v1") process.exitCode = 1
if (!continuing) permissionRejected = true
UI.println(
UI.Style.TEXT_WARNING_BOLD + "!",
UI.Style.TEXT_NORMAL +
@@ -144,12 +171,13 @@ export async function runNonInteractivePrompt(input: Input) {
}
await input.client.permission
.reply({
sessionID: input.sessionID,
sessionID: request.sessionID,
requestID: request.id,
decision: input.auto ? "once" : "reject",
...(continuing ? { message: PERMISSION_REJECTED_FEEDBACK } : {}),
})
.catch(() => {})
if (!input.auto) {
if (!input.auto && !continuing) {
await input.client.session.interrupt({ sessionID: input.sessionID }).catch(() => {})
}
}
@@ -164,6 +192,7 @@ export async function runNonInteractivePrompt(input: Input) {
if (!formAlreadySettled(error)) throw error
}
formCancelled = true
if (input.compatibility !== "v1") process.exitCode = 1
}
const consume = async () => {
@@ -178,14 +207,14 @@ export async function runNonInteractivePrompt(input: Input) {
}
const event = next.value
if (event.type === "permission.asked" && submitted && event.data.sessionID === input.sessionID) {
if (event.type === "permission.asked" && submitted && (await ownsSession(event.data.sessionID))) {
await replyPermission(event.data)
continue
}
if (
event.type === "form.created" &&
submitted &&
(event.data.form.sessionID === input.sessionID ||
((await ownsSession(event.data.form.sessionID)) ||
(!input.attached &&
event.data.form.sessionID === GLOBAL_FORM_SESSION_ID &&
sameLocation(event.location, input.location)))
@@ -18,7 +18,7 @@ export type Resolved = {
readonly service?: ReturnType<typeof managedService>
}
export const resolve = Effect.fn("cli.server-connection.resolve")(function* (args: Args) {
export const resolve = Effect.fn("cli.server-connection.resolve")(function* (args: Args = {}) {
if (args.server !== undefined && args.standalone)
return yield* Effect.fail(new Error("--server and --standalone cannot be combined"))
if (args.server !== undefined) {
@@ -38,7 +38,7 @@ export const resolve = Effect.fn("cli.server-connection.resolve")(function* (arg
)
return { endpoint } satisfies Resolved
}
if (args.standalone) {
if (args.standalone || (yield* ServiceConfig.read()).disabled === true) {
return { endpoint: yield* Standalone.start() } satisfies Resolved
}
+25 -2
View File
@@ -12,6 +12,7 @@ import { selfCommand } from "../util/process"
// registration file (by channel), which version, and how to spawn opencode.
export const Info = Schema.Struct({
disabled: Schema.optional(Schema.Boolean),
hostname: Schema.optional(Schema.String),
port: Schema.optional(Schema.Int.check(Schema.isGreaterThanOrEqualTo(1), Schema.isLessThanOrEqualTo(65_535))),
password: Schema.optional(Schema.String),
@@ -20,7 +21,7 @@ export const Info = Schema.Struct({
})
export type Info = typeof Info.Type
const keys = ["hostname", "port", "password", "cors", "env"] as const
const keys = ["disabled", "hostname", "port", "password", "cors", "env"] as const
type Key = (typeof keys)[number]
const decodeInfo = Schema.decodeUnknownEffect(Schema.fromJsonString(Info))
@@ -78,7 +79,15 @@ export const migrateConfig = Effect.fnUntraced(function* (legacy: string, file:
})
function configKey(key: string): Key {
if (key === "hostname" || key === "port" || key === "password" || key === "cors" || key === "env") return key
if (
key === "disabled" ||
key === "hostname" ||
key === "port" ||
key === "password" ||
key === "cors" ||
key === "env"
)
return key
throw new Error(`Unknown service config key: ${key}`)
}
@@ -151,6 +160,9 @@ export const get = Effect.fn("cli.service-config.get")(function* (key?: string,
const selected = configKey(key)
if (selected !== "env" && name !== undefined) throw new Error(`Usage: opencode service get ${selected}`)
switch (selected) {
case "disabled": {
return String((yield* read()).disabled ?? false)
}
case "hostname": {
return (yield* read()).hostname ?? ""
}
@@ -177,6 +189,12 @@ export const set = Effect.fn("cli.service-config.set")(function* (key: string, v
if (selected !== "env" && nestedValue !== undefined)
throw new Error(`Usage: opencode service set ${selected} <value>`)
switch (selected) {
case "disabled": {
if (value !== "true" && value !== "false") throw new Error("Disabled must be true or false")
if (value === "true") yield* Service.stop(yield* options())
yield* write({ ...(yield* read()), disabled: value === "true" })
return
}
case "hostname": {
yield* Service.stop(yield* options())
yield* write({ ...(yield* read()), hostname: value })
@@ -221,6 +239,11 @@ export const unset = Effect.fn("cli.service-config.unset")(function* (key: strin
const selected = configKey(key)
if (selected !== "env" && name !== undefined) throw new Error(`Usage: opencode service unset ${selected}`)
switch (selected) {
case "disabled": {
const { disabled: _disabled, ...next } = yield* read()
yield* write(next)
return
}
case "hostname": {
yield* Service.stop(yield* options())
const { hostname: _hostname, ...next } = yield* read()
@@ -309,6 +309,7 @@ async function capture(input: Parameters<typeof run>[0]) {
afterEach(() => {
mock.restore()
process.exitCode = 0
})
describe("runNonInteractivePrompt", () => {
@@ -433,6 +434,7 @@ describe("runNonInteractivePrompt", () => {
expect(sdk.form.list).toHaveBeenCalledWith({
location: { directory: "/work tree" },
})
expect(process.exitCode).toBe(1)
})
test("attach mode cancels only session-owned forms", async () => {
@@ -448,6 +450,7 @@ describe("runNonInteractivePrompt", () => {
{ sessionID: "global", formID: "frm_pending_global" },
expect.anything(),
)
expect(process.exitCode).toBe(1)
})
test("V1 JSON output flushes step_start before an unrelated step failure", async () => {
+1 -1
View File
@@ -38,7 +38,7 @@ test("resolution groups Effect-native lifecycle operations only for the managed
pid: process.pid,
}),
)
const resolved = await runPromise(ServerConnection.resolve({}))
const resolved = await runPromise(ServerConnection.resolve())
expect(resolved.endpoint.url).toBe(server.url.toString())
expect(resolved.service).toBeDefined()
+19 -1
View File
@@ -3,7 +3,7 @@ import { Service, type Info } from "@opencode/client/effect/service"
import { Global } from "@opencode/util/global"
import { OPENCODE_VERSION } from "../src/version"
import { expect, test } from "bun:test"
import { Effect, Schema } from "effect"
import { Effect, FileSystem, Schema } from "effect"
import fs from "node:fs/promises"
import os from "node:os"
import path from "node:path"
@@ -21,6 +21,24 @@ test("managed service ports are stable per installation channel", () => {
expect(ServiceConfig.defaultPort("preview-a")).not.toBe(ServiceConfig.defaultPort("preview-b"))
})
test("service disabled accepts only booleans without changing configuration on invalid input", async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-service-disabled-config-"))
const layer = Global.layerWith({ config: path.join(root, "config"), state: path.join(root, "state") })
const run = <A, E>(effect: Effect.Effect<A, E, Global.Service | FileSystem.FileSystem>) =>
Effect.runPromise(effect.pipe(Effect.provide(layer), Effect.provide(NodeFileSystem.layer)))
try {
expect(await run(ServiceConfig.get("disabled"))).toBe("false")
await expect(run(ServiceConfig.set("disabled", "yes"))).rejects.toThrow("Disabled must be true or false")
expect(await run(ServiceConfig.read())).toEqual({})
await run(ServiceConfig.set("disabled", "true"))
expect(await run(ServiceConfig.read())).toEqual({ disabled: true })
await run(ServiceConfig.unset("disabled"))
expect(await run(ServiceConfig.read())).toEqual({})
} finally {
await fs.rm(root, { recursive: true, force: true })
}
})
test("local channel stores service config with the local service filename", async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-service-"))
try {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/client",
"version": "2.0.18",
"version": "2.0.20",
"type": "module",
"license": "MIT",
"repository": {
+47 -5
View File
@@ -710,7 +710,12 @@ export type SessionLogOutput =
| undefined
readonly data: {
readonly sessionID: Session.ID
readonly error: { readonly type: string; readonly message: string; readonly status?: number | undefined }
readonly error: {
readonly type: string
readonly message: string
readonly status?: number | undefined
readonly response?: { readonly body: string } | undefined
}
}
}
| {
@@ -902,7 +907,12 @@ export type SessionLogOutput =
readonly data: {
readonly sessionID: Session.ID
readonly assistantMessageID: SessionMessage.ID
readonly error: { readonly type: string; readonly message: string; readonly status?: number | undefined }
readonly error: {
readonly type: string
readonly message: string
readonly status?: number | undefined
readonly response?: { readonly body: string } | undefined
}
readonly finish?: "content-filter" | undefined
readonly rawFinish?: string | undefined
readonly providerState?: SessionMessage.ProviderState | undefined
@@ -1112,7 +1122,12 @@ export type SessionLogOutput =
readonly sessionID: Session.ID
readonly assistantMessageID: SessionMessage.ID
readonly id: string
readonly error: { readonly type: string; readonly message: string; readonly status?: number | undefined }
readonly error: {
readonly type: string
readonly message: string
readonly status?: number | undefined
readonly response?: { readonly body: string } | undefined
}
readonly content?:
| readonly [
(
@@ -1157,7 +1172,12 @@ export type SessionLogOutput =
readonly assistantMessageID: SessionMessage.ID
readonly attempt: number
readonly at: number
readonly error: { readonly type: string; readonly message: string; readonly status?: number | undefined }
readonly error: {
readonly type: string
readonly message: string
readonly status?: number | undefined
readonly response?: { readonly body: string } | undefined
}
}
}
| {
@@ -1238,7 +1258,12 @@ export type SessionLogOutput =
readonly data: {
readonly sessionID: Session.ID
readonly reason: "auto" | "manual"
readonly error: { readonly type: string; readonly message: string; readonly status?: number | undefined }
readonly error: {
readonly type: string
readonly message: string
readonly status?: number | undefined
readonly response?: { readonly body: string } | undefined
}
readonly inputID?: SessionMessage.ID | undefined
readonly cost?: (number & Brand.Brand<"Money.USD">) | undefined
readonly tokens?:
@@ -1718,6 +1743,21 @@ export interface McpApi<E = never> {
readonly resource: { readonly catalog: McpResourceCatalogOperation<E> }
}
export type CredentialListOutput = ReadonlyArray<Credential.Entry>
export type CredentialListOperation<E = never> = () => Effect.Effect<CredentialListOutput, E>
export type CredentialCreateInput = {
readonly id?: Credential.ID | undefined
readonly integrationID: Integration.ID
readonly label?: string | undefined
readonly value: Credential.Value
readonly activate?: boolean | undefined
}
export type CredentialCreateOutput = Credential.Entry
export type CredentialCreateOperation<E = never> = (
input: CredentialCreateInput,
) => Effect.Effect<CredentialCreateOutput, E>
export type CredentialUpdateInput = { readonly credentialID: Credential.ID; readonly label: string }
export type CredentialUpdateOutput = void
export type CredentialUpdateOperation<E = never> = (
@@ -1737,6 +1777,8 @@ export type CredentialRemoveOperation<E = never> = (
) => Effect.Effect<CredentialRemoveOutput, E>
export interface CredentialApi<E = never> {
readonly list: CredentialListOperation<E>
readonly create: CredentialCreateOperation<E>
readonly update: CredentialUpdateOperation<E>
readonly activate: CredentialActivateOperation<E>
readonly remove: CredentialRemoveOperation<E>
@@ -153,6 +153,9 @@ import type {
McpDisconnectOutput,
McpResourceCatalogInput,
McpResourceCatalogOutput,
CredentialListOutput,
CredentialCreateInput,
CredentialCreateOutput,
CredentialUpdateInput,
CredentialUpdateOutput,
CredentialActivateInput,
@@ -1021,6 +1024,30 @@ const adaptGroupMcp = (raw: RawClient["server.mcp"]) => ({
resource: { catalog: EndpointMcpResourceCatalog(raw) },
})
const EndpointCredentialList = (raw: RawClient["server.credential"]) => () =>
preserveEffect<CredentialListOutput>()(
raw["credential.list"]({}).pipe(
Effect.mapError(mapClientError),
Effect.map((value) => value.data),
),
)
const EndpointCredentialCreate = (raw: RawClient["server.credential"]) => (input: CredentialCreateInput) =>
preserveEffect<CredentialCreateOutput>()(
raw["credential.create"]({
payload: {
id: input["id"],
integrationID: input["integrationID"],
label: input["label"],
value: input["value"],
activate: input["activate"],
},
}).pipe(
Effect.mapError(mapClientError),
Effect.map((value) => value.data),
),
)
const EndpointCredentialUpdate = (raw: RawClient["server.credential"]) => (input: CredentialUpdateInput) =>
preserveEffect<CredentialUpdateOutput>()(
raw["credential.update"]({
@@ -1042,6 +1069,8 @@ const EndpointCredentialRemove = (raw: RawClient["server.credential"]) => (input
)
const adaptGroupCredential = (raw: RawClient["server.credential"]) => ({
list: EndpointCredentialList(raw),
create: EndpointCredentialCreate(raw),
update: EndpointCredentialUpdate(raw),
activate: EndpointCredentialActivate(raw),
remove: EndpointCredentialRemove(raw),
@@ -147,6 +147,9 @@ import type {
McpDisconnectOutput,
McpResourceCatalogInput,
McpResourceCatalogOutput,
CredentialListOutput,
CredentialCreateInput,
CredentialCreateOutput,
CredentialUpdateInput,
CredentialUpdateOutput,
CredentialActivateInput,
@@ -1392,6 +1395,29 @@ export function make(options: ClientOptions) {
},
},
credential: {
list: (requestOptions?: RequestOptions) =>
request<{ readonly data: CredentialListOutput }>(
{ method: "GET", path: `/api/credential`, successStatus: 200, declaredStatuses: [400, 401], empty: false },
requestOptions,
).then((value) => value.data),
create: (input: CredentialCreateInput, requestOptions?: RequestOptions) =>
request<{ readonly data: CredentialCreateOutput }>(
{
method: "POST",
path: `/api/credential`,
body: {
id: input["id"],
integrationID: input["integrationID"],
label: input["label"],
value: input["value"],
activate: input["activate"],
},
successStatus: 200,
declaredStatuses: [400, 401, 409],
empty: false,
},
requestOptions,
).then((value) => value.data),
update: (input: CredentialUpdateInput, requestOptions?: RequestOptions) =>
request<CredentialUpdateOutput>(
{
+235 -19
View File
@@ -133,7 +133,7 @@ export type ToolTextContent = { type: "text"; text: string }
export type ToolFileContent = { type: "file"; uri: string; mime: string; name?: string | null }
export type SessionStructuredError = { type: string; message: string; status?: number }
export type SessionStructuredError = { type: string; message: string; status?: number; response?: { body: string } }
export type SessionMessageCompactionRunning = {
type: "compaction"
@@ -235,9 +235,7 @@ export type IntegrationCommandMethod = { id: string; type: "command"; label: str
export type IntegrationEnvMethod = { type: "env"; names: Array<string> }
export type ConnectionCredentialInfo = { type: "credential"; id: string; label: string; method: "key" | "oauth" }
export type ConnectionEnvInfo = { type: "env"; name: string }
export type ConnectionStatus = { status: "needs_auth"; message: string; url?: string }
export type IntegrationAttempt = {
attemptID: string
@@ -295,6 +293,15 @@ export type McpResourceTemplate = {
mimeType?: string
}
export type CredentialOAuth = {
type: "oauth"
methodID: string
refresh: string
access: string
expires: number
metadata?: { [x: string]: JsonValue }
}
export type ProjectVcs = string
export type ProjectIcon = { url?: string; override?: string; color?: string }
@@ -1469,7 +1476,15 @@ export type ModelCost = {
cache: { read: MoneyUSDPerMillionTokens; write: MoneyUSDPerMillionTokens }
}
export type ConnectionInfo = ConnectionCredentialInfo | ConnectionEnvInfo
export type ConnectionCredentialInfo = {
type: "credential"
id: string
label: string
method: "key" | "oauth"
status?: ConnectionStatus
}
export type ConnectionEnvInfo = { type: "env"; name: string; status?: ConnectionStatus }
export type McpServer = {
name: string
@@ -1650,7 +1665,7 @@ export type FormMultiselectField1 = {
default?: Array<string>
}
export type FormAnswer1 = { [x: string]: FormValue1 }
export type FormAnswer2 = { [x: string]: FormValue1 }
export type SessionStatusUpdated = {
id: string
@@ -1884,6 +1899,15 @@ export type FormField =
export type FormState = { status: "pending" } | { status: "answered"; answer: FormAnswer } | { status: "cancelled" }
export type CredentialKey = {
type: "key"
key: string
metadata?: { [x: string]: JsonValue }
configuration?: FormAnswer
}
export type ConnectionInfo = ConnectionCredentialInfo | ConnectionEnvInfo
export type FormField1 =
| FormStringField1
| FormNumberField1
@@ -1898,7 +1922,7 @@ export type FormReplied = {
metadata?: { [x: string]: any }
type: "form.replied"
location?: LocationRef
data: { id: string; sessionID: string; answer: FormAnswer1 }
data: { id: string; sessionID: string; answer: FormAnswer2 }
}
export type ReferenceInfo = {
@@ -2217,6 +2241,8 @@ export type SessionMessageAssistantTool1 = {
export type FormFields = [FormField, ...Array<FormField>]
export type CredentialValue = CredentialOAuth | CredentialKey
export type FormFields2 = [FormField1, ...Array<FormField1>]
export type SessionsResponse = { data: Array<SessionInfo>; cursor: { previous?: string | null; next?: string | null } }
@@ -2271,6 +2297,14 @@ export type IntegrationOAuthMethod = { id: string; type: "oauth"; label: string;
export type IntegrationKeyMethod = { type: "key"; label?: string; form?: FormFields }
export type CredentialEntry = {
id: string
integrationID: string
label: string
active: boolean
value: CredentialValue
}
export type FormInfo1 = { id: string; sessionID: string; title: string; metadata?: FormMetadata1; fields: FormFields2 }
export type SessionMessageInfo =
@@ -3175,7 +3209,12 @@ export type SessionImportInput = {
| {
readonly status: "error"
readonly input: { readonly [x: string]: JsonValue }
readonly error: { readonly type: string; readonly message: string; readonly status?: number }
readonly error: {
readonly type: string
readonly message: string
readonly status?: number
readonly response?: { readonly body: string }
}
readonly content?: readonly [
(
| { readonly type: "text"; readonly text: string }
@@ -3212,11 +3251,21 @@ export type SessionImportInput = {
readonly reasoning: number
readonly cache: { readonly read: number; readonly write: number }
}
readonly error?: { readonly type: string; readonly message: string; readonly status?: number }
readonly error?: {
readonly type: string
readonly message: string
readonly status?: number
readonly response?: { readonly body: string }
}
readonly retry?: {
readonly attempt: number
readonly at: number
readonly error: { readonly type: string; readonly message: string; readonly status?: number }
readonly error: {
readonly type: string
readonly message: string
readonly status?: number
readonly response?: { readonly body: string }
}
}
}
| (
@@ -3268,7 +3317,12 @@ export type SessionImportInput = {
readonly time: { readonly created: number }
readonly status: "failed"
readonly reason: "auto" | "manual"
readonly error: { readonly type: string; readonly message: string; readonly status?: number }
readonly error: {
readonly type: string
readonly message: string
readonly status?: number
readonly response?: { readonly body: string }
}
readonly cost?: number
readonly tokens?: {
readonly input: number
@@ -3492,7 +3546,12 @@ export type SessionImportInput = {
| {
readonly status: "error"
readonly input: { readonly [x: string]: JsonValue }
readonly error: { readonly type: string; readonly message: string; readonly status?: number }
readonly error: {
readonly type: string
readonly message: string
readonly status?: number
readonly response?: { readonly body: string }
}
readonly content?: readonly [
(
| { readonly type: "text"; readonly text: string }
@@ -3529,11 +3588,21 @@ export type SessionImportInput = {
readonly reasoning: number
readonly cache: { readonly read: number; readonly write: number }
}
readonly error?: { readonly type: string; readonly message: string; readonly status?: number }
readonly error?: {
readonly type: string
readonly message: string
readonly status?: number
readonly response?: { readonly body: string }
}
readonly retry?: {
readonly attempt: number
readonly at: number
readonly error: { readonly type: string; readonly message: string; readonly status?: number }
readonly error: {
readonly type: string
readonly message: string
readonly status?: number
readonly response?: { readonly body: string }
}
}
}
| (
@@ -3585,7 +3654,12 @@ export type SessionImportInput = {
readonly time: { readonly created: number }
readonly status: "failed"
readonly reason: "auto" | "manual"
readonly error: { readonly type: string; readonly message: string; readonly status?: number }
readonly error: {
readonly type: string
readonly message: string
readonly status?: number
readonly response?: { readonly body: string }
}
readonly cost?: number
readonly tokens?: {
readonly input: number
@@ -3809,7 +3883,12 @@ export type SessionImportInput = {
| {
readonly status: "error"
readonly input: { readonly [x: string]: JsonValue }
readonly error: { readonly type: string; readonly message: string; readonly status?: number }
readonly error: {
readonly type: string
readonly message: string
readonly status?: number
readonly response?: { readonly body: string }
}
readonly content?: readonly [
(
| { readonly type: "text"; readonly text: string }
@@ -3846,11 +3925,21 @@ export type SessionImportInput = {
readonly reasoning: number
readonly cache: { readonly read: number; readonly write: number }
}
readonly error?: { readonly type: string; readonly message: string; readonly status?: number }
readonly error?: {
readonly type: string
readonly message: string
readonly status?: number
readonly response?: { readonly body: string }
}
readonly retry?: {
readonly attempt: number
readonly at: number
readonly error: { readonly type: string; readonly message: string; readonly status?: number }
readonly error: {
readonly type: string
readonly message: string
readonly status?: number
readonly response?: { readonly body: string }
}
}
}
| (
@@ -3902,7 +3991,12 @@ export type SessionImportInput = {
readonly time: { readonly created: number }
readonly status: "failed"
readonly reason: "auto" | "manual"
readonly error: { readonly type: string; readonly message: string; readonly status?: number }
readonly error: {
readonly type: string
readonly message: string
readonly status?: number
readonly response?: { readonly body: string }
}
readonly cost?: number
readonly tokens?: {
readonly input: number
@@ -5682,6 +5776,128 @@ export type McpResourceCatalogInput = {
export type McpResourceCatalogOutput = { location: LocationPublicRef; data: McpResourceCatalog }
export type CredentialListOutput = { data: Array<CredentialEntry> }["data"]
export type CredentialCreateInput = {
readonly id?: {
readonly id?: string
readonly integrationID: string
readonly label?: string
readonly value:
| {
readonly type: "oauth"
readonly methodID: string
readonly refresh: string
readonly access: string
readonly expires: number
readonly metadata?: { readonly [x: string]: JsonValue }
}
| {
readonly type: "key"
readonly key: string
readonly metadata?: { readonly [x: string]: JsonValue }
readonly configuration?: {
readonly [x: string]: string | number | "Infinity" | "-Infinity" | "NaN" | boolean | ReadonlyArray<string>
}
}
readonly activate?: boolean
}["id"]
readonly integrationID: {
readonly id?: string
readonly integrationID: string
readonly label?: string
readonly value:
| {
readonly type: "oauth"
readonly methodID: string
readonly refresh: string
readonly access: string
readonly expires: number
readonly metadata?: { readonly [x: string]: JsonValue }
}
| {
readonly type: "key"
readonly key: string
readonly metadata?: { readonly [x: string]: JsonValue }
readonly configuration?: {
readonly [x: string]: string | number | "Infinity" | "-Infinity" | "NaN" | boolean | ReadonlyArray<string>
}
}
readonly activate?: boolean
}["integrationID"]
readonly label?: {
readonly id?: string
readonly integrationID: string
readonly label?: string
readonly value:
| {
readonly type: "oauth"
readonly methodID: string
readonly refresh: string
readonly access: string
readonly expires: number
readonly metadata?: { readonly [x: string]: JsonValue }
}
| {
readonly type: "key"
readonly key: string
readonly metadata?: { readonly [x: string]: JsonValue }
readonly configuration?: {
readonly [x: string]: string | number | "Infinity" | "-Infinity" | "NaN" | boolean | ReadonlyArray<string>
}
}
readonly activate?: boolean
}["label"]
readonly value: {
readonly id?: string
readonly integrationID: string
readonly label?: string
readonly value:
| {
readonly type: "oauth"
readonly methodID: string
readonly refresh: string
readonly access: string
readonly expires: number
readonly metadata?: { readonly [x: string]: JsonValue }
}
| {
readonly type: "key"
readonly key: string
readonly metadata?: { readonly [x: string]: JsonValue }
readonly configuration?: {
readonly [x: string]: string | number | "Infinity" | "-Infinity" | "NaN" | boolean | ReadonlyArray<string>
}
}
readonly activate?: boolean
}["value"]
readonly activate?: {
readonly id?: string
readonly integrationID: string
readonly label?: string
readonly value:
| {
readonly type: "oauth"
readonly methodID: string
readonly refresh: string
readonly access: string
readonly expires: number
readonly metadata?: { readonly [x: string]: JsonValue }
}
| {
readonly type: "key"
readonly key: string
readonly metadata?: { readonly [x: string]: JsonValue }
readonly configuration?: {
readonly [x: string]: string | number | "Infinity" | "-Infinity" | "NaN" | boolean | ReadonlyArray<string>
}
}
readonly activate?: boolean
}["activate"]
}
export type CredentialCreateOutput = { data: CredentialEntry }["data"]
export type CredentialUpdateInput = {
readonly credentialID: { readonly credentialID: string }["credentialID"]
readonly label: { readonly label: string }["label"]

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