diff --git a/package.json b/package.json index b93691d2693..988072e981a 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,8 @@ "bluebird": ">=3.7.2", "glob": ">=11.1.0", "@types/react": "^18.3.23", - "@types/react-dom": "^18.3.5" + "@types/react-dom": "^18.3.5", + "zod": "3.25.76" } } } diff --git a/packages/types/package.json b/packages/types/package.json index 09fac8d672d..d66d87ac72d 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -23,7 +23,7 @@ "clean": "rimraf dist .turbo" }, "dependencies": { - "zod": "^3.25.61" + "zod": "3.25.76" }, "devDependencies": { "@roo-code/config-eslint": "workspace:^", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 60bff02c0b6..b8ca01240be 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,7 @@ overrides: glob: '>=11.1.0' '@types/react': ^18.3.23 '@types/react-dom': ^18.3.5 + zod: 3.25.76 importers: @@ -268,7 +269,7 @@ importers: version: 0.518.0(react@18.3.1) next: specifier: ~15.2.8 - version: 15.2.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 15.2.8(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) next-themes: specifier: ^0.4.6 version: 0.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -303,8 +304,8 @@ importers: specifier: ^1.1.2 version: 1.1.2(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) zod: - specifier: ^3.25.61 - version: 3.25.61 + specifier: 3.25.76 + version: 3.25.76 devDependencies: '@roo-code/config-eslint': specifier: workspace:^ @@ -377,7 +378,7 @@ importers: version: 0.518.0(react@18.3.1) next: specifier: ~15.2.8 - version: 15.2.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 15.2.8(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) next-themes: specifier: ^0.4.6 version: 0.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -418,8 +419,8 @@ importers: specifier: ^6.1.86 version: 6.1.86 zod: - specifier: ^3.25.61 - version: 3.25.61 + specifier: 3.25.76 + version: 3.25.76 devDependencies: '@roo-code/config-eslint': specifier: workspace:^ @@ -444,7 +445,7 @@ importers: version: 10.4.21(postcss@8.5.4) next-sitemap: specifier: ^4.2.3 - version: 4.2.3(next@15.2.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) + version: 4.2.3(next@15.2.8(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) postcss: specifier: ^8.5.4 version: 8.5.4 @@ -455,8 +456,8 @@ importers: packages/build: dependencies: zod: - specifier: ^3.25.61 - version: 3.25.61 + specifier: 3.25.76 + version: 3.25.76 devDependencies: '@roo-code/config-eslint': specifier: workspace:^ @@ -489,7 +490,7 @@ importers: specifier: ^4.8.1 version: 4.8.1 zod: - specifier: ^3.25.76 + specifier: 3.25.76 version: 3.25.76 devDependencies: '@roo-code/config-eslint': @@ -564,7 +565,7 @@ importers: specifier: ^5.12.2 version: 5.12.2(ws@8.18.3)(zod@3.25.76) zod: - specifier: ^3.25.61 + specifier: 3.25.76 version: 3.25.76 devDependencies: '@roo-code/config-eslint': @@ -593,7 +594,7 @@ importers: version: 0.13.0 drizzle-orm: specifier: ^0.44.1 - version: 0.44.1(@libsql/client@0.15.8)(better-sqlite3@11.10.0)(gel@2.1.0)(postgres@3.4.7) + version: 0.44.1(@libsql/client@0.15.8)(@opentelemetry/api@1.9.0)(better-sqlite3@11.10.0)(gel@2.1.0)(postgres@3.4.7) execa: specifier: ^9.6.0 version: 9.6.0 @@ -619,8 +620,8 @@ importers: specifier: ^5.5.5 version: 5.5.5 zod: - specifier: ^3.25.61 - version: 3.25.61 + specifier: 3.25.76 + version: 3.25.76 devDependencies: '@roo-code/config-eslint': specifier: workspace:^ @@ -681,8 +682,8 @@ importers: specifier: ^5.0.0 version: 5.1.1 zod: - specifier: ^3.25.61 - version: 3.25.61 + specifier: 3.25.76 + version: 3.25.76 devDependencies: '@roo-code/config-eslint': specifier: workspace:^ @@ -703,8 +704,8 @@ importers: packages/types: dependencies: zod: - specifier: ^3.25.61 - version: 3.25.61 + specifier: 3.25.76 + version: 3.25.76 devDependencies: '@roo-code/config-eslint': specifier: workspace:^ @@ -765,7 +766,7 @@ importers: version: 1.2.0 '@mistralai/mistralai': specifier: ^1.9.18 - version: 1.9.18(zod@3.25.61) + version: 1.9.18(zod@3.25.76) '@modelcontextprotocol/sdk': specifier: 1.12.0 version: 1.12.0 @@ -879,7 +880,7 @@ importers: version: 0.5.17 openai: specifier: ^5.12.2 - version: 5.12.2(ws@8.18.3)(zod@3.25.61) + version: 5.12.2(ws@8.18.3)(zod@3.25.76) os-name: specifier: ^6.0.0 version: 6.1.0 @@ -986,9 +987,12 @@ importers: specifier: ^2.8.0 version: 2.8.0 zod: - specifier: 3.25.61 - version: 3.25.61 + specifier: 3.25.76 + version: 3.25.76 devDependencies: + '@openrouter/ai-sdk-provider': + specifier: ^2.0.4 + version: 2.1.1(ai@6.0.57(zod@3.25.76))(zod@3.25.76) '@roo-code/build': specifier: workspace:^ version: link:../packages/build @@ -1013,9 +1017,6 @@ importers: '@types/glob': specifier: ^8.1.0 version: 8.1.0 - '@types/json-stream-stringify': - specifier: ^2.0.4 - version: 2.0.4 '@types/lodash.debounce': specifier: ^4.0.9 version: 4.0.9 @@ -1064,6 +1065,9 @@ importers: '@vscode/vsce': specifier: 3.3.2 version: 3.3.2 + ai: + specifier: ^6.0.0 + version: 6.0.57(zod@3.25.76) esbuild-wasm: specifier: ^0.25.0 version: 0.25.12 @@ -1099,7 +1103,7 @@ importers: version: 3.2.4(@types/debug@4.1.12)(@types/node@20.17.50)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0) zod-to-ts: specifier: ^1.2.0 - version: 1.2.0(typescript@5.8.3)(zod@3.25.61) + version: 1.2.0(typescript@5.8.3)(zod@3.25.76) webview-ui: dependencies: @@ -1305,8 +1309,8 @@ importers: specifier: ^0.2.2 version: 0.2.2(@types/react@18.3.23)(react@18.3.1) zod: - specifier: ^3.25.61 - version: 3.25.61 + specifier: 3.25.76 + version: 3.25.76 devDependencies: '@roo-code/config-eslint': specifier: workspace:^ @@ -1374,6 +1378,22 @@ packages: '@adobe/css-tools@4.4.2': resolution: {integrity: sha512-baYZExFpsdkBNuvGKTKWCwKH57HRZLVtycZS05WTQNVOiXVSeAki3nU35zlRbToeMW8aHlJfyS+1C4BOv27q0A==} + '@ai-sdk/gateway@3.0.25': + resolution: {integrity: sha512-j0AQeA7hOVqwImykQlganf/Euj3uEXf0h3G0O4qKTDpEwE+EZGIPnVimCWht5W91lAetPZSfavDyvfpuPDd2PQ==} + engines: {node: '>=18'} + peerDependencies: + zod: 3.25.76 + + '@ai-sdk/provider-utils@4.0.10': + resolution: {integrity: sha512-VeDAiCH+ZK8Xs4hb9Cw7pHlujWNL52RKe8TExOkrw6Ir1AmfajBZTb9XUdKOZO08RwQElIKA8+Ltm+Gqfo8djQ==} + engines: {node: '>=18'} + peerDependencies: + zod: 3.25.76 + + '@ai-sdk/provider@3.0.5': + resolution: {integrity: sha512-2Xmoq6DBJqmSl80U6V9z5jJSJP7ehaJJQMy2iFUqTay06wdCqTnPVBBQbtEL8RCChenL+q5DC5H5WzU3vV3v8w==} + engines: {node: '>=18'} + '@alcalzone/ansi-tokenize@0.2.3': resolution: {integrity: sha512-jsElTJ0sQ4wHRz+C45tfect76BwbTbgkgKByOzpCN9xG61N5V6u/glvg1CsNJhq2xJIFpKHSwG3D2wPPuEYOrQ==} engines: {node: '>=18'} @@ -2392,7 +2412,7 @@ packages: '@mistralai/mistralai@1.9.18': resolution: {integrity: sha512-D/vNAGEvWMsg95tzgLTg7pPnW9leOPyH+nh1Os05NwxVPbUykoYgMAwOEX7J46msahWdvZ4NQQuxUXIUV2P6dg==} peerDependencies: - zod: '>= 3' + zod: 3.25.76 '@mixmark-io/domino@2.2.0': resolution: {integrity: sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==} @@ -2591,6 +2611,17 @@ packages: '@open-draft/until@2.1.0': resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} + '@openrouter/ai-sdk-provider@2.1.1': + resolution: {integrity: sha512-UypPbVnSExxmG/4Zg0usRiit3auvQVrjUXSyEhm0sZ9GQnW/d8p/bKgCk2neh1W5YyRSo7PNQvCrAEBHZnqQkQ==} + engines: {node: '>=18'} + peerDependencies: + ai: ^6.0.0 + zod: 3.25.76 + + '@opentelemetry/api@1.9.0': + resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} + engines: {node: '>=8.0.0'} + '@oxc-resolver/binding-darwin-arm64@11.2.0': resolution: {integrity: sha512-ruKLkS+Dm/YIJaUhzEB7zPI+jh3EXxu0QnNV8I7t9jf0lpD2VnltuyRbhrbJEkksklZj//xCMyFFsILGjiU2Mg==} cpu: [arm64] @@ -3845,6 +3876,9 @@ packages: '@socket.io/component-emitter@3.1.2': resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@standard-schema/utils@0.3.0': resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==} @@ -4268,10 +4302,6 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - '@types/json-stream-stringify@2.0.4': - resolution: {integrity: sha512-xSFsVnoQ8Y/7BiVF3/fEIwRx9RoGzssDKVwhy1g23wkA4GAmA3v8lsl6CxsmUD6vf4EiRd+J0ULLkMbAWRSsgQ==} - deprecated: This is a stub types definition. json-stream-stringify provides its own type definitions, so you do not need this installed. - '@types/katex@0.16.7': resolution: {integrity: sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==} @@ -4469,6 +4499,10 @@ packages: resolution: {integrity: sha512-e4kQK9mP8ntpo3dACWirGod/hHv4qO5JMj9a/0a2AZto7b4persj5YP7t1Er372gTtYFTYxNhMx34jRvHooglw==} engines: {node: '>=16'} + '@vercel/oidc@3.1.0': + resolution: {integrity: sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w==} + engines: {node: '>= 20'} + '@vitejs/plugin-react@4.4.1': resolution: {integrity: sha512-IpEm5ZmeXAP/osiBXVVP5KjFMzbWOonMs0NaQQl+xYnUAcq4oHUBsF2+p4MgKWG4YMmFYJU8A6sxRPuowllm6w==} engines: {node: ^14.18.0 || >=16.0.0} @@ -4621,6 +4655,12 @@ packages: resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==} engines: {node: '>= 8.0.0'} + ai@6.0.57: + resolution: {integrity: sha512-5wYcMQmOaNU71wGv4XX1db3zvn4uLjLbTKIo6cQZPWOJElA0882XI7Eawx6TCd5jbjOvKMIP+KLWbpVomAFT2g==} + engines: {node: '>=18'} + peerDependencies: + zod: 3.25.76 + ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} @@ -6200,6 +6240,10 @@ packages: resolution: {integrity: sha512-6RxOBZ/cYgd8usLwsEl+EC09Au/9BcmCKYF2/xbml6DNczf7nv0MQb+7BA2F+li6//I+28VNlQR37XfQtcAJuA==} engines: {node: '>=18.0.0'} + eventsource-parser@3.0.6: + resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==} + engines: {node: '>=18.0.0'} + eventsource@3.0.7: resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} engines: {node: '>=18.0.0'} @@ -7319,6 +7363,9 @@ packages: json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + json-schema@0.4.0: + resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} + json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} @@ -8317,7 +8364,7 @@ packages: hasBin: true peerDependencies: ws: ^8.18.0 - zod: ^3.23.8 + zod: 3.25.76 peerDependenciesMeta: ws: optional: true @@ -10688,25 +10735,19 @@ packages: zod-to-json-schema@3.24.5: resolution: {integrity: sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==} peerDependencies: - zod: ^3.24.1 + zod: 3.25.76 zod-to-ts@1.2.0: resolution: {integrity: sha512-x30XE43V+InwGpvTySRNz9kB7qFU8DlyEy7BsSTCHPH1R0QasMmHWZDCzYm6bVXtj/9NNJAZF3jW8rzFvH5OFA==} peerDependencies: typescript: ^4.9.4 || ^5.0.2 - zod: ^3 + zod: 3.25.76 zod-validation-error@3.4.1: resolution: {integrity: sha512-1KP64yqDPQ3rupxNv7oXhf7KdhHHgaqbKuspVoiN93TT0xrBjql+Svjkdjq/Qh/7GSMmgQs3AfvBT0heE35thw==} engines: {node: '>=18.0.0'} peerDependencies: - zod: ^3.24.4 - - zod@3.23.8: - resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==} - - zod@3.25.61: - resolution: {integrity: sha512-fzfJgUw78LTNnHujj9re1Ov/JJQkRZZGDMcYqSx7Hp4rPOkKywaFHq0S6GoHeXs0wGNE/sIOutkXgnwzrVOGCQ==} + zod: 3.25.76 zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} @@ -10736,6 +10777,24 @@ snapshots: '@adobe/css-tools@4.4.2': {} + '@ai-sdk/gateway@3.0.25(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 3.0.5 + '@ai-sdk/provider-utils': 4.0.10(zod@3.25.76) + '@vercel/oidc': 3.1.0 + zod: 3.25.76 + + '@ai-sdk/provider-utils@4.0.10(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 3.0.5 + '@standard-schema/spec': 1.1.0 + eventsource-parser: 3.0.6 + zod: 3.25.76 + + '@ai-sdk/provider@3.0.5': + dependencies: + json-schema: 0.4.0 + '@alcalzone/ansi-tokenize@0.2.3': dependencies: ansi-styles: 6.2.3 @@ -12250,10 +12309,10 @@ snapshots: dependencies: exenv-es6: 1.1.1 - '@mistralai/mistralai@1.9.18(zod@3.25.61)': + '@mistralai/mistralai@1.9.18(zod@3.25.76)': dependencies: - zod: 3.25.61 - zod-to-json-schema: 3.24.5(zod@3.25.61) + zod: 3.25.76 + zod-to-json-schema: 3.24.5(zod@3.25.76) '@mixmark-io/domino@2.2.0': {} @@ -12421,6 +12480,13 @@ snapshots: '@open-draft/until@2.1.0': {} + '@openrouter/ai-sdk-provider@2.1.1(ai@6.0.57(zod@3.25.76))(zod@3.25.76)': + dependencies: + ai: 6.0.57(zod@3.25.76) + zod: 3.25.76 + + '@opentelemetry/api@1.9.0': {} + '@oxc-resolver/binding-darwin-arm64@11.2.0': optional: true @@ -13822,6 +13888,8 @@ snapshots: '@socket.io/component-emitter@3.1.2': {} + '@standard-schema/spec@1.1.0': {} + '@standard-schema/utils@0.3.0': {} '@swc/counter@0.1.3': {} @@ -14248,10 +14316,6 @@ snapshots: '@types/json-schema@7.0.15': {} - '@types/json-stream-stringify@2.0.4': - dependencies: - json-stream-stringify: 3.1.6 - '@types/katex@0.16.7': {} '@types/lodash.debounce@4.0.9': @@ -14483,6 +14547,8 @@ snapshots: satori: 0.12.2 yoga-wasm-web: 0.3.3 + '@vercel/oidc@3.1.0': {} + '@vitejs/plugin-react@4.4.1(vite@6.3.6(@types/node@20.17.57)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0))': dependencies: '@babel/core': 7.27.1 @@ -14555,7 +14621,7 @@ snapshots: sirv: 3.0.1 tinyglobby: 0.2.14 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@20.17.50)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.2.1)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0) '@vitest/utils@3.2.4': dependencies: @@ -14705,6 +14771,14 @@ snapshots: dependencies: humanize-ms: 1.2.1 + ai@6.0.57(zod@3.25.76): + dependencies: + '@ai-sdk/gateway': 3.0.25(zod@3.25.76) + '@ai-sdk/provider': 3.0.5 + '@ai-sdk/provider-utils': 4.0.10(zod@3.25.76) + '@opentelemetry/api': 1.9.0 + zod: 3.25.76 + ajv@6.12.6: dependencies: fast-deep-equal: 3.1.3 @@ -15232,7 +15306,7 @@ snapshots: dependencies: devtools-protocol: 0.0.1367902 mitt: 3.0.1 - zod: 3.23.8 + zod: 3.25.76 chromium-bidi@5.1.0(devtools-protocol@0.0.1452169): dependencies: @@ -15910,9 +15984,10 @@ snapshots: transitivePeerDependencies: - supports-color - drizzle-orm@0.44.1(@libsql/client@0.15.8)(better-sqlite3@11.10.0)(gel@2.1.0)(postgres@3.4.7): + drizzle-orm@0.44.1(@libsql/client@0.15.8)(@opentelemetry/api@1.9.0)(better-sqlite3@11.10.0)(gel@2.1.0)(postgres@3.4.7): optionalDependencies: '@libsql/client': 0.15.8 + '@opentelemetry/api': 1.9.0 better-sqlite3: 11.10.0 gel: 2.1.0 postgres: 3.4.7 @@ -16387,6 +16462,8 @@ snapshots: eventsource-parser@3.0.2: {} + eventsource-parser@3.0.6: {} + eventsource@3.0.7: dependencies: eventsource-parser: 3.0.2 @@ -17689,6 +17766,8 @@ snapshots: json-schema-traverse@0.4.1: {} + json-schema@0.4.0: {} + json-stable-stringify-without-jsonify@1.0.1: {} json-stream-stringify@3.1.6: {} @@ -18740,20 +18819,20 @@ snapshots: netmask@2.0.2: {} - next-sitemap@4.2.3(next@15.2.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1)): + next-sitemap@4.2.3(next@15.2.8(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)): dependencies: '@corex/deepmerge': 4.0.43 '@next/env': 13.5.11 fast-glob: 3.3.3 minimist: 1.2.8 - next: 15.2.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + next: 15.2.8(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) next-themes@0.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - next@15.2.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + next@15.2.8(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@next/env': 15.2.8 '@swc/counter': 0.1.3 @@ -18773,6 +18852,7 @@ snapshots: '@next/swc-linux-x64-musl': 15.2.5 '@next/swc-win32-arm64-msvc': 15.2.5 '@next/swc-win32-x64-msvc': 15.2.5 + '@opentelemetry/api': 1.9.0 sharp: 0.33.5 transitivePeerDependencies: - '@babel/core' @@ -18945,11 +19025,6 @@ snapshots: is-inside-container: 1.0.0 is-wsl: 3.1.0 - openai@5.12.2(ws@8.18.3)(zod@3.25.61): - optionalDependencies: - ws: 8.18.3 - zod: 3.25.61 - openai@5.12.2(ws@8.18.3)(zod@3.25.76): optionalDependencies: ws: 8.18.3 @@ -21785,27 +21860,19 @@ snapshots: compress-commons: 6.0.2 readable-stream: 4.7.0 - zod-to-json-schema@3.24.5(zod@3.25.61): - dependencies: - zod: 3.25.61 - zod-to-json-schema@3.24.5(zod@3.25.76): dependencies: zod: 3.25.76 - zod-to-ts@1.2.0(typescript@5.8.3)(zod@3.25.61): + zod-to-ts@1.2.0(typescript@5.8.3)(zod@3.25.76): dependencies: typescript: 5.8.3 - zod: 3.25.61 + zod: 3.25.76 zod-validation-error@3.4.1(zod@3.25.76): dependencies: zod: 3.25.76 - zod@3.23.8: {} - - zod@3.25.61: {} - zod@3.25.76: {} zustand@5.0.9(@types/react@18.3.23)(react@19.2.3): diff --git a/src/api/transform/__tests__/ai-sdk.spec.ts b/src/api/transform/__tests__/ai-sdk.spec.ts new file mode 100644 index 00000000000..4a82ecac4ee --- /dev/null +++ b/src/api/transform/__tests__/ai-sdk.spec.ts @@ -0,0 +1,492 @@ +import { Anthropic } from "@anthropic-ai/sdk" +import OpenAI from "openai" +import { convertToAiSdkMessages, convertToolsForAiSdk, processAiSdkStreamPart } from "../ai-sdk" + +vitest.mock("ai", () => ({ + tool: vitest.fn((t) => t), + jsonSchema: vitest.fn((s) => s), +})) + +describe("AI SDK conversion utilities", () => { + describe("convertToAiSdkMessages", () => { + it("converts simple string messages", () => { + const messages: Anthropic.Messages.MessageParam[] = [ + { role: "user", content: "Hello" }, + { role: "assistant", content: "Hi there" }, + ] + + const result = convertToAiSdkMessages(messages) + + expect(result).toHaveLength(2) + expect(result[0]).toEqual({ role: "user", content: "Hello" }) + expect(result[1]).toEqual({ role: "assistant", content: "Hi there" }) + }) + + it("converts user messages with text content blocks", () => { + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: [{ type: "text", text: "Hello world" }], + }, + ] + + const result = convertToAiSdkMessages(messages) + + expect(result).toHaveLength(1) + expect(result[0]).toEqual({ + role: "user", + content: [{ type: "text", text: "Hello world" }], + }) + }) + + it("converts user messages with image content", () => { + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: [ + { type: "text", text: "What is in this image?" }, + { + type: "image", + source: { + type: "base64", + media_type: "image/png", + data: "base64encodeddata", + }, + }, + ], + }, + ] + + const result = convertToAiSdkMessages(messages) + + expect(result).toHaveLength(1) + expect(result[0]).toEqual({ + role: "user", + content: [ + { type: "text", text: "What is in this image?" }, + { + type: "image", + image: "data:image/png;base64,base64encodeddata", + mimeType: "image/png", + }, + ], + }) + }) + + it("converts user messages with URL image content", () => { + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: [ + { type: "text", text: "What is in this image?" }, + { + type: "image", + source: { + type: "url", + url: "https://example.com/image.png", + }, + } as any, + ], + }, + ] + + const result = convertToAiSdkMessages(messages) + + expect(result).toHaveLength(1) + expect(result[0]).toEqual({ + role: "user", + content: [ + { type: "text", text: "What is in this image?" }, + { + type: "image", + image: "https://example.com/image.png", + }, + ], + }) + }) + + it("converts tool results into separate tool role messages with resolved tool names", () => { + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "assistant", + content: [ + { + type: "tool_use", + id: "call_123", + name: "read_file", + input: { path: "test.ts" }, + }, + ], + }, + { + role: "user", + content: [ + { + type: "tool_result", + tool_use_id: "call_123", + content: "Tool result content", + }, + ], + }, + ] + + const result = convertToAiSdkMessages(messages) + + expect(result).toHaveLength(2) + expect(result[0]).toEqual({ + role: "assistant", + content: [ + { + type: "tool-call", + toolCallId: "call_123", + toolName: "read_file", + input: { path: "test.ts" }, + }, + ], + }) + // Tool results now go to role: "tool" messages per AI SDK v6 schema + expect(result[1]).toEqual({ + role: "tool", + content: [ + { + type: "tool-result", + toolCallId: "call_123", + toolName: "read_file", + output: { type: "text", value: "Tool result content" }, + }, + ], + }) + }) + + it("uses unknown_tool for tool results without matching tool call", () => { + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: [ + { + type: "tool_result", + tool_use_id: "call_orphan", + content: "Orphan result", + }, + ], + }, + ] + + const result = convertToAiSdkMessages(messages) + + expect(result).toHaveLength(1) + // Tool results go to role: "tool" messages + expect(result[0]).toEqual({ + role: "tool", + content: [ + { + type: "tool-result", + toolCallId: "call_orphan", + toolName: "unknown_tool", + output: { type: "text", value: "Orphan result" }, + }, + ], + }) + }) + + it("separates tool results and text content into different messages", () => { + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "assistant", + content: [ + { + type: "tool_use", + id: "call_123", + name: "read_file", + input: { path: "test.ts" }, + }, + ], + }, + { + role: "user", + content: [ + { + type: "tool_result", + tool_use_id: "call_123", + content: "File contents here", + }, + { + type: "text", + text: "Please analyze this file", + }, + ], + }, + ] + + const result = convertToAiSdkMessages(messages) + + expect(result).toHaveLength(3) + expect(result[0]).toEqual({ + role: "assistant", + content: [ + { + type: "tool-call", + toolCallId: "call_123", + toolName: "read_file", + input: { path: "test.ts" }, + }, + ], + }) + // Tool results go first in a "tool" message + expect(result[1]).toEqual({ + role: "tool", + content: [ + { + type: "tool-result", + toolCallId: "call_123", + toolName: "read_file", + output: { type: "text", value: "File contents here" }, + }, + ], + }) + // Text content goes in a separate "user" message + expect(result[2]).toEqual({ + role: "user", + content: [{ type: "text", text: "Please analyze this file" }], + }) + }) + + it("converts assistant messages with tool use", () => { + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "assistant", + content: [ + { type: "text", text: "Let me read that file" }, + { + type: "tool_use", + id: "call_456", + name: "read_file", + input: { path: "test.ts" }, + }, + ], + }, + ] + + const result = convertToAiSdkMessages(messages) + + expect(result).toHaveLength(1) + expect(result[0]).toEqual({ + role: "assistant", + content: [ + { type: "text", text: "Let me read that file" }, + { + type: "tool-call", + toolCallId: "call_456", + toolName: "read_file", + input: { path: "test.ts" }, + }, + ], + }) + }) + + it("handles empty assistant content", () => { + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "assistant", + content: [], + }, + ] + + const result = convertToAiSdkMessages(messages) + + expect(result).toHaveLength(1) + expect(result[0]).toEqual({ + role: "assistant", + content: [{ type: "text", text: "" }], + }) + }) + }) + + describe("convertToolsForAiSdk", () => { + it("returns undefined for empty tools", () => { + expect(convertToolsForAiSdk(undefined)).toBeUndefined() + expect(convertToolsForAiSdk([])).toBeUndefined() + }) + + it("converts function tools to AI SDK format", () => { + const tools: OpenAI.Chat.ChatCompletionTool[] = [ + { + type: "function", + function: { + name: "read_file", + description: "Read a file from disk", + parameters: { + type: "object", + properties: { + path: { type: "string", description: "File path" }, + }, + required: ["path"], + }, + }, + }, + ] + + const result = convertToolsForAiSdk(tools) + + expect(result).toBeDefined() + expect(result!.read_file).toBeDefined() + expect(result!.read_file.description).toBe("Read a file from disk") + }) + + it("converts multiple tools", () => { + const tools: OpenAI.Chat.ChatCompletionTool[] = [ + { + type: "function", + function: { + name: "read_file", + description: "Read a file", + parameters: {}, + }, + }, + { + type: "function", + function: { + name: "write_file", + description: "Write a file", + parameters: {}, + }, + }, + ] + + const result = convertToolsForAiSdk(tools) + + expect(result).toBeDefined() + expect(Object.keys(result!)).toHaveLength(2) + expect(result!.read_file).toBeDefined() + expect(result!.write_file).toBeDefined() + }) + }) + + describe("processAiSdkStreamPart", () => { + it("processes text-delta chunks", () => { + const part = { type: "text-delta" as const, id: "1", text: "Hello" } + const chunks = [...processAiSdkStreamPart(part)] + + expect(chunks).toHaveLength(1) + expect(chunks[0]).toEqual({ type: "text", text: "Hello" }) + }) + + it("processes text chunks (fullStream format)", () => { + const part = { type: "text" as const, text: "Hello from fullStream" } + const chunks = [...processAiSdkStreamPart(part as any)] + + expect(chunks).toHaveLength(1) + expect(chunks[0]).toEqual({ type: "text", text: "Hello from fullStream" }) + }) + + it("processes reasoning-delta chunks", () => { + const part = { type: "reasoning-delta" as const, id: "1", text: "thinking..." } + const chunks = [...processAiSdkStreamPart(part)] + + expect(chunks).toHaveLength(1) + expect(chunks[0]).toEqual({ type: "reasoning", text: "thinking..." }) + }) + + it("processes reasoning chunks (fullStream format)", () => { + const part = { type: "reasoning" as const, text: "reasoning from fullStream" } + const chunks = [...processAiSdkStreamPart(part as any)] + + expect(chunks).toHaveLength(1) + expect(chunks[0]).toEqual({ type: "reasoning", text: "reasoning from fullStream" }) + }) + + it("processes tool-input-start chunks", () => { + const part = { type: "tool-input-start" as const, id: "call_1", toolName: "read_file" } + const chunks = [...processAiSdkStreamPart(part)] + + expect(chunks).toHaveLength(1) + expect(chunks[0]).toEqual({ type: "tool_call_start", id: "call_1", name: "read_file" }) + }) + + it("processes tool-input-delta chunks", () => { + const part = { type: "tool-input-delta" as const, id: "call_1", delta: '{"path":' } + const chunks = [...processAiSdkStreamPart(part)] + + expect(chunks).toHaveLength(1) + expect(chunks[0]).toEqual({ type: "tool_call_delta", id: "call_1", delta: '{"path":' }) + }) + + it("processes tool-input-end chunks", () => { + const part = { type: "tool-input-end" as const, id: "call_1" } + const chunks = [...processAiSdkStreamPart(part)] + + expect(chunks).toHaveLength(1) + expect(chunks[0]).toEqual({ type: "tool_call_end", id: "call_1" }) + }) + + it("processes complete tool-call chunks", () => { + const part = { + type: "tool-call" as const, + toolCallId: "call_1", + toolName: "read_file", + input: { path: "test.ts" }, + } + const chunks = [...processAiSdkStreamPart(part)] + + expect(chunks).toHaveLength(1) + expect(chunks[0]).toEqual({ + type: "tool_call", + id: "call_1", + name: "read_file", + arguments: '{"path":"test.ts"}', + }) + }) + + it("processes source chunks with URL", () => { + const part = { + type: "source" as const, + url: "https://example.com", + title: "Example Source", + } + const chunks = [...processAiSdkStreamPart(part as any)] + + expect(chunks).toHaveLength(1) + expect(chunks[0]).toEqual({ + type: "grounding", + sources: [ + { + title: "Example Source", + url: "https://example.com", + snippet: undefined, + }, + ], + }) + }) + + it("processes error chunks", () => { + const part = { type: "error" as const, error: new Error("Test error") } + const chunks = [...processAiSdkStreamPart(part)] + + expect(chunks).toHaveLength(1) + expect(chunks[0]).toEqual({ + type: "error", + error: "StreamError", + message: "Test error", + }) + }) + + it("ignores lifecycle events", () => { + const lifecycleEvents = [ + { type: "text-start" as const }, + { type: "text-end" as const }, + { type: "reasoning-start" as const }, + { type: "reasoning-end" as const }, + { type: "start-step" as const }, + { type: "finish-step" as const }, + { type: "start" as const }, + { type: "finish" as const }, + { type: "abort" as const }, + ] + + for (const event of lifecycleEvents) { + const chunks = [...processAiSdkStreamPart(event as any)] + expect(chunks).toHaveLength(0) + } + }) + }) +}) diff --git a/src/api/transform/ai-sdk.ts b/src/api/transform/ai-sdk.ts new file mode 100644 index 00000000000..535b932aba7 --- /dev/null +++ b/src/api/transform/ai-sdk.ts @@ -0,0 +1,282 @@ +/** + * AI SDK conversion utilities for transforming between Anthropic/OpenAI formats and Vercel AI SDK formats. + * These utilities are designed to be reused across different AI SDK providers. + */ + +import { Anthropic } from "@anthropic-ai/sdk" +import OpenAI from "openai" +import { tool as createTool, jsonSchema, type ModelMessage, type TextStreamPart } from "ai" +import type { ApiStreamChunk } from "./stream" + +/** + * Convert Anthropic messages to AI SDK ModelMessage format. + * Handles text, images, tool uses, and tool results. + * + * @param messages - Array of Anthropic message parameters + * @returns Array of AI SDK ModelMessage objects + */ +export function convertToAiSdkMessages(messages: Anthropic.Messages.MessageParam[]): ModelMessage[] { + const modelMessages: ModelMessage[] = [] + + // First pass: build a map of tool call IDs to tool names from assistant messages + const toolCallIdToName = new Map() + for (const message of messages) { + if (message.role === "assistant" && typeof message.content !== "string") { + for (const part of message.content) { + if (part.type === "tool_use") { + toolCallIdToName.set(part.id, part.name) + } + } + } + } + + for (const message of messages) { + if (typeof message.content === "string") { + modelMessages.push({ + role: message.role, + content: message.content, + }) + } else { + if (message.role === "user") { + const parts: Array< + { type: "text"; text: string } | { type: "image"; image: string; mimeType?: string } + > = [] + const toolResults: Array<{ + type: "tool-result" + toolCallId: string + toolName: string + output: { type: "text"; value: string } + }> = [] + + for (const part of message.content) { + if (part.type === "text") { + parts.push({ type: "text", text: part.text }) + } else if (part.type === "image") { + // Handle both base64 and URL source types + const source = part.source as { type: string; media_type?: string; data?: string; url?: string } + if (source.type === "base64" && source.media_type && source.data) { + parts.push({ + type: "image", + image: `data:${source.media_type};base64,${source.data}`, + mimeType: source.media_type, + }) + } else if (source.type === "url" && source.url) { + parts.push({ + type: "image", + image: source.url, + }) + } + } else if (part.type === "tool_result") { + // Convert tool results to string content + let content: string + if (typeof part.content === "string") { + content = part.content + } else { + content = + part.content + ?.map((c) => { + if (c.type === "text") return c.text + if (c.type === "image") return "(image)" + return "" + }) + .join("\n") ?? "" + } + // Look up the tool name from the tool call ID + const toolName = toolCallIdToName.get(part.tool_use_id) ?? "unknown_tool" + toolResults.push({ + type: "tool-result", + toolCallId: part.tool_use_id, + toolName, + output: { type: "text", value: content || "(empty)" }, + }) + } + } + + // AI SDK requires tool results in separate "tool" role messages + // UserContent only supports: string | Array + // ToolContent (for role: "tool") supports: Array + if (toolResults.length > 0) { + modelMessages.push({ + role: "tool", + content: toolResults, + } as ModelMessage) + } + + // Add user message with only text/image content (no tool results) + if (parts.length > 0) { + modelMessages.push({ + role: "user", + content: parts, + } as ModelMessage) + } + } else if (message.role === "assistant") { + const textParts: string[] = [] + const toolCalls: Array<{ + type: "tool-call" + toolCallId: string + toolName: string + input: unknown + }> = [] + + for (const part of message.content) { + if (part.type === "text") { + textParts.push(part.text) + } else if (part.type === "tool_use") { + toolCalls.push({ + type: "tool-call", + toolCallId: part.id, + toolName: part.name, + input: part.input, + }) + } + } + + const content: Array< + | { type: "text"; text: string } + | { type: "tool-call"; toolCallId: string; toolName: string; input: unknown } + > = [] + + if (textParts.length > 0) { + content.push({ type: "text", text: textParts.join("\n") }) + } + content.push(...toolCalls) + + modelMessages.push({ + role: "assistant", + content: content.length > 0 ? content : [{ type: "text", text: "" }], + } as ModelMessage) + } + } + } + + return modelMessages +} + +/** + * Convert OpenAI-style function tool definitions to AI SDK tool format. + * + * @param tools - Array of OpenAI tool definitions + * @returns Record of AI SDK tools keyed by tool name, or undefined if no tools + */ +export function convertToolsForAiSdk( + tools: OpenAI.Chat.ChatCompletionTool[] | undefined, +): Record> | undefined { + if (!tools || tools.length === 0) { + return undefined + } + + const toolSet: Record> = {} + + for (const t of tools) { + if (t.type === "function") { + toolSet[t.function.name] = createTool({ + description: t.function.description, + inputSchema: jsonSchema(t.function.parameters as any), + }) + } + } + + return toolSet +} + +/** + * Extended stream part type that includes additional fullStream event types + * that are emitted at runtime but not included in the AI SDK TextStreamPart type definitions. + */ +type ExtendedStreamPart = TextStreamPart | { type: "text"; text: string } | { type: "reasoning"; text: string } + +/** + * Process a single AI SDK stream part and yield the appropriate ApiStreamChunk(s). + * This generator handles all TextStreamPart types and converts them to the + * ApiStreamChunk format used by the application. + * + * @param part - The AI SDK TextStreamPart to process (including fullStream event types) + * @yields ApiStreamChunk objects corresponding to the stream part + */ +export function* processAiSdkStreamPart(part: ExtendedStreamPart): Generator { + switch (part.type) { + case "text": + case "text-delta": + yield { type: "text", text: (part as { text: string }).text } + break + + case "reasoning": + case "reasoning-delta": + yield { type: "reasoning", text: (part as { text: string }).text } + break + + case "tool-input-start": + yield { + type: "tool_call_start", + id: part.id, + name: part.toolName, + } + break + + case "tool-input-delta": + yield { + type: "tool_call_delta", + id: part.id, + delta: part.delta, + } + break + + case "tool-input-end": + yield { + type: "tool_call_end", + id: part.id, + } + break + + case "tool-call": + // Complete tool call - emit for compatibility + yield { + type: "tool_call", + id: part.toolCallId, + name: part.toolName, + arguments: typeof part.input === "string" ? part.input : JSON.stringify(part.input), + } + break + + case "source": + // Handle both URL and document source types + if ("url" in part) { + yield { + type: "grounding", + sources: [ + { + title: part.title || "Source", + url: part.url, + snippet: undefined, + }, + ], + } + } + break + + case "error": + yield { + type: "error", + error: "StreamError", + message: part.error instanceof Error ? part.error.message : String(part.error), + } + break + + // Ignore lifecycle events that don't need to yield chunks + case "text-start": + case "text-end": + case "reasoning-start": + case "reasoning-end": + case "start-step": + case "finish-step": + case "start": + case "finish": + case "abort": + case "file": + case "tool-result": + case "tool-error": + case "raw": + // These events don't need to be yielded + break + } +} diff --git a/src/package.json b/src/package.json index 97d03858989..bf4a009a946 100644 --- a/src/package.json +++ b/src/package.json @@ -529,9 +529,10 @@ "web-tree-sitter": "^0.25.6", "workerpool": "^9.2.0", "yaml": "^2.8.0", - "zod": "3.25.61" + "zod": "3.25.76" }, "devDependencies": { + "@openrouter/ai-sdk-provider": "^2.0.4", "@roo-code/build": "workspace:^", "@roo-code/config-eslint": "workspace:^", "@roo-code/config-typescript": "workspace:^", @@ -540,7 +541,6 @@ "@types/diff": "^5.2.1", "@types/diff-match-patch": "^1.0.36", "@types/glob": "^8.1.0", - "@types/json-stream-stringify": "^2.0.4", "@types/lodash.debounce": "^4.0.9", "@types/mocha": "^10.0.10", "@types/node": "20.x", @@ -557,6 +557,7 @@ "@types/vscode": "^1.84.0", "@vscode/test-electron": "^2.5.2", "@vscode/vsce": "3.3.2", + "ai": "^6.0.0", "esbuild-wasm": "^0.25.0", "execa": "^9.5.2", "glob": "^11.1.0",