Просмотр исходного кода

feat: add AI SDK dependencies and message conversion utilities (#11047)

Daniel 2 недель назад
Родитель
Сommit
fe722dad23
6 измененных файлов с 918 добавлено и 75 удалено
  1. 2 1
      package.json
  2. 1 1
      packages/types/package.json
  3. 138 71
      pnpm-lock.yaml
  4. 492 0
      src/api/transform/__tests__/ai-sdk.spec.ts
  5. 282 0
      src/api/transform/ai-sdk.ts
  6. 3 2
      src/package.json

+ 2 - 1
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"
 		}
 	}
 }

+ 1 - 1
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:^",

+ 138 - 71
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([email protected])
       next:
         specifier: ~15.2.8
-        version: 15.2.8([email protected]([email protected]))([email protected])
+        version: 15.2.8(@opentelemetry/[email protected])([email protected]([email protected]))([email protected])
       next-themes:
         specifier: ^0.4.6
         version: 0.4.6([email protected]([email protected]))([email protected])
@@ -303,8 +304,8 @@ importers:
         specifier: ^1.1.2
         version: 1.1.2(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
       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([email protected])
       next:
         specifier: ~15.2.8
-        version: 15.2.8([email protected]([email protected]))([email protected])
+        version: 15.2.8(@opentelemetry/[email protected])([email protected]([email protected]))([email protected])
       next-themes:
         specifier: ^0.4.6
         version: 0.4.6([email protected]([email protected]))([email protected])
@@ -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([email protected])
       next-sitemap:
         specifier: ^4.2.3
-        version: 4.2.3([email protected]([email protected]([email protected]))([email protected]))
+        version: 4.2.3([email protected](@opentelemetry/[email protected])([email protected]([email protected]))([email protected]))
       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([email protected])([email protected])
       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/[email protected])([email protected])([email protected])([email protected])
+        version: 0.44.1(@libsql/[email protected])(@opentelemetry/[email protected])([email protected])([email protected])([email protected])
       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([email protected]1)
