From 80a3582370758f4dcb2134a338a9c75bdaf01b44 Mon Sep 17 00:00:00 2001 From: Ammar Date: Wed, 5 Aug 2026 10:47:02 -0500 Subject: [PATCH] =?UTF-8?q?=F0=9F=A4=96=20feat:=20add=20Grok=204.5=20prior?= =?UTF-8?q?ity=20processing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- _Generated with `mux` • Model: `openai:gpt-5.6-sol` • Thinking: `high` • Cost: `$152.19`_ --- .github/workflows/pr.yml | 1 + bun.lock | 134 ++------- docs/config/models.mdx | 3 +- flake.nix | 9 +- package.json | 8 +- patches/@ai-sdk%2Fxai@4.0.28.patch | 30 +++ scripts/check-bench-agent.sh | 1 + src/browser/App.tsx | 38 ++- .../ModelSelector/modelFilter.test.ts | 1 - .../ThinkingSelector/ThinkingSelector.tsx | 33 ++- .../Sections/ProvidersSection.stories.tsx | 27 ++ .../Sections/ProvidersSection.test.tsx | 5 + .../Settings/Sections/ProvidersSection.tsx | 73 ++++- .../Settings/Sections/settingsStoryUtils.tsx | 2 + src/browser/utils/commands/sources.test.ts | 38 ++- src/browser/utils/commands/sources.ts | 6 +- src/browser/utils/fastModeServiceTier.test.ts | 49 +++- src/browser/utils/fastModeServiceTier.ts | 60 ++++- .../config/schemas/providersConfig.test.ts | 16 ++ src/common/config/schemas/providersConfig.ts | 5 + src/common/constants/knownModels.test.ts | 10 + src/common/constants/knownModels.ts | 16 +- src/common/orpc/schemas/api.ts | 2 +- src/common/schemas/providerOptions.ts | 10 +- src/common/types/thinking.ts | 9 + src/common/utils/ai/providerOptions.test.ts | 26 ++ src/common/utils/ai/providerOptions.ts | 24 +- src/common/utils/thinking/policy.test.ts | 9 + src/common/utils/thinking/policy.ts | 7 + src/common/utils/tokens/displayUsage.test.ts | 22 ++ src/common/utils/tokens/displayUsage.ts | 21 ++ src/common/utils/tokens/models-extra.ts | 20 ++ src/common/utils/tokens/usageHelpers.test.ts | 9 + src/common/utils/tokens/usageHelpers.ts | 31 ++- .../utils/tools/toolDefinitions.test.ts | 9 + src/common/utils/tools/toolDefinitions.ts | 3 + src/common/utils/tools/tools.test.ts | 255 ++++++++++++++++++ src/common/utils/tools/tools.ts | 199 ++++++++++++++ .../builtInSkillContent.generated.ts | 3 +- src/node/services/aiService.test.ts | 4 +- src/node/services/aiService.ts | 26 +- .../services/providerModelFactory.test.ts | 141 ++++++++++ src/node/services/providerModelFactory.ts | 60 ++++- src/node/services/providerService.test.ts | 24 ++ src/node/services/providerService.ts | 20 +- src/node/services/streamManager.ts | 32 ++- tests/ipc/providers/xaiGrok45.test.ts | 75 ++++++ tests/ui/agents/thinkingSelector.test.ts | 34 ++- tests/ui/compaction/contextExceeded.test.ts | 14 +- tests/ui/harness/createAppHarness.ts | 4 + 50 files changed, 1435 insertions(+), 223 deletions(-) create mode 100644 patches/@ai-sdk%2Fxai@4.0.28.patch create mode 100644 tests/ipc/providers/xaiGrok45.test.ts diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 0014a7f485..e0d0e1d364 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -277,6 +277,7 @@ jobs: fi env: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + XAI_API_KEY: ${{ secrets.XAI_API_KEY }} ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} BACKEND: ${{ needs.changes.outputs.backend }} BROWSER: ${{ needs.changes.outputs.browser }} diff --git a/bun.lock b/bun.lock index fee9f910bf..325911bccb 100644 --- a/bun.lock +++ b/bun.lock @@ -1,5 +1,6 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "mux", @@ -14,7 +15,7 @@ "@ai-sdk/moonshotai": "^3.0.15", "@ai-sdk/openai": "^4.0.11", "@ai-sdk/openai-compatible": "^3.0.7", - "@ai-sdk/xai": "^4.0.10", + "@ai-sdk/xai": "^4.0.28", "@aws-sdk/credential-providers": "^3.940.0", "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", @@ -210,6 +211,9 @@ "cpu-features", "unrs-resolver", ], + "patchedDependencies": { + "@ai-sdk/xai@4.0.28": "patches/@ai-sdk%2Fxai@4.0.28.patch", + }, "packages": { "7zip-bin": ["7zip-bin@5.2.0", "", {}, "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A=="], @@ -245,7 +249,7 @@ "@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@5.0.7", "", { "dependencies": { "@ai-sdk/provider": "4.0.3", "@standard-schema/spec": "^1.1.0", "@workflow/serde": "4.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-OSm5/5kdrHa11WIOo5LYgDKnxYWp5aB/wx5EXRHi0jpUGduMDeB6oht9U6p+UNNWIP3F/EqPpV8d7vdP/iRnqg=="], - "@ai-sdk/xai": ["@ai-sdk/xai@4.0.10", "", { "dependencies": { "@ai-sdk/openai-compatible": "3.0.7", "@ai-sdk/provider": "4.0.3", "@ai-sdk/provider-utils": "5.0.7" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-sM0gd/l9xaHMMsZtWVu3GBbPqLCcV3WW4fAnfH2GNLAoe/wVmxf+mynLY1wXtmFmQEFVfeX093kyJBRjkfbX2Q=="], + "@ai-sdk/xai": ["@ai-sdk/xai@4.0.28", "", { "dependencies": { "@ai-sdk/openai-compatible": "3.0.23", "@ai-sdk/provider": "4.0.5", "@ai-sdk/provider-utils": "5.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-JRXKwOqNqnEonwY0ldWIjKyyUO9PVgGUdaCOC3Qq+rWE4RqLFHsHjpYl5qGSh4tjP6ELFA9LhTFI2oj3ECgSAA=="], "@antfu/install-pkg": ["@antfu/install-pkg@1.1.0", "", { "dependencies": { "package-manager-detector": "^1.3.0", "tinyexec": "^1.0.1" } }, "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ=="], @@ -1455,7 +1459,7 @@ "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], - "@types/node": ["@types/node@24.12.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g=="], + "@types/node": ["@types/node@20.19.25", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ=="], "@types/plist": ["@types/plist@3.0.5", "", { "dependencies": { "@types/node": "*", "xmlbuilder": ">=11.0.1" } }, "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA=="], @@ -3619,7 +3623,7 @@ "undici": ["undici@7.16.0", "", {}, "sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g=="], - "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], "unicode-canonical-property-names-ecmascript": ["unicode-canonical-property-names-ecmascript@2.0.1", "", {}, "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg=="], @@ -3795,6 +3799,12 @@ "@ai-sdk/moonshotai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@5.0.11", "", { "dependencies": { "@ai-sdk/provider": "4.0.3", "@standard-schema/spec": "^1.1.0", "@workflow/serde": "4.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-7/96wE+ZsKB35iS9ASyllrE4Ym/EolXEB7AkuJ5FI++fmS85BVTAs77890C+1Z2jwHfBKjBQSBmsliOsAh0iFQ=="], + "@ai-sdk/xai/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@3.0.23", "", { "dependencies": { "@ai-sdk/provider": "4.0.5", "@ai-sdk/provider-utils": "5.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-rrEABAjpaFPqRV+2JLkgb70r81+yLHdi7ir99lPir8G9tSXWkHjcuTWaKDhkimG/9W1hqsHtkomgPOqK4mC2hQ=="], + + "@ai-sdk/xai/@ai-sdk/provider": ["@ai-sdk/provider@4.0.5", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-9WAerTaPU2jcVi8dh8x27tySEuLur1Kfg7Go74GuCmsC/2MDSvrTkrK8ZrXWjryEevKfcPPmO1enveE7eqlzoA=="], + + "@ai-sdk/xai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@5.0.21", "", { "dependencies": { "@ai-sdk/provider": "4.0.5", "@standard-schema/spec": "^1.1.0", "@workflow/serde": "4.1.0", "eventsource-parser": "^3.0.8", "undici": "^7.28.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Q4z27c5vqKuXZN65QuF7FVm+uvm3qxEioiQ+8c3s5xXlhbE1Y/SLGBB4BuF+UWia4KaDu2HmNpdR1RGW02/eGg=="], + "@aws-crypto/sha256-browser/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], "@aws-crypto/util/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], @@ -3857,26 +3867,14 @@ "@istanbuljs/load-nyc-config/js-yaml": ["js-yaml@3.14.2", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg=="], - "@jest/console/@types/node": ["@types/node@20.19.25", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ=="], - "@jest/console/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - "@jest/core/@types/node": ["@types/node@20.19.25", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ=="], - "@jest/core/ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="], "@jest/core/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "@jest/core/ci-info": ["ci-info@4.3.1", "", {}, "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA=="], - "@jest/environment/@types/node": ["@types/node@20.19.25", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ=="], - - "@jest/fake-timers/@types/node": ["@types/node@20.19.25", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ=="], - - "@jest/pattern/@types/node": ["@types/node@20.19.25", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ=="], - - "@jest/reporters/@types/node": ["@types/node@20.19.25", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ=="], - "@jest/reporters/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "@jest/reporters/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], @@ -3893,8 +3891,6 @@ "@jest/transform/write-file-atomic": ["write-file-atomic@5.0.1", "", { "dependencies": { "imurmurhash": "^0.1.4", "signal-exit": "^4.0.1" } }, "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw=="], - "@jest/types/@types/node": ["@types/node@20.19.25", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ=="], - "@jest/types/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "@malept/flatpak-bundler/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="], @@ -3999,18 +3995,18 @@ "@types/body-parser/@types/node": ["@types/node@22.19.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ=="], - "@types/connect/@types/node": ["@types/node@22.19.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ=="], + "@types/cacheable-request/@types/node": ["@types/node@24.12.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g=="], - "@types/cors/@types/node": ["@types/node@20.19.25", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ=="], + "@types/connect/@types/node": ["@types/node@22.19.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ=="], "@types/express-serve-static-core/@types/node": ["@types/node@22.19.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ=="], - "@types/fs-extra/@types/node": ["@types/node@20.19.25", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ=="], - - "@types/jsdom/@types/node": ["@types/node@20.19.25", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ=="], + "@types/keyv/@types/node": ["@types/node@24.12.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g=="], "@types/plist/@types/node": ["@types/node@22.19.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ=="], + "@types/responselike/@types/node": ["@types/node@24.12.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g=="], + "@types/send/@types/node": ["@types/node@22.19.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ=="], "@types/serve-static/@types/node": ["@types/node@22.19.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ=="], @@ -4021,10 +4017,6 @@ "@types/wait-on/@types/node": ["@types/node@22.19.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ=="], - "@types/write-file-atomic/@types/node": ["@types/node@20.19.25", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ=="], - - "@types/ws/@types/node": ["@types/node@20.19.25", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ=="], - "@types/yauzl/@types/node": ["@types/node@22.19.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ=="], "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], @@ -4061,8 +4053,6 @@ "builder-util/https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="], - "bun-types/@types/node": ["@types/node@20.19.25", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ=="], - "caching-transform/write-file-atomic": ["write-file-atomic@3.0.3", "", { "dependencies": { "imurmurhash": "^0.1.4", "is-typedarray": "^1.0.0", "signal-exit": "^3.0.2", "typedarray-to-buffer": "^3.1.5" } }, "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q=="], "chokidar/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], @@ -4101,6 +4091,8 @@ "dom-serializer/entities": ["entities@2.2.0", "", {}, "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A=="], + "electron/@types/node": ["@types/node@24.12.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g=="], + "electron-builder/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "electron-publish/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], @@ -4149,8 +4141,6 @@ "globby/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], - "happy-dom/@types/node": ["@types/node@20.19.25", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ=="], - "hasha/type-fest": ["type-fest@0.8.1", "", {}, "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA=="], "hast-util-to-parse5/property-information": ["property-information@6.5.0", "", {}, "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig=="], @@ -4191,8 +4181,6 @@ "jest-environment-node/@types/node": ["@types/node@22.19.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ=="], - "jest-haste-map/@types/node": ["@types/node@20.19.25", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ=="], - "jest-haste-map/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], "jest-junit/mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="], @@ -4203,8 +4191,6 @@ "jest-message-util/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - "jest-mock/@types/node": ["@types/node@20.19.25", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ=="], - "jest-process-manager/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "jest-process-manager/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], @@ -4217,8 +4203,6 @@ "jest-runner/source-map-support": ["source-map-support@0.5.13", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w=="], - "jest-runtime/@types/node": ["@types/node@20.19.25", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ=="], - "jest-runtime/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "jest-runtime/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], @@ -4227,8 +4211,6 @@ "jest-snapshot/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - "jest-util/@types/node": ["@types/node@20.19.25", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ=="], - "jest-util/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "jest-util/ci-info": ["ci-info@4.3.1", "", {}, "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA=="], @@ -4237,16 +4219,12 @@ "jest-watch-typeahead/slash": ["slash@5.1.0", "", {}, "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg=="], - "jest-watcher/@types/node": ["@types/node@20.19.25", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ=="], - "jest-watcher/ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="], "jest-watcher/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "jest-watcher/string-length": ["string-length@4.0.2", "", { "dependencies": { "char-regex": "^1.0.2", "strip-ansi": "^6.0.0" } }, "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ=="], - "jest-worker/@types/node": ["@types/node@20.19.25", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ=="], - "jsdom/parse5": ["parse5@8.0.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA=="], "jsdom/whatwg-mimetype": ["whatwg-mimetype@4.0.0", "", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="], @@ -4389,6 +4367,8 @@ "zip-stream/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + "@ai-sdk/xai/@ai-sdk/provider-utils/undici": ["undici@7.29.0", "", {}, "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw=="], + "@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], "@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], @@ -4411,28 +4391,16 @@ "@istanbuljs/load-nyc-config/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], - "@jest/console/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - "@jest/console/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "@jest/console/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - "@jest/core/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - "@jest/core/ansi-escapes/type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="], "@jest/core/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "@jest/core/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - "@jest/environment/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - - "@jest/fake-timers/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - - "@jest/pattern/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - - "@jest/reporters/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - "@jest/reporters/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "@jest/reporters/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], @@ -4455,8 +4423,6 @@ "@jest/transform/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - "@jest/types/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - "@jest/types/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "@jest/types/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], @@ -4507,38 +4473,14 @@ "@testing-library/jest-dom/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - "@types/asn1/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - - "@types/body-parser/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - - "@types/connect/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - - "@types/cors/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + "@types/cacheable-request/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], - "@types/express-serve-static-core/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + "@types/keyv/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], - "@types/fs-extra/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - - "@types/jsdom/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - - "@types/plist/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - - "@types/send/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - - "@types/serve-static/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + "@types/responselike/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], "@types/ssh2/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], - "@types/sshpk/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - - "@types/wait-on/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - - "@types/write-file-atomic/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - - "@types/ws/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - - "@types/yauzl/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], "@vitest/expect/@vitest/utils/@vitest/pretty-format": ["@vitest/pretty-format@2.0.5", "", { "dependencies": { "tinyrainbow": "^1.2.0" } }, "sha512-h8k+1oWHfwTkyTkb9egzwNMfJAEx4veaPSnMeKbVSjp4euqGSbQlm5+6VHwTr7u4FJslVVsUG5nopCaAYdOmSQ=="], @@ -4573,8 +4515,6 @@ "builder-util/https-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], - "bun-types/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - "caching-transform/write-file-atomic/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], "cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], @@ -4613,6 +4553,8 @@ "electron-publish/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + "electron/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + "eslint/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], "eslint/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], @@ -4633,14 +4575,10 @@ "global-prefix/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], - "happy-dom/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - "hosted-git-info/lru-cache/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], "htmlparser2/readable-stream/string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], - "jest-circus/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - "jest-circus/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "jest-circus/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], @@ -4669,10 +4607,6 @@ "jest-each/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - "jest-environment-node/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - - "jest-haste-map/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - "jest-junit/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "jest-matcher-utils/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], @@ -4683,8 +4617,6 @@ "jest-message-util/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - "jest-mock/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - "jest-process-manager/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "jest-process-manager/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], @@ -4693,14 +4625,10 @@ "jest-resolve/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - "jest-runner/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - "jest-runner/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "jest-runner/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - "jest-runtime/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - "jest-runtime/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "jest-runtime/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], @@ -4717,8 +4645,6 @@ "jest-snapshot/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - "jest-util/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - "jest-util/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "jest-util/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], @@ -4727,8 +4653,6 @@ "jest-validate/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - "jest-watcher/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - "jest-watcher/ansi-escapes/type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="], "jest-watcher/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], @@ -4737,8 +4661,6 @@ "jest-watcher/string-length/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "jest-worker/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - "jsdom/parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], "mlly/pkg-types/confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="], diff --git a/docs/config/models.mdx b/docs/config/models.mdx index 722c351f7e..beb8c4b954 100644 --- a/docs/config/models.mdx +++ b/docs/config/models.mdx @@ -30,8 +30,7 @@ Mux ships with curated models kept up to date with the frontier. Use any custom | Codex Max 5.1 | openai:gpt-5.1-codex-max | `codex-max` | | | Gemini 3.1 Pro Preview | google:gemini-3.1-pro-preview | `gemini`, `gemini-pro` | | | Gemini 3.6 Flash | google:gemini-3.6-flash | `gemini-flash` | | -| Grok 4 1 Fast | xai:grok-4-1-fast | `grok`, `grok-4`, `grok-4.1`, `grok-4-1` | | -| Grok Code Fast 1 | xai:grok-code-fast-1 | `grok-code` | | +| Grok 4.5 | xai:grok-4.5 | `grok`, `grok-4.5` | | | DeepSeek V4 Pro | deepseek:deepseek-v4-pro | `deepseek`, `deepseek-pro`, `deepseek-v4`, `deepseek-v4-pro` | | | DeepSeek V4 Flash | deepseek:deepseek-v4-flash | `deepseek-flash`, `deepseek-v4-flash` | | | Kimi K3 | moonshotai:kimi-k3 | `kimi`, `k3`, `kimi-k3` | | diff --git a/flake.nix b/flake.nix index 1c0b896819..35a4468493 100644 --- a/flake.nix +++ b/flake.nix @@ -46,15 +46,16 @@ stdenv.cc.cc.lib # Provides libstdc++ for native modules like sharp ]; - # Fetch dependencies in a separate fixed-output derivation - # Use only package.json and bun.lock to ensure consistent hashing - # regardless of how the flake is evaluated (local vs remote) + # Fetch dependencies in a separate fixed-output derivation. + # Include Bun patch files alongside package.json and bun.lock so patched + # dependencies install identically in local and remote Nix evaluations. offlineCache = pkgs.stdenvNoCC.mkDerivation { name = "mux-deps-${version}"; src = pkgs.runCommand "mux-lock-files" { } '' mkdir -p $out cp ${./package.json} $out/package.json + cp -r ${./patches} $out/patches cp ${./bun.lock} $out/bun.lock ''; @@ -83,7 +84,7 @@ outputHashMode = "recursive"; # Marker used by scripts/update_flake_hash.sh to update this hash in place. - outputHash = "sha256-hK0zwN0pShMv8NlfMXud+9Jx2J32hz5+qtLMqzunZF8="; # mux-offline-cache-hash + outputHash = "sha256-ufRtgBViq/5RAGzL/f79pIYK/+3BCQNNAD1nCz/edRg="; # mux-offline-cache-hash }; configurePhase = '' diff --git a/package.json b/package.json index 6424b843cc..b526f2c2c4 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "@ai-sdk/moonshotai": "^3.0.15", "@ai-sdk/openai": "^4.0.11", "@ai-sdk/openai-compatible": "^3.0.7", - "@ai-sdk/xai": "^4.0.10", + "@ai-sdk/xai": "^4.0.28", "@aws-sdk/credential-providers": "^3.940.0", "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", @@ -252,6 +252,7 @@ "dist/**/*.json", "dist/**/*.png", "dist/assets/**/*", + "patches/**/*", "scripts/postinstall.sh", "npm-shrinkwrap.json", "README.md", @@ -331,5 +332,8 @@ "sharp", "ssh2", "unrs-resolver" - ] + ], + "patchedDependencies": { + "@ai-sdk/xai@4.0.28": "patches/@ai-sdk%2Fxai@4.0.28.patch" + } } diff --git a/patches/@ai-sdk%2Fxai@4.0.28.patch b/patches/@ai-sdk%2Fxai@4.0.28.patch new file mode 100644 index 0000000000..5bb90f2ad3 --- /dev/null +++ b/patches/@ai-sdk%2Fxai@4.0.28.patch @@ -0,0 +1,30 @@ +diff --git a/dist/index.js b/dist/index.js +index ea833a84c3323dcc72534e076d68473dfdb77128..41568478bea4246ff9acd7943bbd4d4ee6bd28e3 100644 +--- a/dist/index.js ++++ b/dist/index.js +@@ -2096,8 +2096,8 @@ import { z as z12 } from "zod/v4"; + var xSearchArgsSchema = lazySchema4( + () => zodSchema4( + z12.object({ +- allowedXHandles: z12.array(z12.string()).max(10).optional(), +- excludedXHandles: z12.array(z12.string()).max(10).optional(), ++ allowedXHandles: z12.array(z12.string()).max(20).optional(), ++ excludedXHandles: z12.array(z12.string()).max(20).optional(), + fromDate: z12.string().optional(), + toDate: z12.string().optional(), + enableImageUnderstanding: z12.boolean().optional(), +diff --git a/src/tool/x-search.ts b/src/tool/x-search.ts +index 5566aa9c391f67cab60fe50ff2a2b55ca71b0b73..6d0dd586f07c5ea9d2bd2dcbed4be211be2e3c8a 100644 +--- a/src/tool/x-search.ts ++++ b/src/tool/x-search.ts +@@ -8,8 +8,8 @@ import { z } from 'zod/v4'; + export const xSearchArgsSchema = lazySchema(() => + zodSchema( + z.object({ +- allowedXHandles: z.array(z.string()).max(10).optional(), +- excludedXHandles: z.array(z.string()).max(10).optional(), ++ allowedXHandles: z.array(z.string()).max(20).optional(), ++ excludedXHandles: z.array(z.string()).max(20).optional(), + fromDate: z.string().optional(), + toDate: z.string().optional(), + enableImageUnderstanding: z.boolean().optional(), diff --git a/scripts/check-bench-agent.sh b/scripts/check-bench-agent.sh index 33cb18079a..3577b0ece2 100755 --- a/scripts/check-bench-agent.sh +++ b/scripts/check-bench-agent.sh @@ -95,6 +95,7 @@ trap 'rm -rf "$CHECK_DIR"' EXIT # Copy built dist and package.json — but NO lockfile, shrinkwrap, or node_modules. cp -r "$REPO_ROOT/dist" "$CHECK_DIR/dist" +cp -r "$REPO_ROOT/patches" "$CHECK_DIR/patches" cp "$REPO_ROOT/package.json" "$CHECK_DIR/package.json" # Strip devDependencies so bun only resolves production deps (matching published package). diff --git a/src/browser/App.tsx b/src/browser/App.tsx index 89c9c77454..8b1903912b 100644 --- a/src/browser/App.tsx +++ b/src/browser/App.tsx @@ -17,7 +17,7 @@ import { } from "./hooks/usePersistedState"; import { useResizableSidebar } from "./hooks/useResizableSidebar"; import { matchesKeybind, KEYBINDS } from "./utils/ui/keybinds"; -import { applyFastModeServiceTierChange } from "./utils/fastModeServiceTier"; +import { applyFastModeServiceTierChange, getFastModeProvider } from "./utils/fastModeServiceTier"; import { handleLayoutSlotHotkeys } from "./utils/ui/layoutSlotHotkeys"; import { buildSortedWorkspacesByProject } from "./utils/ui/workspaceFiltering"; import { @@ -76,7 +76,6 @@ import { LEFT_SIDEBAR_WIDTH_KEY, } from "@/common/constants/storage"; import { normalizeToCanonical } from "@/common/utils/ai/models"; -import { openaiDirectProviderOptionsAvailable } from "@/common/utils/ai/openaiProviderOptionsAvailability"; import { getDefaultModel } from "@/browser/hooks/useModelsFromSettings"; import type { BranchListResult } from "@/common/orpc/types"; import { useTelemetry } from "./hooks/useTelemetry"; @@ -680,8 +679,19 @@ function AppInner() { [api, getModelForWorkspace, getReasoningModeForWorkspace, getThinkingLevelForWorkspace] ); + const getFastModeActive = useCallback(() => { + const scopeId = selectedWorkspace?.workspaceId ?? creationScopeId; + if (!scopeId || providersConfig == null) return false; + + const model = getModelForWorkspace(scopeId); + const provider = getFastModeProvider(model, { + providersConfig, + resolvedRouteProvider: getRouteForModel(normalizeToCanonical(model)), + }); + return provider != null && providersConfig[provider]?.serviceTier === "priority"; + }, [creationScopeId, getModelForWorkspace, getRouteForModel, providersConfig, selectedWorkspace]); + const fastModeToggleInFlightRef = useRef(false); - const fastModeActive = providersConfig?.openai?.serviceTier === "priority"; const toggleFastMode = useCallback(async () => { const scopeId = selectedWorkspace?.workspaceId ?? creationScopeId; if (!api || !scopeId || providersConfig == null || fastModeToggleInFlightRef.current) return; @@ -691,25 +701,25 @@ function AppInner() { // Serialize requests so a quick double press cannot compute two writes from stale config. fastModeToggleInFlightRef.current = true; const model = getModelForWorkspace(scopeId); - const route = getRouteForModel(normalizeToCanonical(model)); - if ( - !openaiDirectProviderOptionsAvailable(model, { - providersConfig, - resolvedRouteProvider: route, - }) - ) { + const provider = getFastModeProvider(model, { + providersConfig, + resolvedRouteProvider: getRouteForModel(normalizeToCanonical(model)), + }); + if (provider == null) { fastModeToggleInFlightRef.current = false; return; } try { + const providerConfig = providersConfig[provider]; const change = await applyFastModeServiceTierChange( api.providers, - providersConfig.openai?.serviceTier, - providersConfig.openai?.fastModePreviousServiceTier + provider, + providerConfig?.serviceTier, + providerConfig?.fastModePreviousServiceTier ); if (change) { - updateOptimistically("openai", { + updateOptimistically(provider, { serviceTier: change.serviceTier, fastModePreviousServiceTier: change.previousServiceTier, }); @@ -972,7 +982,7 @@ function AppInner() { onSetThinkingLevel: setThinkingLevelFromPalette, getReasoningMode: getReasoningModeForWorkspace, onToggleReasoningMode: toggleReasoningModeFromPalette, - getFastMode: () => fastModeActive, + getFastMode: getFastModeActive, onToggleFastMode: toggleFastMode, getEffectiveComposerModel: getModelForWorkspace, providersConfig, diff --git a/src/browser/components/ModelSelector/modelFilter.test.ts b/src/browser/components/ModelSelector/modelFilter.test.ts index 7b44e85ce6..c35c056f76 100644 --- a/src/browser/components/ModelSelector/modelFilter.test.ts +++ b/src/browser/components/ModelSelector/modelFilter.test.ts @@ -10,7 +10,6 @@ describe("modelMatchesQuery", () => { test("matches documented aliases that are not model string substrings", () => { expect(modelMatchesQuery("google:gemini-3.6-flash", "gemini-flash")).toBe(true); expect(modelMatchesQuery("deepseek:deepseek-v4-pro", "deepseek-pro")).toBe(true); - expect(modelMatchesQuery("xai:grok-4-1-fast", "grok-4.1")).toBe(true); }); test("matches aliases for gateway-prefixed model strings", () => { diff --git a/src/browser/components/ThinkingSelector/ThinkingSelector.tsx b/src/browser/components/ThinkingSelector/ThinkingSelector.tsx index 4c44befbef..574235a1ae 100644 --- a/src/browser/components/ThinkingSelector/ThinkingSelector.tsx +++ b/src/browser/components/ThinkingSelector/ThinkingSelector.tsx @@ -8,11 +8,13 @@ import { useReasoningMode } from "@/browser/hooks/useReasoningMode"; import { useRouting } from "@/browser/hooks/useRouting"; import { useThinkingLevel } from "@/browser/hooks/useThinkingLevel"; import { stopKeyboardPropagation } from "@/browser/utils/events"; -import { applyFastModeServiceTierChange } from "@/browser/utils/fastModeServiceTier"; +import { + applyFastModeServiceTierChange, + getFastModeProvider, +} from "@/browser/utils/fastModeServiceTier"; import { formatKeybind, KEYBINDS } from "@/browser/utils/ui/keybinds"; import { cn } from "@/common/lib/utils"; import { getThinkingDisplayLabel, type ThinkingLevel } from "@/common/types/thinking"; -import { openaiDirectProviderOptionsAvailable } from "@/common/utils/ai/openaiProviderOptionsAvailability"; import { openaiProModeAvailable } from "@/common/utils/ai/proMode"; import { normalizeToCanonical } from "@/common/utils/ai/models"; import { enforceThinkingPolicy, getAvailableThinkingLevels } from "@/common/utils/thinking/policy"; @@ -60,20 +62,23 @@ export const ThinkingSelector: React.FC = (props) => { ); const displayLabel = getThinkingDisplayLabel(effectiveThinkingLevel, props.modelString); const resolvedRoute = routing.resolveRoute(normalizeToCanonical(props.modelString)).route; - const directOpenAIOptionsAvailable = openaiDirectProviderOptionsAvailable(props.modelString, { - providersConfig, - resolvedRouteProvider: resolvedRoute, - }); const proModeAvailable = props.allowProMode !== false && openaiProModeAvailable(props.modelString, { providersConfig, resolvedRouteProvider: resolvedRoute, }); - const fastModeAvailable = - props.allowFastMode !== false && providersConfig != null && directOpenAIOptionsAvailable; + const fastModeProvider = + props.allowFastMode !== false && providersConfig != null + ? getFastModeProvider(props.modelString, { + providersConfig, + resolvedRouteProvider: resolvedRoute, + }) + : null; + const fastModeAvailable = fastModeProvider != null; const proModeActive = proModeAvailable && reasoningMode === "pro"; - const fastModeActive = fastModeAvailable && providersConfig?.openai?.serviceTier === "priority"; + const fastModeActive = + fastModeProvider != null && providersConfig?.[fastModeProvider]?.serviceTier === "priority"; const hasMenu = allowed.length > 1 || proModeAvailable || fastModeAvailable; // The menu is rendered inline for happy-dom coverage, so a document listener is the @@ -92,17 +97,19 @@ export const ThinkingSelector: React.FC = (props) => { }, [isOpen]); const handleFastModeToggle = async () => { - if (!api || fastModeSaving || providersConfig == null) return; + if (!api || fastModeSaving || providersConfig == null || fastModeProvider == null) return; setFastModeSaving(true); try { + const providerConfig = providersConfig[fastModeProvider]; const change = await applyFastModeServiceTierChange( api.providers, - providersConfig.openai?.serviceTier, - providersConfig.openai?.fastModePreviousServiceTier + fastModeProvider, + providerConfig?.serviceTier, + providerConfig?.fastModePreviousServiceTier ); if (change) { - updateOptimistically("openai", { + updateOptimistically(fastModeProvider, { serviceTier: change.serviceTier, fastModePreviousServiceTier: change.previousServiceTier, }); diff --git a/src/browser/features/Settings/Sections/ProvidersSection.stories.tsx b/src/browser/features/Settings/Sections/ProvidersSection.stories.tsx index 9b0a4775d7..6f9e7c9c25 100644 --- a/src/browser/features/Settings/Sections/ProvidersSection.stories.tsx +++ b/src/browser/features/Settings/Sections/ProvidersSection.stories.tsx @@ -103,6 +103,33 @@ export const ProvidersEnvSourced: Story = { }, }; +export const XAIProcessingMode: Story = { + render: () => ( + + setupSettingsStory({ + providersConfig: { + xai: { + apiKeySet: true, + isEnabled: true, + isConfigured: true, + serviceTier: "priority", + }, + }, + }) + } + > + + + ), + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const xaiButton = await canvas.findByRole("button", { name: /xAI/i }); + await userEvent.click(xaiButton); + await canvas.findByText("fast (priority)"); + }, +}; + export const ProvidersExpanded: Story = { render: () => ( (null); + const [xaiServiceTierSaving, setXAIServiceTierSaving] = useState(false); const routing = useRouting(); @@ -2660,6 +2665,72 @@ export function ProvidersSection() { ); })()} + {provider === "xai" && ( +
+
+ + + + + + ? + + + +
+
xAI processing mode
+
+ standard: normal + scheduling and token pricing. +
+
+ fast: priority + scheduling for lower latency at 2× token pricing. +
+
+
+
+
+
+ +
+ )} + {isCustomOpenAICompatible && (
diff --git a/src/browser/features/Settings/Sections/settingsStoryUtils.tsx b/src/browser/features/Settings/Sections/settingsStoryUtils.tsx index 779bb7172a..c53c6415f2 100644 --- a/src/browser/features/Settings/Sections/settingsStoryUtils.tsx +++ b/src/browser/features/Settings/Sections/settingsStoryUtils.tsx @@ -24,6 +24,7 @@ import { SIDEBAR_AGE_GROUPING_KEY, UI_THEME_KEY, } from "@/common/constants/storage"; +import type { ServiceTier } from "@/common/config/schemas/providersConfig"; import type { ServerAuthSession } from "@/common/orpc/types"; import type { AgentAiDefaults } from "@/common/types/agentAiDefaults"; import type { ProjectConfig } from "@/common/types/project"; @@ -133,6 +134,7 @@ interface SetupSettingsStoryOptions { baseUrl?: string; baseUrlSource?: "config" | "env"; baseUrlResolved?: string; + serviceTier?: ServiceTier; models?: string[]; } >; diff --git a/src/browser/utils/commands/sources.test.ts b/src/browser/utils/commands/sources.test.ts index e0b115f772..0ff2b9e5fb 100644 --- a/src/browser/utils/commands/sources.test.ts +++ b/src/browser/utils/commands/sources.test.ts @@ -1100,6 +1100,26 @@ test("fast mode command is route-aware and keyboard accessible", async () => { await fastAction?.run(); expect(onToggleFastMode).toHaveBeenCalledTimes(1); + const directXAIActions = getActions({ + selectedWorkspaceState: { + lifecycle: "active", + goal: null, + currentModel: "xai:grok-4.5", + } as unknown as WorkspaceState, + getEffectiveComposerModel: () => "xai:grok-4.5", + providersConfig: { + xai: { apiKeySet: true, isEnabled: true, isConfigured: true }, + }, + getRouteForModel: () => "direct", + onToggleFastMode, + }); + const xaiFastAction = directXAIActions.find( + (action) => action.id === "thinking:toggle-fast-mode" + ); + expect(xaiFastAction?.shortcutHint).toBeDefined(); + await xaiFastAction?.run(); + expect(onToggleFastMode).toHaveBeenCalledTimes(2); + const creationScopeId = "project:/repo/a"; const creationActions = getActions({ selectedWorkspace: null, @@ -1118,7 +1138,7 @@ test("fast mode command is route-aware and keyboard accessible", async () => { ); expect(creationFastAction?.shortcutHint).toBeDefined(); await creationFastAction?.run(); - expect(onToggleFastMode).toHaveBeenCalledTimes(2); + expect(onToggleFastMode).toHaveBeenCalledTimes(3); const staleOpenAIActions = getActions({ selectedWorkspaceState: { @@ -1136,6 +1156,22 @@ test("fast mode command is route-aware and keyboard accessible", async () => { false ); + const gatewayXAIActions = getActions({ + selectedWorkspaceState: { + lifecycle: "active", + goal: null, + currentModel: "xai:grok-4.5", + } as unknown as WorkspaceState, + getEffectiveComposerModel: () => "xai:grok-4.5", + providersConfig: { + xai: { apiKeySet: true, isEnabled: true, isConfigured: true }, + }, + getRouteForModel: () => "openrouter", + }); + expect(gatewayXAIActions.some((action) => action.id === "thinking:toggle-fast-mode")).toBe( + false + ); + const unloadedActions = getActions({ selectedWorkspaceState: { lifecycle: "active", diff --git a/src/browser/utils/commands/sources.ts b/src/browser/utils/commands/sources.ts index 4bc1b1c0c3..bf743aee8d 100644 --- a/src/browser/utils/commands/sources.ts +++ b/src/browser/utils/commands/sources.ts @@ -12,7 +12,7 @@ import { } from "@/common/types/thinking"; import type { ProvidersConfigMap } from "@/common/orpc/types"; import { normalizeToCanonical } from "@/common/utils/ai/models"; -import { openaiDirectProviderOptionsAvailable } from "@/common/utils/ai/openaiProviderOptionsAvailability"; +import { getFastModeProvider } from "@/browser/utils/fastModeServiceTier"; import { openaiProModeAvailable } from "@/common/utils/ai/proMode"; import { enforceThinkingPolicy, @@ -1238,10 +1238,10 @@ export function buildCoreSources(p: BuildSourcesParams): Array<() => CommandActi : undefined; const fastModeAction: CommandAction | null = p.providersConfig != null && - openaiDirectProviderOptionsAvailable(providerOptionGateModel ?? "", { + getFastModeProvider(providerOptionGateModel ?? "", { providersConfig: p.providersConfig, resolvedRouteProvider: providerOptionRoute, - }) + }) != null ? { id: CommandIds.toggleFastMode(), title: "Toggle Fast Mode", diff --git a/src/browser/utils/fastModeServiceTier.test.ts b/src/browser/utils/fastModeServiceTier.test.ts index c589011d13..e9b63f5db5 100644 --- a/src/browser/utils/fastModeServiceTier.test.ts +++ b/src/browser/utils/fastModeServiceTier.test.ts @@ -3,6 +3,7 @@ import { describe, expect, mock, test } from "bun:test"; import type { APIClient } from "@/browser/contexts/API"; import { applyFastModeServiceTierChange, + getFastModeProvider, getFastModeServiceTierChange, } from "./fastModeServiceTier"; @@ -19,8 +20,20 @@ function createWriter() { } describe("fast mode service tier", () => { + test("resolves direct native providers and rejects gateway routes", () => { + expect(getFastModeProvider("openai:gpt-5.6-sol", { resolvedRouteProvider: "direct" })).toBe( + "openai" + ); + expect(getFastModeProvider("xai:grok-4.5", { resolvedRouteProvider: "direct" })).toBe("xai"); + expect(getFastModeProvider("xai:grok-4.5", { resolvedRouteProvider: "openrouter" })).toBeNull(); + expect( + getFastModeProvider("xai:grok-code-fast-1", { resolvedRouteProvider: "direct" }) + ).toBeNull(); + expect(getFastModeProvider("anthropic:claude-sonnet-4-5")).toBeNull(); + }); + test("restores flex from the shared provider config", () => { - expect(getFastModeServiceTierChange("priority", "flex")).toEqual({ + expect(getFastModeServiceTierChange("openai", "priority", "flex")).toEqual({ apiValue: "flex", serviceTier: "flex", previousServiceTier: undefined, @@ -30,7 +43,7 @@ describe("fast mode service tier", () => { test("persists the restore tier before enabling Fast mode", async () => { const { providers, setProviderConfig } = createWriter(); - const change = await applyFastModeServiceTierChange(providers, "flex"); + const change = await applyFastModeServiceTierChange(providers, "openai", "flex"); expect(change).toEqual({ apiValue: "priority", @@ -58,7 +71,7 @@ describe("fast mode service tier", () => { test("restores and clears the shared tier when disabling Fast mode", async () => { const { providers, setProviderConfig } = createWriter(); - const change = await applyFastModeServiceTierChange(providers, "priority", "default"); + const change = await applyFastModeServiceTierChange(providers, "openai", "priority", "default"); expect(change).toEqual({ apiValue: "default", @@ -86,7 +99,7 @@ describe("fast mode service tier", () => { test("restores an unset tier with the backend removal value", async () => { const { providers, setProviderConfig } = createWriter(); - const change = await applyFastModeServiceTierChange(providers, "priority", "unset"); + const change = await applyFastModeServiceTierChange(providers, "openai", "priority", "unset"); expect(change?.serviceTier).toBeUndefined(); expect(setProviderConfig.mock.calls[0]).toEqual([ @@ -98,7 +111,31 @@ describe("fast mode service tier", () => { ]); }); - test("falls back to auto for legacy priority config without a restore tier", () => { - expect(getFastModeServiceTierChange("priority").serviceTier).toBe("auto"); + test("uses provider-valid fallbacks for legacy priority config without a restore tier", () => { + expect(getFastModeServiceTierChange("openai", "priority").serviceTier).toBe("auto"); + expect(getFastModeServiceTierChange("xai", "priority").serviceTier).toBe("default"); + }); + + test("writes xAI fast mode to the xAI provider config", async () => { + const { providers, setProviderConfig } = createWriter(); + + await applyFastModeServiceTierChange(providers, "xai", "default"); + + expect(setProviderConfig.mock.calls).toEqual([ + [ + { + provider: "xai", + keyPath: ["fastModePreviousServiceTier"], + value: "default", + }, + ], + [ + { + provider: "xai", + keyPath: ["serviceTier"], + value: "priority", + }, + ], + ]); }); }); diff --git a/src/browser/utils/fastModeServiceTier.ts b/src/browser/utils/fastModeServiceTier.ts index 89c26fb1ff..648d9f2e7e 100644 --- a/src/browser/utils/fastModeServiceTier.ts +++ b/src/browser/utils/fastModeServiceTier.ts @@ -3,6 +3,13 @@ import type { FastModePreviousServiceTier, ServiceTier, } from "@/common/config/schemas/providersConfig"; +import { PROVIDER_DEFINITIONS } from "@/common/constants/providers"; +import type { ProvidersConfigMap } from "@/common/orpc/types"; +import { isGrok45Model } from "@/common/types/thinking"; +import { getExplicitGatewayPrefix, normalizeToCanonical } from "@/common/utils/ai/models"; +import { openaiDirectProviderOptionsAvailable } from "@/common/utils/ai/openaiProviderOptionsAvailability"; + +export type FastModeProvider = "openai" | "xai"; export interface FastModeServiceTierChange { apiValue: ServiceTier | ""; @@ -10,13 +17,51 @@ export interface FastModeServiceTierChange { previousServiceTier: FastModePreviousServiceTier | undefined; } +export interface FastModeAvailabilityOptions { + resolvedRouteProvider?: string | null; + providersConfig?: ProvidersConfigMap | null; +} + type ProviderConfigWriter = Pick; +/** Return the direct provider whose priority tier powers Fast mode for this model. */ +export function getFastModeProvider( + modelString: string, + options?: FastModeAvailabilityOptions +): FastModeProvider | null { + if (openaiDirectProviderOptionsAvailable(modelString, options)) { + return "openai"; + } + + const normalized = normalizeToCanonical(modelString); + const [origin] = normalized.split(":", 2); + if (origin !== "xai" || !isGrok45Model(normalized)) return null; + + // xAI service_tier is also provider-native and cannot survive a gateway route. + const explicitGateway = getExplicitGatewayPrefix(modelString); + if (explicitGateway != null) { + const gatewayConfig = options?.providersConfig?.[explicitGateway]; + const gatewayDefinition = PROVIDER_DEFINITIONS[explicitGateway]; + const gatewayWinsRoute = + options?.providersConfig == null || + (gatewayConfig?.isConfigured === true && + gatewayConfig.isEnabled !== false && + gatewayDefinition.kind === "gateway" && + (gatewayDefinition.routes as readonly string[]).includes("xai")); + if (gatewayWinsRoute) return null; + } + + return options?.resolvedRouteProvider == null || options.resolvedRouteProvider === "direct" + ? "xai" + : null; +} + /** * Fast mode is a temporary priority-tier override. The restore target lives in * providers.jsonc so every browser origin and desktop client observes the same state. */ export function getFastModeServiceTierChange( + provider: FastModeProvider, currentServiceTier: ServiceTier | undefined, previousServiceTier?: FastModePreviousServiceTier ): FastModeServiceTierChange { @@ -28,7 +73,9 @@ export function getFastModeServiceTierChange( }; } - const restoreServiceTier = previousServiceTier ?? "auto"; + // Legacy OpenAI priority configs predate the restore field. xAI's only standard + // tier is default, so its equivalent fallback must not emit unsupported "auto". + const restoreServiceTier = previousServiceTier ?? (provider === "openai" ? "auto" : "default"); return { apiValue: restoreServiceTier === "unset" ? "" : restoreServiceTier, serviceTier: restoreServiceTier === "unset" ? undefined : restoreServiceTier, @@ -36,17 +83,18 @@ export function getFastModeServiceTierChange( }; } -/** Persist the shared restore target and service-tier override in a safe order. */ +/** Persist the provider-specific restore target and service-tier override in a safe order. */ export async function applyFastModeServiceTierChange( providers: ProviderConfigWriter, + provider: FastModeProvider, currentServiceTier: ServiceTier | undefined, previousServiceTier?: FastModePreviousServiceTier ): Promise { - const change = getFastModeServiceTierChange(currentServiceTier, previousServiceTier); + const change = getFastModeServiceTierChange(provider, currentServiceTier, previousServiceTier); if (currentServiceTier !== "priority") { const rememberResult = await providers.setProviderConfig({ - provider: "openai", + provider, keyPath: ["fastModePreviousServiceTier"], value: change.previousServiceTier ?? "unset", }); @@ -54,7 +102,7 @@ export async function applyFastModeServiceTierChange( } const tierResult = await providers.setProviderConfig({ - provider: "openai", + provider, keyPath: ["serviceTier"], value: change.apiValue, }); @@ -62,7 +110,7 @@ export async function applyFastModeServiceTierChange( if (currentServiceTier === "priority") { const clearResult = await providers.setProviderConfig({ - provider: "openai", + provider, keyPath: ["fastModePreviousServiceTier"], value: "", }); diff --git a/src/common/config/schemas/providersConfig.test.ts b/src/common/config/schemas/providersConfig.test.ts index a36f277212..fca4872a21 100644 --- a/src/common/config/schemas/providersConfig.test.ts +++ b/src/common/config/schemas/providersConfig.test.ts @@ -83,6 +83,22 @@ describe("ProvidersConfigSchema", () => { expect(ProvidersConfigSchema.safeParse(invalid).success).toBe(false); }); + it("accepts only xAI service tiers supported by Priority Processing", () => { + expect(ProvidersConfigSchema.safeParse({ xai: { serviceTier: "priority" } }).success).toBe( + true + ); + expect(ProvidersConfigSchema.safeParse({ xai: { serviceTier: "default" } }).success).toBe(true); + expect( + ProvidersConfigSchema.safeParse({ + xai: { serviceTier: "priority", fastModePreviousServiceTier: "default" }, + }).success + ).toBe(true); + expect(ProvidersConfigSchema.safeParse({ xai: { serviceTier: "flex" } }).success).toBe(false); + expect( + ProvidersConfigSchema.safeParse({ xai: { fastModePreviousServiceTier: "flex" } }).success + ).toBe(false); + }); + describe("modelParameters", () => { it("accepts valid per-model and wildcard overrides", () => { const valid = { diff --git a/src/common/config/schemas/providersConfig.ts b/src/common/config/schemas/providersConfig.ts index a0a6a9fab6..9621bdd6c6 100644 --- a/src/common/config/schemas/providersConfig.ts +++ b/src/common/config/schemas/providersConfig.ts @@ -8,6 +8,9 @@ export const ServiceTierSchema = z.enum(["auto", "default", "flex", "priority"]) export type ServiceTier = z.infer; export const FastModePreviousServiceTierSchema = z.enum(["auto", "default", "flex", "unset"]); export type FastModePreviousServiceTier = z.infer; +export const XAIServiceTierSchema = z.enum(["default", "priority"]); +export type XAIServiceTier = z.infer; +export const XAIFastModePreviousServiceTierSchema = z.enum(["default", "unset"]); export const CodexOauthDefaultAuthSchema = z.enum(["oauth", "apiKey"]); export const BaseProviderConfigSchema = z @@ -62,6 +65,8 @@ export const OpenRouterProviderConfigSchema = BaseProviderConfigSchema.extend({ export const XAIProviderConfigSchema = BaseProviderConfigSchema.extend({ searchParameters: z.record(z.string(), z.unknown()).optional(), + serviceTier: XAIServiceTierSchema.optional(), + fastModePreviousServiceTier: XAIFastModePreviousServiceTierSchema.optional(), }); export const MuxGatewayProviderConfigSchema = BaseProviderConfigSchema.extend({ diff --git a/src/common/constants/knownModels.test.ts b/src/common/constants/knownModels.test.ts index 745d0a6028..0f2209c2c8 100644 --- a/src/common/constants/knownModels.test.ts +++ b/src/common/constants/knownModels.test.ts @@ -46,6 +46,16 @@ describe("Known Models Integration", () => { expect(MODEL_ABBREVIATIONS["gpt-5.5"]).toBeUndefined(); }); + test("grok aliases resolve only to Grok 4.5 in the curated registry", () => { + expect(MODEL_ABBREVIATIONS.grok).toBe("xai:grok-4.5"); + expect(MODEL_ABBREVIATIONS["grok-4.5"]).toBe("xai:grok-4.5"); + expect(MODEL_ABBREVIATIONS["grok-4.1"]).toBeUndefined(); + expect(MODEL_ABBREVIATIONS["grok-code"]).toBeUndefined(); + expect(Object.values(KNOWN_MODELS).filter((model) => model.provider === "xai")).toEqual([ + KNOWN_MODELS.GROK_45, + ]); + }); + test("kimi aliases resolve to the direct Moonshot Kimi K3 model", () => { expect(MODEL_ABBREVIATIONS.kimi).toBe("moonshotai:kimi-k3"); expect(MODEL_ABBREVIATIONS.k3).toBe("moonshotai:kimi-k3"); diff --git a/src/common/constants/knownModels.ts b/src/common/constants/knownModels.ts index e537359af8..0bec2d4c33 100644 --- a/src/common/constants/knownModels.ts +++ b/src/common/constants/knownModels.ts @@ -185,20 +185,18 @@ const MODEL_DEFINITIONS = { aliases: ["gemini-flash"], tokenizerOverride: "google/gemini-2.5-pro", }, - GROK_4_1: { + // Grok 4.5 - xAI's flagship coding and agentic model, released July 2026. + // Legacy Grok 4.1 Fast and Grok Code remain usable as custom model strings but are + // intentionally omitted from the curated index after their May 15, 2026 retirement. + GROK_45: { provider: "xai", - providerModelId: "grok-4-1-fast", - aliases: ["grok", "grok-4", "grok-4.1", "grok-4-1"], - }, - GROK_CODE: { - provider: "xai", - providerModelId: "grok-code-fast-1", - aliases: ["grok-code"], + providerModelId: "grok-4.5", + aliases: ["grok", "grok-4.5"], }, // DeepSeek V4 Pro is the flagship V4 tier (1.6T total / 49B active params, 1M context, // 384K max output). Bare `deepseek` alias points here per the convention that the // shortest alias tracks each provider's flagship model (mirrors `gemini` → Gemini Pro, - // `grok` → Grok 4.1). + // `grok` → Grok 4.5). DEEPSEEK_V4_PRO: { provider: "deepseek", providerModelId: "deepseek-v4-pro", diff --git a/src/common/orpc/schemas/api.ts b/src/common/orpc/schemas/api.ts index 29276ed735..38f440f54d 100644 --- a/src/common/orpc/schemas/api.ts +++ b/src/common/orpc/schemas/api.ts @@ -247,7 +247,7 @@ export const ProviderConfigInfoSchema = z.object({ displayName: z.string().optional(), isCustom: z.boolean().optional(), models: z.array(ProviderModelEntrySchema).optional(), - /** OpenAI-specific fields */ + /** Provider processing tier (OpenAI supports all values; xAI supports default/priority). */ serviceTier: ServiceTierSchema.optional(), fastModePreviousServiceTier: FastModePreviousServiceTierSchema.optional(), wireFormat: z.enum(["responses", "chatCompletions"]).optional(), diff --git a/src/common/schemas/providerOptions.ts b/src/common/schemas/providerOptions.ts index 9dbe888bc9..7eb2d65aed 100644 --- a/src/common/schemas/providerOptions.ts +++ b/src/common/schemas/providerOptions.ts @@ -1,6 +1,10 @@ import { z } from "zod"; -import { CacheTtlSchema, ServiceTierSchema } from "../config/schemas/providersConfig"; +import { + CacheTtlSchema, + ServiceTierSchema, + XAIServiceTierSchema, +} from "../config/schemas/providersConfig"; export const MuxProviderOptionsSchema = z.object({ anthropic: z @@ -56,6 +60,10 @@ export const MuxProviderOptionsSchema = z.object({ openrouter: z.record(z.string(), z.unknown()).optional(), xai: z .object({ + serviceTier: XAIServiceTierSchema.nullish().meta({ + description: + 'xAI processing tier: "priority" requests faster processing at 2× token pricing; "default" uses standard processing', + }), searchParameters: z .object({ mode: z.enum(["auto", "off", "on"]), diff --git a/src/common/types/thinking.ts b/src/common/types/thinking.ts index c3c3a4dafe..f05ce956ca 100644 --- a/src/common/types/thinking.ts +++ b/src/common/types/thinking.ts @@ -303,6 +303,15 @@ export function openaiSupportsProMode(modelString: string): boolean { return isGpt56FamilyModel(modelString); } +/** + * Whether the model is Grok 4.5 (including provider/gateway prefixes and aliases). + * Grok 4.5 always reasons and accepts low, medium, or high effort. + */ +export function isGrok45Model(modelString: string): boolean { + const withoutPrefix = stripModelProviderPrefixes(modelString); + return /^grok-4\.5(?:$|-)/.test(withoutPrefix); +} + /** * Kimi K3 (Moonshot AI) always reasons and supports only the max reasoning * effort; the thinking policy and the Moonshot/OpenRouter provider-options diff --git a/src/common/utils/ai/providerOptions.test.ts b/src/common/utils/ai/providerOptions.test.ts index fee2f2bcda..c67bf786eb 100644 --- a/src/common/utils/ai/providerOptions.test.ts +++ b/src/common/utils/ai/providerOptions.test.ts @@ -1639,6 +1639,32 @@ describe("buildProviderOptions - OpenRouter", () => { }); }); +describe("buildProviderOptions - xAI", () => { + test("maps Grok 4.5 thinking levels to reasoning effort without deprecated search defaults", () => { + expect(buildProviderOptions("xai:grok-4.5", "medium")).toEqual({ + xai: { reasoningEffort: "medium" }, + }); + expect(buildProviderOptions("xai:grok-4.5", "max")).toEqual({ + xai: { reasoningEffort: "high" }, + }); + }); + + test("omits deprecated search parameters and strips service tier from Grok 4.5 SDK options", () => { + expect( + buildProviderOptions("xai:grok-4.5", "high", undefined, undefined, { + xai: { + serviceTier: "priority", + searchParameters: { mode: "off" }, + }, + }) + ).toEqual({ + xai: { + reasoningEffort: "high", + }, + }); + }); +}); + describe("buildRequestHeaders", () => { for (const { name, model, options, expected } of [ { diff --git a/src/common/utils/ai/providerOptions.ts b/src/common/utils/ai/providerOptions.ts index ff55856fa7..9cf7a7cae0 100644 --- a/src/common/utils/ai/providerOptions.ts +++ b/src/common/utils/ai/providerOptions.ts @@ -22,6 +22,7 @@ import { ANTHROPIC_THINKING_BUDGETS, GEMINI_THINKING_BUDGETS, getOpenAIReasoningEffort, + isGrok45Model, isKimiK3Model, openaiSupportsProMode, OPENROUTER_REASONING_EFFORT, @@ -539,7 +540,21 @@ export function buildProviderOptions( // Build xAI-specific options if (formatProvider === "xai") { - const overrides = muxProviderOptions?.xai ?? {}; + // serviceTier is applied by providerModelFactory's fetch wrapper because the + // current xAI AI SDK provider does not expose service_tier as a typed option. + const { + serviceTier: _serviceTier, + searchParameters, + ...overrides + } = muxProviderOptions?.xai ?? {}; + const isGrok45 = isGrok45Model(capabilityModel); + const reasoningEffort: XaiProviderOptions["reasoningEffort"] = isGrok45 + ? effectiveThinking === "xhigh" || effectiveThinking === "max" + ? "high" + : effectiveThinking === "off" + ? "low" + : effectiveThinking + : undefined; const defaultSearchParameters: XaiProviderOptions["searchParameters"] = { mode: "auto", @@ -549,7 +564,12 @@ export function buildProviderOptions( const options = { xai: { ...overrides, - searchParameters: overrides.searchParameters ?? defaultSearchParameters, + ...(reasoningEffort != null && { reasoningEffort }), + // Grok 4.5 uses xAI's modern Responses tools; getToolsForModel translates + // legacy Live Search settings instead of sending deprecated search_parameters. + ...(!isGrok45 && { + searchParameters: searchParameters ?? defaultSearchParameters, + }), }, } satisfies { xai: XaiProviderOptions }; log.debug("buildProviderOptions: Returning xAI options", options); diff --git a/src/common/utils/thinking/policy.test.ts b/src/common/utils/thinking/policy.test.ts index aaef6d6620..ba966011f0 100644 --- a/src/common/utils/thinking/policy.test.ts +++ b/src/common/utils/thinking/policy.test.ts @@ -897,6 +897,15 @@ describe("resolveMinimumThinkingLevel", () => { }); }); +describe("Grok 4.5 thinking policy", () => { + test("offers only the reasoning efforts accepted by xAI", () => { + expect(getThinkingPolicyForModel("xai:grok-4.5")).toEqual(["low", "medium", "high"]); + expect(getDefaultMinimumThinkingLevel("xai:grok-4.5")).toBe("medium"); + expect(enforceThinkingPolicy("xai:grok-4.5", "off")).toBe("low"); + expect(enforceThinkingPolicy("xai:grok-4.5", "max")).toBe("high"); + }); +}); + describe("getAvailableThinkingLevels", () => { test("returns the raw capability when no floor is provided", () => { expect(getAvailableThinkingLevels("anthropic:claude-sonnet-4-5")).toEqual([ diff --git a/src/common/utils/thinking/policy.ts b/src/common/utils/thinking/policy.ts index 30a6e516f7..53be165b12 100644 --- a/src/common/utils/thinking/policy.ts +++ b/src/common/utils/thinking/policy.ts @@ -19,6 +19,7 @@ import { THINKING_LEVEL_OFF, anthropicRejectsDisabledThinking, anthropicSupportsNativeXhigh, + isGrok45Model, isKimiK3Model, openaiSupportsNativeMaxEffort, stripModelProviderPrefixes, @@ -64,6 +65,7 @@ export function isGeminiFlashThinkingLevelModelName(modelName: string): boolean * - openai:gpt-5-pro → ["high"] (only supported level, legacy) * - Gemini Flash chat variants → ["off", "low", "medium", "high"] * - gemini-3 Pro variants → ["low", "high"] (thinking level only) + * - xai:grok-4.5 → ["low", "medium", "high"] (reasoning cannot be disabled) * - default → ["off", "low", "medium", "high"] (standard 4 levels; xhigh is opt-in per model) * * Tolerates version suffixes (e.g., gpt-5-pro-2025-10-06). @@ -171,6 +173,11 @@ function getExplicitThinkingPolicy(modelString: string): ThinkingPolicy | null { return ["low", "high"]; } + // Grok 4.5 always reasons and supports configurable low/medium/high effort. + if (isGrok45Model(withoutProviderNamespace)) { + return ["low", "medium", "high"]; + } + // Kimi K3 always reasons and supports only the max reasoning effort, so the // policy is a fixed single level. if (isKimiK3Model(withoutProviderNamespace)) { diff --git a/src/common/utils/tokens/displayUsage.test.ts b/src/common/utils/tokens/displayUsage.test.ts index 34f53769e0..d59c78d8c7 100644 --- a/src/common/utils/tokens/displayUsage.test.ts +++ b/src/common/utils/tokens/displayUsage.test.ts @@ -252,6 +252,28 @@ describe("createDisplayUsage", () => { expect(result!.output.cost_usd).toBeCloseTo(0.0012, 10); }); + test("reconciles Grok usage to xAI's exact billed cost", () => { + const usage: LanguageModelV2Usage = { + inputTokens: 1000, + outputTokens: 500, + reasoningTokens: 100, + totalTokens: 1500, + }; + + const result = createDisplayUsage(usage, "xai:grok-4.5", { + xai: { costInUsdTicks: 50_000_000 }, // $0.005 exact billed cost + }); + + expect(result).toBeDefined(); + const totalCost = + result!.input.cost_usd! + + result!.cached.cost_usd! + + result!.cacheCreate.cost_usd! + + result!.output.cost_usd! + + result!.reasoning.cost_usd!; + expect(totalCost).toBeCloseTo(0.005, 12); + }); + describe("tiered long-context pricing", () => { test("keeps GPT-5.5 on base rates at the published 272K boundary", () => { const usage: LanguageModelV2Usage = { diff --git a/src/common/utils/tokens/displayUsage.ts b/src/common/utils/tokens/displayUsage.ts index 9d3c1a35b1..92f4c51dce 100644 --- a/src/common/utils/tokens/displayUsage.ts +++ b/src/common/utils/tokens/displayUsage.ts @@ -174,6 +174,27 @@ export function createDisplayUsage( reasoningCost = costs.reasoningCost; } + const xaiCostInUsdTicks = (providerMetadata?.xai as { costInUsdTicks?: number } | undefined) + ?.costInUsdTicks; + if (xaiCostInUsdTicks != null && Number.isFinite(xaiCostInUsdTicks) && xaiCostInUsdTicks >= 0) { + // xAI reports the exact amount billed after caching, tools, and Priority Processing. + // Scale the estimated component breakdown so its total reconciles to the billed amount. + const exactCostUsd = xaiCostInUsdTicks / 10_000_000_000; + const estimatedCosts = [inputCost, cachedCost, cacheCreateCost, outputCost, reasoningCost]; + const estimatedTotal = estimatedCosts.reduce((sum, cost) => sum + (cost ?? 0), 0); + + if (estimatedTotal > 0) { + const scale = exactCostUsd / estimatedTotal; + inputCost = (inputCost ?? 0) * scale; + cachedCost = (cachedCost ?? 0) * scale; + cacheCreateCost = (cacheCreateCost ?? 0) * scale; + outputCost = (outputCost ?? 0) * scale; + reasoningCost = (reasoningCost ?? 0) * scale; + } else { + outputCost = exactCostUsd; + } + } + if (costsIncluded) { inputCost = 0; cachedCost = 0; diff --git a/src/common/utils/tokens/models-extra.ts b/src/common/utils/tokens/models-extra.ts index b83d61928b..a7310e5b71 100644 --- a/src/common/utils/tokens/models-extra.ts +++ b/src/common/utils/tokens/models-extra.ts @@ -69,6 +69,26 @@ const GPT_56_SOL_STATS: ModelData = { }; export const modelsExtra: Record = { + // Grok 4.5 - Released July 2026. xAI's flagship coding and agentic model. + // Pricing doubles for prompts above 200K tokens; Priority Processing applies a + // separate 2× multiplier at request time and is therefore not baked into these rates. + "xai/grok-4.5": { + max_input_tokens: 500000, + input_cost_per_token: 0.000002, // $2 per million input tokens + input_cost_per_token_above_200k_tokens: 0.000004, // $4 per million input tokens + output_cost_per_token: 0.000006, // $6 per million output tokens + output_cost_per_token_above_200k_tokens: 0.000012, // $12 per million output tokens + cache_read_input_token_cost: 0.0000003, // $0.30 per million cached input tokens + cache_read_input_token_cost_above_200k_tokens: 0.0000006, // $0.60 per million cached input tokens + litellm_provider: "xai", + mode: "chat", + supports_function_calling: true, + supports_vision: true, + supports_reasoning: true, + supports_response_schema: true, + supported_endpoints: ["/v1/chat/completions", "/v1/responses"], + }, + // GPT Image 2 - image-generation model not yet in LiteLLM's bundled models.json. // OpenAI prices text input at $5/M tokens, image input at $8/M, cached text input at // $1.25/M, cached image input at $2/M, and generated image output at $30/M. diff --git a/src/common/utils/tokens/usageHelpers.test.ts b/src/common/utils/tokens/usageHelpers.test.ts index df4d5c7142..8cdc318d6c 100644 --- a/src/common/utils/tokens/usageHelpers.test.ts +++ b/src/common/utils/tokens/usageHelpers.test.ts @@ -252,6 +252,15 @@ describe("accumulateProviderMetadata", () => { }); }); + test("sums xAI exact cost ticks across model steps", () => { + const existing = { xai: { costInUsdTicks: 10_000_000 } }; + const step = { xai: { costInUsdTicks: 25_000_000, requestId: "req_2" } }; + + expect(accumulateProviderMetadata(existing, step)).toEqual({ + xai: { costInUsdTicks: 35_000_000, requestId: "req_2" }, + }); + }); + test("handles non-anthropic providers (returns step as-is when no cache tokens)", () => { const existing = { openai: { reasoningTokens: 100 } }; const step = { openai: { reasoningTokens: 200 } }; diff --git a/src/common/utils/tokens/usageHelpers.ts b/src/common/utils/tokens/usageHelpers.ts index 5985137cdb..d5650c6158 100644 --- a/src/common/utils/tokens/usageHelpers.ts +++ b/src/common/utils/tokens/usageHelpers.ts @@ -105,10 +105,10 @@ export function addUsage( } /** - * Accumulate provider metadata across steps, specifically for cache creation tokens. + * Accumulate provider metadata across steps for additive billing metadata. * - * For Anthropic, cache creation tokens are reported per-step and need to be summed. - * Other provider metadata is taken from the latest step. + * Anthropic cache creation tokens and xAI exact-cost ticks are reported per request/step + * and must be summed. Other provider metadata is taken from the latest step. */ export function accumulateProviderMetadata( existing: Record | undefined, @@ -126,19 +126,30 @@ export function accumulateProviderMetadata( ?.cacheCreationInputTokens ?? 0; const totalCacheCreate = existingCacheCreate + stepCacheCreate; + const existingXaiCostTicks = + (existing.xai as { costInUsdTicks?: number } | undefined)?.costInUsdTicks ?? 0; + const stepXaiCostTicks = + (step.xai as { costInUsdTicks?: number } | undefined)?.costInUsdTicks ?? 0; + const totalXaiCostTicks = existingXaiCostTicks + stepXaiCostTicks; - // If no cache creation tokens to aggregate, just return step's metadata - if (totalCacheCreate === 0) { + if (totalCacheCreate === 0 && totalXaiCostTicks === 0) { return step; } - // Merge with accumulated cache creation tokens return { ...step, - anthropic: { - ...(step.anthropic as Record | undefined), - cacheCreationInputTokens: totalCacheCreate, - }, + ...(totalCacheCreate > 0 && { + anthropic: { + ...(step.anthropic as Record | undefined), + cacheCreationInputTokens: totalCacheCreate, + }, + }), + ...(totalXaiCostTicks > 0 && { + xai: { + ...(step.xai as Record | undefined), + costInUsdTicks: totalXaiCostTicks, + }, + }), }; } diff --git a/src/common/utils/tools/toolDefinitions.test.ts b/src/common/utils/tools/toolDefinitions.test.ts index abd573092f..b526437cc2 100644 --- a/src/common/utils/tools/toolDefinitions.test.ts +++ b/src/common/utils/tools/toolDefinitions.test.ts @@ -802,6 +802,15 @@ describe("TOOL_DEFINITIONS", () => { } }); + it("exposes xAI native search tools only for Grok 4.5", () => { + for (const modelString of ["xai:grok-4.5", "xai:grok-4.5-latest"]) { + expect(getAvailableTools(modelString)).toEqual( + expect.arrayContaining(["web_search", "x_search"]) + ); + } + expect(getAvailableTools("xai:grok-4-1-fast")).not.toContain("x_search"); + }); + it("classifies Gemini 3 as supporting mixed native Google and function tools", () => { expect(supportsGoogleNativeToolsWithFunctionTools("gemini-2.5-pro")).toBe(false); expect(supportsGoogleNativeToolsWithFunctionTools("gemini-3.1-pro-preview")).toBe(true); diff --git a/src/common/utils/tools/toolDefinitions.ts b/src/common/utils/tools/toolDefinitions.ts index 4d0d545f9c..214f4ea594 100644 --- a/src/common/utils/tools/toolDefinitions.ts +++ b/src/common/utils/tools/toolDefinitions.ts @@ -26,6 +26,7 @@ * by our own backend code and always use `undefined` for absent fields. */ +import { isGrok45Model } from "@/common/types/thinking"; import { z } from "zod"; import { AgentIdSchema, @@ -3220,6 +3221,8 @@ export function getAvailableTools( return [...baseTools, "web_search"]; } return baseTools; + case "xai": + return isGrok45Model(modelString) ? [...baseTools, "web_search", "x_search"] : baseTools; case "google": if (supportsGoogleNativeToolsWithFunctionTools(modelId)) { return [...baseTools, "google_search", "url_context"]; diff --git a/src/common/utils/tools/tools.test.ts b/src/common/utils/tools/tools.test.ts index c84f1d075d..458e8b8303 100644 --- a/src/common/utils/tools/tools.test.ts +++ b/src/common/utils/tools/tools.test.ts @@ -7,8 +7,10 @@ import type { InitStateManager } from "@/node/services/initStateManager"; import type { DesktopSessionManager } from "@/node/services/desktop/DesktopSessionManager"; import { LocalRuntime } from "@/node/runtime/LocalRuntime"; import { + getForcedXaiSearchToolNames, getToolsForModel, supportsAnthropicNativeWebFetch, + type ToolConfiguration, type WorkspaceHeartbeatToolService, } from "./tools"; @@ -451,6 +453,259 @@ describe("getToolsForModel", () => { expect(gemini35Tools.url_context).toBeDefined(); }); + test("adds xAI native web and X search tools for Grok 4.5", async () => { + const runtime = new LocalRuntime(process.cwd()); + const initStateManager = createInitStateManager(); + + const tools = await getToolsForModel( + "xai:grok-4.5", + { + cwd: process.cwd(), + runtime, + runtimeTempDir: "/tmp", + workspaceId: "ws-1", + xaiSearchParameters: { + mode: "auto", + fromDate: "2026-07-01", + sources: [ + { type: "web", allowedWebsites: ["example.com"] }, + { type: "x", includedXHandles: ["xai"] }, + ], + }, + }, + "ws-1", + initStateManager + ); + + expect(tools.web_search).toBeDefined(); + expect(tools.x_search).toBeDefined(); + expect(tools.web_search).toMatchObject({ + type: "provider", + id: "xai.web_search", + args: { allowedDomains: ["example.com"] }, + }); + expect(tools.x_search).toMatchObject({ + type: "provider", + id: "xai.x_search", + args: { allowedXHandles: ["xai"], fromDate: "2026-07-01" }, + }); + }); + + test("omits xAI native search tools when the resolved route is not xAI Responses", async () => { + const runtime = new LocalRuntime(process.cwd()); + const initStateManager = createInitStateManager(); + + const tools = await getToolsForModel( + "xai:grok-4.5", + { + cwd: process.cwd(), + runtime, + runtimeTempDir: "/tmp", + workspaceId: "ws-1", + xaiNativeToolsEnabled: false, + }, + "ws-1", + initStateManager + ); + + expect(tools.web_search).toBeUndefined(); + expect(tools.x_search).toBeUndefined(); + }); + + test("ignores malformed persisted xAI search sources instead of blocking requests", async () => { + const runtime = new LocalRuntime(process.cwd()); + const initStateManager = createInitStateManager(); + const malformedSearchParameters = { + mode: "auto", + sources: "not-an-array", + fromDate: 123, + toDate: "2026-99-99", + } as unknown as ToolConfiguration["xaiSearchParameters"]; + + const tools = await getToolsForModel( + "xai:grok-4.5", + { + cwd: process.cwd(), + runtime, + runtimeTempDir: "/tmp", + workspaceId: "ws-1", + xaiSearchParameters: malformedSearchParameters, + }, + "ws-1", + initStateManager + ); + + expect((tools.x_search as { args?: { fromDate?: string } }).args?.fromDate).toBeUndefined(); + expect((tools.x_search as { args?: { toDate?: string } }).args?.toDate).toBeUndefined(); + expect(tools.web_search).toBeDefined(); + expect(tools.x_search).toBeDefined(); + }); + + test("adds xAI native search tools for Grok 4.5 aliases", async () => { + const runtime = new LocalRuntime(process.cwd()); + const initStateManager = createInitStateManager(); + + const tools = await getToolsForModel( + "xai:grok-4.5-latest", + { + cwd: process.cwd(), + runtime, + runtimeTempDir: "/tmp", + workspaceId: "ws-1", + }, + "ws-1", + initStateManager + ); + + expect(tools.web_search).toBeDefined(); + expect(tools.x_search).toBeDefined(); + }); + + test("merges every configured xAI web, news, and X source entry", async () => { + const runtime = new LocalRuntime(process.cwd()); + const initStateManager = createInitStateManager(); + + const tools = await getToolsForModel( + "xai:grok-4.5", + { + cwd: process.cwd(), + runtime, + runtimeTempDir: "/tmp", + workspaceId: "ws-1", + xaiSearchParameters: { + mode: "auto", + sources: [ + { type: "web", allowedWebsites: ["one.example"], excludedWebsites: ["bad.one"] }, + { type: "web", allowedWebsites: ["two.example"], excludedWebsites: ["bad.two"] }, + { type: "news", excludedWebsites: ["bad.news"] }, + { type: "x", includedXHandles: ["xai"], excludedXHandles: ["spam_one"] }, + { type: "x", xHandles: ["grok"], excludedXHandles: ["spam_two"] }, + ], + }, + }, + "ws-1", + initStateManager + ); + + expect(tools.web_search).toMatchObject({ + args: { + allowedDomains: ["one.example", "two.example"], + }, + }); + expect( + (tools.web_search as { args?: { excludedDomains?: string[] } }).args?.excludedDomains + ).toBeUndefined(); + expect(tools.x_search).toMatchObject({ + args: { + allowedXHandles: ["xai", "grok"], + }, + }); + expect( + (tools.x_search as { args?: { excludedXHandles?: string[] } }).args?.excludedXHandles + ).toBeUndefined(); + }); + + test("merges exclusion-only xAI source entries without mutually exclusive allowlists", async () => { + const runtime = new LocalRuntime(process.cwd()); + const initStateManager = createInitStateManager(); + + const tools = await getToolsForModel( + "xai:grok-4.5", + { + cwd: process.cwd(), + runtime, + runtimeTempDir: "/tmp", + workspaceId: "ws-1", + xaiSearchParameters: { + mode: "auto", + sources: [ + { type: "web", excludedWebsites: ["bad.one"] }, + { type: "news", excludedWebsites: ["bad.news"] }, + { type: "x", excludedXHandles: ["spam_one"] }, + { type: "x", excludedXHandles: ["spam_two"] }, + ], + }, + }, + "ws-1", + initStateManager + ); + + expect(tools.web_search).toMatchObject({ + args: { excludedDomains: ["bad.one", "bad.news"] }, + }); + expect(tools.x_search).toMatchObject({ + args: { excludedXHandles: ["spam_one", "spam_two"] }, + }); + }); + + test("preserves RSS-only xAI search configs as domain-scoped web search", async () => { + const runtime = new LocalRuntime(process.cwd()); + const initStateManager = createInitStateManager(); + + const tools = await getToolsForModel( + "xai:grok-4.5", + { + cwd: process.cwd(), + runtime, + runtimeTempDir: "/tmp", + workspaceId: "ws-1", + xaiSearchParameters: { + mode: "auto", + sources: [ + { + type: "rss", + links: [ + "https://feeds.example.com/news.xml", + "https://updates.example.org/releases.xml", + ], + }, + ], + }, + }, + "ws-1", + initStateManager + ); + + expect(tools.web_search).toMatchObject({ + type: "provider", + id: "xai.web_search", + args: { allowedDomains: ["feeds.example.com", "updates.example.org"] }, + }); + expect(tools.x_search).toBeUndefined(); + }); + + test("marks mode:on xAI search tools as required on the first model step", () => { + expect( + getForcedXaiSearchToolNames("xai:grok-4.5", { + mode: "on", + sources: [{ type: "x", includedXHandles: ["xai"] }], + }) + ).toEqual(["x_search"]); + expect(getForcedXaiSearchToolNames("xai:grok-4.5", { mode: "auto" })).toBeUndefined(); + expect(getForcedXaiSearchToolNames("xai:grok-4-1-fast", { mode: "on" })).toBeUndefined(); + }); + + test("honors disabled xAI search mode for Grok 4.5", async () => { + const runtime = new LocalRuntime(process.cwd()); + const initStateManager = createInitStateManager(); + + const tools = await getToolsForModel( + "xai:grok-4.5", + { + cwd: process.cwd(), + runtime, + runtimeTempDir: "/tmp", + workspaceId: "ws-1", + xaiSearchParameters: { mode: "off" }, + }, + "ws-1", + initStateManager + ); + + expect(tools.web_search).toBeUndefined(); + expect(tools.x_search).toBeUndefined(); + }); + test("returns tool keys in sorted order", async () => { const runtime = new LocalRuntime(process.cwd()); const initStateManager = createInitStateManager(); diff --git a/src/common/utils/tools/tools.ts b/src/common/utils/tools/tools.ts index f28e6f0e76..bcbb8a6e04 100644 --- a/src/common/utils/tools/tools.ts +++ b/src/common/utils/tools/tools.ts @@ -1,5 +1,8 @@ +import { xai } from "@ai-sdk/xai"; import { type LanguageModel, type Tool } from "ai"; import type { LanguageModelV2Usage } from "@ai-sdk/provider"; +import type { MuxProviderOptions } from "@/common/types/providerOptions"; +import { isGrok45Model } from "@/common/types/thinking"; import type { BackgroundWorkAttentionPolicy } from "@/common/types/backgroundWorkAttention"; import { cloneToolPreservingDescriptors } from "@/common/utils/tools/cloneToolPreservingDescriptors"; import { createFileReadTool } from "@/node/services/tools/file_read"; @@ -151,6 +154,10 @@ export interface ToolConfiguration { runtimeTempDir: string; /** OpenAI wire format — webSearch requires "responses" */ openaiWireFormat?: "responses" | "chatCompletions"; + /** Whether the resolved route supports xAI Responses-native tools. */ + xaiNativeToolsEnabled?: boolean; + /** Legacy xAI Live Search settings translated to Responses native search tools. */ + xaiSearchParameters?: NonNullable["searchParameters"]>; /** Overflow policy for bash tool output (optional, not exposed to AI) */ overflow_policy?: "truncate" | "tmpfile"; /** Background process manager for bash tool (optional, AI-only) */ @@ -526,6 +533,181 @@ export function supportsAnthropicNativeWebFetch(modelId: string): boolean { return major > 4 || (major === 4 && minor !== undefined && minor >= 6); } +interface XaiWebSearchOptions { + allowedDomains?: string[]; + excludedDomains?: string[]; +} + +interface XaiXSearchOptions { + allowedXHandles?: string[]; + excludedXHandles?: string[]; + fromDate?: string; + toDate?: string; +} + +interface XaiSearchSourceRecord extends Record { + type: "web" | "x" | "news" | "rss"; +} + +function getXaiSearchSources( + searchParameters: ToolConfiguration["xaiSearchParameters"] +): XaiSearchSourceRecord[] { + const rawSources = (searchParameters as { sources?: unknown } | undefined)?.sources; + if (!Array.isArray(rawSources)) { + return []; + } + + return rawSources.filter((source): source is XaiSearchSourceRecord => { + if (typeof source !== "object" || source === null) return false; + const type = (source as { type?: unknown }).type; + return type === "web" || type === "x" || type === "news" || type === "rss"; + }); +} + +function readStringArray(value: unknown): string[] { + return Array.isArray(value) + ? value.filter((entry): entry is string => typeof entry === "string") + : []; +} + +function readIsoDate(value: unknown): string | undefined { + if (typeof value !== "string" || !/^\d{4}-\d{2}-\d{2}$/.test(value)) { + return undefined; + } + const parsed = new Date(`${value}T00:00:00.000Z`); + return Number.isNaN(parsed.getTime()) || parsed.toISOString().slice(0, 10) !== value + ? undefined + : value; +} + +function getXaiNativeSearchConfiguration( + searchParameters: ToolConfiguration["xaiSearchParameters"] +): { + webSearch?: XaiWebSearchOptions; + xSearch?: XaiXSearchOptions; +} { + if (searchParameters?.mode === "off") { + return {}; + } + + const rawSearchParameters = searchParameters as Record | undefined; + const fromDate = readIsoDate(rawSearchParameters?.fromDate); + const toDate = readIsoDate(rawSearchParameters?.toDate); + const sources = getXaiSearchSources(searchParameters); + const webSources = sources.filter((source) => source.type === "web"); + const newsSources = sources.filter((source) => source.type === "news"); + const xSources = sources.filter((source) => source.type === "x"); + const rssDomains = Array.from( + new Set( + sources + .filter((source) => source.type === "rss") + .flatMap((source) => readStringArray(source.links)) + .map((link) => { + try { + return new URL(link).hostname; + } catch { + return undefined; + } + }) + .filter((domain): domain is string => domain != null) + ) + ); + const configuredAllowedDomains = Array.from( + new Set([ + ...webSources.flatMap((source) => readStringArray(source.allowedWebsites)), + ...rssDomains, + ]) + ); + const configuredExcludedDomains = Array.from( + new Set([ + ...webSources.flatMap((source) => readStringArray(source.excludedWebsites)), + ...newsSources.flatMap((source) => readStringArray(source.excludedWebsites)), + ]) + ); + const configuredAllowedXHandles = Array.from( + new Set( + xSources.flatMap((source) => { + const included = readStringArray(source.includedXHandles); + return included.length > 0 ? included : readStringArray(source.xHandles); + }) + ) + ); + const configuredExcludedXHandles = Array.from( + new Set(xSources.flatMap((source) => readStringArray(source.excludedXHandles))) + ); + // Native search caps domain filters at five and X-handle filters at twenty. The + // installed SDK is patched to match xAI's documented 20-handle limit. Allowed and + // excluded filters are mutually exclusive, so an allowlist wins after subtracting + // excluded entries. If a legacy config exceeds a cap, omit that restriction instead + // of dropping only later source entries and creating a misleading partial translation. + const effectiveAllowedDomains = configuredAllowedDomains.filter( + (domain) => !configuredExcludedDomains.includes(domain) + ); + const allowedDomains = + effectiveAllowedDomains.length > 0 && effectiveAllowedDomains.length <= 5 + ? effectiveAllowedDomains + : undefined; + const excludedDomains = + allowedDomains == null && + configuredExcludedDomains.length > 0 && + configuredExcludedDomains.length <= 5 + ? configuredExcludedDomains + : undefined; + const effectiveAllowedXHandles = configuredAllowedXHandles.filter( + (handle) => !configuredExcludedXHandles.includes(handle) + ); + const allowedXHandles = + effectiveAllowedXHandles.length > 0 && effectiveAllowedXHandles.length <= 20 + ? effectiveAllowedXHandles + : undefined; + const excludedXHandles = + allowedXHandles == null && + configuredExcludedXHandles.length > 0 && + configuredExcludedXHandles.length <= 20 + ? configuredExcludedXHandles + : undefined; + const hasExplicitSources = sources.length > 0; + const enableWebSearch = + !hasExplicitSources || + sources.some( + (source) => source.type === "web" || source.type === "news" || source.type === "rss" + ); + const enableXSearch = !hasExplicitSources || xSources.length > 0; + + return { + ...(enableWebSearch && { + webSearch: { + ...(allowedDomains != null && { allowedDomains }), + ...(excludedDomains != null && { excludedDomains }), + }, + }), + ...(enableXSearch && { + xSearch: { + ...(allowedXHandles != null && { allowedXHandles }), + ...(excludedXHandles != null && { excludedXHandles }), + ...(fromDate != null && { fromDate }), + ...(toDate != null && { toDate }), + }, + }), + }; +} + +export function getForcedXaiSearchToolNames( + modelString: string, + searchParameters: ToolConfiguration["xaiSearchParameters"] +): string[] | undefined { + if (!isGrok45Model(modelString) || searchParameters?.mode !== "on") { + return undefined; + } + + const nativeSearch = getXaiNativeSearchConfiguration(searchParameters); + const toolNames = [ + ...(nativeSearch.webSearch != null ? ["web_search"] : []), + ...(nativeSearch.xSearch != null ? ["x_search"] : []), + ]; + return toolNames.length > 0 ? toolNames : undefined; +} + export async function getToolsForModel( modelString: string, config: ToolConfiguration, @@ -718,6 +900,23 @@ export async function getToolsForModel( break; } + case "xai": { + if (isGrok45Model(modelString) && config.xaiNativeToolsEnabled !== false) { + const nativeSearch = getXaiNativeSearchConfiguration(config.xaiSearchParameters); + allTools = { + ...baseTools, + ...(mcpTools ?? {}), + ...(nativeSearch.webSearch != null && { + web_search: xai.tools.webSearch(nativeSearch.webSearch) as Tool, + }), + ...(nativeSearch.xSearch != null && { + x_search: xai.tools.xSearch(nativeSearch.xSearch) as Tool, + }), + }; + } + break; + } + case "google": { if (supportsGoogleNativeToolsWithFunctionTools(modelId)) { const { google } = await import("@ai-sdk/google"); diff --git a/src/node/services/agentSkills/builtInSkillContent.generated.ts b/src/node/services/agentSkills/builtInSkillContent.generated.ts index d84f0a2dfe..10eabfe0f0 100644 --- a/src/node/services/agentSkills/builtInSkillContent.generated.ts +++ b/src/node/services/agentSkills/builtInSkillContent.generated.ts @@ -3195,8 +3195,7 @@ export const BUILTIN_SKILL_FILES: Record> = { "| Codex Max 5.1 | openai:gpt-5.1-codex-max | `codex-max` | |", "| Gemini 3.1 Pro Preview | google:gemini-3.1-pro-preview | `gemini`, `gemini-pro` | |", "| Gemini 3.6 Flash | google:gemini-3.6-flash | `gemini-flash` | |", - "| Grok 4 1 Fast | xai:grok-4-1-fast | `grok`, `grok-4`, `grok-4.1`, `grok-4-1` | |", - "| Grok Code Fast 1 | xai:grok-code-fast-1 | `grok-code` | |", + "| Grok 4.5 | xai:grok-4.5 | `grok`, `grok-4.5` | |", "| DeepSeek V4 Pro | deepseek:deepseek-v4-pro | `deepseek`, `deepseek-pro`, `deepseek-v4`, `deepseek-v4-pro` | |", "| DeepSeek V4 Flash | deepseek:deepseek-v4-flash | `deepseek-flash`, `deepseek-v4-flash` | |", "| Kimi K3 | moonshotai:kimi-k3 | `kimi`, `k3`, `kimi-k3` | |", diff --git a/src/node/services/aiService.test.ts b/src/node/services/aiService.test.ts index 0313fcb48e..70587bc046 100644 --- a/src/node/services/aiService.test.ts +++ b/src/node/services/aiService.test.ts @@ -798,7 +798,7 @@ describe("AIService.resolveGatewayModelString", () => { const variant = "xai:grok-4-1-fast-reasoning"; await writeMainConfig(muxHome.path, { muxGatewayEnabled: true, - muxGatewayModels: [KNOWN_MODELS.GROK_4_1.id], + muxGatewayModels: ["xai:grok-4-1-fast"], }); await writeProvidersConfig(muxHome.path, { "mux-gateway": { couponCode: "test-coupon" }, @@ -809,7 +809,7 @@ describe("AIService.resolveGatewayModelString", () => { // @ts-expect-error - accessing private field for testing const resolved = service.providerModelFactory.resolveGatewayModelString( variant, - KNOWN_MODELS.GROK_4_1.id + "xai:grok-4-1-fast" ); expect(resolved).toBe(toGatewayModelString(variant)); diff --git a/src/node/services/aiService.ts b/src/node/services/aiService.ts index 65d47b0c3f..272b94f67b 100644 --- a/src/node/services/aiService.ts +++ b/src/node/services/aiService.ts @@ -27,6 +27,7 @@ import { runLanguageModelCleanup } from "./languageModelCleanup"; import type { InitStateManager } from "./initStateManager"; import type { SendMessageError } from "@/common/types/errors"; import { + getForcedXaiSearchToolNames, getToolsForModel, type AdvisorStepCaptureRef, type ToolConfiguration, @@ -2073,6 +2074,8 @@ export class AIService extends EventEmitter { : {}), ...(toolSearchRuntime ? { toolSearchRuntime } : {}), openaiWireFormat: effectiveMuxProviderOptions?.openai?.wireFormat, + xaiNativeToolsEnabled: routeProvider === "xai", + xaiSearchParameters: effectiveMuxProviderOptions.xai?.searchParameters, backgroundProcessManager: this.backgroundProcessManager, // Plan agent configuration for plan file access. // - read: plan file is readable in all agents (useful context) @@ -2719,7 +2722,10 @@ export class AIService extends EventEmitter { // versa silently drops web tooling). const nextAllTools = await getToolsForModel( next.canonicalModelString, - toolsForModelConfig, + { + ...toolsForModelConfig, + xaiNativeToolsEnabled: next.routeProvider === "xai", + }, workspaceId, this.initStateManager, toolInstructions, @@ -2942,6 +2948,13 @@ export class AIService extends EventEmitter { callSettingsOverrides: nextOverrides.standard, anthropicCacheTtl: effectiveMuxProviderOptions.anthropic?.cacheTtl ?? undefined, thinkingLevel: nextThinkingLevel, + forcedFirstStepToolNames: + next.routeProvider === "xai" + ? getForcedXaiSearchToolNames( + next.canonicalModelString, + effectiveMuxProviderOptions.xai?.searchParameters + )?.filter((toolName) => toolName in nextTools) + : undefined, rebuildProviderOptionsForThinkingLevel: rebuildNextProviderOptionsForThinkingLevel, initialMetadataPatch: { @@ -2964,6 +2977,14 @@ export class AIService extends EventEmitter { } : undefined; + const forcedFirstStepToolNames = + routeProvider === "xai" + ? getForcedXaiSearchToolNames( + canonicalModelString, + effectiveMuxProviderOptions.xai?.searchParameters + )?.filter((toolName) => toolName in toolsForStream) + : undefined; + emitStartupBreadcrumb("starting_stream"); const startStreamStartedAt = Date.now(); const streamResult = await this.streamManager.startStream( @@ -3013,7 +3034,8 @@ export class AIService extends EventEmitter { modelFallback, toolSearchRuntime?.state, activeTurnThinkingOverride, - rebuildProviderOptionsForThinkingLevel + rebuildProviderOptionsForThinkingLevel, + forcedFirstStepToolNames ); recordStartupPhaseTiming("startStreamMs", startStreamStartedAt); diff --git a/src/node/services/providerModelFactory.test.ts b/src/node/services/providerModelFactory.test.ts index 54b3595a9f..362d4151f3 100644 --- a/src/node/services/providerModelFactory.test.ts +++ b/src/node/services/providerModelFactory.test.ts @@ -1,4 +1,6 @@ import { describe, expect, it } from "bun:test"; +import { generateText, type Tool } from "ai"; +import { xai } from "@ai-sdk/xai"; import { writeFile } from "node:fs/promises"; import * as fs from "fs"; import * as os from "os"; @@ -20,6 +22,7 @@ import { resolveAIProviderHeaderSource, resolveOpenAIWebSocketResponsesUrl, wrapFetchWithAnthropicCacheControl, + wrapFetchWithXAIServiceTier, } from "./providerModelFactory"; import { hasLanguageModelCleanup } from "./languageModelCleanup"; import type { DevToolsService } from "./devToolsService"; @@ -500,6 +503,112 @@ describe("ProviderModelFactory.createModel", () => { }); }); +describe("ProviderModelFactory xAI API selection", () => { + it("uses Responses for Grok 4.5 so exact billed cost metadata is available", async () => { + await withTempConfig(async (config, factory) => { + config.saveProvidersConfig({ xai: { apiKey: "xai-test-key" } }); + + const result = await factory.createModel("xai:grok-4.5"); + + expect(result.success).toBe(true); + if (!result.success) return; + expect((result.data as { provider?: unknown }).provider).toBe("xai.responses"); + }); + }); + + it("surfaces exact xAI billed cost metadata through the installed Responses SDK", async () => { + await withTempConfig(async (config, factory) => { + const originalXaiRegistry = PROVIDER_REGISTRY.xai; + config.saveProvidersConfig({ xai: { apiKey: "xai-test-key" } }); + + PROVIDER_REGISTRY.xai = async () => { + const module = await originalXaiRegistry(); + return { + ...module, + createXai: (options) => { + const responseFetch = Object.assign( + () => + Promise.resolve( + new Response( + JSON.stringify({ + id: "resp_test", + created_at: 1, + model: "grok-4.5", + object: "response", + output: [ + { + type: "message", + role: "assistant", + content: [{ type: "output_text", text: "ok", annotations: [] }], + id: "msg_test", + status: "completed", + }, + ], + usage: { + input_tokens: 10, + output_tokens: 2, + total_tokens: 12, + cost_in_usd_ticks: 12_345, + }, + status: "completed", + }), + { headers: { "content-type": "application/json" } } + ) + ), + fetch + ) as typeof fetch; + return module.createXai({ ...options, fetch: responseFetch }); + }, + }; + }; + + try { + const result = await factory.createModel("xai:grok-4.5"); + expect(result.success).toBe(true); + if (!result.success) return; + + const generated = await generateText({ + model: result.data, + prompt: "hi", + tools: { + x_search: xai.tools.xSearch({ + // xAI documents a 20-handle limit; exercise >10 to guard the SDK patch. + allowedXHandles: Array.from({ length: 11 }, (_, index) => `handle_${index}`), + }) as Tool, + }, + }); + expect(generated.providerMetadata).toEqual({ xai: { costInUsdTicks: 12_345 } }); + } finally { + PROVIDER_REGISTRY.xai = originalXaiRegistry; + } + }); + }); + + it("uses Responses for Grok 4.5 aliases", async () => { + await withTempConfig(async (config, factory) => { + config.saveProvidersConfig({ xai: { apiKey: "xai-test-key" } }); + + const result = await factory.createModel("xai:grok-4.5-latest"); + + expect(result.success).toBe(true); + if (!result.success) return; + expect((result.data as { provider?: unknown }).provider).toBe("xai.responses"); + }); + }); + + it("keeps legacy custom Grok model strings on Chat Completions", async () => { + await withTempConfig(async (config, factory) => { + config.saveProvidersConfig({ xai: { apiKey: "xai-test-key" } }); + + const result = await factory.createModel("xai:grok-4-1-fast"); + + expect(result.success).toBe(true); + if (!result.success) return; + expect((result.data as { provider?: unknown }).provider).toBe("xai.chat"); + }); + }); +}); + describe("ProviderModelFactory GitHub Copilot", () => { it("creates routed gpt-5.5 models with the chat completions API mode", async () => { await withTempConfig(async (config, factory) => { @@ -1633,6 +1742,38 @@ function parseSentBody(call: CapturedFetchCall): Record { return JSON.parse(call.init.body as string) as Record; } +describe("wrapFetchWithXAIServiceTier", () => { + it("injects priority processing into xAI request bodies", async () => { + const { calls, fakeFetch } = createCapturingFetch(); + const wrapped = wrapFetchWithXAIServiceTier(fakeFetch, "priority"); + + await wrapped("https://api.x.ai/v1/chat/completions", { + method: "POST", + headers: { "content-length": "123", "content-type": "application/json" }, + body: JSON.stringify({ model: "grok-4.5", messages: [] }), + }); + + expect(calls).toHaveLength(1); + expect(parseSentBody(calls[0])).toEqual({ + model: "grok-4.5", + messages: [], + service_tier: "priority", + }); + expect(new Headers(calls[0].init.headers).has("content-length")).toBe(false); + }); + + it("leaves requests unchanged when no tier is configured", async () => { + const { calls, fakeFetch } = createCapturingFetch(); + const wrapped = wrapFetchWithXAIServiceTier(fakeFetch); + const body = JSON.stringify({ model: "grok-4.5", messages: [] }); + + await wrapped("https://api.x.ai/v1/chat/completions", { method: "POST", body }); + + expect(calls).toHaveLength(1); + expect(calls[0].init.body).toBe(body); + }); +}); + // Effort "xhigh" and thinking.display flow through the SDK directly as of // @ai-sdk/anthropic 4.0.11 (see buildProviderOptions), so the wrapper must NOT // rewrite reasoning fields — it only normalizes cache_control. diff --git a/src/node/services/providerModelFactory.ts b/src/node/services/providerModelFactory.ts index e7bc771b2d..9fe48c5359 100644 --- a/src/node/services/providerModelFactory.ts +++ b/src/node/services/providerModelFactory.ts @@ -3,7 +3,7 @@ import { createOpenAICompatible } from "@ai-sdk/openai-compatible"; import type { XaiProviderOptions } from "@ai-sdk/xai"; import { fromNodeProviderChain } from "@aws-sdk/credential-providers"; import { wrapLanguageModel, type LanguageModel } from "ai"; -import type { ThinkingLevel } from "@/common/types/thinking"; +import { isGrok45Model, type ThinkingLevel } from "@/common/types/thinking"; import { Ok, Err } from "@/common/types/result"; import type { Result } from "@/common/types/result"; import type { SendMessageError } from "@/common/types/errors"; @@ -20,7 +20,7 @@ import { import { parseCodexOauthAuth } from "@/node/utils/codexOauthAuth"; import type { Config, ProviderConfig, ProvidersConfig } from "@/node/config"; import type { MuxProviderOptions } from "@/common/types/providerOptions"; -import type { ServiceTier } from "@/common/config/schemas/providersConfig"; +import type { ServiceTier, XAIServiceTier } from "@/common/config/schemas/providersConfig"; import type { ExternalSecretResolver } from "@/common/types/secrets"; import { isOpReference } from "@/common/utils/opRef"; import { resolveConfigBaseUrl } from "@/common/utils/providers/baseUrl"; @@ -208,6 +208,43 @@ export function resolveOpenAIWebSocketResponsesUrl(baseURL: unknown): string | u return url.toString(); } +/** + * Add xAI's service_tier request field until @ai-sdk/xai exposes it directly. + * Priority Processing is a scheduling/billing choice, not a separate model id. + */ +export function wrapFetchWithXAIServiceTier( + baseFetch: typeof fetch, + serviceTier?: XAIServiceTier +): typeof fetch { + if (serviceTier == null) { + return baseFetch; + } + + const tieredFetch = async ( + input: Parameters[0], + init?: Parameters[1] + ): Promise => { + if (init?.method?.toUpperCase() !== "POST" || typeof init.body !== "string") { + return baseFetch(input, init); + } + + try { + const body = JSON.parse(init.body) as Record; + const headers = new Headers(init.headers); + headers.delete("content-length"); + return baseFetch(input, { + ...init, + headers, + body: JSON.stringify({ ...body, service_tier: serviceTier }), + }); + } catch { + return baseFetch(input, init); + } + }; + + return Object.assign(tieredFetch, baseFetch) as typeof fetch; +} + type FetchWithBunExtensions = typeof fetch & { preconnect?: typeof fetch extends { preconnect: infer P } ? P : unknown; certificate?: typeof fetch extends { certificate: infer C } ? C : unknown; @@ -1445,8 +1482,9 @@ export class ProviderModelFactory { const baseFetch = getProviderFetch(providerConfig); const { apiKey: _apiKey, baseURL, headers, ...extraOptions } = providerConfig; - const { searchParameters, ...restOptions } = extraOptions as { + const { searchParameters, serviceTier, ...restOptions } = extraOptions as { searchParameters?: Record; + serviceTier?: unknown; } & Record; if (searchParameters && muxProviderOptions) { @@ -1459,8 +1497,12 @@ export class ProviderModelFactory { }; } + const configuredServiceTier = + serviceTier === "default" || serviceTier === "priority" ? serviceTier : undefined; + const effectiveServiceTier = muxProviderOptions?.xai?.serviceTier ?? configuredServiceTier; + const { createXai } = await PROVIDER_REGISTRY.xai(); - const providerFetch = baseFetch; + const providerFetch = wrapFetchWithXAIServiceTier(baseFetch, effectiveServiceTier); const provider = createXai({ apiKey: resolvedApiKey, baseURL: creds.baseUrl ?? baseURL, @@ -1468,10 +1510,12 @@ export class ProviderModelFactory { ...restOptions, fetch: providerFetch, }); - // AI SDK 7 switched xai(modelId) to the Responses API by default. - // Pin the Chat Completions API to preserve pre-upgrade behavior - // (e.g. providerOptions.xai.searchParameters routing). - return Ok(provider.chat(modelId)); + // Grok 4.5 uses the Responses API so @ai-sdk/xai surfaces exact billed + // cost metadata (including Priority Processing). Keep older custom model + // strings on Chat Completions for legacy search_parameters compatibility. + return Ok( + isGrok45Model(`xai:${modelId}`) ? provider.responses(modelId) : provider.chat(modelId) + ); } // Handle Ollama provider diff --git a/src/node/services/providerService.test.ts b/src/node/services/providerService.test.ts index 174a1825b0..6eb5a78df5 100644 --- a/src/node/services/providerService.test.ts +++ b/src/node/services/providerService.test.ts @@ -233,6 +233,30 @@ describe("ProviderService.getConfig", () => { }); } + it("surfaces only supported xAI processing tiers", () => { + withTempConfig((config, service) => { + config.saveProvidersConfig({ + xai: { + apiKey: "xai-key", + serviceTier: "priority", + fastModePreviousServiceTier: "default", + }, + }); + expect(service.getConfig().xai.serviceTier).toBe("priority"); + expect(service.getConfig().xai.fastModePreviousServiceTier).toBe("default"); + + config.saveProvidersConfig({ + xai: { + apiKey: "xai-key", + serviceTier: "flex", + fastModePreviousServiceTier: "flex", + }, + } as Parameters[0]); + expect(service.getConfig().xai.serviceTier).toBeUndefined(); + expect(service.getConfig().xai.fastModePreviousServiceTier).toBeUndefined(); + }); + }); + it("surfaces non-secret op:// API key references", () => { withTempConfig((config, service) => { const opRef = "op://Personal/Anthropic/credential"; diff --git a/src/node/services/providerService.ts b/src/node/services/providerService.ts index 1d31ee54aa..c5e52b1fdf 100644 --- a/src/node/services/providerService.ts +++ b/src/node/services/providerService.ts @@ -361,26 +361,32 @@ export class ProviderService { models: filteredModels, }; - // OpenAI-specific fields + // Provider processing tier. OpenAI and xAI share the service_tier field, + // but xAI only accepts default/priority. const serviceTier = config.serviceTier; - if ( + const validOpenAIServiceTier = provider === "openai" && (serviceTier === "auto" || serviceTier === "default" || serviceTier === "flex" || - serviceTier === "priority") - ) { + serviceTier === "priority"); + const validXAIServiceTier = + provider === "xai" && (serviceTier === "default" || serviceTier === "priority"); + if (validOpenAIServiceTier || validXAIServiceTier) { providerInfo.serviceTier = serviceTier; } const fastModePreviousServiceTier = config.fastModePreviousServiceTier; - if ( + const validOpenAIFastModePreviousTier = provider === "openai" && (fastModePreviousServiceTier === "auto" || fastModePreviousServiceTier === "default" || fastModePreviousServiceTier === "flex" || - fastModePreviousServiceTier === "unset") - ) { + fastModePreviousServiceTier === "unset"); + const validXAIFastModePreviousTier = + provider === "xai" && + (fastModePreviousServiceTier === "default" || fastModePreviousServiceTier === "unset"); + if (validOpenAIFastModePreviousTier || validXAIFastModePreviousTier) { providerInfo.fastModePreviousServiceTier = fastModePreviousServiceTier; } diff --git a/src/node/services/streamManager.ts b/src/node/services/streamManager.ts index c5ccb4ef3a..a5d1b2ac91 100644 --- a/src/node/services/streamManager.ts +++ b/src/node/services/streamManager.ts @@ -208,6 +208,8 @@ interface StreamRequestConfig { * options for the stream's model. `null` ⇒ not applicable / no-op. */ rebuildProviderOptionsForThinkingLevel?: RebuildProviderOptionsForThinkingLevel; + /** First step must call one of these tools; later steps restore the full toolset. */ + forcedFirstStepToolNames?: string[]; } /** @@ -244,6 +246,7 @@ export interface PreparedModelFallback { * options for the wrong model). */ rebuildProviderOptionsForThinkingLevel?: RebuildProviderOptionsForThinkingLevel; + forcedFirstStepToolNames?: string[]; } export interface ModelFallbackPrepareOptions { @@ -1603,7 +1606,8 @@ export class StreamManager extends EventEmitter { toolSearchState?: ToolSearchStreamState, onToolExecutionStart?: (toolCallId: string) => void, thinkingOverrideState?: ActiveTurnThinkingOverride, - rebuildProviderOptionsForThinkingLevel?: RebuildProviderOptionsForThinkingLevel + rebuildProviderOptionsForThinkingLevel?: RebuildProviderOptionsForThinkingLevel, + forcedFirstStepToolNames?: string[] ): StreamRequestConfig { // Mid-turn thinking overrides mutate providerOptions IN PLACE (the SDK's // per-step deep-merge reads the object passed at streamText() time, so @@ -1684,6 +1688,7 @@ export class StreamManager extends EventEmitter { toolPolicy, toolSearchState, thinkingOverrideState, + forcedFirstStepToolNames, rebuildProviderOptionsForThinkingLevel, }; } @@ -1809,7 +1814,7 @@ export class StreamManager extends EventEmitter { // Trusted: mux builds these messages server-side. allowSystemInMessages: true, abortSignal: abortController.signal, - prepareStep: async ({ messages: stepMessages }) => { + prepareStep: async ({ messages: stepMessages, stepNumber }) => { // streamText runs multiple internal LLM calls (steps) when tools are enabled. // Strip workflow run records from same-turn tool results (history-level redaction in // applyToolOutputRedaction can't see these), then extract supported attachments out of @@ -1827,7 +1832,12 @@ export class StreamManager extends EventEmitter { // activated by tool_catalog_search.execute appear on the following step. // undefined when the feature is inactive, keeping the return value // byte-identical to the pre-feature behavior. - const activeTools = computeActiveToolNames(request.toolSearchState); + const searchedActiveTools = computeActiveToolNames(request.toolSearchState); + const forceFirstStepTools = + stepNumber === 0 && request.forcedFirstStepToolNames?.length + ? request.forcedFirstStepToolNames + : undefined; + const activeTools = forceFirstStepTools ?? searchedActiveTools; // Mid-turn thinking-level change: consume a pending override before // this step's provider request is built. const thinkingOverride = this.applyPendingThinkingOverride(request); @@ -1840,6 +1850,7 @@ export class StreamManager extends EventEmitter { } return { ...(rewritten === stepMessages ? {} : { messages: rewritten }), + ...(forceFirstStepTools !== undefined ? { toolChoice: "required" as const } : {}), ...(activeTools !== undefined ? { activeTools } : {}), // Defense in depth: the in-place request mutation is authoritative // (per-step deep-merge cannot delete keys); returning the rebuilt @@ -1891,7 +1902,8 @@ export class StreamManager extends EventEmitter { modelFallback?: ModelFallbackOptions, toolSearchState?: ToolSearchStreamState, thinkingOverrideState?: ActiveTurnThinkingOverride, - rebuildProviderOptionsForThinkingLevel?: RebuildProviderOptionsForThinkingLevel + rebuildProviderOptionsForThinkingLevel?: RebuildProviderOptionsForThinkingLevel, + forcedFirstStepToolNames?: string[] ): WorkspaceStreamInfo { // abortController is created and linked to the caller-provided abortSignal in startStream(). @@ -1916,7 +1928,8 @@ export class StreamManager extends EventEmitter { toolSearchState, (toolCallId) => this.handleToolExecutionStart(workspaceId, messageId, toolCallId), thinkingOverrideState, - rebuildProviderOptionsForThinkingLevel + rebuildProviderOptionsForThinkingLevel, + forcedFirstStepToolNames ); // Start streaming - this can throw immediately if API key is missing @@ -2628,7 +2641,8 @@ export class StreamManager extends EventEmitter { // hop) with a closure bound to the FALLBACK model. Attached before // createStreamResult below in case the SDK eagerly prepares step 1. streamInfo.request.thinkingOverrideState, - prepared.data.rebuildProviderOptionsForThinkingLevel + prepared.data.rebuildProviderOptionsForThinkingLevel, + prepared.data.forcedFirstStepToolNames ); // createStreamResult may eagerly prepare the first fallback step and update // latestMessages. Clear stale source-step messages before starting it so a @@ -4080,7 +4094,8 @@ export class StreamManager extends EventEmitter { modelFallback?: ModelFallbackOptions, toolSearchState?: ToolSearchStreamState, thinkingOverrideState?: ActiveTurnThinkingOverride, - rebuildProviderOptionsForThinkingLevel?: RebuildProviderOptionsForThinkingLevel + rebuildProviderOptionsForThinkingLevel?: RebuildProviderOptionsForThinkingLevel, + forcedFirstStepToolNames?: string[] ): Promise> { const typedWorkspaceId = workspaceId as WorkspaceId; @@ -4166,7 +4181,8 @@ export class StreamManager extends EventEmitter { modelFallback, toolSearchState, thinkingOverrideState, - rebuildProviderOptionsForThinkingLevel + rebuildProviderOptionsForThinkingLevel, + forcedFirstStepToolNames ); // Guard against a narrow race: diff --git a/tests/ipc/providers/xaiGrok45.test.ts b/tests/ipc/providers/xaiGrok45.test.ts new file mode 100644 index 0000000000..67e13976dc --- /dev/null +++ b/tests/ipc/providers/xaiGrok45.test.ts @@ -0,0 +1,75 @@ +import { KNOWN_MODELS } from "@/common/constants/knownModels"; +import { isStreamEnd } from "@/common/orpc/types"; +import { + assertStreamSuccess, + configureTestRetries, + createStreamCollector, + sendMessageWithModel, +} from "../helpers"; +import { setupWorkspace, shouldRunIntegrationTests, validateApiKeys } from "../setup"; + +const describeIntegration = shouldRunIntegrationTests() ? describe : describe.skip; + +if (shouldRunIntegrationTests()) { + validateApiKeys(["XAI_API_KEY"]); +} + +describeIntegration("xAI Grok 4.5 integration", () => { + configureTestRetries(3); + + test("streams a priority request and reports exact billed cost metadata", async () => { + const { env, workspaceId, cleanup } = await setupWorkspace("xai", "grok-4-5"); + const collector = createStreamCollector(env.orpc, workspaceId); + collector.start(); + + try { + const result = await sendMessageWithModel( + env, + workspaceId, + "Reply with exactly: GROK45_OK", + KNOWN_MODELS.GROK_45.id, + { + // Grok 4.5's built-in minimum thinking floor is medium. + thinkingLevel: "medium", + providerOptions: { + xai: { + serviceTier: "priority", + searchParameters: { mode: "off" }, + }, + }, + } + ); + + expect(result.success).toBe(true); + + const terminalEvent = await Promise.race([ + collector.waitForEvent("stream-end", 60_000), + collector.waitForEvent("stream-error", 60_000), + ]); + if (!terminalEvent) { + throw new Error("Expected terminal stream event from Grok 4.5"); + } + if (terminalEvent.type === "stream-error") { + throw new Error(`Grok 4.5 stream failed: ${terminalEvent.error}`); + } + if (!isStreamEnd(terminalEvent)) { + throw new Error(`Expected stream-end event, received ${terminalEvent.type}`); + } + const streamEnd = terminalEvent; + + assertStreamSuccess(collector); + expect(streamEnd.metadata.model).toBe(KNOWN_MODELS.GROK_45.id); + expect(streamEnd.metadata.thinkingLevel).toBe("medium"); + + const xaiMetadata = streamEnd.metadata.providerMetadata?.xai as + | { costInUsdTicks?: unknown } + | undefined; + expect(typeof xaiMetadata?.costInUsdTicks).toBe("number"); + expect(xaiMetadata?.costInUsdTicks).toBeGreaterThan(0); + expect(collector.getDeltas().join("").trim().length).toBeGreaterThan(0); + } finally { + collector.stop(); + await cleanup(); + } + }, 90_000); +}); diff --git a/tests/ui/agents/thinkingSelector.test.ts b/tests/ui/agents/thinkingSelector.test.ts index cff95612f4..a305a5c61f 100644 --- a/tests/ui/agents/thinkingSelector.test.ts +++ b/tests/ui/agents/thinkingSelector.test.ts @@ -14,6 +14,7 @@ import { readPersistedState } from "@/browser/hooks/usePersistedState"; import { formatModelDisplayName } from "@/common/utils/ai/modelDisplay"; import { shouldRunIntegrationTests } from "../../testUtils"; +import { setupProviders } from "../../ipc/setup"; import { createAppHarness } from "../harness"; const describeIntegration = shouldRunIntegrationTests() ? describe : describe.skip; @@ -91,7 +92,12 @@ async function expectPressed(toggle: HTMLElement, pressed: boolean): Promise { test("selects effort, gates Pro, and exposes fast mode with a closed-state indicator", async () => { - const harness = await createAppHarness({ branchPrefix: "thinking-selector" }); + const harness = await createAppHarness({ + branchPrefix: "thinking-selector", + beforeRenderEnvironment: async (env) => { + await setupProviders(env, { xai: { apiKey: "dummy" } }); + }, + }); try { const { container } = harness.view; @@ -167,12 +173,34 @@ describeIntegration("Thinking selector", () => { } fireEvent.click(container.querySelector("[data-thinking-selector-trigger]")!); - // Workspace-scoped Pro state survives model switches; fast mode is global - // provider config, so its indicator remains active too. + // Workspace-scoped Pro state survives model switches; OpenAI fast mode remains + // in its provider config and becomes active again when returning to OpenAI. await selectModel(container, harness.workspaceId, SOL_MODEL); menu = await openThinkingSelector(container); await expectPressed(within(menu).getByRole("button", { name: /Pro mode/i }), true); await expectPressed(within(menu).getByRole("button", { name: /Fast mode/i }), true); + fireEvent.click(container.querySelector("[data-thinking-selector-trigger]")!); + + // Grok uses the same thinking selector with its native low/medium/high ladder, + // while Fast mode writes xAI's priority tier instead of OpenAI's. + await selectModel(container, harness.workspaceId, KNOWN_MODELS.GROK_45.id); + menu = await openThinkingSelector(container); + if (menu.querySelector('[data-component="ProModeToggle"]')) { + throw new Error("Pro toggle should not render for Grok"); + } + if (within(menu).queryByRole("option", { name: "Off" })) { + throw new Error("Grok should not offer thinking Off"); + } + within(menu).getByRole("option", { name: "Medium" }); + within(menu).getByRole("option", { name: "High" }); + + const grokFastToggle = within(menu).getByRole("button", { name: /Fast mode/i }); + await expectPressed(grokFastToggle, false); + fireEvent.click(grokFastToggle); + await expectPressed(grokFastToggle, true); + if (!container.querySelector("[data-fast-mode-indicator]")) { + throw new Error("Grok fast-mode indicator was not rendered"); + } } finally { await harness.dispose(); } diff --git a/tests/ui/compaction/contextExceeded.test.ts b/tests/ui/compaction/contextExceeded.test.ts index 7593456530..5f9a8aab9e 100644 --- a/tests/ui/compaction/contextExceeded.test.ts +++ b/tests/ui/compaction/contextExceeded.test.ts @@ -32,13 +32,10 @@ describeIntegration("Context exceeded compaction suggestion (UI)", () => { await cleanupSharedRepo(); }); - test("auto-compacts when a higher-context model is available", async () => { + test("shows manual compaction when no higher-context model is available", async () => { await withSharedWorkspace("openai", async ({ env, workspaceId, metadata }) => { const cleanupDom = installDom(); - await setupProviders(env, { xai: { apiKey: "dummy" } }); - const expectedCompactionCommand = "/compact -m xai:grok-4-1-fast"; - const apiClient = env.orpc as unknown as APIClient; const view = renderApp({ apiClient, metadata }); @@ -68,13 +65,12 @@ describeIntegration("Context exceeded compaction suggestion (UI)", () => { }, }); - // Auto-compaction should trigger automatically when context_exceeded occurs - // and a higher-context model suggestion is available. - // We assert on the rendered /compact command (from muxMetadata.rawCommand). + // GPT-5.6 already has the largest curated context window, so removing + // legacy Grok models should leave the explicit manual recovery action. await waitFor( () => { - if (!view.container.textContent?.includes(expectedCompactionCommand)) { - throw new Error(`Expected auto-compaction command: ${expectedCompactionCommand}`); + if (!view.container.textContent?.includes("Insert /compact")) { + throw new Error("Expected manual compaction action"); } }, { timeout: 30_000 } diff --git a/tests/ui/harness/createAppHarness.ts b/tests/ui/harness/createAppHarness.ts index 64f5433baf..238daf8f66 100644 --- a/tests/ui/harness/createAppHarness.ts +++ b/tests/ui/harness/createAppHarness.ts @@ -46,6 +46,8 @@ export async function createAppHarness(options?: { branchPrefix?: string; aiMode?: "mock-router" | "none"; runtimeConfig?: RuntimeConfig; + /** Configure backend state that must exist before the full app reads it. */ + beforeRenderEnvironment?: (env: TestEnvironment) => void | Promise; /** * Optional hook to set up DOM-dependent globals (e.g. localStorage) before * the App is rendered. Receives the created workspace id so tests can seed @@ -66,6 +68,8 @@ export async function createAppHarness(options?: { let cleanupDom: (() => void) | undefined; try { + await options?.beforeRenderEnvironment?.(env); + const trunkBranch = await detectDefaultTrunkBranch(repoPath); const branchName = generateBranchName(options?.branchPrefix ?? "ui");