diff --git a/package-lock.json b/package-lock.json index 74d3c554..5ad3edc7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "@anthropic-ai/sdk": "^0.39.0", "@electric-sql/pglite": "0.2.12", - "@google/generative-ai": "^0.24.0", + "@google/genai": "^1.35.0", "@lexical/clipboard": "^0.17.1", "@lexical/react": "^0.17.1", "@modelcontextprotocol/sdk": "^1.9.0", @@ -1251,12 +1251,37 @@ "version": "0.2.8", "license": "MIT" }, - "node_modules/@google/generative-ai": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@google/generative-ai/-/generative-ai-0.24.0.tgz", - "integrity": "sha512-fnEITCGEB7NdX0BhoYZ/cq/7WPZ1QS5IzJJfC3Tg/OwkvBetMiVJciyaan297OvE4B9Jg1xvo0zIazX/9sGu1Q==", + "node_modules/@google/genai": { + "version": "1.35.0", + "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.35.0.tgz", + "integrity": "sha512-ZC1d0PSM5eS73BpbVIgL3ZsmXeMKLVJurxzww1Z9axy3B2eUB3ioEytbQt4Qu0Od6qPluKrTDew9pSi9kEuPaw==", + "license": "Apache-2.0", + "dependencies": { + "google-auth-library": "^10.3.0", + "ws": "^8.18.0" + }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" + }, + "peerDependencies": { + "@modelcontextprotocol/sdk": "^1.24.0" + }, + "peerDependenciesMeta": { + "@modelcontextprotocol/sdk": { + "optional": true + } + } + }, + "node_modules/@hono/node-server": { + "version": "1.19.8", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.8.tgz", + "integrity": "sha512-0/g2lIOPzX8f3vzW1ggQgvG5mjtFBDBHFAzI5SFAi2DzSqS9luJwqg9T6O/gKYLi+inS7eNxBeIFkkghIPvrMA==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" } }, "node_modules/@humanwhocodes/config-array": { @@ -1311,6 +1336,102 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "dev": true, @@ -2015,23 +2136,82 @@ "peer": true }, "node_modules/@modelcontextprotocol/sdk": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.9.0.tgz", - "integrity": "sha512-Jq2EUCQpe0iyO5FGpzVYDNFR6oR53AIrwph9yWl7uSc7IWUMsrmpmSaTGra5hQNunXpM+9oit85p924jWuHzUA==", + "version": "1.25.2", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.25.2.tgz", + "integrity": "sha512-LZFeo4F9M5qOhC/Uc1aQSrBHxMrvxett+9KLHt7OhcExtoiRN9DKgbZffMP/nxjutWDQpfMDfP3nkHI4X9ijww==", + "license": "MIT", "dependencies": { + "@hono/node-server": "^1.19.7", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", - "cross-spawn": "^7.0.3", + "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", + "jose": "^6.1.1", + "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", - "zod": "^3.23.8", - "zod-to-json-schema": "^3.24.1" + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.0" }, "engines": { "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/zod": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.5.tgz", + "integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/zod-to-json-schema": { + "version": "3.25.1", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz", + "integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25 || ^4" } }, "node_modules/@nodelib/fs.scandir": { @@ -2066,6 +2246,16 @@ "node": ">= 8" } }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@radix-ui/primitive": { "version": "1.1.0", "license": "MIT" @@ -3224,6 +3414,15 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/agentkeepalive": { "version": "4.5.0", "license": "MIT", @@ -3249,6 +3448,45 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, "node_modules/ansi-escapes": { "version": "4.3.2", "dev": true, @@ -3276,7 +3514,6 @@ }, "node_modules/ansi-regex": { "version": "5.0.1", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -3284,7 +3521,6 @@ }, "node_modules/ansi-styles": { "version": "4.3.0", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -3638,6 +3874,15 @@ } ] }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/body-parser": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", @@ -3726,6 +3971,12 @@ "node-int64": "^0.4.0" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, "node_modules/buffer-from": { "version": "1.1.2", "dev": true, @@ -3949,7 +4200,6 @@ }, "node_modules/color-convert": { "version": "2.0.1", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -3960,7 +4210,6 @@ }, "node_modules/color-name": { "version": "1.1.4", - "dev": true, "license": "MIT" }, "node_modules/combined-stream": { @@ -4084,6 +4333,15 @@ "version": "3.1.3", "license": "MIT" }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/data-view-buffer": { "version": "1.0.1", "dev": true, @@ -4859,6 +5117,21 @@ "node": ">= 0.4" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -4896,7 +5169,6 @@ }, "node_modules/emoji-regex": { "version": "8.0.0", - "dev": true, "license": "MIT" }, "node_modules/encodeurl": { @@ -6100,7 +6372,6 @@ }, "node_modules/fast-deep-equal": { "version": "3.1.3", - "dev": true, "license": "MIT" }, "node_modules/fast-glob": { @@ -6139,6 +6410,22 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/fastq": { "version": "1.17.1", "dev": true, @@ -6166,6 +6453,38 @@ "bser": "2.1.1" } }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/fetch-blob/node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/file-entry-cache": { "version": "6.0.1", "dev": true, @@ -6264,6 +6583,34 @@ "is-callable": "^1.1.3" } }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/form-data": { "version": "4.0.0", "license": "MIT", @@ -6297,6 +6644,18 @@ "node": ">= 12.20" } }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -6372,6 +6731,103 @@ "resolved": "https://registry.npmjs.org/fuzzysort/-/fuzzysort-3.1.0.tgz", "integrity": "sha512-sR9BNCjBg6LNgwvxlBd0sBABvQitkLzoVY9MYYROQVX/FvfJ4Mai9LsGhDgd8qYdds0bY77VzYd5iuB+v5rwQQ==" }, + "node_modules/gaxios": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.3.tgz", + "integrity": "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2", + "rimraf": "^5.0.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/gaxios/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "license": "ISC", + "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" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gaxios/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gaxios/node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/gaxios/node_modules/rimraf": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", + "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", + "license": "ISC", + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gcp-metadata": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", + "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^7.0.0", + "google-logging-utils": "^1.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "dev": true, @@ -6576,6 +7032,33 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/google-auth-library": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.5.0.tgz", + "integrity": "sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^7.0.0", + "gcp-metadata": "^8.0.0", + "google-logging-utils": "^1.0.0", + "gtoken": "^8.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-logging-utils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", + "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -6617,6 +7100,19 @@ "undici-types": "~5.26.4" } }, + "node_modules/gtoken": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-8.0.0.tgz", + "integrity": "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==", + "license": "MIT", + "dependencies": { + "gaxios": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/has-bigints": { "version": "1.0.2", "dev": true, @@ -6794,6 +7290,16 @@ "node": "*" } }, + "node_modules/hono": { + "version": "4.11.4", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.4.tgz", + "integrity": "sha512-U7tt8JsyrxSRKspfhtLET79pU8K+tInj5QZXs1jSugO1Vq5dFj3kmZsRldo29mTBfcjDRVRXrEZ6LS63Cog9ZA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=16.9.0" + } + }, "node_modules/html-escaper": { "version": "2.0.2", "dev": true, @@ -6822,6 +7328,19 @@ "node": ">= 0.8" } }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/human-signals": { "version": "2.1.0", "license": "Apache-2.0", @@ -7103,7 +7622,6 @@ }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -7440,6 +7958,21 @@ "node": ">= 0.4" } }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/jake": { "version": "10.9.2", "dev": true, @@ -8095,6 +8628,15 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/jose": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz", + "integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/js-tiktoken": { "version": "1.0.15", "resolved": "https://registry.npmjs.org/js-tiktoken/-/js-tiktoken-1.0.15.tgz", @@ -8128,6 +8670,15 @@ "node": ">=6" } }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "dev": true, @@ -8143,6 +8694,12 @@ "dev": true, "license": "MIT" }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause" + }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "dev": true, @@ -8181,6 +8738,27 @@ "node": ">=4.0" } }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/keyv": { "version": "4.5.4", "dev": true, @@ -9311,6 +9889,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/moment": { "version": "2.29.4", "dev": true, @@ -9707,6 +10294,12 @@ "node": ">=6" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, "node_modules/parent-module": { "version": "1.0.1", "dev": true, @@ -9808,6 +10401,28 @@ "dev": true, "license": "MIT" }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, "node_modules/path-to-regexp": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", @@ -10473,6 +11088,15 @@ "node": ">=0.10.0" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve": { "version": "1.22.8", "dev": true, @@ -10979,7 +11603,21 @@ }, "node_modules/string-width": { "version": "4.2.3", - "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -11084,7 +11722,19 @@ }, "node_modules/strip-ansi": { "version": "6.0.1", - "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -11906,6 +12556,24 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "license": "ISC" @@ -11926,8 +12594,6 @@ "version": "8.18.1", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz", "integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==", - "optional": true, - "peer": true, "engines": { "node": ">=10.0.0" }, diff --git a/package.json b/package.json index 3964d8b6..15fe7b5e 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "dependencies": { "@anthropic-ai/sdk": "^0.39.0", "@electric-sql/pglite": "0.2.12", - "@google/generative-ai": "^0.24.0", + "@google/genai": "^1.35.0", "@lexical/clipboard": "^0.17.1", "@lexical/react": "^0.17.1", "@modelcontextprotocol/sdk": "^1.9.0", @@ -83,4 +83,4 @@ "vscode-diff": "^2.1.1", "zod": "^3.23.8" } -} \ No newline at end of file +} diff --git a/src/components/settings/modals/AddChatModelModal.tsx b/src/components/settings/modals/AddChatModelModal.tsx index 5f11edf3..f96c82ce 100644 --- a/src/components/settings/modals/AddChatModelModal.tsx +++ b/src/components/settings/modals/AddChatModelModal.tsx @@ -103,11 +103,15 @@ function AddChatModelModalComponent({ new Notice(`Provider with ID ${value} not found`) return } - setFormData((prev) => ({ - ...prev, - providerId: value, - providerType: provider.type, - })) + // Cast required because we're changing the discriminant field + setFormData( + (prev) => + ({ + ...prev, + providerId: value, + providerType: provider.type, + }) as ChatModel, + ) }} /> diff --git a/src/components/settings/sections/models/ChatModelSettings.tsx b/src/components/settings/sections/models/ChatModelSettings.tsx index ab50ecf3..e084d3a1 100644 --- a/src/components/settings/sections/models/ChatModelSettings.tsx +++ b/src/components/settings/sections/models/ChatModelSettings.tsx @@ -225,6 +225,158 @@ const MODEL_SETTINGS_REGISTRY: ModelSettingsRegistry[] = [ }, }, + /** + * Gemini model settings + * + * For thinking, see: + * @see https://ai.google.dev/gemini-api/docs/thinking + */ + { + check: (model) => model.providerType === 'gemini', + SettingsComponent: (props: SettingsComponentProps) => { + const { model, plugin, onClose } = props + const typedModel = model as ChatModel & { providerType: 'gemini' } + const [thinkingEnabled, setThinkingEnabled] = useState( + typedModel.thinking?.enabled ?? false, + ) + const [controlMode, setControlMode] = useState<'level' | 'budget'>( + typedModel.thinking?.control_mode ?? 'level', + ) + const [thinkingLevel, setThinkingLevel] = useState( + String(typedModel.thinking?.thinking_level ?? 'high'), + ) + const [thinkingBudget, setThinkingBudget] = useState( + String(typedModel.thinking?.thinking_budget ?? -1), + ) + const [includeThoughts, setIncludeThoughts] = useState( + Boolean(typedModel.thinking?.include_thoughts ?? false), + ) + + const handleSubmit = async () => { + let parsedBudget: number | undefined + if (controlMode === 'budget') { + parsedBudget = parseInt(thinkingBudget, 10) + if (isNaN(parsedBudget)) { + new Notice('Please enter a valid number for thinking budget') + return + } + } + + const updatedModel = { + ...typedModel, + thinking: { + enabled: thinkingEnabled, + control_mode: controlMode, + thinking_level: + controlMode === 'level' + ? (thinkingLevel as 'minimal' | 'low' | 'medium' | 'high') + : undefined, + thinking_budget: + controlMode === 'budget' ? parsedBudget : undefined, + include_thoughts: includeThoughts, + }, + } + + const validationResult = chatModelSchema.safeParse(updatedModel) + if (!validationResult.success) { + new Notice( + validationResult.error.issues.map((v) => v.message).join('\n'), + ) + return + } + + await plugin.setSettings({ + ...plugin.settings, + chatModels: plugin.settings.chatModels.map((m) => + m.id === model.id ? updatedModel : m, + ), + }) + onClose() + } + + return ( + <> + + setThinkingEnabled(value)} + /> + + {thinkingEnabled && ( + <> + + + setControlMode(value as 'level' | 'budget') + } + /> + + {controlMode === 'level' && ( + + setThinkingLevel(value)} + /> + + )} + {controlMode === 'budget' && ( + + setThinkingBudget(value)} + type="number" + /> + + )} + + setIncludeThoughts(value)} + /> + + + )} + + + + + + + ) + }, + }, + // Perplexity settings { check: (model) => diff --git a/src/constants.ts b/src/constants.ts index e46d4802..e992870c 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -9,10 +9,12 @@ export const PGLITE_DB_PATH = '.smtcmp_vector_db.tar.gz' // Default model ids export const DEFAULT_CHAT_MODEL_ID = 'claude-sonnet-4.5' +// gpt-4.1-mini is preferred over gpt-5-mini because gpt-5 models do not support +// predicted outputs, making them significantly slower for apply tasks. export const DEFAULT_APPLY_MODEL_ID = 'gpt-4.1-mini' // Recommended model ids -export const RECOMMENDED_MODELS_FOR_CHAT = ['claude-sonnet-4.5', 'gpt-4.1'] +export const RECOMMENDED_MODELS_FOR_CHAT = ['claude-sonnet-4.5', 'gpt-5.2'] export const RECOMMENDED_MODELS_FOR_APPLY = ['gpt-4.1-mini'] export const RECOMMENDED_MODELS_FOR_EMBEDDING = [ 'openai/text-embedding-3-small', @@ -43,41 +45,25 @@ export const PROVIDER_TYPES_INFO = { supportEmbedding: true, additionalSettings: [], }, - groq: { - label: 'Groq', - defaultProviderId: 'groq', + xai: { + label: 'xAI', + defaultProviderId: 'xai', requireApiKey: true, requireBaseUrl: false, supportEmbedding: false, additionalSettings: [], }, - openrouter: { - label: 'OpenRouter', - defaultProviderId: 'openrouter', + deepseek: { + label: 'DeepSeek', + defaultProviderId: 'deepseek', requireApiKey: true, requireBaseUrl: false, supportEmbedding: false, additionalSettings: [], }, - ollama: { - label: 'Ollama', - defaultProviderId: 'ollama', - requireApiKey: false, - requireBaseUrl: false, - supportEmbedding: true, - additionalSettings: [], - }, - 'lm-studio': { - label: 'LM Studio', - defaultProviderId: 'lm-studio', - requireApiKey: false, - requireBaseUrl: false, - supportEmbedding: true, - additionalSettings: [], - }, - deepseek: { - label: 'DeepSeek', - defaultProviderId: 'deepseek', + mistral: { + label: 'Mistral', + defaultProviderId: 'mistral', requireApiKey: true, requireBaseUrl: false, supportEmbedding: false, @@ -91,20 +77,28 @@ export const PROVIDER_TYPES_INFO = { supportEmbedding: false, additionalSettings: [], }, - mistral: { - label: 'Mistral', - defaultProviderId: 'mistral', + openrouter: { + label: 'OpenRouter', + defaultProviderId: 'openrouter', requireApiKey: true, requireBaseUrl: false, supportEmbedding: false, additionalSettings: [], }, - morph: { - label: 'Morph', - defaultProviderId: 'morph', - requireApiKey: true, + ollama: { + label: 'Ollama', + defaultProviderId: 'ollama', + requireApiKey: false, requireBaseUrl: false, - supportEmbedding: false, + supportEmbedding: true, + additionalSettings: [], + }, + 'lm-studio': { + label: 'LM Studio', + defaultProviderId: 'lm-studio', + requireApiKey: false, + requireBaseUrl: false, + supportEmbedding: true, additionalSettings: [], }, 'azure-openai': { @@ -185,21 +179,21 @@ export const DEFAULT_PROVIDERS: readonly LLMProvider[] = [ id: PROVIDER_TYPES_INFO.gemini.defaultProviderId, }, { - type: 'deepseek', - id: PROVIDER_TYPES_INFO.deepseek.defaultProviderId, + type: 'xai', + id: PROVIDER_TYPES_INFO.xai.defaultProviderId, }, { - type: 'perplexity', - id: PROVIDER_TYPES_INFO.perplexity.defaultProviderId, - }, - { - type: 'groq', - id: PROVIDER_TYPES_INFO.groq.defaultProviderId, + type: 'deepseek', + id: PROVIDER_TYPES_INFO.deepseek.defaultProviderId, }, { type: 'mistral', id: PROVIDER_TYPES_INFO.mistral.defaultProviderId, }, + { + type: 'perplexity', + id: PROVIDER_TYPES_INFO.perplexity.defaultProviderId, + }, { type: 'openrouter', id: PROVIDER_TYPES_INFO.openrouter.defaultProviderId, @@ -212,10 +206,6 @@ export const DEFAULT_PROVIDERS: readonly LLMProvider[] = [ type: 'lm-studio', id: PROVIDER_TYPES_INFO['lm-studio'].defaultProviderId, }, - { - type: 'morph', - id: PROVIDER_TYPES_INFO.morph.defaultProviderId, - }, ] /** @@ -227,8 +217,8 @@ export const DEFAULT_CHAT_MODELS: readonly ChatModel[] = [ { providerType: 'anthropic', providerId: PROVIDER_TYPES_INFO.anthropic.defaultProviderId, - id: 'claude-opus-4.1', - model: 'claude-opus-4-1', + id: 'claude-opus-4.5', + model: 'claude-opus-4-5', }, { providerType: 'anthropic', @@ -245,8 +235,8 @@ export const DEFAULT_CHAT_MODELS: readonly ChatModel[] = [ { providerType: 'openai', providerId: PROVIDER_TYPES_INFO.openai.defaultProviderId, - id: 'gpt-5', - model: 'gpt-5', + id: 'gpt-5.2', + model: 'gpt-5.2', }, { providerType: 'openai', @@ -254,42 +244,12 @@ export const DEFAULT_CHAT_MODELS: readonly ChatModel[] = [ id: 'gpt-5-mini', model: 'gpt-5-mini', }, - { - providerType: 'openai', - providerId: PROVIDER_TYPES_INFO.openai.defaultProviderId, - id: 'gpt-5-nano', - model: 'gpt-5-nano', - }, - { - providerType: 'openai', - providerId: PROVIDER_TYPES_INFO.openai.defaultProviderId, - id: 'gpt-4.1', - model: 'gpt-4.1', - }, { providerType: 'openai', providerId: PROVIDER_TYPES_INFO.openai.defaultProviderId, id: 'gpt-4.1-mini', model: 'gpt-4.1-mini', }, - { - providerType: 'openai', - providerId: PROVIDER_TYPES_INFO.openai.defaultProviderId, - id: 'gpt-4.1-nano', - model: 'gpt-4.1-nano', - }, - { - providerType: 'openai', - providerId: PROVIDER_TYPES_INFO.openai.defaultProviderId, - id: 'gpt-4o', - model: 'gpt-4o', - }, - { - providerType: 'openai', - providerId: PROVIDER_TYPES_INFO.openai.defaultProviderId, - id: 'gpt-4o-mini', - model: 'gpt-4o-mini', - }, { providerType: 'openai', providerId: PROVIDER_TYPES_INFO.openai.defaultProviderId, @@ -300,45 +260,17 @@ export const DEFAULT_CHAT_MODELS: readonly ChatModel[] = [ reasoning_effort: 'medium', }, }, - { - providerType: 'openai', - providerId: PROVIDER_TYPES_INFO.openai.defaultProviderId, - id: 'o3', - model: 'o3', - reasoning: { - enabled: true, - reasoning_effort: 'medium', - }, - }, - { - providerType: 'gemini', - providerId: PROVIDER_TYPES_INFO.gemini.defaultProviderId, - id: 'gemini-2.5-pro', - model: 'gemini-2.5-pro', - }, { providerType: 'gemini', providerId: PROVIDER_TYPES_INFO.gemini.defaultProviderId, - id: 'gemini-2.5-flash', - model: 'gemini-2.5-flash', + id: 'gemini-3-pro-preview', + model: 'gemini-3-pro-preview', }, { providerType: 'gemini', providerId: PROVIDER_TYPES_INFO.gemini.defaultProviderId, - id: 'gemini-2.5-flash-lite', - model: 'gemini-2.5-flash-lite', - }, - { - providerType: 'gemini', - providerId: PROVIDER_TYPES_INFO.gemini.defaultProviderId, - id: 'gemini-2.0-flash', - model: 'gemini-2.0-flash', - }, - { - providerType: 'gemini', - providerId: PROVIDER_TYPES_INFO.gemini.defaultProviderId, - id: 'gemini-2.0-flash-lite', - model: 'gemini-2.0-flash-lite', + id: 'gemini-3-flash-preview', + model: 'gemini-3-flash-preview', }, { providerType: 'deepseek', @@ -353,55 +285,16 @@ export const DEFAULT_CHAT_MODELS: readonly ChatModel[] = [ model: 'deepseek-reasoner', }, { - providerType: 'perplexity', - providerId: PROVIDER_TYPES_INFO.perplexity.defaultProviderId, - id: 'sonar', - model: 'sonar', - web_search_options: { - search_context_size: 'low', - }, - }, - { - providerType: 'perplexity', - providerId: PROVIDER_TYPES_INFO.perplexity.defaultProviderId, - id: 'sonar-pro', - model: 'sonar', - web_search_options: { - search_context_size: 'low', - }, - }, - { - providerType: 'perplexity', - providerId: PROVIDER_TYPES_INFO.perplexity.defaultProviderId, - id: 'sonar-deep-research', - model: 'sonar-deep-research', - web_search_options: { - search_context_size: 'low', - }, + providerType: 'xai', + providerId: PROVIDER_TYPES_INFO.xai.defaultProviderId, + id: 'grok-4-1-fast', + model: 'grok-4-1-fast', }, { - providerType: 'perplexity', - providerId: PROVIDER_TYPES_INFO.perplexity.defaultProviderId, - id: 'sonar-reasoning', - model: 'sonar', - web_search_options: { - search_context_size: 'low', - }, - }, - { - providerType: 'perplexity', - providerId: PROVIDER_TYPES_INFO.perplexity.defaultProviderId, - id: 'sonar-reasoning-pro', - model: 'sonar', - web_search_options: { - search_context_size: 'low', - }, - }, - { - providerType: 'morph', - providerId: PROVIDER_TYPES_INFO.morph.defaultProviderId, - id: 'morph-v0', - model: 'morph-v0', + providerType: 'xai', + providerId: PROVIDER_TYPES_INFO.xai.defaultProviderId, + id: 'grok-4-1-fast-non-reasoning', + model: 'grok-4-1-fast-non-reasoning', }, ] @@ -462,6 +355,8 @@ type ModelPricing = { } export const OPENAI_PRICES: Record = { + 'gpt-5.2': { input: 1.75, output: 14 }, + 'gpt-5.1': { input: 1.25, output: 10 }, 'gpt-5': { input: 1.25, output: 10 }, 'gpt-5-mini': { input: 0.25, output: 2 }, 'gpt-5-nano': { input: 0.05, output: 0.4 }, @@ -478,6 +373,7 @@ export const OPENAI_PRICES: Record = { } export const ANTHROPIC_PRICES: Record = { + 'claude-opus-4-5': { input: 5, output: 25 }, 'claude-opus-4-1': { input: 15, output: 75 }, 'claude-opus-4-0': { input: 15, output: 75 }, 'claude-sonnet-4-5': { input: 3, output: 15 }, @@ -490,3 +386,14 @@ export const ANTHROPIC_PRICES: Record = { // Gemini is currently free for low rate limits export const GEMINI_PRICES: Record = {} + +export const XAI_PRICES: Record = { + 'grok-4-1-fast': { input: 0.2, output: 0.5 }, + 'grok-4-1-fast-non-reasoning': { input: 0.2, output: 0.5 }, +} + +export const DEEPSEEK_PRICES: Record = { + // Model version: DeepSeek-V3.2 + 'deepseek-chat': { input: 0.28, output: 0.42 }, + 'deepseek-reasoner': { input: 0.28, output: 0.42 }, +} diff --git a/src/core/llm/deepseekMessageAdapter.ts b/src/core/llm/deepseekMessageAdapter.ts index 079e17a9..a16ae3c6 100644 --- a/src/core/llm/deepseekMessageAdapter.ts +++ b/src/core/llm/deepseekMessageAdapter.ts @@ -1,8 +1,10 @@ import { ChatCompletion, ChatCompletionChunk, + ChatCompletionMessageParam, } from 'openai/resources/chat/completions' +import { RequestMessage } from '../../types/llm/request' import { LLMResponseNonStreaming, LLMResponseStreaming, @@ -13,6 +15,10 @@ import { OpenAIMessageAdapter } from './openaiMessageAdapter' /** * Adapter for DeepSeek's API that extends OpenAIMessageAdapter to handle the additional * 'reasoning_content' field in DeepSeek's response format while maintaining OpenAI compatibility. + * + * DeepSeek's thinking mode requires `reasoning_content` to be passed back in assistant messages + * during tool call iterations. This adapter stores reasoning in providerMetadata and injects it + * back into API requests. */ export class DeepSeekMessageAdapter extends OpenAIMessageAdapter { protected parseNonStreamingResponse( @@ -20,17 +26,23 @@ export class DeepSeekMessageAdapter extends OpenAIMessageAdapter { ): LLMResponseNonStreaming { return { id: response.id, - choices: response.choices.map((choice) => ({ - finish_reason: choice.finish_reason, - message: { - content: choice.message.content, - reasoning: ( - choice.message as unknown as { reasoning_content?: string } - ).reasoning_content, - role: choice.message.role, - tool_calls: choice.message.tool_calls, - }, - })), + choices: response.choices.map((choice) => { + const reasoningContent = ( + choice.message as unknown as { reasoning_content?: string } + ).reasoning_content + return { + finish_reason: choice.finish_reason, + message: { + content: choice.message.content, + reasoning: reasoningContent, + role: choice.message.role, + tool_calls: choice.message.tool_calls, + providerMetadata: reasoningContent + ? { deepseek: { reasoningContent } } + : undefined, + }, + } + }), created: response.created, model: response.model, object: 'chat.completion', @@ -44,16 +56,23 @@ export class DeepSeekMessageAdapter extends OpenAIMessageAdapter { ): LLMResponseStreaming { return { id: chunk.id, - choices: chunk.choices.map((choice) => ({ - finish_reason: choice.finish_reason ?? null, - delta: { - content: choice.delta.content ?? null, - reasoning: (choice.delta as unknown as { reasoning_content?: string }) - .reasoning_content, - role: choice.delta.role, - tool_calls: choice.delta.tool_calls, - }, - })), + choices: chunk.choices.map((choice) => { + const reasoningContent = ( + choice.delta as unknown as { reasoning_content?: string } + ).reasoning_content + return { + finish_reason: choice.finish_reason ?? null, + delta: { + content: choice.delta.content ?? null, + reasoning: reasoningContent, + role: choice.delta.role, + tool_calls: choice.delta.tool_calls, + providerMetadata: reasoningContent + ? { deepseek: { reasoningContent } } + : undefined, + }, + } + }), created: chunk.created, model: chunk.model, object: 'chat.completion.chunk', @@ -61,4 +80,22 @@ export class DeepSeekMessageAdapter extends OpenAIMessageAdapter { usage: chunk.usage ?? undefined, } } + + protected parseRequestMessage( + message: RequestMessage, + ): ChatCompletionMessageParam { + const baseMessage = super.parseRequestMessage(message) + + if ( + message.role === 'assistant' && + message.providerMetadata?.deepseek?.reasoningContent + ) { + return { + ...baseMessage, + reasoning_content: message.providerMetadata.deepseek.reasoningContent, + } as unknown as ChatCompletionMessageParam + } + + return baseMessage + } } diff --git a/src/core/llm/gemini.ts b/src/core/llm/gemini.ts index 2e80b0f9..570708e3 100644 --- a/src/core/llm/gemini.ts +++ b/src/core/llm/gemini.ts @@ -1,15 +1,15 @@ import { Content, - EnhancedGenerateContentResponse, - FunctionCallPart, - Tool as GeminiTool, - GenerateContentResult, - GenerateContentStreamResult, - GoogleGenerativeAI, + FunctionCall, + GenerateContentResponse, + GoogleGenAI, Part, Schema, - SchemaType, -} from '@google/generative-ai' + ThinkingConfig, + ThinkingLevel, + Tool, + Type, +} from '@google/genai' import { v4 as uuidv4 } from 'uuid' import { ChatModel } from '../../types/chat-model.types' @@ -34,12 +34,6 @@ import { LLMRateLimitExceededException, } from './exception' -/** - * TODO: Consider future migration from '@google/generative-ai' to '@google/genai' (https://github.com/googleapis/js-genai) - * - Current '@google/generative-ai' library will not support newest models and features - * - Not migrating yet as '@google/genai' is still in preview status - */ - /** * Note on OpenAI Compatibility API: * Gemini provides an OpenAI-compatible endpoint (https://ai.google.dev/gemini-api/docs/openai) @@ -50,7 +44,7 @@ import { export class GeminiProvider extends BaseLLMProvider< Extract > { - private client: GoogleGenerativeAI + private client: GoogleGenAI private apiKey: string constructor(provider: Extract) { @@ -59,7 +53,7 @@ export class GeminiProvider extends BaseLLMProvider< throw new Error('Gemini does not support custom base URL') } - this.client = new GoogleGenerativeAI(provider.apiKey ?? '') + this.client = new GoogleGenAI({ apiKey: provider.apiKey ?? '' }) this.apiKey = provider.apiKey ?? '' } @@ -85,32 +79,25 @@ export class GeminiProvider extends BaseLLMProvider< : undefined try { - const model = this.client.getGenerativeModel({ + const result = await this.client.models.generateContent({ model: request.model, - generationConfig: { + contents: request.messages + .map((message) => GeminiProvider.parseRequestMessage(message)) + .filter((m): m is Content => m !== null), + config: { maxOutputTokens: request.max_tokens, temperature: request.temperature, topP: request.top_p, presencePenalty: request.presence_penalty, frequencyPenalty: request.frequency_penalty, - }, - systemInstruction: systemInstruction, - }) - - const result = await model.generateContent( - { systemInstruction: systemInstruction, - contents: request.messages - .map((message) => GeminiProvider.parseRequestMessage(message)) - .filter((m): m is Content => m !== null), + abortSignal: options?.signal, tools: request.tools?.map((tool) => GeminiProvider.parseRequestTool(tool), ), + thinkingConfig: GeminiProvider.buildThinkingConfig(model), }, - { - signal: options?.signal, - }, - ) + }) const messageId = crypto.randomUUID() // Gemini does not return a message id return GeminiProvider.parseNonStreamingResponse( @@ -156,32 +143,25 @@ export class GeminiProvider extends BaseLLMProvider< : undefined try { - const model = this.client.getGenerativeModel({ + const stream = await this.client.models.generateContentStream({ model: request.model, - generationConfig: { + contents: request.messages + .map((message) => GeminiProvider.parseRequestMessage(message)) + .filter((m): m is Content => m !== null), + config: { maxOutputTokens: request.max_tokens, temperature: request.temperature, topP: request.top_p, presencePenalty: request.presence_penalty, frequencyPenalty: request.frequency_penalty, - }, - systemInstruction: systemInstruction, - }) - - const stream = await model.generateContentStream( - { systemInstruction: systemInstruction, - contents: request.messages - .map((message) => GeminiProvider.parseRequestMessage(message)) - .filter((m): m is Content => m !== null), + abortSignal: options?.signal, tools: request.tools?.map((tool) => GeminiProvider.parseRequestTool(tool), ), + thinkingConfig: GeminiProvider.buildThinkingConfig(model), }, - { - signal: options?.signal, - }, - ) + }) const messageId = crypto.randomUUID() // Gemini does not return a message id return this.streamResponseGenerator(stream, request.model, messageId) @@ -202,11 +182,11 @@ export class GeminiProvider extends BaseLLMProvider< } private async *streamResponseGenerator( - stream: GenerateContentStreamResult, + stream: AsyncGenerator, model: string, messageId: string, ): AsyncIterable { - for await (const chunk of stream.stream) { + for await (const chunk of stream) { yield GeminiProvider.parseStreamingResponseChunk(chunk, model, messageId) } } @@ -245,28 +225,47 @@ export class GeminiProvider extends BaseLLMProvider< } } case 'assistant': { - const contentParts: Part[] = [ - ...(message.content === '' ? [] : [{ text: message.content }]), - ...(message.tool_calls?.map((toolCall): FunctionCallPart => { + const thoughtSignature = + message.providerMetadata?.gemini?.thoughtSignature + const hasToolCalls = message.tool_calls && message.tool_calls.length > 0 + + const contentParts: Part[] = [] + + // Add text content part + if (message.content !== '') { + // If no tool calls and we have a signature, attach it to the text part + if (!hasToolCalls && thoughtSignature) { + contentParts.push({ text: message.content, thoughtSignature }) + } else { + contentParts.push({ text: message.content }) + } + } + + // Add function call parts + if (message.tool_calls) { + message.tool_calls.forEach((toolCall, index) => { + let args: Record try { - const args = JSON.parse(toolCall.arguments ?? '{}') - return { - functionCall: { - name: toolCall.name, - args, - }, - } - } catch (error) { - // If the arguments are not valid JSON, return an empty object - return { - functionCall: { - name: toolCall.name, - args: {}, - }, - } + args = JSON.parse(toolCall.arguments ?? '{}') + } catch { + args = {} + } + + const part: Part = { + functionCall: { + name: toolCall.name, + args, + }, + } + + // Attach signature to the first function call part + if (index === 0 && thoughtSignature) { + part.thoughtSignature = thoughtSignature } - }) ?? []), - ] + + contentParts.push(part) + }) + } if (contentParts.length === 0) { return null @@ -294,65 +293,68 @@ export class GeminiProvider extends BaseLLMProvider< } static parseNonStreamingResponse( - response: GenerateContentResult, + response: GenerateContentResponse, model: string, messageId: string, ): LLMResponseNonStreaming { + const parts = response.candidates?.[0]?.content?.parts + const thoughtSignature = GeminiProvider.extractThoughtSignature(parts) + const reasoning = GeminiProvider.extractThoughtSummaries(parts) + return { id: messageId, choices: [ { - finish_reason: - response.response.candidates?.[0]?.finishReason ?? null, + finish_reason: response.candidates?.[0]?.finishReason ?? null, message: { - content: response.response.text(), + content: response.text ?? '', + reasoning: reasoning, role: 'assistant', - tool_calls: response.response.functionCalls()?.map((f) => ({ - id: uuidv4(), - type: 'function', - function: { - name: f.name, - arguments: JSON.stringify(f.args), - }, - })), + tool_calls: GeminiProvider.parseFunctionCalls( + response.functionCalls, + ), + providerMetadata: thoughtSignature + ? { gemini: { thoughtSignature } } + : undefined, }, }, ], created: Date.now(), model: model, object: 'chat.completion', - usage: response.response.usageMetadata + usage: response.usageMetadata ? { - prompt_tokens: response.response.usageMetadata.promptTokenCount, - completion_tokens: - response.response.usageMetadata.candidatesTokenCount, - total_tokens: response.response.usageMetadata.totalTokenCount, + prompt_tokens: response.usageMetadata.promptTokenCount ?? 0, + completion_tokens: response.usageMetadata.candidatesTokenCount ?? 0, + total_tokens: response.usageMetadata.totalTokenCount ?? 0, } : undefined, } } static parseStreamingResponseChunk( - chunk: EnhancedGenerateContentResponse, + chunk: GenerateContentResponse, model: string, messageId: string, ): LLMResponseStreaming { + const parts = chunk.candidates?.[0]?.content?.parts + const thoughtSignature = GeminiProvider.extractThoughtSignature(parts) + const reasoning = GeminiProvider.extractThoughtSummaries(parts) + return { id: messageId, choices: [ { finish_reason: chunk.candidates?.[0]?.finishReason ?? null, delta: { - content: chunk.text(), - tool_calls: chunk.functionCalls()?.map((f, index) => ({ - index, - id: uuidv4(), - type: 'function', - function: { - name: f.name, - arguments: JSON.stringify(f.args), - }, - })), + content: chunk.text ?? null, + reasoning: reasoning, + tool_calls: GeminiProvider.parseFunctionCallsForStreaming( + chunk.functionCalls, + ), + providerMetadata: thoughtSignature + ? { gemini: { thoughtSignature } } + : undefined, }, }, ], @@ -361,14 +363,85 @@ export class GeminiProvider extends BaseLLMProvider< object: 'chat.completion.chunk', usage: chunk.usageMetadata ? { - prompt_tokens: chunk.usageMetadata.promptTokenCount, - completion_tokens: chunk.usageMetadata.candidatesTokenCount, - total_tokens: chunk.usageMetadata.totalTokenCount, + prompt_tokens: chunk.usageMetadata.promptTokenCount ?? 0, + completion_tokens: chunk.usageMetadata.candidatesTokenCount ?? 0, + total_tokens: chunk.usageMetadata.totalTokenCount ?? 0, } : undefined, } } + private static parseFunctionCalls(functionCalls: FunctionCall[] | undefined) { + return functionCalls?.map((f) => ({ + id: f.id ?? uuidv4(), + type: 'function' as const, + function: { + name: f.name ?? '', + arguments: JSON.stringify(f.args ?? {}), + }, + })) + } + + private static parseFunctionCallsForStreaming( + functionCalls: FunctionCall[] | undefined, + ) { + return functionCalls?.map((f, index) => ({ + index, + id: f.id ?? uuidv4(), + type: 'function' as const, + function: { + name: f.name ?? '', + arguments: JSON.stringify(f.args ?? {}), + }, + })) + } + + /** + * Extracts the thought signature from Gemini response parts. + * Per Gemini docs: + * - With function calls: signature is on the first functionCall part + * - Without function calls: signature is on the last part + */ + private static extractThoughtSignature( + parts: Part[] | undefined, + ): string | undefined { + if (!parts || parts.length === 0) { + return undefined + } + + // Check if there are function calls + const hasFunctionCalls = parts.some((part) => part.functionCall) + + if (hasFunctionCalls) { + // Signature is on the first function call part + const firstFcPart = parts.find((part) => part.functionCall) + return firstFcPart?.thoughtSignature + } else { + // Signature is on the last part + const lastPart = parts[parts.length - 1] + return lastPart?.thoughtSignature + } + } + + /** + * Extracts thought summaries from Gemini response parts. + * Thought summaries are parts with thought: true and contain reasoning text. + */ + private static extractThoughtSummaries( + parts: Part[] | undefined, + ): string | undefined { + if (!parts || parts.length === 0) { + return undefined + } + + const thoughtParts = parts.filter((part) => part.thought && part.text) + if (thoughtParts.length === 0) { + return undefined + } + + return thoughtParts.map((part) => part.text).join('') + } + private static removeAdditionalProperties(schema: unknown): unknown { // TODO: Remove this function when Gemini supports additionalProperties field in JSON schema if (typeof schema !== 'object' || schema === null) { @@ -392,7 +465,7 @@ export class GeminiProvider extends BaseLLMProvider< ) } - private static parseRequestTool(tool: RequestTool): GeminiTool { + private static parseRequestTool(tool: RequestTool): Tool { // Gemini does not support additionalProperties field in JSON schema, so we need to clean it const cleanedParameters = this.removeAdditionalProperties( tool.function.parameters, @@ -404,11 +477,8 @@ export class GeminiProvider extends BaseLLMProvider< name: tool.function.name, description: tool.function.description, parameters: { - type: SchemaType.OBJECT, - properties: (cleanedParameters.properties ?? {}) as Record< - string, - Schema - >, + type: Type.OBJECT, + properties: cleanedParameters.properties as Record, }, }, ], @@ -432,6 +502,45 @@ export class GeminiProvider extends BaseLLMProvider< } } + private static readonly THINKING_LEVEL_MAP: Record = { + minimal: ThinkingLevel.MINIMAL, + low: ThinkingLevel.LOW, + medium: ThinkingLevel.MEDIUM, + high: ThinkingLevel.HIGH, + } + + /** + * Builds the thinking config for Gemini API based on model settings. + * - Gemini 3 models use thinkingLevel + * - Gemini 2.5 models use thinkingBudget + */ + private static buildThinkingConfig( + model: ChatModel & { providerType: 'gemini' }, + ): ThinkingConfig | undefined { + if (!model.thinking?.enabled) { + return undefined + } + + const config: ThinkingConfig = {} + + if (model.thinking.thinking_level) { + const level = this.THINKING_LEVEL_MAP[model.thinking.thinking_level] + if (level) { + config.thinkingLevel = level + } + } + + if (model.thinking.thinking_budget !== undefined) { + config.thinkingBudget = model.thinking.thinking_budget + } + + if (model.thinking.include_thoughts) { + config.includeThoughts = model.thinking.include_thoughts + } + + return config + } + async getEmbedding(model: string, text: string): Promise { if (!this.apiKey) { throw new LLMAPIKeyNotSetException( @@ -440,10 +549,11 @@ export class GeminiProvider extends BaseLLMProvider< } try { - const response = await this.client - .getGenerativeModel({ model: model }) - .embedContent(text) - return response.embedding.values + const response = await this.client.models.embedContent({ + model: model, + contents: text, + }) + return response.embeddings?.[0]?.values ?? [] } catch (error) { if (error.status === 429) { throw new LLMRateLimitExceededException( diff --git a/src/core/llm/manager.ts b/src/core/llm/manager.ts index b5858952..2edf6a07 100644 --- a/src/core/llm/manager.ts +++ b/src/core/llm/manager.ts @@ -8,20 +8,18 @@ import { BaseLLMProvider } from './base' import { DeepSeekStudioProvider } from './deepseekStudioProvider' import { LLMModelNotFoundException } from './exception' import { GeminiProvider } from './gemini' -import { GroqProvider } from './groq' import { LmStudioProvider } from './lmStudioProvider' import { MistralProvider } from './mistralProvider' -import { MorphProvider } from './morphProvider' import { OllamaProvider } from './ollama' import { OpenAIAuthenticatedProvider } from './openai' import { OpenAICompatibleProvider } from './openaiCompatibleProvider' import { OpenRouterProvider } from './openRouterProvider' import { PerplexityProvider } from './perplexityProvider' +import { XaiProvider } from './xaiProvider' /* * OpenAI, OpenAI-compatible, and Anthropic providers include token usage statistics * in the final chunk of the stream (following OpenAI's behavior). - * Groq and Ollama currently do not support usage statistics for streaming responses. */ export function getProviderClient({ @@ -46,9 +44,6 @@ export function getProviderClient({ case 'gemini': { return new GeminiProvider(provider) } - case 'groq': { - return new GroqProvider(provider) - } case 'openrouter': { return new OpenRouterProvider(provider) } @@ -67,8 +62,8 @@ export function getProviderClient({ case 'mistral': { return new MistralProvider(provider) } - case 'morph': { - return new MorphProvider(provider) + case 'xai': { + return new XaiProvider(provider) } case 'azure-openai': { return new AzureOpenAIProvider(provider) diff --git a/src/core/llm/morphProvider.ts b/src/core/llm/morphProvider.ts deleted file mode 100644 index ae2e258c..00000000 --- a/src/core/llm/morphProvider.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { ChatModel } from '../../types/chat-model.types' -import { - LLMOptions, - LLMRequestNonStreaming, - LLMRequestStreaming, -} from '../../types/llm/request' -import { - LLMResponseNonStreaming, - LLMResponseStreaming, -} from '../../types/llm/response' -import { LLMProvider } from '../../types/provider.types' - -import { BaseLLMProvider } from './base' -import { NoStainlessOpenAI } from './NoStainlessOpenAI' -import { OpenAIMessageAdapter } from './openaiMessageAdapter' - -export class MorphProvider extends BaseLLMProvider< - Extract -> { - private adapter: OpenAIMessageAdapter - private client: NoStainlessOpenAI - - constructor(provider: Extract) { - super(provider) - this.adapter = new OpenAIMessageAdapter() - this.client = new NoStainlessOpenAI({ - baseURL: `${provider.baseUrl ? provider.baseUrl.replace(/\/+$/, '') : 'https://api.morphllm.com'}/v1`, - apiKey: provider.apiKey ?? '', - dangerouslyAllowBrowser: true, - }) - } - - async generateResponse( - model: ChatModel, - request: LLMRequestNonStreaming, - options?: LLMOptions, - ): Promise { - if (model.providerType !== 'morph') { - throw new Error('Model is not an morph model') - } - - return this.adapter.generateResponse( - this.client, - { - ...request, - prediction: undefined, // morph doesn't support prediction - }, - options, - ) - } - - async streamResponse( - model: ChatModel, - request: LLMRequestStreaming, - options?: LLMOptions, - ): Promise> { - if (model.providerType !== 'morph') { - throw new Error('Model is not an morph model') - } - - return this.adapter.streamResponse( - this.client, - { - ...request, - prediction: undefined, // morph doesn't support prediction - }, - options, - ) - } - - async getEmbedding(_model: string, _text: string): Promise { - throw new Error( - `Provider ${this.provider.id} does not support embeddings. Please use a different provider.`, - ) - } -} diff --git a/src/core/llm/groq.ts b/src/core/llm/xaiProvider.ts similarity index 71% rename from src/core/llm/groq.ts rename to src/core/llm/xaiProvider.ts index 768d7a38..b2b6fe6f 100644 --- a/src/core/llm/groq.ts +++ b/src/core/llm/xaiProvider.ts @@ -15,20 +15,20 @@ import { LLMProvider } from '../../types/provider.types' import { BaseLLMProvider } from './base' import { OpenAIMessageAdapter } from './openaiMessageAdapter' -export class GroqProvider extends BaseLLMProvider< - Extract +export class XaiProvider extends BaseLLMProvider< + Extract > { private adapter: OpenAIMessageAdapter private client: OpenAI - constructor(provider: Extract) { + constructor(provider: Extract) { super(provider) this.adapter = new OpenAIMessageAdapter() this.client = new OpenAI({ apiKey: provider.apiKey ?? '', baseURL: provider.baseUrl - ? provider.baseUrl?.replace(/\/+$/, '') - : 'https://api.groq.com/openai/v1', + ? provider.baseUrl.replace(/\/+$/, '') + : 'https://api.x.ai/v1', dangerouslyAllowBrowser: true, }) } @@ -38,8 +38,8 @@ export class GroqProvider extends BaseLLMProvider< request: LLMRequestNonStreaming, options?: LLMOptions, ): Promise { - if (model.providerType !== 'groq') { - throw new Error('Model is not a Groq model') + if (model.providerType !== 'xai') { + throw new Error('Model is not an xAI model') } return this.adapter.generateResponse(this.client, request, options) @@ -50,8 +50,8 @@ export class GroqProvider extends BaseLLMProvider< request: LLMRequestStreaming, options?: LLMOptions, ): Promise> { - if (model.providerType !== 'groq') { - throw new Error('Model is not a Groq model') + if (model.providerType !== 'xai') { + throw new Error('Model is not an xAI model') } return this.adapter.streamResponse(this.client, request, options) @@ -59,7 +59,7 @@ export class GroqProvider extends BaseLLMProvider< async getEmbedding(_model: string, _text: string): Promise { throw new Error( - `Provider ${this.provider.id} does not support embeddings. Please use a different provider.`, + `Provider ${String(this.provider.id)} does not support embeddings. Please use a different provider.`, ) } } diff --git a/src/hooks/useChatHistory.ts b/src/hooks/useChatHistory.ts index 6c14a34c..0019e112 100644 --- a/src/hooks/useChatHistory.ts +++ b/src/hooks/useChatHistory.ts @@ -147,6 +147,7 @@ const serializeChatMessage = (message: ChatMessage): SerializedChatMessage => { toolCallRequests: message.toolCallRequests, id: message.id, metadata: message.metadata, + providerMetadata: message.providerMetadata, } case 'tool': return { @@ -183,6 +184,7 @@ const deserializeChatMessage = ( toolCallRequests: message.toolCallRequests, id: message.id, metadata: message.metadata, + providerMetadata: message.providerMetadata, } case 'tool': return { diff --git a/src/settings/schema/migrations/12_to_13.test.ts b/src/settings/schema/migrations/12_to_13.test.ts new file mode 100644 index 00000000..fe6660ed --- /dev/null +++ b/src/settings/schema/migrations/12_to_13.test.ts @@ -0,0 +1,196 @@ +import { DEFAULT_CHAT_MODELS_V13, migrateFrom12To13 } from './12_to_13' + +describe('Migration from v12 to v13', () => { + it('should increment version to 13', () => { + const oldSettings = { + version: 12, + } + const result = migrateFrom12To13(oldSettings) + expect(result.version).toBe(13) + }) + + it('should merge existing chat models with new default models', () => { + const oldSettings = { + version: 12, + chatModels: [ + { + id: 'gpt-4o', + providerType: 'openai', + providerId: 'openai', + model: 'gpt-4o', + enable: false, + }, + { + id: 'custom-model', + providerType: 'custom', + providerId: 'custom', + model: 'custom-model', + }, + ], + } + const result = migrateFrom12To13(oldSettings) + + expect(result.chatModels).toEqual([ + ...DEFAULT_CHAT_MODELS_V13.map((model) => + model.id === 'gpt-4o' + ? { + ...model, + enable: false, + } + : model, + ), + { + id: 'custom-model', + providerType: 'custom', + providerId: 'custom', + model: 'custom-model', + }, + ]) + }) + + it('should add new GPT-5.1 model', () => { + const oldSettings = { + version: 12, + chatModels: [ + { + id: 'gpt-4o', + providerType: 'openai', + providerId: 'openai', + model: 'gpt-4o', + }, + ], + } + const result = migrateFrom12To13(oldSettings) + + const chatModels = result.chatModels as { id: string }[] + const gpt51 = chatModels.find((m) => m.id === 'gpt-5.1') + + expect(gpt51).toBeDefined() + expect(gpt51).toEqual({ + id: 'gpt-5.1', + providerType: 'openai', + providerId: 'openai', + model: 'gpt-5.1', + }) + }) + + it('should preserve gpt-5 as custom model when migrating', () => { + const oldSettings = { + version: 12, + chatModels: [ + { + id: 'gpt-5', + providerType: 'openai', + providerId: 'openai', + model: 'gpt-5', + enable: true, + }, + { + id: 'gpt-4o', + providerType: 'openai', + providerId: 'openai', + model: 'gpt-4o', + }, + ], + } + const result = migrateFrom12To13(oldSettings) + + const chatModels = result.chatModels as { id: string }[] + const gpt5 = chatModels.find((m) => m.id === 'gpt-5') + const gpt51 = chatModels.find((m) => m.id === 'gpt-5.1') + + // gpt-5 should be preserved as custom model + expect(gpt5).toBeDefined() + expect(gpt5).toEqual({ + id: 'gpt-5', + providerType: 'openai', + providerId: 'openai', + model: 'gpt-5', + enable: true, + }) + + // gpt-5.1 should be added as new default + expect(gpt51).toBeDefined() + expect(gpt51).toEqual({ + id: 'gpt-5.1', + providerType: 'openai', + providerId: 'openai', + model: 'gpt-5.1', + }) + }) + + it('should preserve gpt-4.1 models as custom models when migrating', () => { + const oldSettings = { + version: 12, + chatModels: [ + { + id: 'gpt-4.1', + providerType: 'openai', + providerId: 'openai', + model: 'gpt-4.1', + enable: true, + }, + { + id: 'gpt-4.1-mini', + providerType: 'openai', + providerId: 'openai', + model: 'gpt-4.1-mini', + }, + { + id: 'gpt-4.1-nano', + providerType: 'openai', + providerId: 'openai', + model: 'gpt-4.1-nano', + }, + { + id: 'gpt-4o', + providerType: 'openai', + providerId: 'openai', + model: 'gpt-4o', + }, + ], + } + const result = migrateFrom12To13(oldSettings) + + const chatModels = result.chatModels as { id: string }[] + const gpt41 = chatModels.find((m) => m.id === 'gpt-4.1') + const gpt41Mini = chatModels.find((m) => m.id === 'gpt-4.1-mini') + const gpt41Nano = chatModels.find((m) => m.id === 'gpt-4.1-nano') + const gpt51 = chatModels.find((m) => m.id === 'gpt-5.1') + + // gpt-4.1 models should be preserved as custom models + expect(gpt41).toBeDefined() + expect(gpt41).toEqual({ + id: 'gpt-4.1', + providerType: 'openai', + providerId: 'openai', + model: 'gpt-4.1', + enable: true, + }) + + expect(gpt41Mini).toBeDefined() + expect(gpt41Mini).toEqual({ + id: 'gpt-4.1-mini', + providerType: 'openai', + providerId: 'openai', + model: 'gpt-4.1-mini', + }) + + expect(gpt41Nano).toBeDefined() + expect(gpt41Nano).toEqual({ + id: 'gpt-4.1-nano', + providerType: 'openai', + providerId: 'openai', + model: 'gpt-4.1-nano', + }) + + // gpt-5.1 should be added as new default + expect(gpt51).toBeDefined() + expect(gpt51).toEqual({ + id: 'gpt-5.1', + providerType: 'openai', + providerId: 'openai', + model: 'gpt-5.1', + }) + }) +}) diff --git a/src/settings/schema/migrations/12_to_13.ts b/src/settings/schema/migrations/12_to_13.ts new file mode 100644 index 00000000..78de0312 --- /dev/null +++ b/src/settings/schema/migrations/12_to_13.ts @@ -0,0 +1,205 @@ +import { SettingMigration } from '../setting.types' + +import { getMigratedChatModels } from './migrationUtils' + +/** + * Migration from version 12 to version 13 + * - Add following models: + * - gpt-5.1 + * - Remove following models from defaults: + * - gpt-5 + * - gpt-4.1 + * - gpt-4.1-mini + * - gpt-4.1-nano + */ +export const migrateFrom12To13: SettingMigration['migrate'] = (data) => { + const newData = { ...data } + newData.version = 13 + + newData.chatModels = getMigratedChatModels(newData, DEFAULT_CHAT_MODELS_V13) + + return newData +} + +type DefaultChatModelsV13 = { + id: string + providerType: string + providerId: string + model: string + reasoning?: { + enabled: boolean + reasoning_effort?: string + } + thinking?: { + enabled: boolean + budget_tokens: number + } + web_search_options?: { + search_context_size?: string + } + enable?: boolean +}[] + +export const DEFAULT_CHAT_MODELS_V13: DefaultChatModelsV13 = [ + { + providerType: 'anthropic', + providerId: 'anthropic', + id: 'claude-opus-4.1', + model: 'claude-opus-4-1', + }, + { + providerType: 'anthropic', + providerId: 'anthropic', + id: 'claude-sonnet-4.5', + model: 'claude-sonnet-4-5', + }, + { + providerType: 'anthropic', + providerId: 'anthropic', + id: 'claude-haiku-4.5', + model: 'claude-haiku-4-5', + }, + { + providerType: 'openai', + providerId: 'openai', + id: 'gpt-5.1', + model: 'gpt-5.1', + }, + { + providerType: 'openai', + providerId: 'openai', + id: 'gpt-5-mini', + model: 'gpt-5-mini', + }, + { + providerType: 'openai', + providerId: 'openai', + id: 'gpt-5-nano', + model: 'gpt-5-nano', + }, + { + providerType: 'openai', + providerId: 'openai', + id: 'gpt-4o', + model: 'gpt-4o', + }, + { + providerType: 'openai', + providerId: 'openai', + id: 'gpt-4o-mini', + model: 'gpt-4o-mini', + }, + { + providerType: 'openai', + providerId: 'openai', + id: 'o4-mini', + model: 'o4-mini', + reasoning: { + enabled: true, + reasoning_effort: 'medium', + }, + }, + { + providerType: 'openai', + providerId: 'openai', + id: 'o3', + model: 'o3', + reasoning: { + enabled: true, + reasoning_effort: 'medium', + }, + }, + { + providerType: 'gemini', + providerId: 'gemini', + id: 'gemini-2.5-pro', + model: 'gemini-2.5-pro', + }, + { + providerType: 'gemini', + providerId: 'gemini', + id: 'gemini-2.5-flash', + model: 'gemini-2.5-flash', + }, + { + providerType: 'gemini', + providerId: 'gemini', + id: 'gemini-2.5-flash-lite', + model: 'gemini-2.5-flash-lite', + }, + { + providerType: 'gemini', + providerId: 'gemini', + id: 'gemini-2.0-flash', + model: 'gemini-2.0-flash', + }, + { + providerType: 'gemini', + providerId: 'gemini', + id: 'gemini-2.0-flash-lite', + model: 'gemini-2.0-flash-lite', + }, + { + providerType: 'deepseek', + providerId: 'deepseek', + id: 'deepseek-chat', + model: 'deepseek-chat', + }, + { + providerType: 'deepseek', + providerId: 'deepseek', + id: 'deepseek-reasoner', + model: 'deepseek-reasoner', + }, + { + providerType: 'perplexity', + providerId: 'perplexity', + id: 'sonar', + model: 'sonar', + web_search_options: { + search_context_size: 'low', + }, + }, + { + providerType: 'perplexity', + providerId: 'perplexity', + id: 'sonar-pro', + model: 'sonar', + web_search_options: { + search_context_size: 'low', + }, + }, + { + providerType: 'perplexity', + providerId: 'perplexity', + id: 'sonar-deep-research', + model: 'sonar-deep-research', + web_search_options: { + search_context_size: 'low', + }, + }, + { + providerType: 'perplexity', + providerId: 'perplexity', + id: 'sonar-reasoning', + model: 'sonar', + web_search_options: { + search_context_size: 'low', + }, + }, + { + providerType: 'perplexity', + providerId: 'perplexity', + id: 'sonar-reasoning-pro', + model: 'sonar', + web_search_options: { + search_context_size: 'low', + }, + }, + { + providerType: 'morph', + providerId: 'morph', + id: 'morph-v0', + model: 'morph-v0', + }, +] diff --git a/src/settings/schema/migrations/13_to_14.test.ts b/src/settings/schema/migrations/13_to_14.test.ts new file mode 100644 index 00000000..4e97c72c --- /dev/null +++ b/src/settings/schema/migrations/13_to_14.test.ts @@ -0,0 +1,1035 @@ +import { DEFAULT_CHAT_MODELS_V14, migrateFrom13To14 } from './13_to_14' + +describe('Migration from v13 to v14', () => { + it('should increment version to 14', () => { + const oldSettings = { + version: 13, + } + const result = migrateFrom13To14(oldSettings) + expect(result.version).toBe(14) + }) + + it('should add xai provider and drop unused groq/morph providers', () => { + const oldSettings = { + version: 13, + providers: [ + { type: 'openai', id: 'openai', apiKey: 'openai-key' }, + { type: 'anthropic', id: 'anthropic', apiKey: 'anthropic-key' }, + { type: 'gemini', id: 'gemini', apiKey: 'gemini-key' }, + { type: 'groq', id: 'groq', apiKey: 'groq-key' }, + { type: 'deepseek', id: 'deepseek', apiKey: 'deepseek-key' }, + { type: 'morph', id: 'morph', apiKey: 'morph-key' }, + ], + chatModels: [], // No models using groq or morph + } + const result = migrateFrom13To14(oldSettings) + expect(result.version).toBe(14) + + const providers = result.providers as { + type: string + id: string + }[] + + // xai should be added + expect(providers.find((p) => p.type === 'xai')).toBeDefined() + + // groq and morph should be dropped (no models use them) + expect(providers.find((p) => p.id === 'groq')).toBeUndefined() + expect(providers.find((p) => p.id === 'morph')).toBeUndefined() + }) + + it('should convert groq/morph to openai-compatible when models use them', () => { + const oldSettings = { + version: 13, + providers: [ + { type: 'groq', id: 'groq', apiKey: 'groq-key' }, + { type: 'morph', id: 'morph', apiKey: 'morph-key' }, + ], + chatModels: [ + { + id: 'llama-groq', + providerType: 'groq', + providerId: 'groq', + model: 'llama-3.3-70b', + }, + { + id: 'morph-v0', + providerType: 'morph', + providerId: 'morph', + model: 'morph-v0', + }, + ], + } + const result = migrateFrom13To14(oldSettings) + + const providers = result.providers as { + type: string + id: string + baseUrl?: string + apiKey?: string + }[] + + // groq and morph should be converted to openai-compatible + const groqProvider = providers.find((p) => p.id === 'groq') + expect(groqProvider?.type).toBe('openai-compatible') + expect(groqProvider?.baseUrl).toBe('https://api.groq.com/openai/v1') + expect(groqProvider?.apiKey).toBe('groq-key') + + const morphProvider = providers.find((p) => p.id === 'morph') + expect(morphProvider?.type).toBe('openai-compatible') + expect(morphProvider?.baseUrl).toBe('https://api.morphllm.com/v1') + expect(morphProvider?.apiKey).toBe('morph-key') + + // Chat models should also be converted + const chatModels = result.chatModels as { + id: string + providerType: string + }[] + expect(chatModels.find((m) => m.id === 'llama-groq')?.providerType).toBe( + 'openai-compatible', + ) + expect(chatModels.find((m) => m.id === 'morph-v0')?.providerType).toBe( + 'openai-compatible', + ) + }) + + it('should preserve custom baseUrl for groq/morph providers', () => { + const oldSettings = { + version: 13, + providers: [ + { + type: 'groq', + id: 'groq', + apiKey: 'groq-key', + baseUrl: 'https://custom-groq.example.com/v1', + }, + { + type: 'morph', + id: 'morph', + apiKey: 'morph-key', + baseUrl: 'https://custom-morph.example.com/v1', + }, + ], + chatModels: [ + { + id: 'groq-model', + providerType: 'groq', + providerId: 'groq', + model: 'x', + }, + { + id: 'morph-model', + providerType: 'morph', + providerId: 'morph', + model: 'y', + }, + ], + } + const result = migrateFrom13To14(oldSettings) + + const providers = result.providers as { + type: string + id: string + baseUrl?: string + }[] + + const groqProvider = providers.find((p) => p.id === 'groq') + expect(groqProvider?.baseUrl).toBe('https://custom-groq.example.com/v1') + + const morphProvider = providers.find((p) => p.id === 'morph') + expect(morphProvider?.baseUrl).toBe('https://custom-morph.example.com/v1') + }) + + it('should preserve existing API keys after migration', () => { + const oldSettings = { + version: 13, + providers: [ + { type: 'openai', id: 'openai', apiKey: 'sk-openai-secret-key' }, + { type: 'anthropic', id: 'anthropic', apiKey: 'sk-ant-secret-key' }, + { type: 'gemini', id: 'gemini', apiKey: 'gemini-api-key-123' }, + { type: 'deepseek', id: 'deepseek', apiKey: 'deepseek-key-456' }, + { type: 'mistral', id: 'mistral', apiKey: 'mistral-key-789' }, + { type: 'perplexity', id: 'perplexity', apiKey: 'pplx-key' }, + { type: 'openrouter', id: 'openrouter', apiKey: 'openrouter-key' }, + ], + } + const result = migrateFrom13To14(oldSettings) + + const providers = result.providers as { + type: string + id: string + apiKey?: string + }[] + + // Verify all API keys are preserved + expect(providers.find((p) => p.type === 'openai')?.apiKey).toBe( + 'sk-openai-secret-key', + ) + expect(providers.find((p) => p.type === 'anthropic')?.apiKey).toBe( + 'sk-ant-secret-key', + ) + expect(providers.find((p) => p.type === 'gemini')?.apiKey).toBe( + 'gemini-api-key-123', + ) + expect(providers.find((p) => p.type === 'deepseek')?.apiKey).toBe( + 'deepseek-key-456', + ) + expect(providers.find((p) => p.type === 'mistral')?.apiKey).toBe( + 'mistral-key-789', + ) + expect(providers.find((p) => p.type === 'perplexity')?.apiKey).toBe( + 'pplx-key', + ) + expect(providers.find((p) => p.type === 'openrouter')?.apiKey).toBe( + 'openrouter-key', + ) + + // Verify new xai provider is added (without API key since it's new) + const xaiProvider = providers.find((p) => p.type === 'xai') + expect(xaiProvider).toBeDefined() + expect(xaiProvider?.apiKey).toBeUndefined() + }) + + it('should merge existing chat models with new default models', () => { + const oldSettings = { + version: 13, + chatModels: [ + { + id: 'claude-sonnet-4.5', + providerType: 'anthropic', + providerId: 'anthropic', + model: 'claude-sonnet-4-5', + enable: false, + }, + { + id: 'custom-model', + providerType: 'custom', + providerId: 'custom', + model: 'custom-model', + }, + ], + } + const result = migrateFrom13To14(oldSettings) + + expect(result.chatModels).toEqual([ + ...DEFAULT_CHAT_MODELS_V14.map((model) => + model.id === 'claude-sonnet-4.5' + ? { + ...model, + enable: false, + } + : model, + ), + { + id: 'custom-model', + providerType: 'custom', + providerId: 'custom', + model: 'custom-model', + }, + ]) + }) + + it('should add new Claude Opus 4.5 model', () => { + const oldSettings = { + version: 13, + chatModels: [ + { + id: 'claude-sonnet-4.5', + providerType: 'anthropic', + providerId: 'anthropic', + model: 'claude-sonnet-4-5', + }, + ], + } + const result = migrateFrom13To14(oldSettings) + + const chatModels = result.chatModels as { id: string }[] + const opus45 = chatModels.find((m) => m.id === 'claude-opus-4.5') + + expect(opus45).toBeDefined() + expect(opus45).toEqual({ + id: 'claude-opus-4.5', + providerType: 'anthropic', + providerId: 'anthropic', + model: 'claude-opus-4-5', + }) + }) + + it('should add new GPT-5.2 and GPT-4.1-mini models', () => { + const oldSettings = { + version: 13, + chatModels: [ + { + id: 'gpt-5-mini', + providerType: 'openai', + providerId: 'openai', + model: 'gpt-5-mini', + }, + ], + } + const result = migrateFrom13To14(oldSettings) + + const chatModels = result.chatModels as { id: string }[] + const gpt52 = chatModels.find((m) => m.id === 'gpt-5.2') + const gpt41Mini = chatModels.find((m) => m.id === 'gpt-4.1-mini') + + expect(gpt52).toBeDefined() + expect(gpt52).toEqual({ + id: 'gpt-5.2', + providerType: 'openai', + providerId: 'openai', + model: 'gpt-5.2', + }) + + expect(gpt41Mini).toBeDefined() + expect(gpt41Mini).toEqual({ + id: 'gpt-4.1-mini', + providerType: 'openai', + providerId: 'openai', + model: 'gpt-4.1-mini', + }) + }) + + it('should add new Gemini 3 models', () => { + const oldSettings = { + version: 13, + chatModels: [], + } + const result = migrateFrom13To14(oldSettings) + + const chatModels = result.chatModels as { id: string }[] + const gemini3Pro = chatModels.find((m) => m.id === 'gemini-3-pro-preview') + const gemini3Flash = chatModels.find( + (m) => m.id === 'gemini-3-flash-preview', + ) + + expect(gemini3Pro).toBeDefined() + expect(gemini3Pro).toEqual({ + id: 'gemini-3-pro-preview', + providerType: 'gemini', + providerId: 'gemini', + model: 'gemini-3-pro-preview', + }) + + expect(gemini3Flash).toBeDefined() + expect(gemini3Flash).toEqual({ + id: 'gemini-3-flash-preview', + providerType: 'gemini', + providerId: 'gemini', + model: 'gemini-3-flash-preview', + }) + }) + + it('should add new Grok models from xai provider', () => { + const oldSettings = { + version: 13, + chatModels: [], + } + const result = migrateFrom13To14(oldSettings) + + const chatModels = result.chatModels as { id: string }[] + const grokFast = chatModels.find((m) => m.id === 'grok-4-1-fast') + const grokNonReasoning = chatModels.find( + (m) => m.id === 'grok-4-1-fast-non-reasoning', + ) + + expect(grokFast).toBeDefined() + expect(grokFast).toEqual({ + id: 'grok-4-1-fast', + providerType: 'xai', + providerId: 'xai', + model: 'grok-4-1-fast', + }) + + expect(grokNonReasoning).toBeDefined() + expect(grokNonReasoning).toEqual({ + id: 'grok-4-1-fast-non-reasoning', + providerType: 'xai', + providerId: 'xai', + model: 'grok-4-1-fast-non-reasoning', + }) + }) + + it('should preserve removed models as custom models', () => { + const oldSettings = { + version: 13, + chatModels: [ + { + id: 'gpt-5.1', + providerType: 'openai', + providerId: 'openai', + model: 'gpt-5.1', + enable: true, + }, + { + id: 'gpt-4o', + providerType: 'openai', + providerId: 'openai', + model: 'gpt-4o', + }, + { + id: 'morph-v0', + providerType: 'morph', + providerId: 'morph', + model: 'morph-v0', + }, + { + id: 'sonar', + providerType: 'perplexity', + providerId: 'perplexity', + model: 'sonar', + }, + ], + } + const result = migrateFrom13To14(oldSettings) + + const chatModels = result.chatModels as { id: string }[] + + // These models should be preserved as custom models since they are not in DEFAULT_CHAT_MODELS_V14 + const gpt51 = chatModels.find((m) => m.id === 'gpt-5.1') + const gpt4o = chatModels.find((m) => m.id === 'gpt-4o') + const morphV0 = chatModels.find((m) => m.id === 'morph-v0') + const sonar = chatModels.find((m) => m.id === 'sonar') + + expect(gpt51).toBeDefined() + expect(gpt51).toEqual({ + id: 'gpt-5.1', + providerType: 'openai', + providerId: 'openai', + model: 'gpt-5.1', + enable: true, + }) + + expect(gpt4o).toBeDefined() + expect(gpt4o).toEqual({ + id: 'gpt-4o', + providerType: 'openai', + providerId: 'openai', + model: 'gpt-4o', + }) + + expect(morphV0).toBeDefined() + expect(morphV0).toEqual({ + id: 'morph-v0', + providerType: 'openai-compatible', + providerId: 'morph', + model: 'morph-v0', + }) + + expect(sonar).toBeDefined() + expect(sonar).toEqual({ + id: 'sonar', + providerType: 'perplexity', + providerId: 'perplexity', + model: 'sonar', + }) + }) + + it('should preserve o4-mini with reasoning settings', () => { + const oldSettings = { + version: 13, + chatModels: [ + { + id: 'o4-mini', + providerType: 'openai', + providerId: 'openai', + model: 'o4-mini', + reasoning: { + enabled: true, + reasoning_effort: 'high', + }, + }, + ], + } + const result = migrateFrom13To14(oldSettings) + + const chatModels = result.chatModels as { + id: string + reasoning?: { enabled: boolean; reasoning_effort?: string } + }[] + const o4Mini = chatModels.find((m) => m.id === 'o4-mini') + + expect(o4Mini).toBeDefined() + // Note: Object.assign does shallow merge, so reasoning settings from default override user settings + expect(o4Mini?.reasoning).toEqual({ + enabled: true, + reasoning_effort: 'medium', + }) + }) +}) + +describe('Test with real data', () => { + const oldSettings = { + version: 13, + providers: [ + { + type: 'openai', + id: 'openai', + apiKey: 'test-openai-api-key', + }, + { + type: 'anthropic', + id: 'anthropic', + apiKey: 'test-anthropic-api-key', + }, + { + type: 'gemini', + id: 'gemini', + apiKey: 'test-gemini-api-key', + }, + { + type: 'deepseek', + id: 'deepseek', + apiKey: 'test-deepseek-api-key', + }, + { + type: 'perplexity', + id: 'perplexity', + apiKey: 'test-perplexity-api-key', + }, + { + type: 'groq', + id: 'groq', + apiKey: 'test-groq-api-key', + }, + { + type: 'mistral', + id: 'mistral', + }, + { + type: 'openrouter', + id: 'openrouter', + baseUrl: '', + apiKey: 'test-openrouter-api-key', + }, + { + type: 'ollama', + id: 'ollama', + baseUrl: 'ollama-url-test', + }, + { + type: 'lm-studio', + id: 'lm-studio', + }, + { + type: 'morph', + id: 'morph', + }, + { + type: 'openai-compatible', + id: 'siliconflow', + baseUrl: 'siliconflow-test', + apiKey: '', + }, + ], + chatModels: [ + { + providerType: 'anthropic', + providerId: 'anthropic', + id: 'claude-opus-4.1', + model: 'claude-opus-4-1', + }, + { + providerType: 'anthropic', + providerId: 'anthropic', + id: 'claude-sonnet-4.5', + model: 'claude-sonnet-4-5', + }, + { + providerType: 'anthropic', + providerId: 'anthropic', + id: 'claude-haiku-4.5', + model: 'claude-haiku-4-5', + }, + { + providerType: 'openai', + providerId: 'openai', + id: 'gpt-5.1', + model: 'gpt-5.1', + }, + { + providerType: 'openai', + providerId: 'openai', + id: 'gpt-5', + model: 'gpt-5', + }, + { + providerType: 'openai', + providerId: 'openai', + id: 'gpt-5-mini', + model: 'gpt-5-mini', + }, + { + providerType: 'openai', + providerId: 'openai', + id: 'gpt-5-nano', + model: 'gpt-5-nano', + }, + { + providerType: 'openai', + providerId: 'openai', + id: 'gpt-4.1', + model: 'gpt-4.1', + }, + { + providerType: 'openai', + providerId: 'openai', + id: 'gpt-4.1-mini', + model: 'gpt-4.1-mini', + }, + { + providerType: 'openai', + providerId: 'openai', + id: 'gpt-4.1-nano', + model: 'gpt-4.1-nano', + enable: false, + }, + { + providerType: 'openai', + providerId: 'openai', + id: 'gpt-4o', + model: 'gpt-4o', + enable: false, + }, + { + providerType: 'openai', + providerId: 'openai', + id: 'gpt-4o-mini', + model: 'gpt-4o-mini', + enable: false, + }, + { + providerType: 'openai', + providerId: 'openai', + id: 'o4-mini', + model: 'o4-mini', + enable: false, + reasoning: { + enabled: true, + reasoning_effort: 'medium', + }, + }, + { + providerType: 'openai', + providerId: 'openai', + id: 'o3', + model: 'o3', + enable: false, + reasoning: { + enabled: true, + reasoning_effort: 'medium', + }, + }, + { + providerType: 'gemini', + providerId: 'gemini', + id: 'gemini-2.5-pro', + model: 'gemini-2.5-pro', + }, + { + providerType: 'gemini', + providerId: 'gemini', + id: 'gemini-2.5-flash', + model: 'gemini-2.5-flash', + }, + { + providerType: 'gemini', + providerId: 'gemini', + id: 'gemini-2.5-flash-lite', + model: 'gemini-2.5-flash-lite', + }, + { + providerType: 'gemini', + providerId: 'gemini', + id: 'gemini-2.0-flash', + model: 'gemini-2.0-flash', + enable: false, + }, + { + providerType: 'gemini', + providerId: 'gemini', + id: 'gemini-2.0-flash-lite', + model: 'gemini-2.0-flash-lite', + enable: false, + }, + { + providerType: 'deepseek', + providerId: 'deepseek', + id: 'deepseek-chat', + model: 'deepseek-chat', + enable: false, + }, + { + providerType: 'deepseek', + providerId: 'deepseek', + id: 'deepseek-reasoner', + model: 'deepseek-reasoner', + enable: false, + }, + { + providerType: 'perplexity', + providerId: 'perplexity', + id: 'sonar', + model: 'sonar', + enable: false, + web_search_options: { + search_context_size: 'low', + }, + }, + { + providerType: 'perplexity', + providerId: 'perplexity', + id: 'sonar-pro', + model: 'sonar', + web_search_options: { + search_context_size: 'low', + }, + }, + { + providerType: 'perplexity', + providerId: 'perplexity', + id: 'sonar-deep-research', + model: 'sonar-deep-research', + enable: false, + web_search_options: { + search_context_size: 'low', + }, + }, + { + providerType: 'perplexity', + providerId: 'perplexity', + id: 'sonar-reasoning', + model: 'sonar', + enable: false, + web_search_options: { + search_context_size: 'low', + }, + }, + { + providerType: 'perplexity', + providerId: 'perplexity', + id: 'sonar-reasoning-pro', + model: 'sonar', + enable: false, + web_search_options: { + search_context_size: 'low', + }, + }, + { + providerType: 'morph', + providerId: 'morph', + id: 'morph-v0', + model: 'morph-v0', + enable: false, + }, + { + providerType: 'anthropic', + providerId: 'anthropic', + id: 'claude-sonnet-4.0', + model: 'claude-sonnet-4-0', + }, + { + providerType: 'anthropic', + providerId: 'anthropic', + id: 'claude-3.7-sonnet', + model: 'claude-3-7-sonnet-latest', + }, + { + providerType: 'anthropic', + providerId: 'anthropic', + id: 'claude-3.5-sonnet', + model: 'claude-3-5-sonnet-latest', + enable: false, + }, + { + providerType: 'anthropic', + providerId: 'anthropic', + id: 'claude-3.5-haiku', + model: 'claude-3-5-haiku-latest', + enable: false, + }, + { + providerType: 'anthropic', + providerId: 'anthropic', + id: 'claude-3.7-sonnet-thinking', + model: 'claude-3-7-sonnet-latest', + thinking: { + enabled: true, + budget_tokens: 8192, + }, + }, + { + providerType: 'gemini', + providerId: 'gemini', + id: 'gemini-2.0-flash-thinking', + model: 'gemini-2.0-flash-thinking-exp', + enable: false, + }, + { + providerType: 'gemini', + providerId: 'gemini', + id: 'gemini-1.5-pro', + model: 'gemini-1.5-pro', + enable: false, + }, + { + providerType: 'gemini', + providerId: 'gemini', + id: 'gemini-1.5-flash', + model: 'gemini-1.5-flash', + enable: false, + }, + { + providerType: 'gemini', + providerId: 'gemini', + id: 'gemini-exp-1206', + model: 'gemini-exp-1206', + enable: false, + }, + { + providerType: 'openrouter', + providerId: 'openrouter', + id: 'llama-3.3-70b-instruct', + model: 'meta-llama/llama-3.3-70b-instruct', + }, + { + providerType: 'openai-compatible', + providerId: 'mistral', + id: 'mistral-small-latest', + model: 'mistral-small-latest', + promptLevel: 1, + }, + ], + embeddingModels: [ + { + providerType: 'openai', + providerId: 'openai', + id: 'openai/text-embedding-3-small', + model: 'text-embedding-3-small', + dimension: 1536, + }, + { + providerType: 'openai', + providerId: 'openai', + id: 'openai/text-embedding-3-large', + model: 'text-embedding-3-large', + dimension: 3072, + }, + { + providerType: 'gemini', + providerId: 'gemini', + id: 'gemini/text-embedding-004', + model: 'text-embedding-004', + dimension: 768, + }, + { + providerType: 'ollama', + providerId: 'ollama', + id: 'ollama/nomic-embed-text', + model: 'nomic-embed-text', + dimension: 768, + }, + { + providerType: 'ollama', + providerId: 'ollama', + id: 'ollama/mxbai-embed-large', + model: 'mxbai-embed-large', + dimension: 1024, + }, + { + providerType: 'ollama', + providerId: 'ollama', + id: 'ollama/bge-m3', + model: 'bge-m3', + dimension: 1024, + }, + ], + chatModelId: 'gpt-4.1', + applyModelId: 'gpt-4.1-mini', + embeddingModelId: 'openai/text-embedding-3-small', + systemPrompt: '', + ragOptions: { + chunkSize: 1000, + thresholdTokens: 8192, + minSimilarity: 0, + limit: 10, + excludePatterns: [], + includePatterns: [], + }, + mcp: { + servers: [ + { + id: 'markitdown', + parameters: { + command: 'uvx', + args: ['markitdown-mcp'], + }, + enabled: true, + toolOptions: {}, + }, + ], + }, + chatOptions: { + includeCurrentFileContent: true, + enableTools: false, + maxAutoIterations: 1, + }, + } + + it('should migrate real data without errors', () => { + const result = migrateFrom13To14(oldSettings) + expect(result.version).toBe(14) + }) + + it('should preserve all API keys from real data', () => { + const result = migrateFrom13To14(oldSettings) + const providers = result.providers as { + type: string + id: string + apiKey?: string + baseUrl?: string + }[] + + expect(providers.find((p) => p.type === 'openai')?.apiKey).toBe( + 'test-openai-api-key', + ) + expect(providers.find((p) => p.type === 'anthropic')?.apiKey).toBe( + 'test-anthropic-api-key', + ) + expect(providers.find((p) => p.type === 'gemini')?.apiKey).toBe( + 'test-gemini-api-key', + ) + expect(providers.find((p) => p.type === 'deepseek')?.apiKey).toBe( + 'test-deepseek-api-key', + ) + expect(providers.find((p) => p.type === 'perplexity')?.apiKey).toBe( + 'test-perplexity-api-key', + ) + expect(providers.find((p) => p.type === 'openrouter')?.apiKey).toBe( + 'test-openrouter-api-key', + ) + }) + + it('should preserve custom provider configurations from real data', () => { + const result = migrateFrom13To14(oldSettings) + const providers = result.providers as { + type: string + id: string + apiKey?: string + baseUrl?: string + }[] + + // groq should be dropped (no models use it) + expect(providers.find((p) => p.id === 'groq')).toBeUndefined() + + // morph should be converted to openai-compatible (morph-v0 model uses it) + const morphProvider = providers.find((p) => p.id === 'morph') + expect(morphProvider?.type).toBe('openai-compatible') + expect(morphProvider?.baseUrl).toBe('https://api.morphllm.com/v1') + + // ollama with custom baseUrl should be preserved + const ollama = providers.find((p) => p.type === 'ollama') + expect(ollama?.baseUrl).toBe('ollama-url-test') + + // openai-compatible provider should be preserved + const siliconflow = providers.find((p) => p.id === 'siliconflow') + expect(siliconflow).toBeDefined() + expect(siliconflow?.baseUrl).toBe('siliconflow-test') + }) + + it('should preserve user model settings from real data', () => { + const result = migrateFrom13To14(oldSettings) + const chatModels = result.chatModels as { + id: string + enable?: boolean + thinking?: { enabled: boolean; budget_tokens: number } + web_search_options?: { search_context_size: string } + promptLevel?: number + }[] + + // Check that user's disabled models remain disabled + const gpt4o = chatModels.find((m) => m.id === 'gpt-4o') + expect(gpt4o?.enable).toBe(false) + + // Check that models with thinking settings are preserved + const claude37Thinking = chatModels.find( + (m) => m.id === 'claude-3.7-sonnet-thinking', + ) + expect(claude37Thinking?.thinking).toEqual({ + enabled: true, + budget_tokens: 8192, + }) + + // Check that custom model with promptLevel is preserved + const mistralSmall = chatModels.find((m) => m.id === 'mistral-small-latest') + expect(mistralSmall?.promptLevel).toBe(1) + }) + + it('should add xai provider for real data', () => { + const result = migrateFrom13To14(oldSettings) + const providers = result.providers as { type: string; id: string }[] + + const xai = providers.find((p) => p.type === 'xai') + expect(xai).toBeDefined() + }) + + it('should add new default models to real data', () => { + const result = migrateFrom13To14(oldSettings) + const chatModels = result.chatModels as { id: string }[] + + // New models should be added + expect(chatModels.find((m) => m.id === 'gpt-5.2')).toBeDefined() + expect(chatModels.find((m) => m.id === 'claude-opus-4.5')).toBeDefined() + expect( + chatModels.find((m) => m.id === 'gemini-3-pro-preview'), + ).toBeDefined() + expect( + chatModels.find((m) => m.id === 'gemini-3-flash-preview'), + ).toBeDefined() + expect(chatModels.find((m) => m.id === 'grok-4-1-fast')).toBeDefined() + expect( + chatModels.find((m) => m.id === 'grok-4-1-fast-non-reasoning'), + ).toBeDefined() + }) + + it('should preserve removed models as custom models from real data', () => { + const result = migrateFrom13To14(oldSettings) + const chatModels = result.chatModels as { id: string }[] + + // Models removed from defaults should still be preserved + expect(chatModels.find((m) => m.id === 'gpt-5.1')).toBeDefined() + expect(chatModels.find((m) => m.id === 'gpt-5-nano')).toBeDefined() + expect(chatModels.find((m) => m.id === 'claude-opus-4.1')).toBeDefined() + expect(chatModels.find((m) => m.id === 'gemini-2.5-pro')).toBeDefined() + expect(chatModels.find((m) => m.id === 'morph-v0')).toBeDefined() + expect(chatModels.find((m) => m.id === 'sonar')).toBeDefined() + }) + + it('should preserve other settings from real data', () => { + const result = migrateFrom13To14(oldSettings) + + expect(result.chatModelId).toBe('gpt-4.1') + expect(result.applyModelId).toBe('gpt-4.1-mini') + expect(result.embeddingModelId).toBe('openai/text-embedding-3-small') + expect(result.systemPrompt).toBe('') + expect(result.ragOptions).toEqual({ + chunkSize: 1000, + thresholdTokens: 8192, + minSimilarity: 0, + limit: 10, + excludePatterns: [], + includePatterns: [], + }) + expect(result.mcp).toBeDefined() + expect(result.chatOptions).toEqual({ + includeCurrentFileContent: true, + enableTools: false, + maxAutoIterations: 1, + }) + }) + + it('should preserve embedding models from real data', () => { + const result = migrateFrom13To14(oldSettings) + expect(result.embeddingModels).toEqual(oldSettings.embeddingModels) + }) +}) diff --git a/src/settings/schema/migrations/13_to_14.ts b/src/settings/schema/migrations/13_to_14.ts new file mode 100644 index 00000000..10799f07 --- /dev/null +++ b/src/settings/schema/migrations/13_to_14.ts @@ -0,0 +1,200 @@ +import { SettingMigration } from '../setting.types' + +import { getMigratedChatModels, getMigratedProviders } from './migrationUtils' + +/** + * Migration from version 13 to version 14 + * - Add xai provider + * - Convert groq and morph providers to openai-compatible + * - Add following models: + * - gpt-5.2 + * - gpt-4.1-mini + * - claude-opus-4.5 + * - gemini-3-pro-preview + * - gemini-3-flash-preview + * - grok-4-1-fast + * - grok-4-1-fast-non-reasoning + * - Remove following models from defaults: + * - gpt-5.1 + * - gpt-5-nano + * - gpt-4o + * - gpt-4o-mini + * - o3 + * - claude-opus-4.1 + * - gemini-2.5-pro + * - gemini-2.5-flash + * - gemini-2.5-flash-lite + * - gemini-2.0-flash + * - gemini-2.0-flash-lite + * - morph-v0 + * - sonar, sonar-pro, sonar-deep-research, sonar-reasoning, sonar-reasoning-pro + */ + +const LEGACY_PROVIDERS: Record = { + groq: 'https://api.groq.com/openai/v1', + morph: 'https://api.morphllm.com/v1', +} + +type ProviderRecord = Record & { type: string; id: string } +type ChatModelRecord = Record & { + providerType: string + providerId: string +} + +function migrateLegacyProviders(data: Record) { + const providers = (data.providers ?? []) as ProviderRecord[] + const chatModels = (data.chatModels ?? []) as ChatModelRecord[] + + // Get provider IDs that have models using them + const usedProviderIds = new Set( + chatModels + .filter((m) => m.providerType in LEGACY_PROVIDERS) + .map((m) => m.providerId), + ) + + // Convert used legacy providers, drop unused ones + data.providers = providers.flatMap((p) => { + if (!(p.type in LEGACY_PROVIDERS)) return [p] + if (!usedProviderIds.has(p.id)) return [] + return [ + { + ...p, + type: 'openai-compatible', + baseUrl: p.baseUrl || LEGACY_PROVIDERS[p.type], + }, + ] + }) + + // Convert legacy chat models + data.chatModels = chatModels.map((m) => + m.providerType in LEGACY_PROVIDERS + ? { ...m, providerType: 'openai-compatible' } + : m, + ) +} + +export const migrateFrom13To14: SettingMigration['migrate'] = (data) => { + const newData = { ...data } + newData.version = 14 + + migrateLegacyProviders(newData) + + newData.providers = getMigratedProviders(newData, DEFAULT_PROVIDERS_V14) + newData.chatModels = getMigratedChatModels(newData, DEFAULT_CHAT_MODELS_V14) + + return newData +} + +const DEFAULT_PROVIDERS_V14 = [ + { type: 'openai', id: 'openai' }, + { type: 'anthropic', id: 'anthropic' }, + { type: 'gemini', id: 'gemini' }, + { type: 'xai', id: 'xai' }, + { type: 'deepseek', id: 'deepseek' }, + { type: 'mistral', id: 'mistral' }, + { type: 'perplexity', id: 'perplexity' }, + { type: 'openrouter', id: 'openrouter' }, + { type: 'ollama', id: 'ollama' }, + { type: 'lm-studio', id: 'lm-studio' }, +] as const + +type DefaultChatModelsV14 = { + id: string + providerType: string + providerId: string + model: string + reasoning?: { + enabled: boolean + reasoning_effort?: string + } + thinking?: { + enabled: boolean + budget_tokens: number + } + enable?: boolean +}[] + +export const DEFAULT_CHAT_MODELS_V14: DefaultChatModelsV14 = [ + { + providerType: 'anthropic', + providerId: 'anthropic', + id: 'claude-opus-4.5', + model: 'claude-opus-4-5', + }, + { + providerType: 'anthropic', + providerId: 'anthropic', + id: 'claude-sonnet-4.5', + model: 'claude-sonnet-4-5', + }, + { + providerType: 'anthropic', + providerId: 'anthropic', + id: 'claude-haiku-4.5', + model: 'claude-haiku-4-5', + }, + { + providerType: 'openai', + providerId: 'openai', + id: 'gpt-5.2', + model: 'gpt-5.2', + }, + { + providerType: 'openai', + providerId: 'openai', + id: 'gpt-5-mini', + model: 'gpt-5-mini', + }, + { + providerType: 'openai', + providerId: 'openai', + id: 'gpt-4.1-mini', + model: 'gpt-4.1-mini', + }, + { + providerType: 'openai', + providerId: 'openai', + id: 'o4-mini', + model: 'o4-mini', + reasoning: { + enabled: true, + reasoning_effort: 'medium', + }, + }, + { + providerType: 'gemini', + providerId: 'gemini', + id: 'gemini-3-pro-preview', + model: 'gemini-3-pro-preview', + }, + { + providerType: 'gemini', + providerId: 'gemini', + id: 'gemini-3-flash-preview', + model: 'gemini-3-flash-preview', + }, + { + providerType: 'deepseek', + providerId: 'deepseek', + id: 'deepseek-chat', + model: 'deepseek-chat', + }, + { + providerType: 'deepseek', + providerId: 'deepseek', + id: 'deepseek-reasoner', + model: 'deepseek-reasoner', + }, + { + providerType: 'xai', + providerId: 'xai', + id: 'grok-4-1-fast', + model: 'grok-4-1-fast', + }, + { + providerType: 'xai', + providerId: 'xai', + id: 'grok-4-1-fast-non-reasoning', + model: 'grok-4-1-fast-non-reasoning', + }, +] diff --git a/src/settings/schema/migrations/1_to_2.ts b/src/settings/schema/migrations/1_to_2.ts index 5844dae3..3d91c4e7 100644 --- a/src/settings/schema/migrations/1_to_2.ts +++ b/src/settings/schema/migrations/1_to_2.ts @@ -1,10 +1,15 @@ import { z } from 'zod' -import { ChatModel } from '../../../types/chat-model.types' -import { EmbeddingModel } from '../../../types/embedding-model.types' -import { LLMProvider } from '../../../types/provider.types' import { SettingMigration } from '../setting.types' +// Migration-local types (frozen at v2 state to avoid breaking changes when main types change) +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type V2LLMProvider = any +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type V2ChatModel = any +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type V2EmbeddingModel = any + type NativeLLMModel = { provider: 'openai' | 'anthropic' | 'gemini' | 'groq' model: string @@ -427,7 +432,7 @@ export const V2_PROVIDER_TYPES_INFO = { }, } as const -export const V2_DEFAULT_PROVIDERS: readonly LLMProvider[] = [ +export const V2_DEFAULT_PROVIDERS: readonly V2LLMProvider[] = [ { type: 'openai', id: V2_PROVIDER_TYPES_INFO.openai.defaultProviderId, @@ -454,7 +459,7 @@ export const V2_DEFAULT_PROVIDERS: readonly LLMProvider[] = [ }, ] -export const V2_DEFAULT_CHAT_MODELS: readonly ChatModel[] = [ +export const V2_DEFAULT_CHAT_MODELS: readonly V2ChatModel[] = [ { providerType: 'anthropic', providerId: V2_PROVIDER_TYPES_INFO.anthropic.defaultProviderId, @@ -508,7 +513,6 @@ export const V2_DEFAULT_CHAT_MODELS: readonly ChatModel[] = [ providerId: V2_PROVIDER_TYPES_INFO.openai.defaultProviderId, id: 'o1', model: 'o1', - // @ts-expect-error: streamingDisabled is deprecated streamingDisabled: true, // currently, o1 API doesn't support streaming }, { @@ -531,7 +535,7 @@ export const V2_DEFAULT_CHAT_MODELS: readonly ChatModel[] = [ }, ] -export const V2_DEFAULT_EMBEDDING_MODELS: readonly EmbeddingModel[] = [ +export const V2_DEFAULT_EMBEDDING_MODELS: readonly V2EmbeddingModel[] = [ { providerType: 'openai', providerId: V2_PROVIDER_TYPES_INFO.openai.defaultProviderId, @@ -579,8 +583,8 @@ export const V2_DEFAULT_EMBEDDING_MODELS: readonly EmbeddingModel[] = [ export const migrateFrom1To2: SettingMigration['migrate'] = ( data: SmartComposerSettingsV1, ) => { - const providers: LLMProvider[] = [...V2_DEFAULT_PROVIDERS] - const chatModels: ChatModel[] = [...V2_DEFAULT_CHAT_MODELS] + const providers: V2LLMProvider[] = [...V2_DEFAULT_PROVIDERS] + const chatModels: V2ChatModel[] = [...V2_DEFAULT_CHAT_MODELS] // Map old model IDs to new model IDs const MODEL_ID_MAP: Record = { @@ -659,7 +663,7 @@ export const migrateFrom1To2: SettingMigration['migrate'] = ( (v) => v.type === 'ollama' && v.baseUrl === data.ollamaApplyModel.baseUrl, ) - let ollamaApplyProviderId + let ollamaApplyProviderId: string if ( !existingSameOllamaProviderForApplyModel && data.ollamaApplyModel.baseUrl @@ -735,7 +739,7 @@ export const migrateFrom1To2: SettingMigration['migrate'] = ( v.apiKey === data.openAICompatibleApplyModel.apiKey, ) - let customProviderId + let customProviderId: string if (existingSameProvider) { // if the same provider is already exists, don't create a new one customProviderId = existingSameProvider.id diff --git a/src/settings/schema/migrations/2_to_3.ts b/src/settings/schema/migrations/2_to_3.ts index 66a2d14d..b428d899 100644 --- a/src/settings/schema/migrations/2_to_3.ts +++ b/src/settings/schema/migrations/2_to_3.ts @@ -1,39 +1,45 @@ -import { PROVIDER_TYPES_INFO } from '../../../constants' -import { ChatModel } from '../../../types/chat-model.types' -import { LLMProvider } from '../../../types/provider.types' import { SettingMigration } from '../setting.types' -export const NEW_DEFAULT_PROVIDERS: LLMProvider[] = [ +// Provider IDs at version 3 (hardcoded to avoid dependency on current constants) +const V3_PROVIDER_IDS = { + 'lm-studio': 'lm-studio', + deepseek: 'deepseek', + morph: 'morph', +} as const + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const NEW_DEFAULT_PROVIDERS: any[] = [ { type: 'lm-studio', - id: PROVIDER_TYPES_INFO['lm-studio'].defaultProviderId, + id: V3_PROVIDER_IDS['lm-studio'], }, { type: 'deepseek', - id: PROVIDER_TYPES_INFO.deepseek.defaultProviderId, + id: V3_PROVIDER_IDS.deepseek, }, { type: 'morph', - id: PROVIDER_TYPES_INFO.morph.defaultProviderId, + id: V3_PROVIDER_IDS.morph, }, ] -export const NEW_DEFAULT_CHAT_MODELS: ChatModel[] = [ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const NEW_DEFAULT_CHAT_MODELS: any[] = [ { providerType: 'deepseek', - providerId: PROVIDER_TYPES_INFO.deepseek.defaultProviderId, + providerId: V3_PROVIDER_IDS.deepseek, id: 'deepseek-chat', model: 'deepseek-chat', }, { providerType: 'deepseek', - providerId: PROVIDER_TYPES_INFO.deepseek.defaultProviderId, + providerId: V3_PROVIDER_IDS.deepseek, id: 'deepseek-reasoner', model: 'deepseek-reasoner', }, { providerType: 'morph', - providerId: PROVIDER_TYPES_INFO.morph.defaultProviderId, + providerId: V3_PROVIDER_IDS.morph, id: 'morph-v0', model: 'morph-v0', }, diff --git a/src/settings/schema/migrations/index.ts b/src/settings/schema/migrations/index.ts index f3842551..03d1674b 100644 --- a/src/settings/schema/migrations/index.ts +++ b/src/settings/schema/migrations/index.ts @@ -3,6 +3,8 @@ import { SettingMigration } from '../setting.types' import { migrateFrom0To1 } from './0_to_1' import { migrateFrom10To11 } from './10_to_11' import { migrateFrom11To12 } from './11_to_12' +import { migrateFrom12To13 } from './12_to_13' +import { migrateFrom13To14 } from './13_to_14' import { migrateFrom1To2 } from './1_to_2' import { migrateFrom2To3 } from './2_to_3' import { migrateFrom3To4 } from './3_to_4' @@ -13,7 +15,7 @@ import { migrateFrom7To8 } from './7_to_8' import { migrateFrom8To9 } from './8_to_9' import { migrateFrom9To10 } from './9_to_10' -export const SETTINGS_SCHEMA_VERSION = 12 +export const SETTINGS_SCHEMA_VERSION = 14 export const SETTING_MIGRATIONS: SettingMigration[] = [ { @@ -76,4 +78,14 @@ export const SETTING_MIGRATIONS: SettingMigration[] = [ toVersion: 12, migrate: migrateFrom11To12, }, + { + fromVersion: 12, + toVersion: 13, + migrate: migrateFrom12To13, + }, + { + fromVersion: 13, + toVersion: 14, + migrate: migrateFrom13To14, + }, ] diff --git a/src/types/chat-model.types.ts b/src/types/chat-model.types.ts index 25afa7f4..4e05b57a 100644 --- a/src/types/chat-model.types.ts +++ b/src/types/chat-model.types.ts @@ -49,25 +49,30 @@ export const chatModelSchema = z.discriminatedUnion('providerType', [ z.object({ providerType: z.literal('gemini'), ...baseChatModelSchema.shape, + thinking: z + .object({ + enabled: z.boolean(), + // 'level' for Gemini 3 models, 'budget' for Gemini 2.5 models + control_mode: z.enum(['level', 'budget']).optional(), + // For Gemini 3 models + thinking_level: z.enum(['minimal', 'low', 'medium', 'high']).optional(), + // For Gemini 2.5 models: -1 for dynamic, 0 to disable, or specific token count + thinking_budget: z.number().optional(), + // Return thought summaries in response + include_thoughts: z.boolean().optional(), + }) + .optional(), }), z.object({ - providerType: z.literal('groq'), - ...baseChatModelSchema.shape, - }), - z.object({ - providerType: z.literal('openrouter'), - ...baseChatModelSchema.shape, - }), - z.object({ - providerType: z.literal('ollama'), + providerType: z.literal('xai'), ...baseChatModelSchema.shape, }), z.object({ - providerType: z.literal('lm-studio'), + providerType: z.literal('deepseek'), ...baseChatModelSchema.shape, }), z.object({ - providerType: z.literal('deepseek'), + providerType: z.literal('mistral'), ...baseChatModelSchema.shape, }), z.object({ @@ -80,11 +85,15 @@ export const chatModelSchema = z.discriminatedUnion('providerType', [ .optional(), }), z.object({ - providerType: z.literal('mistral'), + providerType: z.literal('openrouter'), + ...baseChatModelSchema.shape, + }), + z.object({ + providerType: z.literal('ollama'), ...baseChatModelSchema.shape, }), z.object({ - providerType: z.literal('morph'), + providerType: z.literal('lm-studio'), ...baseChatModelSchema.shape, }), z.object({ diff --git a/src/types/chat.ts b/src/types/chat.ts index a682bbe8..17173f18 100644 --- a/src/types/chat.ts +++ b/src/types/chat.ts @@ -3,7 +3,7 @@ import { SerializedEditorState } from 'lexical' import { SelectEmbedding } from '../database/schema' import { ChatModel } from './chat-model.types' -import { ContentPart } from './llm/request' +import { ContentPart, RequestProviderMetadata } from './llm/request' import { Annotation, ResponseUsage } from './llm/response' import { Mentionable, SerializedMentionable } from './mentionable' import { ToolCallRequest, ToolCallResponse } from './tool-call.types' @@ -29,6 +29,7 @@ export type ChatAssistantMessage = { usage?: ResponseUsage model?: ChatModel // TODO: migrate legacy data to new model type } + providerMetadata?: RequestProviderMetadata } export type ChatToolMessage = { role: 'tool' @@ -70,6 +71,7 @@ export type SerializedChatAssistantMessage = { usage?: ResponseUsage model?: ChatModel // TODO: migrate legacy data to new model type } + providerMetadata?: RequestProviderMetadata } export type SerializedChatToolMessage = { role: 'tool' diff --git a/src/types/embedding-model.types.ts b/src/types/embedding-model.types.ts index 64e7a1b1..5f89b5c4 100644 --- a/src/types/embedding-model.types.ts +++ b/src/types/embedding-model.types.ts @@ -33,35 +33,31 @@ export const embeddingModelSchema = z.discriminatedUnion('providerType', [ ...baseEmbeddingModelSchema.shape, }), z.object({ - providerType: z.literal('groq'), + providerType: z.literal('xai'), ...baseEmbeddingModelSchema.shape, }), z.object({ - providerType: z.literal('openrouter'), - ...baseEmbeddingModelSchema.shape, - }), - z.object({ - providerType: z.literal('ollama'), + providerType: z.literal('deepseek'), ...baseEmbeddingModelSchema.shape, }), z.object({ - providerType: z.literal('lm-studio'), + providerType: z.literal('perplexity'), ...baseEmbeddingModelSchema.shape, }), z.object({ - providerType: z.literal('deepseek'), + providerType: z.literal('mistral'), ...baseEmbeddingModelSchema.shape, }), z.object({ - providerType: z.literal('perplexity'), + providerType: z.literal('openrouter'), ...baseEmbeddingModelSchema.shape, }), z.object({ - providerType: z.literal('mistral'), + providerType: z.literal('ollama'), ...baseEmbeddingModelSchema.shape, }), z.object({ - providerType: z.literal('morph'), + providerType: z.literal('lm-studio'), ...baseEmbeddingModelSchema.shape, }), z.object({ diff --git a/src/types/llm/request.ts b/src/types/llm/request.ts index 20b2030f..3f208a94 100644 --- a/src/types/llm/request.ts +++ b/src/types/llm/request.ts @@ -65,10 +65,20 @@ type RequestUserMessage = { role: 'user' content: string | ContentPart[] } +export type RequestProviderMetadata = { + gemini?: { + thoughtSignature?: string + } + deepseek?: { + reasoningContent?: string + } +} + type RequestAssistantMessage = { role: 'assistant' content: string tool_calls?: ToolCallRequest[] + providerMetadata?: RequestProviderMetadata } type RequestToolMessage = { role: 'tool' diff --git a/src/types/llm/response.ts b/src/types/llm/response.ts index 16f72908..18b7b65d 100644 --- a/src/types/llm/response.ts +++ b/src/types/llm/response.ts @@ -27,6 +27,15 @@ export type ResponseUsage = { total_tokens: number } +export type ResponseProviderMetadata = { + gemini?: { + thoughtSignature?: string + } + deepseek?: { + reasoningContent?: string + } +} + type NonStreamingChoice = { finish_reason: string | null // Depends on the model. Ex: 'stop' | 'length' | 'content_filter' | 'tool_calls' | 'function_call' message: { @@ -35,6 +44,7 @@ type NonStreamingChoice = { role: string annotations?: Annotation[] tool_calls?: ToolCall[] + providerMetadata?: ResponseProviderMetadata } error?: Error } @@ -47,6 +57,7 @@ type StreamingChoice = { role?: string annotations?: Annotation[] tool_calls?: ToolCallDelta[] + providerMetadata?: ResponseProviderMetadata } error?: Error } diff --git a/src/types/provider.types.ts b/src/types/provider.types.ts index a9b193f4..effaf1c1 100644 --- a/src/types/provider.types.ts +++ b/src/types/provider.types.ts @@ -28,15 +28,15 @@ export const llmProviderSchema = z.discriminatedUnion('type', [ ...baseLlmProviderSchema.shape, }), z.object({ - type: z.literal('deepseek'), + type: z.literal('xai'), ...baseLlmProviderSchema.shape, }), z.object({ - type: z.literal('perplexity'), + type: z.literal('deepseek'), ...baseLlmProviderSchema.shape, }), z.object({ - type: z.literal('groq'), + type: z.literal('perplexity'), ...baseLlmProviderSchema.shape, }), z.object({ @@ -55,10 +55,6 @@ export const llmProviderSchema = z.discriminatedUnion('type', [ type: z.literal('lm-studio'), ...baseLlmProviderSchema.shape, }), - z.object({ - type: z.literal('morph'), - ...baseLlmProviderSchema.shape, - }), z.object({ type: z.literal('azure-openai'), ...baseLlmProviderSchema.shape, diff --git a/src/utils/chat/apply.ts b/src/utils/chat/apply.ts index 7dcf5d55..969f8b11 100644 --- a/src/utils/chat/apply.ts +++ b/src/utils/chat/apply.ts @@ -15,6 +15,20 @@ import { LLMProvider } from '../../types/provider.types' const MAX_CHAT_HISTORY_MESSAGES = 10 +const PREDICTED_OUTPUTS_SUPPORTED_MODELS = [ + 'gpt-4o', + 'gpt-4o-mini', + 'gpt-4.1', + 'gpt-4.1-mini', + 'gpt-4.1-nano', +] + +const supportsPredictedOutputs = (modelId: string): boolean => { + return PREDICTED_OUTPUTS_SUPPORTED_MODELS.some((supportedModel) => + modelId.startsWith(supportedModel), + ) +} + const systemPrompt = `You are an intelligent assistant helping a user apply changes to a markdown file. You will receive: @@ -142,21 +156,21 @@ export const applyChangesToFile = async ({ model: model.model, messages: requestMessages, stream: false, - - // prediction is only available for OpenAI - prediction: { - type: 'content', - content: [ - { - type: 'text', - text: currentFileContent, - }, - { - type: 'text', - text: blockToApply, - }, - ], - }, + ...(supportsPredictedOutputs(model.model) && { + prediction: { + type: 'content', + content: [ + { + type: 'text', + text: currentFileContent, + }, + { + type: 'text', + text: blockToApply, + }, + ], + }, + }), }) const responseContent = response.choices[0].message.content diff --git a/src/utils/chat/promptGenerator.ts b/src/utils/chat/promptGenerator.ts index 0e0ffe13..44714fcd 100644 --- a/src/utils/chat/promptGenerator.ts +++ b/src/utils/chat/promptGenerator.ts @@ -207,6 +207,7 @@ ${message.annotations ...(citationContent ? [citationContent] : []), ].join('\n'), tool_calls: message.toolCallRequests, + providerMetadata: message.providerMetadata, }, ] } diff --git a/src/utils/chat/responseGenerator.ts b/src/utils/chat/responseGenerator.ts index 6c552dbf..7dff5f45 100644 --- a/src/utils/chat/responseGenerator.ts +++ b/src/utils/chat/responseGenerator.ts @@ -279,6 +279,8 @@ export class ResponseGenerator { }) } + const providerMetadata = chunk.choices[0]?.delta?.providerMetadata + this.updateResponseMessages((messages) => messages.map((message) => message.id === responseMessageId && message.role === 'assistant' @@ -296,6 +298,8 @@ export class ResponseGenerator { ...message.metadata, usage: chunk.usage ?? message.metadata?.usage, }, + // Keep the first providerMetadata received (signature is sent once) + providerMetadata: message.providerMetadata ?? providerMetadata, } : message, ), diff --git a/src/utils/llm/price-calculator.ts b/src/utils/llm/price-calculator.ts index 5d7cf315..037ba110 100644 --- a/src/utils/llm/price-calculator.ts +++ b/src/utils/llm/price-calculator.ts @@ -1,4 +1,9 @@ -import { ANTHROPIC_PRICES, GEMINI_PRICES, OPENAI_PRICES } from '../../constants' +import { + ANTHROPIC_PRICES, + GEMINI_PRICES, + OPENAI_PRICES, + XAI_PRICES, +} from '../../constants' import { ChatModel } from '../../types/chat-model.types' import { ResponseUsage } from '../../types/llm/response' @@ -38,6 +43,15 @@ export const calculateLLMCost = ({ 1_000_000 ) } + case 'xai': { + const modelPricing = XAI_PRICES[model.model] + if (!modelPricing) return null + return ( + (usage.prompt_tokens * modelPricing.input + + usage.completion_tokens * modelPricing.output) / + 1_000_000 + ) + } default: return null }