+        version: 1.9.18([email protected].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([email protected])([email protected]1)
+        version: 5.12.2([email protected])([email protected].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([email protected]([email protected]))([email protected])
       '@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([email protected])
       esbuild-wasm:
         specifier: ^0.25.0
         version: 0.25.12
@@ -1099,7 +1103,7 @@ importers:
         version: 3.2.4(@types/[email protected])(@types/[email protected])(@vitest/[email protected])([email protected])([email protected])([email protected])([email protected])([email protected])
       zod-to-ts:
         specifier: ^1.2.0
-        version: 1.2.0([email protected])([email protected]1)
+        version: 1.2.0([email protected])([email protected].76)
 
   webview-ui:
     dependencies:
@@ -1305,8 +1309,8 @@ importers:
         specifier: ^0.2.2
         version: 0.2.2(@types/[email protected])([email protected])
       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/[email protected]':
     resolution: {integrity: sha512-baYZExFpsdkBNuvGKTKWCwKH57HRZLVtycZS05WTQNVOiXVSeAki3nU35zlRbToeMW8aHlJfyS+1C4BOv27q0A==}
 
+  '@ai-sdk/[email protected]':
+    resolution: {integrity: sha512-j0AQeA7hOVqwImykQlganf/Euj3uEXf0h3G0O4qKTDpEwE+EZGIPnVimCWht5W91lAetPZSfavDyvfpuPDd2PQ==}
+    engines: {node: '>=18'}
+    peerDependencies:
+      zod: 3.25.76
+
+  '@ai-sdk/[email protected]':
+    resolution: {integrity: sha512-VeDAiCH+ZK8Xs4hb9Cw7pHlujWNL52RKe8TExOkrw6Ir1AmfajBZTb9XUdKOZO08RwQElIKA8+Ltm+Gqfo8djQ==}
+    engines: {node: '>=18'}
+    peerDependencies:
+      zod: 3.25.76
+
+  '@ai-sdk/[email protected]':
+    resolution: {integrity: sha512-2Xmoq6DBJqmSl80U6V9z5jJSJP7ehaJJQMy2iFUqTay06wdCqTnPVBBQbtEL8RCChenL+q5DC5H5WzU3vV3v8w==}
+    engines: {node: '>=18'}
+
   '@alcalzone/[email protected]':
     resolution: {integrity: sha512-jsElTJ0sQ4wHRz+C45tfect76BwbTbgkgKByOzpCN9xG61N5V6u/glvg1CsNJhq2xJIFpKHSwG3D2wPPuEYOrQ==}
     engines: {node: '>=18'}
@@ -2392,7 +2412,7 @@ packages:
   '@mistralai/[email protected]':
     resolution: {integrity: sha512-D/vNAGEvWMsg95tzgLTg7pPnW9leOPyH+nh1Os05NwxVPbUykoYgMAwOEX7J46msahWdvZ4NQQuxUXIUV2P6dg==}
     peerDependencies:
-      zod: '>= 3'
+      zod: 3.25.76
 
   '@mixmark-io/[email protected]':
     resolution: {integrity: sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==}
@@ -2591,6 +2611,17 @@ packages:
   '@open-draft/[email protected]':
     resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==}
 
+  '@openrouter/[email protected]':
+    resolution: {integrity: sha512-UypPbVnSExxmG/4Zg0usRiit3auvQVrjUXSyEhm0sZ9GQnW/d8p/bKgCk2neh1W5YyRSo7PNQvCrAEBHZnqQkQ==}
+    engines: {node: '>=18'}
+    peerDependencies:
+      ai: ^6.0.0
+      zod: 3.25.76
+
+  '@opentelemetry/[email protected]':
+    resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==}
+    engines: {node: '>=8.0.0'}
+
   '@oxc-resolver/[email protected]':
     resolution: {integrity: sha512-ruKLkS+Dm/YIJaUhzEB7zPI+jh3EXxu0QnNV8I7t9jf0lpD2VnltuyRbhrbJEkksklZj//xCMyFFsILGjiU2Mg==}
     cpu: [arm64]
@@ -3845,6 +3876,9 @@ packages:
   '@socket.io/[email protected]':
     resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==}
 
+  '@standard-schema/[email protected]':
+    resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
+
   '@standard-schema/[email protected]':
     resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==}
 
@@ -4268,10 +4302,6 @@ packages:
   '@types/[email protected]':
     resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
 
-  '@types/[email protected]':
-    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/[email protected]':
     resolution: {integrity: sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==}
 
@@ -4469,6 +4499,10 @@ packages:
     resolution: {integrity: sha512-e4kQK9mP8ntpo3dACWirGod/hHv4qO5JMj9a/0a2AZto7b4persj5YP7t1Er372gTtYFTYxNhMx34jRvHooglw==}
     engines: {node: '>=16'}
 
+  '@vercel/[email protected]':
+    resolution: {integrity: sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w==}
+    engines: {node: '>= 20'}
+
   '@vitejs/[email protected]':
     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'}
 
+  [email protected]:
+    resolution: {integrity: sha512-5wYcMQmOaNU71wGv4XX1db3zvn4uLjLbTKIo6cQZPWOJElA0882XI7Eawx6TCd5jbjOvKMIP+KLWbpVomAFT2g==}
+    engines: {node: '>=18'}
+    peerDependencies:
+      zod: 3.25.76
+
   [email protected]:
     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'}
 
+  [email protected]:
+    resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==}
+    engines: {node: '>=18.0.0'}
+
   [email protected]:
     resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==}
     engines: {node: '>=18.0.0'}
@@ -7319,6 +7363,9 @@ packages:
   [email protected]:
     resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
 
+  [email protected]:
+    resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==}
+
   [email protected]:
     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:
   [email protected]:
     resolution: {integrity: sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==}
     peerDependencies:
-      zod: ^3.24.1
+      zod: 3.25.76
 
   [email protected]:
     resolution: {integrity: sha512-x30XE43V+InwGpvTySRNz9kB7qFU8DlyEy7BsSTCHPH1R0QasMmHWZDCzYm6bVXtj/9NNJAZF3jW8rzFvH5OFA==}
     peerDependencies:
       typescript: ^4.9.4 || ^5.0.2
-      zod: ^3
+      zod: 3.25.76
 
   [email protected]:
     resolution: {integrity: sha512-1KP64yqDPQ3rupxNv7oXhf7KdhHHgaqbKuspVoiN93TT0xrBjql+Svjkdjq/Qh/7GSMmgQs3AfvBT0heE35thw==}
     engines: {node: '>=18.0.0'}
     peerDependencies:
-      zod: ^3.24.4
-
-  [email protected]:
-    resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==}
-
-  [email protected]:
-    resolution: {integrity: sha512-fzfJgUw78LTNnHujj9re1Ov/JJQkRZZGDMcYqSx7Hp4rPOkKywaFHq0S6GoHeXs0wGNE/sIOutkXgnwzrVOGCQ==}
+      zod: 3.25.76
 
   [email protected]:
     resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
@@ -10736,6 +10777,24 @@ snapshots:
 
   '@adobe/[email protected]': {}
 
+  '@ai-sdk/[email protected]([email protected])':
+    dependencies:
+      '@ai-sdk/provider': 3.0.5
+      '@ai-sdk/provider-utils': 4.0.10([email protected])
+      '@vercel/oidc': 3.1.0
+      zod: 3.25.76
+
+  '@ai-sdk/[email protected]([email protected])':
+    dependencies:
+      '@ai-sdk/provider': 3.0.5
+      '@standard-schema/spec': 1.1.0
+      eventsource-parser: 3.0.6
+      zod: 3.25.76
+
+  '@ai-sdk/[email protected]':
+    dependencies:
+      json-schema: 0.4.0
+
   '@alcalzone/[email protected]':
     dependencies:
       ansi-styles: 6.2.3
@@ -12250,10 +12309,10 @@ snapshots:
     dependencies:
       exenv-es6: 1.1.1
 
-  '@mistralai/[email protected]([email protected]1)':
+  '@mistralai/[email protected]([email protected].76)':
     dependencies:
-      zod: 3.25.61
-      zod-to-json-schema: 3.24.5([email protected]1)
+      zod: 3.25.76
+      zod-to-json-schema: 3.24.5([email protected].76)
 
   '@mixmark-io/[email protected]': {}
 
@@ -12421,6 +12480,13 @@ snapshots:
 
   '@open-draft/[email protected]': {}
 
+  '@openrouter/[email protected]([email protected]([email protected]))([email protected])':
+    dependencies:
+      ai: 6.0.57([email protected])
+      zod: 3.25.76
+
+  '@opentelemetry/[email protected]': {}
+
   '@oxc-resolver/[email protected]':
     optional: true
 
@@ -13822,6 +13888,8 @@ snapshots:
 
   '@socket.io/[email protected]': {}
 
+  '@standard-schema/[email protected]': {}
+
   '@standard-schema/[email protected]': {}
 
   '@swc/[email protected]': {}
@@ -14248,10 +14316,6 @@ snapshots:
 
   '@types/[email protected]': {}
 
-  '@types/[email protected]':
-    dependencies:
-      json-stream-stringify: 3.1.6
-
   '@types/[email protected]': {}
 
   '@types/[email protected]':
@@ -14483,6 +14547,8 @@ snapshots:
       satori: 0.12.2
       yoga-wasm-web: 0.3.3
 
+  '@vercel/[email protected]': {}
+
   '@vitejs/[email protected]([email protected](@types/[email protected])([email protected])([email protected])([email protected])([email protected]))':
     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/[email protected])(@types/node@20.17.50)(@vitest/[email protected])([email protected])([email protected])([email protected])([email protected])([email protected])
+      vitest: 3.2.4(@types/[email protected])(@types/node@24.2.1)(@vitest/[email protected])([email protected])([email protected])([email protected])([email protected])([email protected])
 
   '@vitest/[email protected]':
     dependencies:
@@ -14705,6 +14771,14 @@ snapshots:
     dependencies:
       humanize-ms: 1.2.1
 
+  [email protected]([email protected]):
+    dependencies:
+      '@ai-sdk/gateway': 3.0.25([email protected])
+      '@ai-sdk/provider': 3.0.5
+      '@ai-sdk/provider-utils': 4.0.10([email protected])
+      '@opentelemetry/api': 1.9.0
+      zod: 3.25.76
+
   [email protected]:
     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
 
   [email protected]([email protected]):
     dependencies:
@@ -15910,9 +15984,10 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  [email protected](@libsql/[email protected])([email protected])([email protected])([email protected]):
+  [email protected](@libsql/[email protected])(@opentelemetry/[email protected])([email protected])([email protected])([email protected]):
     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:
 
   [email protected]: {}
 
+  [email protected]: {}
+
   [email protected]:
     dependencies:
       eventsource-parser: 3.0.2
@@ -17689,6 +17766,8 @@ snapshots:
 
   [email protected]: {}
 
+  [email protected]: {}
+
   [email protected]: {}
 
   [email protected]: {}
@@ -18740,20 +18819,20 @@ snapshots:
 
   [email protected]: {}
 
-  [email protected]([email protected]([email protected]([email protected]))([email protected])):
+  [email protected]([email protected](@opentelemetry/[email protected])([email protected]([email protected]))([email protected])):
     dependencies:
       '@corex/deepmerge': 4.0.43
       '@next/env': 13.5.11
       fast-glob: 3.3.3
       minimist: 1.2.8
-      next: 15.2.8([email protected]([email protected]))([email protected])
+      next: 15.2.8(@opentelemetry/[email protected])([email protected]([email protected]))([email protected])
 
   [email protected]([email protected]([email protected]))([email protected]):
     dependencies:
       react: 18.3.1
       react-dom: 18.3.1([email protected])
 
-  [email protected]([email protected]([email protected]))([email protected]):
+  [email protected](@opentelemetry/[email protected])([email protected]([email protected]))([email protected]):
     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
 
-  [email protected]([email protected])([email protected]):
-    optionalDependencies:
-      ws: 8.18.3
-      zod: 3.25.61
-
   [email protected]([email protected])([email protected]):
     optionalDependencies:
       ws: 8.18.3
@@ -21785,27 +21860,19 @@ snapshots:
       compress-commons: 6.0.2
       readable-stream: 4.7.0
 
-  [email protected]([email protected]):
-    dependencies:
-      zod: 3.25.61
-
   [email protected]([email protected]):
     dependencies:
       zod: 3.25.76
 
-  [email protected]([email protected])([email protected]1):
+  [email protected]([email protected])([email protected].76):
     dependencies:
       typescript: 5.8.3
-      zod: 3.25.61
+      zod: 3.25.76
 
   [email protected]([email protected]):
     dependencies:
       zod: 3.25.76
 
-  [email protected]: {}
-
-  [email protected]: {}
-
   [email protected]: {}
 
   [email protected](@types/[email protected])([email protected]):

+ 492 - 0
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)
+			}
+		})
+	})
+})

+ 282 - 0
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<string, string>()
+	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<TextPart | ImagePart | FilePart>
+				// ToolContent (for role: "tool") supports: Array<ToolResultPart | ToolApprovalResponse>
+				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<string, ReturnType<typeof createTool>> | undefined {
+	if (!tools || tools.length === 0) {
+		return undefined
+	}
+
+	const toolSet: Record<string, ReturnType<typeof createTool>> = {}
+
+	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<any> | { 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<ApiStreamChunk> {
+	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
+	}
+}

+ 3 - 2
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",