From a8e5a71e3b28aefe5b13fbc22a43907dc60b0d9a Mon Sep 17 00:00:00 2001 From: Prasang-A-Prajapati Date: Mon, 25 Aug 2025 10:22:48 -0400 Subject: [PATCH 01/20] add ibm-cloud watsonx AI npm dependency --- pnpm-lock.yaml | 215 +++++++++++++++++++++++++++++++++++++++++++++-- src/package.json | 2 + 2 files changed, 211 insertions(+), 6 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4134d3945a..25fe6bdd64 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -577,6 +577,9 @@ importers: '@google/genai': specifier: ^1.0.0 version: 1.3.0(@modelcontextprotocol/sdk@1.12.0) + '@ibm-cloud/watsonx-ai': + specifier: ^1.6.8 + version: 1.6.10 '@lmstudio/sdk': specifier: ^1.1.1 version: 1.2.0 @@ -800,6 +803,9 @@ importers: '@types/glob': specifier: ^8.1.0 version: 8.1.0 + '@types/lodash': + specifier: ^4.14.201 + version: 4.17.17 '@types/mocha': specifier: ^10.0.10 version: 10.0.10 @@ -1845,6 +1851,10 @@ packages: resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} + '@ibm-cloud/watsonx-ai@1.6.10': + resolution: {integrity: sha512-aZV50/s8VZc7w0t/qcaBw3RLT3WDsAeZUJlP8EbG/csZJF3a8F7alihbGOM4lJFM7R4Z81Lucz3nfHi2KR7J4Q==} + engines: {node: '>=18.0.0'} + '@iconify/types@2.0.0': resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} @@ -3665,6 +3675,9 @@ packages: peerDependencies: '@testing-library/dom': '>=7.21.4' + '@tokenizer/token@0.3.0': + resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} + '@tootallnate/quickjs-emscripten@0.23.0': resolution: {integrity: sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==} @@ -3933,6 +3946,9 @@ packages: '@types/tmp@0.2.6': resolution: {integrity: sha512-chhaNf2oKHlRkDGt+tiKE2Z5aJ6qalm7Z9rlLdBwmOiAAf09YQvvoLXjWK4HWPF1xU/fqvMgfNfpVoBscA/tKA==} + '@types/tough-cookie@4.0.5': + resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} + '@types/trusted-types@2.0.7': resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} @@ -4310,6 +4326,9 @@ packages: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} + axios@1.11.0: + resolution: {integrity: sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==} + axios@1.9.0: resolution: {integrity: sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==} @@ -4438,6 +4457,9 @@ packages: buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + buffers@0.1.1: resolution: {integrity: sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==} engines: {node: '>=0.2.0'} @@ -5592,6 +5614,10 @@ packages: eventemitter3@5.0.1: resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + eventsource-parser@3.0.2: resolution: {integrity: sha512-6RxOBZ/cYgd8usLwsEl+EC09Au/9BcmCKYF2/xbml6DNczf7nv0MQb+7BA2F+li6//I+28VNlQR37XfQtcAJuA==} engines: {node: '>=18.0.0'} @@ -5756,6 +5782,10 @@ packages: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} + file-type@16.5.4: + resolution: {integrity: sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw==} + engines: {node: '>=10'} + file-uri-to-path@1.0.0: resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} @@ -6202,6 +6232,10 @@ packages: typescript: optional: true + ibm-cloud-sdk-core@5.4.2: + resolution: {integrity: sha512-5VFkKYU/vSIWFJTVt392XEdPmiEwUJqhxjn1MRO3lfELyU2FB+yYi8brbmXUgq+D1acHR1fpS7tIJ6IlnrR9Cg==} + engines: {node: '>=18'} + iconv-lite@0.4.24: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} @@ -6532,6 +6566,9 @@ packages: resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} engines: {node: '>=0.10.0'} + isstream@0.1.2: + resolution: {integrity: sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==} + istanbul-lib-coverage@3.2.2: resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} engines: {node: '>=8'} @@ -7801,6 +7838,10 @@ packages: resolution: {integrity: sha512-v6ZJ/efsBpGrGGknjtq9J/oC8tZWq0KWL5vQrk2GlzLEQPUDB1ex+13Rmidl1neNN358Jn9EHZw5y07FFtaC7A==} engines: {node: '>=6.8.1'} + peek-readable@4.1.0: + resolution: {integrity: sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg==} + engines: {node: '>=8'} + pend@1.2.0: resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} @@ -7998,6 +8039,10 @@ packages: process-nextick-args@2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + process@0.11.10: + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} + engines: {node: '>= 0.6.0'} + progress@2.0.3: resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} engines: {node: '>=0.4.0'} @@ -8037,6 +8082,9 @@ packages: engines: {node: '>= 0.10'} hasBin: true + psl@1.15.0: + resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==} + pump@3.0.2: resolution: {integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==} @@ -8066,6 +8114,9 @@ packages: quansync@0.2.10: resolution: {integrity: sha512-t41VRkMYbkHyCYmOvx/6URnN80H7k4X0lLdBMGsz+maAwrJQYB1djpV6vHrQIBE0WBSGqhtEHrK9U3DWWH8v7A==} + querystringify@2.2.0: + resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} + queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -8236,6 +8287,14 @@ packages: resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} engines: {node: '>= 6'} + readable-stream@4.7.0: + resolution: {integrity: sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + readable-web-to-node-stream@3.0.4: + resolution: {integrity: sha512-9nX56alTf5bwXQ3ZDipHJhusu9NTQJ/CVPtb/XHAJCXihZeitfJvIRS4GqQ/mfIoOE3IelHMrpayVrosdHBuLw==} + engines: {node: '>=8'} + readdir-glob@1.1.3: resolution: {integrity: sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==} @@ -8323,6 +8382,9 @@ packages: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} + requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + resize-observer-polyfill@1.5.1: resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==} @@ -8350,6 +8412,12 @@ packages: resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} engines: {node: '>=18'} + retry-axios@2.6.0: + resolution: {integrity: sha512-pOLi+Gdll3JekwuFjXO3fTq+L9lzMQGcSq7M5gIjExcl3Gu1hd4XXuf5o3+LuSBsaULQH7DiNbsqPd1chVpQGQ==} + engines: {node: '>=10.7.0'} + peerDependencies: + axios: '*' + retry@0.12.0: resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} engines: {node: '>= 4'} @@ -8753,6 +8821,9 @@ packages: string_decoder@1.1.1: resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + stringify-entities@4.0.4: resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} @@ -8818,6 +8889,10 @@ packages: resolution: {integrity: sha512-X5Z6riticuH5GnhUyzijfDi1SoXas8ODDyN7K8lJeQK+Jfi4dKdoJGL4CXTskY/ATBcN+rz5lROGn1tAUkOX7g==} engines: {node: '>=12.21.0'} + strtok3@6.3.0: + resolution: {integrity: sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw==} + engines: {node: '>=10'} + style-to-js@1.1.16: resolution: {integrity: sha512-/Q6ld50hKYPH3d/r6nr117TZkHR0w0kGGIVfpG9N6D8NymRPM9RqCUv4pRpJ62E5DqOYx2AFpbZMyCPnjQCnOw==} @@ -9010,10 +9085,18 @@ packages: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} + token-types@4.2.1: + resolution: {integrity: sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ==} + engines: {node: '>=10'} + totalist@3.0.1: resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} engines: {node: '>=6'} + tough-cookie@4.1.4: + resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==} + engines: {node: '>=6'} + tough-cookie@5.1.2: resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==} engines: {node: '>=16'} @@ -9283,6 +9366,10 @@ packages: resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} engines: {node: '>= 4.0.0'} + universalify@0.2.0: + resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} + engines: {node: '>= 4.0.0'} + unpipe@1.0.0: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} @@ -9306,6 +9393,9 @@ packages: url-join@4.0.1: resolution: {integrity: sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==} + url-parse@1.5.10: + resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + use-callback-ref@1.3.3: resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} engines: {node: '>=10'} @@ -10970,6 +11060,14 @@ snapshots: '@humanwhocodes/retry@0.4.3': {} + '@ibm-cloud/watsonx-ai@1.6.10': + dependencies: + '@types/node': 18.19.100 + extend: 3.0.2 + ibm-cloud-sdk-core: 5.4.2 + transitivePeerDependencies: + - supports-color + '@iconify/types@2.0.0': {} '@iconify/utils@2.3.0': @@ -11088,7 +11186,7 @@ snapshots: '@jest/schemas': 29.6.3 '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 20.17.57 + '@types/node': 20.19.1 '@types/yargs': 17.0.33 chalk: 4.1.2 @@ -12892,6 +12990,8 @@ snapshots: dependencies: '@testing-library/dom': 10.4.0 + '@tokenizer/token@0.3.0': {} + '@tootallnate/quickjs-emscripten@0.23.0': {} '@tybys/wasm-util@0.9.0': @@ -13194,6 +13294,8 @@ snapshots: '@types/tmp@0.2.6': {} + '@types/tough-cookie@4.0.5': {} + '@types/trusted-types@2.0.7': optional: true @@ -13222,7 +13324,7 @@ snapshots: '@types/yauzl@2.10.3': dependencies: - '@types/node': 20.17.57 + '@types/node': 20.19.1 optional: true '@typescript-eslint/eslint-plugin@8.32.1(@typescript-eslint/parser@8.32.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3)': @@ -13705,9 +13807,17 @@ snapshots: dependencies: possible-typed-array-names: 1.1.0 + axios@1.11.0(debug@4.4.1): + dependencies: + follow-redirects: 1.15.9(debug@4.4.1) + form-data: 4.0.4 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + axios@1.9.0: dependencies: - follow-redirects: 1.15.9 + follow-redirects: 1.15.9(debug@4.4.1) form-data: 4.0.4 proxy-from-env: 1.1.0 transitivePeerDependencies: @@ -13839,6 +13949,11 @@ snapshots: base64-js: 1.5.1 ieee754: 1.2.1 + buffer@6.0.3: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + buffers@0.1.1: {} bundle-name@4.1.0: @@ -15073,6 +15188,8 @@ snapshots: eventemitter3@5.0.1: {} + events@3.3.0: {} + eventsource-parser@3.0.2: {} eventsource@3.0.7: @@ -15305,6 +15422,12 @@ snapshots: dependencies: flat-cache: 4.0.1 + file-type@16.5.4: + dependencies: + readable-web-to-node-stream: 3.0.4 + strtok3: 6.3.0 + token-types: 4.2.1 + file-uri-to-path@1.0.0: optional: true @@ -15348,7 +15471,9 @@ snapshots: flatted@3.3.3: {} - follow-redirects@1.15.9: {} + follow-redirects@1.15.9(debug@4.4.1): + optionalDependencies: + debug: 4.4.1(supports-color@8.1.1) for-each@0.3.5: dependencies: @@ -15856,6 +15981,26 @@ snapshots: optionalDependencies: typescript: 5.8.3 + ibm-cloud-sdk-core@5.4.2: + dependencies: + '@types/debug': 4.1.12 + '@types/node': 18.19.100 + '@types/tough-cookie': 4.0.5 + axios: 1.11.0(debug@4.4.1) + camelcase: 6.3.0 + debug: 4.4.1(supports-color@8.1.1) + dotenv: 16.5.0 + extend: 3.0.2 + file-type: 16.5.4 + form-data: 4.0.4 + isstream: 0.1.2 + jsonwebtoken: 9.0.2 + mime-types: 2.1.35 + retry-axios: 2.6.0(axios@1.11.0(debug@4.4.1)) + tough-cookie: 4.1.4 + transitivePeerDependencies: + - supports-color + iconv-lite@0.4.24: dependencies: safer-buffer: 2.1.2 @@ -16135,6 +16280,8 @@ snapshots: isobject@3.0.1: {} + isstream@0.1.2: {} + istanbul-lib-coverage@3.2.2: {} istanbul-lib-report@3.0.1: @@ -16198,7 +16345,7 @@ snapshots: jest-util@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 20.17.57 + '@types/node': 20.19.1 chalk: 4.1.2 ci-info: 3.9.0 graceful-fs: 4.2.11 @@ -17542,7 +17689,7 @@ snapshots: dependencies: '@vscode/vsce': 3.3.2 commander: 6.2.1 - follow-redirects: 1.15.9 + follow-redirects: 1.15.9(debug@4.4.1) is-ci: 2.0.0 leven: 3.1.0 semver: 7.7.2 @@ -17729,6 +17876,8 @@ snapshots: transitivePeerDependencies: - supports-color + peek-readable@4.1.0: {} + pend@1.2.0: {} picocolors@1.1.1: {} @@ -17901,6 +18050,8 @@ snapshots: process-nextick-args@2.0.1: {} + process@0.11.10: {} + progress@2.0.3: {} promise-limit@2.7.0: @@ -17950,6 +18101,10 @@ snapshots: dependencies: event-stream: 3.3.4 + psl@1.15.0: + dependencies: + punycode: 2.3.1 + pump@3.0.2: dependencies: end-of-stream: 1.4.4 @@ -18005,6 +18160,8 @@ snapshots: quansync@0.2.10: {} + querystringify@2.2.0: {} + queue-microtask@1.2.3: {} randombytes@2.1.0: @@ -18217,6 +18374,18 @@ snapshots: string_decoder: 1.1.1 util-deprecate: 1.0.2 + readable-stream@4.7.0: + dependencies: + abort-controller: 3.0.0 + buffer: 6.0.3 + events: 3.3.0 + process: 0.11.10 + string_decoder: 1.3.0 + + readable-web-to-node-stream@3.0.4: + dependencies: + readable-stream: 4.7.0 + readdir-glob@1.1.3: dependencies: minimatch: 5.1.6 @@ -18369,6 +18538,8 @@ snapshots: require-directory@2.1.1: {} + requires-port@1.0.0: {} + resize-observer-polyfill@1.5.1: {} resolve-from@4.0.0: {} @@ -18394,6 +18565,10 @@ snapshots: onetime: 7.0.0 signal-exit: 4.1.0 + retry-axios@2.6.0(axios@1.11.0(debug@4.4.1)): + dependencies: + axios: 1.11.0(debug@4.4.1) + retry@0.12.0: {} reusify@1.1.0: {} @@ -18903,6 +19078,10 @@ snapshots: dependencies: safe-buffer: 5.1.2 + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + stringify-entities@4.0.4: dependencies: character-entities-html4: 2.1.0 @@ -18949,6 +19128,11 @@ snapshots: strong-type@1.1.0: {} + strtok3@6.3.0: + dependencies: + '@tokenizer/token': 0.3.0 + peek-readable: 4.1.0 + style-to-js@1.1.16: dependencies: style-to-object: 1.0.8 @@ -19165,8 +19349,20 @@ snapshots: toidentifier@1.0.1: {} + token-types@4.2.1: + dependencies: + '@tokenizer/token': 0.3.0 + ieee754: 1.2.1 + totalist@3.0.1: {} + tough-cookie@4.1.4: + dependencies: + psl: 1.15.0 + punycode: 2.3.1 + universalify: 0.2.0 + url-parse: 1.5.10 + tough-cookie@5.1.2: dependencies: tldts: 6.1.86 @@ -19467,6 +19663,8 @@ snapshots: universalify@0.1.2: {} + universalify@0.2.0: {} + unpipe@1.0.0: {} untildify@4.0.0: {} @@ -19496,6 +19694,11 @@ snapshots: url-join@4.0.1: {} + url-parse@1.5.10: + dependencies: + querystringify: 2.2.0 + requires-port: 1.0.0 + use-callback-ref@1.3.3(@types/react@18.3.23)(react@18.3.1): dependencies: react: 18.3.1 diff --git a/src/package.json b/src/package.json index e1dc8ae72e..acb005d9d2 100644 --- a/src/package.json +++ b/src/package.json @@ -416,6 +416,7 @@ "@aws-sdk/client-bedrock-runtime": "^3.848.0", "@aws-sdk/credential-providers": "^3.848.0", "@google/genai": "^1.0.0", + "@ibm-cloud/watsonx-ai": "^1.6.8", "@lmstudio/sdk": "^1.1.1", "@mistralai/mistralai": "^1.3.6", "@modelcontextprotocol/sdk": "^1.9.0", @@ -492,6 +493,7 @@ "@types/diff": "^5.2.1", "@types/diff-match-patch": "^1.0.36", "@types/glob": "^8.1.0", + "@types/lodash": "^4.14.201", "@types/mocha": "^10.0.10", "@types/node": "20.x", "@types/node-cache": "^4.1.3", From feda684e197d51d7ce3ddf82ae92a70b3b04104c Mon Sep 17 00:00:00 2001 From: Prasang-A-Prajapati Date: Tue, 26 Aug 2025 11:18:18 -0400 Subject: [PATCH 02/20] add IBM watsonx AI provider and IBM watsonx embedded AI provider for code indexing.. --- packages/types/src/codebase-index.ts | 7 +- packages/types/src/global-settings.ts | 3 + packages/types/src/provider-settings.ts | 10 + packages/types/src/providers/index.ts | 1 + packages/types/src/providers/watsonx.ts | 97 ++++++ src/api/index.ts | 3 + src/api/providers/fetchers/watsonx.ts | 67 +++++ src/api/providers/index.ts | 1 + src/api/providers/watsonx.ts | 165 ++++++++++ src/api/transform/watsonxai-format.ts | 249 +++++++++++++++ src/core/webview/webviewMessageHandler.ts | 39 ++- src/i18n/locales/en/embeddings.json | 1 + src/services/code-index/config-manager.ts | 47 ++- src/services/code-index/embedders/watsonx.ts | 283 ++++++++++++++++++ src/services/code-index/interfaces/config.ts | 6 + .../code-index/interfaces/embedder.ts | 2 +- src/services/code-index/interfaces/manager.ts | 2 +- src/services/code-index/service-factory.ts | 10 + src/shared/ExtensionMessage.ts | 2 + src/shared/ProfileValidator.ts | 2 + src/shared/WebviewMessage.ts | 3 + src/shared/embeddingModels.ts | 8 +- .../src/components/chat/CodeIndexPopover.tsx | 178 ++++++++++- .../src/components/settings/ApiOptions.tsx | 7 + .../src/components/settings/constants.ts | 3 + .../settings/providers/WatsonxAI.tsx | 86 ++++++ .../components/settings/providers/index.ts | 1 + .../components/ui/hooks/useSelectedModel.ts | 10 + webview-ui/src/i18n/locales/en/settings.json | 8 +- webview-ui/src/utils/validate.ts | 8 + 30 files changed, 1285 insertions(+), 24 deletions(-) create mode 100644 packages/types/src/providers/watsonx.ts create mode 100644 src/api/providers/fetchers/watsonx.ts create mode 100644 src/api/providers/watsonx.ts create mode 100644 src/api/transform/watsonxai-format.ts create mode 100644 src/services/code-index/embedders/watsonx.ts create mode 100644 webview-ui/src/components/settings/providers/WatsonxAI.tsx diff --git a/packages/types/src/codebase-index.ts b/packages/types/src/codebase-index.ts index 89d5b168d7..e4f700b5b4 100644 --- a/packages/types/src/codebase-index.ts +++ b/packages/types/src/codebase-index.ts @@ -21,7 +21,9 @@ export const CODEBASE_INDEX_DEFAULTS = { export const codebaseIndexConfigSchema = z.object({ codebaseIndexEnabled: z.boolean().optional(), codebaseIndexQdrantUrl: z.string().optional(), - codebaseIndexEmbedderProvider: z.enum(["openai", "ollama", "openai-compatible", "gemini", "mistral"]).optional(), + codebaseIndexEmbedderProvider: z + .enum(["openai", "ollama", "openai-compatible", "gemini", "mistral", "watsonx"]) + .optional(), codebaseIndexEmbedderBaseUrl: z.string().optional(), codebaseIndexEmbedderModelId: z.string().optional(), codebaseIndexEmbedderModelDimension: z.number().optional(), @@ -48,6 +50,7 @@ export const codebaseIndexModelsSchema = z.object({ "openai-compatible": z.record(z.string(), z.object({ dimension: z.number() })).optional(), gemini: z.record(z.string(), z.object({ dimension: z.number() })).optional(), mistral: z.record(z.string(), z.object({ dimension: z.number() })).optional(), + watsonx: z.record(z.string(), z.object({ dimension: z.number() })).optional(), }) export type CodebaseIndexModels = z.infer @@ -64,6 +67,8 @@ export const codebaseIndexProviderSchema = z.object({ codebaseIndexOpenAiCompatibleModelDimension: z.number().optional(), codebaseIndexGeminiApiKey: z.string().optional(), codebaseIndexMistralApiKey: z.string().optional(), + codebaseIndexWatsonxApiKey: z.string().optional(), + codebaseIndexWatsonxProjectId: z.string().optional(), }) export type CodebaseIndexProvider = z.infer diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts index dc5a9e6744..39dfb7fd93 100644 --- a/packages/types/src/global-settings.ts +++ b/packages/types/src/global-settings.ts @@ -187,6 +187,9 @@ export const SECRET_STATE_KEYS = [ "codebaseIndexGeminiApiKey", "codebaseIndexMistralApiKey", "huggingFaceApiKey", + "watsonxApiKey", + "codebaseIndexWatsonxApiKey", + "codebaseIndexWatsonxProjectId", ] as const satisfies readonly (keyof ProviderSettings)[] export type SecretState = Pick diff --git a/packages/types/src/provider-settings.ts b/packages/types/src/provider-settings.ts index 8cdb5296b2..4a5a431439 100644 --- a/packages/types/src/provider-settings.ts +++ b/packages/types/src/provider-settings.ts @@ -33,6 +33,7 @@ export const providerNames = [ "chutes", "litellm", "huggingface", + "watsonx", ] as const export const providerNamesSchema = z.enum(providerNames) @@ -241,6 +242,13 @@ const litellmSchema = baseProviderSettingsSchema.extend({ litellmUsePromptCache: z.boolean().optional(), }) +const watsonxSchema = baseProviderSettingsSchema.extend({ + watsonxBaseUrl: z.string().optional(), + watsonxApiKey: z.string().optional(), + watsonxProjectId: z.string().optional(), + watsonxModelId: z.string().optional(), +}) + const defaultSchema = z.object({ apiProvider: z.undefined(), }) @@ -271,6 +279,7 @@ export const providerSettingsSchemaDiscriminated = z.discriminatedUnion("apiProv huggingFaceSchema.merge(z.object({ apiProvider: z.literal("huggingface") })), chutesSchema.merge(z.object({ apiProvider: z.literal("chutes") })), litellmSchema.merge(z.object({ apiProvider: z.literal("litellm") })), + watsonxSchema.merge(z.object({ apiProvider: z.literal("watsonx") })), defaultSchema, ]) @@ -302,6 +311,7 @@ export const providerSettingsSchema = z.object({ ...chutesSchema.shape, ...litellmSchema.shape, ...codebaseIndexProviderSchema.shape, + ...watsonxSchema.shape, }) export type ProviderSettings = z.infer diff --git a/packages/types/src/providers/index.ts b/packages/types/src/providers/index.ts index f5061f152c..55827bbb68 100644 --- a/packages/types/src/providers/index.ts +++ b/packages/types/src/providers/index.ts @@ -19,3 +19,4 @@ export * from "./unbound.js" export * from "./vertex.js" export * from "./vscode-llm.js" export * from "./xai.js" +export * from "./watsonx.js" diff --git a/packages/types/src/providers/watsonx.ts b/packages/types/src/providers/watsonx.ts new file mode 100644 index 0000000000..1f78730128 --- /dev/null +++ b/packages/types/src/providers/watsonx.ts @@ -0,0 +1,97 @@ +import type { ModelInfo } from "../model.js" + +export type WatsonxAIModelId = keyof typeof watsonxAiModels +export const watsonxAiDefaultModelId: WatsonxAIModelId = "ibm/granite-3-3-8b-instruct" + +// Common model properties +const baseModelInfo: ModelInfo = { + maxTokens: 4096, + contextWindow: 128000, + supportsImages: false, + supportsPromptCache: true, + supportsReasoningEffort: false, + supportsReasoningBudget: false, + requiredReasoningBudget: false, + inputPrice: 0, + outputPrice: 0, +} + +export const watsonxAiModels = { + // IBM Granite model + "ibm/granite-3-3-8b-instruct": { + ...baseModelInfo, + description: "Granite 3.3 8b Instruct - IBM-trained, dense decoder-only model", + }, + "ibm/granite-3-2-8b-instruct": { + ...baseModelInfo, + description: "Granite 3.2 8b Instruct - Text-only model capable of reasoning", + }, + "ibm/granite-3-2b-instruct": { + ...baseModelInfo, + description: "Granite 3 2b Instruct - IBM-trained, dense decoder-only model", + }, + "ibm/granite-3-8b-instruct": { + ...baseModelInfo, + description: "Granite 3 8b Instruct - IBM-trained, dense decoder-only model", + }, + "ibm/granite-guardian-3-2b": { + ...baseModelInfo, + description: "Granite Guardian 3 2b - IBM-trained, dense decoder-only model", + }, + "ibm/granite-guardian-3-8b": { + ...baseModelInfo, + description: "Granite Guardian 3 8b - IBM-trained, dense decoder-only model", + }, + "ibm/granite-vision-3-2-2b": { + ...baseModelInfo, + supportsImages: true, + description: "Granite 3 Vision - Image-text, text-out model capable of understanding images", + }, + // Meta Llama models + "meta-llama/llama-3-2-11b-vision-instruct": { + ...baseModelInfo, + supportsImages: true, + description: "Llama 3 2 11b Vision Instruct - Auto-regressive language model with transformer architecture", + }, + "meta-llama/llama-3-2-1b-instruct": { + ...baseModelInfo, + description: "Llama 3 2 1b Instruct - Auto-regressive language model with transformer architecture", + }, + "meta-llama/llama-3-2-3b-instruct": { + ...baseModelInfo, + description: "Llama 3 2 3b Instruct - Auto-regressive language model with transformer architecture", + }, + "meta-llama/llama-3-2-90b-vision-instruct": { + ...baseModelInfo, + supportsImages: true, + description: "Llama 3 2 90b Vision Instruct - Auto-regressive language model with transformer architecture", + }, + "meta-llama/llama-3-3-70b-instruct": { + ...baseModelInfo, + description: "Llama 3 3 70b Instruct - FP8 quantized version of the original FP16 weights", + }, + "meta-llama/llama-3-405b-instruct": { + ...baseModelInfo, + contextWindow: 128000, + description: "Llama 3 405b Instruct - Meta's largest open-source foundation model with 405 billion parameters", + }, + "meta-llama/llama-4-maverick-17b-1-0": { + ...baseModelInfo, + contextWindow: 128000, + description: "Llama 4 Maverick - 17 billion active parameter model with 128 experts", + }, + "meta-llama/llama-guard-3-11b-vision": { + ...baseModelInfo, + supportsImages: true, + description: "Llama Guard 3 11b Vision - Auto-regressive language model with transformer architecture", + }, + // Mistral AI models + "mistralai/mistral-medium-2505": { + ...baseModelInfo, + description: "Mistral Medium - Latest iteration of the Mistral Medium model family", + }, + "mistralai/mistral-small-3-1-24b-instruct-2503": { + ...baseModelInfo, + description: "Mistral Small 3.1 24B Base 2503 - Instruction-finetuned version of Mistral Small", + }, +} as const satisfies Record diff --git a/src/api/index.ts b/src/api/index.ts index bda390848c..a28cde55ea 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -30,6 +30,7 @@ import { ChutesHandler, LiteLLMHandler, ClaudeCodeHandler, + WatsonxAIHandler, } from "./providers" export interface SingleCompletionHandler { @@ -115,6 +116,8 @@ export function buildApiHandler(configuration: ProviderSettings): ApiHandler { return new ChutesHandler(options) case "litellm": return new LiteLLMHandler(options) + case "watsonx": + return new WatsonxAIHandler(options) default: apiProvider satisfies "gemini-cli" | undefined return new AnthropicHandler(options) diff --git a/src/api/providers/fetchers/watsonx.ts b/src/api/providers/fetchers/watsonx.ts new file mode 100644 index 0000000000..dcf049eb29 --- /dev/null +++ b/src/api/providers/fetchers/watsonx.ts @@ -0,0 +1,67 @@ +import { ModelInfo } from "@roo-code/types" +import { IamAuthenticator } from "ibm-cloud-sdk-core" +import { WatsonXAI } from "@ibm-cloud/watsonx-ai" + +/** + * Fetches available watsonx models + * + * @param apiKey - The watsonx API key + * @param projectId - Optional project ID for watsonx + * @param baseUrl - Optional base URL for the watsonx API + * @returns A promise resolving to an object with model IDs as keys and model info as values + */ +export async function getWatsonxModels( + apiKey: string, + projectId?: string, + baseUrl?: string, +): Promise> { + try { + const service = WatsonXAI.newInstance({ + version: "2024-05-31", + serviceUrl: baseUrl || "https://us-south.ml.cloud.ibm.com", + authenticator: new IamAuthenticator({ + apikey: apiKey, + }), + }) + + await service.getAuthenticator().authenticate() + let knownModels: Record = {} + + try { + const response = await service.listFoundationModelSpecs() + + if (response && response.result) { + const result = response.result as any + const modelsList = result.models || result.resources || result.foundation_models || [] + if (Array.isArray(modelsList)) { + for (const model of modelsList) { + const modelId = model.id || model.name || model.model_id + const modelInfo = JSON.stringify(model).toLowerCase() + if ( + modelId && + !modelInfo.includes("embed") && + !modelInfo.includes("rtrvr") && + !modelInfo.includes("retriev") + ) { + const contextWindow = model.context_length || model.max_input_tokens || 8192 + const maxTokens = model.max_output_tokens || Math.floor(contextWindow / 2) + + knownModels[modelId] = { + contextWindow, + maxTokens, + supportsPromptCache: false, + } + } + } + } + } + } catch (apiError) { + console.warn("Error fetching models from IBM watsonx API:", apiError) + } + + return knownModels + } catch (error) { + console.error("Error fetching IBM watsonx models:", error) + return {} + } +} diff --git a/src/api/providers/index.ts b/src/api/providers/index.ts index 1cefd0616b..e16a0be7f5 100644 --- a/src/api/providers/index.ts +++ b/src/api/providers/index.ts @@ -23,3 +23,4 @@ export { UnboundHandler } from "./unbound" export { VertexHandler } from "./vertex" export { VsCodeLmHandler } from "./vscode-lm" export { XAIHandler } from "./xai" +export { WatsonxAIHandler } from "./watsonx" diff --git a/src/api/providers/watsonx.ts b/src/api/providers/watsonx.ts new file mode 100644 index 0000000000..94691da4b2 --- /dev/null +++ b/src/api/providers/watsonx.ts @@ -0,0 +1,165 @@ +import * as vscode from "vscode" +import { Anthropic } from "@anthropic-ai/sdk" +import { ModelInfo, watsonxAiDefaultModelId, watsonxAiModels, WatsonxAIModelId } from "@roo-code/types" +import type { ApiHandlerOptions } from "../../shared/api" +import { IamAuthenticator } from "ibm-cloud-sdk-core" +import { ApiStream } from "../transform/stream" +import { BaseProvider } from "./base-provider" +import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index" +import { WatsonXAI } from "@ibm-cloud/watsonx-ai" +import { convertToWatsonxAiMessages } from "../transform/watsonxai-format" + +export class WatsonxAIHandler extends BaseProvider implements SingleCompletionHandler { + private options: ApiHandlerOptions + private projectId?: string + private service: WatsonXAI + + constructor(options: ApiHandlerOptions) { + super() + this.options = options + this.projectId = (this.options as any).watsonxProjectId + if (!this.projectId) { + throw new Error("You must provide a valid IBM watsonx project ID.") + } + const apiKey = (this.options as any).watsonxApiKey + if (!apiKey) { + throw new Error("You must provide a valid IBM watsonx API key.") + } + const serviceUrl = (this.options as any).watsonxBaseUrl || "https://us-south.ml.cloud.ibm.com" + + try { + const serviceOptions: any = { + version: "2024-05-31", + serviceUrl: serviceUrl, + authenticator: new IamAuthenticator({ + apikey: apiKey, + }), + } + this.service = WatsonXAI.newInstance(serviceOptions) + + this.service.getAuthenticator().authenticate() + } catch (error) { + throw new Error( + `IBM watsonx Authentication Error: ${error instanceof Error ? error.message : String(error)}`, + ) + } + } + + /** + * Creates parameters for WatsonX text chat API + * + * @param projectId - The IBM watsonx project ID + * @param modelId - The model ID to use + * @param messages - The messages to send + * @returns The parameters object for the API call + */ + private createTextChatParams(projectId: string, modelId: string, messages: any[]) { + const maxTokens = this.options.modelMaxTokens || 2048 + const temperature = this.options.modelTemperature || 0.7 + return { + projectId, + modelId, + messages, + maxTokens, + temperature, + } + } + + /** + * Creates a message using the IBM watsonx API directly + * + * @param systemPrompt - The system prompt to use + * @param messages - The conversation messages + * @param metadata - Optional metadata for the request + * @returns An async generator that yields the response + */ + async *createMessage( + systemPrompt: string, + messages: Anthropic.Messages.MessageParam[], + metadata?: ApiHandlerCreateMessageMetadata, + ): ApiStream { + const { id: modelId } = this.getModel() + + try { + // Convert messages to WatsonX format with system prompt + const watsonxMessages = [{ role: "system", content: systemPrompt }, ...convertToWatsonxAiMessages(messages)] + + const params = this.createTextChatParams(this.projectId!, modelId, watsonxMessages) + let responseText = "" + let usageInfo: any = null + + // Call the IBM watsonx API using textChat (non-streaming); can be changed to streaming.. + const response = await this.service.textChat(params) + + if (!response?.result?.choices?.[0]?.message?.content) { + throw new Error("Invalid or empty response from IBM watsonx API") + } + + responseText = response.result.choices[0].message.content + + yield { + type: "text", + text: responseText, + } + + usageInfo = response.result.usage || {} + const outputTokens = usageInfo.completion_tokens + + yield { + type: "usage", + inputTokens: usageInfo?.prompt_tokens, + outputTokens, + totalCost: 0, // Actual cost calculation could be added if available + } + } catch (error) { + await vscode.window.showErrorMessage(error.message) + yield { + type: "error", + error: error.type, + message: error.message, + } + } + } + + /** + * Completes a prompt using the IBM watsonx API directly with textChat + * + * @param prompt - The prompt to complete + * @returns The generated text + * @throws Error if the API call fails + */ + async completePrompt(prompt: string): Promise { + try { + const { id: modelId } = this.getModel() + const messages = [{ role: "user", content: prompt }] + const params = this.createTextChatParams(this.projectId!, modelId, messages) + const response = await this.service.textChat(params) + + if (!response?.result?.choices?.[0]?.message?.content) { + throw new Error("Invalid or empty response from IBM watsonx API") + } + + // Extract the message content directly + return response.result.choices[0].message.content + } catch (error) { + if (error instanceof Error) { + throw new Error(`IBM watsonx completion error: ${error.message}`) + } + throw new Error(`IBM watsonx completion error: ${error.message}`) + } + } + + /** + * Returns the model ID and model information for the current watsonx configuration + * + * @returns An object containing the model ID and model information + */ + override getModel(): { id: string; info: ModelInfo } { + return { + id: (this.options as any).watsonxModelId || watsonxAiDefaultModelId, + info: + watsonxAiModels[(this.options as any).watsonxModelId as WatsonxAIModelId] || + watsonxAiModels[watsonxAiDefaultModelId], + } + } +} diff --git a/src/api/transform/watsonxai-format.ts b/src/api/transform/watsonxai-format.ts new file mode 100644 index 0000000000..a81502a981 --- /dev/null +++ b/src/api/transform/watsonxai-format.ts @@ -0,0 +1,249 @@ +import { Anthropic } from "@anthropic-ai/sdk" +import OpenAI from "openai" + +/** + * Converts Anthropic message format to IBM watsonx.ai message format + * + * IBM watsonx.ai supports four message types: + * - TextChatMessageUser: Messages from the user + * - TextChatMessageAssistant: Messages from the assistant + * - TextChatMessageSystem: System instructions + * - TextChatMessageTool: Tool responses + * + * @param anthropicMessages - Messages in Anthropic format + * @returns Messages in IBM watsonx.ai format + */ +export function convertToWatsonxAiMessages( + anthropicMessages: Anthropic.Messages.MessageParam[], +): OpenAI.Chat.ChatCompletionMessageParam[] { + const watsonxAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [] + + for (const anthropicMessage of anthropicMessages) { + if ( + !anthropicMessage.content || + (Array.isArray(anthropicMessage.content) && anthropicMessage.content.length === 0) + ) { + continue + } + + switch (anthropicMessage.role) { + case "user": + // TextChatMessageUser + if (typeof anthropicMessage.content === "string") { + watsonxAiMessages.push({ + role: "user", + content: anthropicMessage.content, + }) + } else { + processUserMessage(anthropicMessage, watsonxAiMessages) + } + break + + case "assistant": + // TextChatMessageAssistant + if (typeof anthropicMessage.content === "string") { + watsonxAiMessages.push({ + role: "assistant", + content: anthropicMessage.content, + }) + } else { + processAssistantMessage(anthropicMessage, watsonxAiMessages) + } + break + + case "system" as any: + // TextChatMessageSystem + if (typeof anthropicMessage.content === "string") { + watsonxAiMessages.push({ + role: "system", + content: anthropicMessage.content, + }) + } else { + const textContent = anthropicMessage.content + .filter((block) => block.type === "text") + .map((block) => (block as any).text) + .join("\n") + + if (textContent) { + watsonxAiMessages.push({ + role: "system", + content: textContent, + }) + } + } + break + + default: + if (anthropicMessage.role === "tool") { + // TextChatMessageTool + const toolMessage = anthropicMessage as any + const toolCallId = toolMessage.tool_call_id + + if (typeof toolCallId === "string") { + const content = + typeof anthropicMessage.content === "string" + ? anthropicMessage.content + : anthropicMessage.content + .filter((block) => block.type === "text") + .map((block) => (block as any).text) + .join("\n") + + watsonxAiMessages.push({ + role: "tool", + tool_call_id: toolCallId, + content: content, + }) + } + } else if (typeof anthropicMessage.content === "string") { + watsonxAiMessages.push({ + role: anthropicMessage.role, + content: anthropicMessage.content, + }) + } + break + } + } + + return watsonxAiMessages +} + +function processUserMessage( + anthropicMessage: Anthropic.Messages.MessageParam, + watsonxAiMessages: OpenAI.Chat.ChatCompletionMessageParam[], +) { + const { contentBlocks, toolResultBlocks } = categorizeUserContent(anthropicMessage.content as any[]) + processToolResultBlocks(toolResultBlocks, watsonxAiMessages) + + if (contentBlocks.length > 0) { + const textBlocks = contentBlocks.filter((part) => part.type === "text") + + if (textBlocks.length === 1 && contentBlocks.length === 1) { + watsonxAiMessages.push({ + role: "user", + content: textBlocks[0].text, + }) + } else { + watsonxAiMessages.push({ + role: "user", + content: contentBlocks.map((part) => { + if (part.type === "image") { + return { + type: "image_url", + image_url: { url: `data:${part.source.media_type};base64,${part.source.data}` }, + } + } + return { type: "text", text: part.text } + }), + }) + } + } +} + +function processAssistantMessage( + anthropicMessage: Anthropic.Messages.MessageParam, + watsonxAiMessages: OpenAI.Chat.ChatCompletionMessageParam[], +) { + const { contentBlocks, toolUseBlocks } = categorizeAssistantContent(anthropicMessage.content as any[]) + + let content: string | undefined + if (contentBlocks.length > 0) { + content = contentBlocks.map((part) => (part.type === "text" ? part.text : "")).join("\n") + } + + const toolCalls = convertToolUseBlocksToToolCalls(toolUseBlocks) + + if (content || toolCalls.length > 0) { + watsonxAiMessages.push({ + role: "assistant", + content: content || "", + tool_calls: toolCalls.length > 0 ? toolCalls : undefined, + }) + } +} + +function categorizeUserContent(content: any[]) { + return content.reduce<{ + contentBlocks: (Anthropic.TextBlockParam | Anthropic.ImageBlockParam)[] + toolResultBlocks: Anthropic.ToolResultBlockParam[] + }>( + (acc, part) => { + if (part.type === "tool_result") { + acc.toolResultBlocks.push(part) + } else if (part.type === "text" || part.type === "image") { + acc.contentBlocks.push(part) + } + return acc + }, + { contentBlocks: [], toolResultBlocks: [] }, + ) +} + +function categorizeAssistantContent(content: any[]) { + return content.reduce<{ + contentBlocks: (Anthropic.TextBlockParam | Anthropic.ImageBlockParam)[] + toolUseBlocks: Anthropic.ToolUseBlockParam[] + }>( + (acc, part) => { + if (part.type === "tool_use") { + acc.toolUseBlocks.push(part) + } else if (part.type === "text" || part.type === "image") { + acc.contentBlocks.push(part) + } + return acc + }, + { contentBlocks: [], toolUseBlocks: [] }, + ) +} + +/** + * Process tool result blocks into IBM watsonx.ai TextChatMessageTool format + * + * @param toolResultBlocks - Tool result blocks from Anthropic + * @param watsonxAiMessages - Array to add the formatted messages to + */ +function processToolResultBlocks( + toolResultBlocks: Anthropic.ToolResultBlockParam[], + watsonxAiMessages: OpenAI.Chat.ChatCompletionMessageParam[], +) { + toolResultBlocks.forEach((toolResult) => { + if (!toolResult.tool_use_id) { + return + } + + let content: string + if (typeof toolResult.content === "string") { + content = toolResult.content + } else { + content = + toolResult.content + ?.map((part) => { + if (part.type === "image") { + return "(see following user message for image)" + } + return part.text + }) + .join("\n") ?? "" + } + + if (content.trim()) { + watsonxAiMessages.push({ + role: "tool", + tool_call_id: toolResult.tool_use_id, + content: content, + }) + } + }) +} + +function convertToolUseBlocksToToolCalls( + toolUseBlocks: Anthropic.ToolUseBlockParam[], +): OpenAI.Chat.ChatCompletionMessageToolCall[] { + return toolUseBlocks.map((toolUse) => ({ + id: toolUse.id, + type: "function", + function: { + name: toolUse.name, + arguments: JSON.stringify(toolUse.input), + }, + })) +} diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 763e118125..bad54c365a 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -43,7 +43,6 @@ import { getVsCodeLmModels } from "../../api/providers/vscode-lm" import { openMention } from "../mentions" import { TelemetrySetting } from "../../shared/TelemetrySetting" import { getWorkspacePath } from "../../utils/path" -import { ensureSettingsDirectoryExists } from "../../utils/globalContext" import { Mode, defaultModeSlug } from "../../shared/modes" import { getModels, flushModels } from "../../api/providers/fetchers/modelCache" import { GetModelsOptions } from "../../shared/api" @@ -54,6 +53,7 @@ const ALLOWED_VSCODE_SETTINGS = new Set(["terminal.integrated.inheritEnv"]) import { MarketplaceManager, MarketplaceItemType } from "../../services/marketplace" import { setPendingTodoList } from "../tools/updateTodoListTool" +import { getWatsonxModels } from "../../api/providers/fetchers/watsonx" export const webviewMessageHandler = async ( provider: ClineProvider, @@ -674,6 +674,29 @@ export const webviewMessageHandler = async ( // TODO: Cache like we do for OpenRouter, etc? provider.postMessageToWebview({ type: "vsCodeLmModels", vsCodeLmModels }) break + case "requestWatsonxModels": + if (message?.values?.apiKey) { + try { + const watsonxModels = await getWatsonxModels(message.values.apiKey, message.values.projectId) + const formattedModels: Record = {} + Object.entries(watsonxModels).forEach(([modelId]) => { + formattedModels[modelId] = { + dimension: 1536, + } + }) + provider.postMessageToWebview({ + type: "watsonxModels", + watsonxModels: formattedModels, + }) + } catch (error) { + console.error("Failed to fetch watsonx models:", error) + provider.postMessageToWebview({ + type: "watsonxModels", + watsonxModels: {}, + }) + } + } + break case "requestHuggingFaceModels": try { const { getHuggingFaceModelsWithMetadata } = await import("../../api/providers/fetchers/huggingface") @@ -2036,6 +2059,18 @@ export const webviewMessageHandler = async ( settings.codebaseIndexMistralApiKey, ) } + if (settings.codebaseIndexWatsonxApiKey !== undefined) { + await provider.contextProxy.storeSecret( + "codebaseIndexWatsonxApiKey", + settings.codebaseIndexWatsonxApiKey, + ) + } + if (settings.codebaseIndexWatsonxProjectId !== undefined) { + await provider.contextProxy.storeSecret( + "codebaseIndexWatsonxProjectId", + settings.codebaseIndexWatsonxProjectId, + ) + } // Send success response first - settings are saved regardless of validation await provider.postMessageToWebview({ @@ -2157,6 +2192,7 @@ export const webviewMessageHandler = async ( )) const hasGeminiApiKey = !!(await provider.context.secrets.get("codebaseIndexGeminiApiKey")) const hasMistralApiKey = !!(await provider.context.secrets.get("codebaseIndexMistralApiKey")) + const hasWatsonxApiKey = !!(await provider.context.secrets.get("codebaseIndexWatsonxApiKey")) provider.postMessageToWebview({ type: "codeIndexSecretStatus", @@ -2166,6 +2202,7 @@ export const webviewMessageHandler = async ( hasOpenAiCompatibleApiKey, hasGeminiApiKey, hasMistralApiKey, + hasWatsonxApiKey, }, }) break diff --git a/src/i18n/locales/en/embeddings.json b/src/i18n/locales/en/embeddings.json index 66465d8c35..84dd4fe97c 100644 --- a/src/i18n/locales/en/embeddings.json +++ b/src/i18n/locales/en/embeddings.json @@ -47,6 +47,7 @@ "openAiCompatibleConfigMissing": "OpenAI Compatible configuration missing for embedder creation", "geminiConfigMissing": "Gemini configuration missing for embedder creation", "mistralConfigMissing": "Mistral configuration missing for embedder creation", + "watsonxConfigMissing": "IBM watsonx configuration missing for embedder creation", "invalidEmbedderType": "Invalid embedder type configured: {{embedderProvider}}", "vectorDimensionNotDeterminedOpenAiCompatible": "Could not determine vector dimension for model '{{modelId}}' with provider '{{provider}}'. Please ensure the 'Embedding Dimension' is correctly set in the OpenAI-Compatible provider settings.", "vectorDimensionNotDetermined": "Could not determine vector dimension for model '{{modelId}}' with provider '{{provider}}'. Check model profiles or configuration.", diff --git a/src/services/code-index/config-manager.ts b/src/services/code-index/config-manager.ts index 1723f1c2a0..b0ca2d9e64 100644 --- a/src/services/code-index/config-manager.ts +++ b/src/services/code-index/config-manager.ts @@ -19,6 +19,10 @@ export class CodeIndexConfigManager { private openAiCompatibleOptions?: { baseUrl: string; apiKey: string } private geminiOptions?: { apiKey: string } private mistralOptions?: { apiKey: string } + private watsonxOptions?: { + codebaseIndexWatsonxApiKey: string + codebaseIndexWatsonxProjectId?: string + } private qdrantUrl?: string = "http://localhost:6333" private qdrantApiKey?: string private searchMinScore?: number @@ -69,6 +73,8 @@ export class CodeIndexConfigManager { const openAiCompatibleApiKey = this.contextProxy?.getSecret("codebaseIndexOpenAiCompatibleApiKey") ?? "" const geminiApiKey = this.contextProxy?.getSecret("codebaseIndexGeminiApiKey") ?? "" const mistralApiKey = this.contextProxy?.getSecret("codebaseIndexMistralApiKey") ?? "" + const codebaseIndexWatsonxApiKey = this.contextProxy?.getSecret("codebaseIndexWatsonxApiKey") ?? "" + const codebaseIndexWatsonxProjectId = this.contextProxy?.getSecret("codebaseIndexWatsonxProjectId") ?? "" // Update instance variables with configuration this.codebaseIndexEnabled = codebaseIndexEnabled ?? true @@ -96,14 +102,17 @@ export class CodeIndexConfigManager { this.openAiOptions = { openAiNativeApiKey: openAiKey } // Set embedder provider with support for openai-compatible - if (codebaseIndexEmbedderProvider === "ollama") { + const provider = codebaseIndexEmbedderProvider as string + if (provider === "ollama") { this.embedderProvider = "ollama" - } else if (codebaseIndexEmbedderProvider === "openai-compatible") { + } else if (provider === "openai-compatible") { this.embedderProvider = "openai-compatible" - } else if (codebaseIndexEmbedderProvider === "gemini") { + } else if (provider === "gemini") { this.embedderProvider = "gemini" - } else if (codebaseIndexEmbedderProvider === "mistral") { + } else if (provider === "mistral") { this.embedderProvider = "mistral" + } else if (provider === "watsonx") { + this.embedderProvider = "watsonx" } else { this.embedderProvider = "openai" } @@ -124,6 +133,15 @@ export class CodeIndexConfigManager { this.geminiOptions = geminiApiKey ? { apiKey: geminiApiKey } : undefined this.mistralOptions = mistralApiKey ? { apiKey: mistralApiKey } : undefined + if (codebaseIndexWatsonxApiKey) { + this.watsonxOptions = { + codebaseIndexWatsonxApiKey: codebaseIndexWatsonxApiKey, + codebaseIndexWatsonxProjectId: codebaseIndexWatsonxProjectId, + } + this.contextProxy.storeSecret("codebaseIndexWatsonxProjectId", codebaseIndexWatsonxProjectId) + } else { + this.watsonxOptions = undefined + } } /** @@ -141,6 +159,10 @@ export class CodeIndexConfigManager { openAiCompatibleOptions?: { baseUrl: string; apiKey: string } geminiOptions?: { apiKey: string } mistralOptions?: { apiKey: string } + watsonxOptions?: { + codebaseIndexWatsonxApiKey: string + codebaseIndexWatsonxProjectId?: string + } qdrantUrl?: string qdrantApiKey?: string searchMinScore?: number @@ -160,6 +182,8 @@ export class CodeIndexConfigManager { openAiCompatibleApiKey: this.openAiCompatibleOptions?.apiKey ?? "", geminiApiKey: this.geminiOptions?.apiKey ?? "", mistralApiKey: this.mistralOptions?.apiKey ?? "", + codebaseIndexWatsonxApiKey: this.watsonxOptions?.codebaseIndexWatsonxApiKey ?? "", + codebaseIndexWatsonxProjectId: this.watsonxOptions?.codebaseIndexWatsonxProjectId ?? "", qdrantUrl: this.qdrantUrl ?? "", qdrantApiKey: this.qdrantApiKey ?? "", } @@ -184,6 +208,7 @@ export class CodeIndexConfigManager { openAiCompatibleOptions: this.openAiCompatibleOptions, geminiOptions: this.geminiOptions, mistralOptions: this.mistralOptions, + watsonxOptions: this.watsonxOptions, qdrantUrl: this.qdrantUrl, qdrantApiKey: this.qdrantApiKey, searchMinScore: this.currentSearchMinScore, @@ -221,6 +246,11 @@ export class CodeIndexConfigManager { const qdrantUrl = this.qdrantUrl const isConfigured = !!(apiKey && qdrantUrl) return isConfigured + } else if (this.embedderProvider === "watsonx") { + const apiKey = this.watsonxOptions?.codebaseIndexWatsonxApiKey + const qdrantUrl = this.qdrantUrl + const isConfigured = !!(apiKey && qdrantUrl) + return isConfigured } return false // Should not happen if embedderProvider is always set correctly } @@ -255,6 +285,8 @@ export class CodeIndexConfigManager { const prevModelDimension = prev?.modelDimension const prevGeminiApiKey = prev?.geminiApiKey ?? "" const prevMistralApiKey = prev?.mistralApiKey ?? "" + const prevWatsonxApiKey = prev?.codebaseIndexWatsonxApiKey ?? "" + const prevWatsonxProjectId = prev?.codebaseIndexWatsonxProjectId ?? "" const prevQdrantUrl = prev?.qdrantUrl ?? "" const prevQdrantApiKey = prev?.qdrantApiKey ?? "" @@ -292,6 +324,8 @@ export class CodeIndexConfigManager { const currentModelDimension = this.modelDimension const currentGeminiApiKey = this.geminiOptions?.apiKey ?? "" const currentMistralApiKey = this.mistralOptions?.apiKey ?? "" + const currentWatsonxApiKey = this.watsonxOptions?.codebaseIndexWatsonxApiKey ?? "" + const currentWatsonxProjectId = this.watsonxOptions?.codebaseIndexWatsonxProjectId ?? "" const currentQdrantUrl = this.qdrantUrl ?? "" const currentQdrantApiKey = this.qdrantApiKey ?? "" @@ -318,6 +352,10 @@ export class CodeIndexConfigManager { return true } + if (prevWatsonxApiKey !== currentWatsonxApiKey || prevWatsonxProjectId !== currentWatsonxProjectId) { + return true + } + // Check for model dimension changes (generic for all providers) if (prevModelDimension !== currentModelDimension) { return true @@ -375,6 +413,7 @@ export class CodeIndexConfigManager { openAiCompatibleOptions: this.openAiCompatibleOptions, geminiOptions: this.geminiOptions, mistralOptions: this.mistralOptions, + watsonxOptions: this.watsonxOptions, qdrantUrl: this.qdrantUrl, qdrantApiKey: this.qdrantApiKey, searchMinScore: this.currentSearchMinScore, diff --git a/src/services/code-index/embedders/watsonx.ts b/src/services/code-index/embedders/watsonx.ts new file mode 100644 index 0000000000..a7f6deb108 --- /dev/null +++ b/src/services/code-index/embedders/watsonx.ts @@ -0,0 +1,283 @@ +import { IEmbedder, EmbeddingResponse, EmbedderInfo } from "../interfaces/embedder" +import { MAX_ITEM_TOKENS } from "../constants" +import { t } from "../../../i18n" +import { TelemetryEventName } from "@roo-code/types" +import { TelemetryService } from "@roo-code/telemetry" +import { WatsonXAI } from "@ibm-cloud/watsonx-ai" +import { IamAuthenticator } from "ibm-cloud-sdk-core" + +/** + * IBM watsonx embedder implementation using the native IBM Cloud watsonx.ai package. + * + * Supported models: + * - ibm/slate-125m-english-rtrvr-v2 (dimension: 1536) + */ +export class WatsonxEmbedder implements IEmbedder { + private readonly watsonxClient: WatsonXAI + private static readonly WATSONX_VERSION = "2024-05-31" + private static readonly WATSONX_REGION = "us-south" + private static readonly DEFAULT_MODEL = "ibm/slate-125m-english-rtrvr-v2" + private readonly modelId: string + private readonly projectId?: string + + /** + * Creates a new watsonx embedder + * @param apiKey The watsonx API key for authentication + * @param modelId The model ID to use (defaults to ibm/slate-125m-english-rtrvr-v2) + * @param projectId Optional IBM Cloud project ID for watsonx + * @param proxyUrl Optional proxy URL for connecting through MCP servers + */ + constructor(apiKey: string, modelId?: string, projectId?: string) { + if (!apiKey) { + throw new Error(t("embeddings:validation.apiKeyRequired")) + } + this.modelId = modelId || WatsonxEmbedder.DEFAULT_MODEL + this.projectId = projectId + + const options: any = { + version: WatsonxEmbedder.WATSONX_VERSION, + authenticator: new IamAuthenticator({ + apikey: apiKey, + }), + serviceUrl: `https://${WatsonxEmbedder.WATSONX_REGION}.ml.cloud.ibm.com`, + } + + this.watsonxClient = new WatsonXAI(options) + + try { + this.watsonxClient.getAuthenticator().authenticate() + } catch (error) { + console.error("WatsonX authentication failed:", error) + throw new Error(t("embeddings:validation.authenticationFailed")) + } + } + + /** + * Creates embeddings for the given texts using watsonx's embedding API + * @param texts Array of text strings to embed + * @param model Optional model identifier (uses constructor model if not provided) + * @returns Promise resolving to embedding response + */ + async createEmbeddings(texts: string[], model?: string): Promise { + const MAX_RETRIES = 3 + const INITIAL_DELAY_MS = 1000 + + try { + const modelToUse = model || this.modelId + + const embeddings: number[][] = [] + let promptTokens = 0 + let totalTokens = 0 + + for (const text of texts) { + if (!text.trim()) { + embeddings.push([]) + continue + } + + const estimatedTokens = Math.ceil(text.length / 4) + if (estimatedTokens > MAX_ITEM_TOKENS) { + console.warn( + t("embeddings:textExceedsTokenLimit", { + index: texts.indexOf(text), + itemTokens: estimatedTokens, + maxTokens: MAX_ITEM_TOKENS, + }), + ) + embeddings.push([]) + continue + } + + let lastError + for (let attempt = 0; attempt < MAX_RETRIES; attempt++) { + try { + const response = await this.watsonxClient.embedText({ + modelId: modelToUse, + inputs: [text], + projectId: this.projectId, + parameters: { + truncate_input_tokens: MAX_ITEM_TOKENS, + return_options: { + input_text: true, + }, + }, + }) + + if (response.result && response.result.results && response.result.results.length > 0) { + embeddings.push(response.result.results[0].embedding) + + if (response.result.input_token_count) { + promptTokens += response.result.input_token_count + totalTokens += response.result.input_token_count + } + break + } else { + embeddings.push([]) + break + } + } catch (error) { + lastError = error + + if (attempt < MAX_RETRIES - 1) { + const delayMs = INITIAL_DELAY_MS * Math.pow(2, attempt) + console.warn( + `IBM watsonx API call failed, retrying in ${delayMs}ms (attempt ${attempt + 1}/${MAX_RETRIES})`, + ) + await new Promise((resolve) => setTimeout(resolve, delayMs)) + } + } + } + + if (lastError && embeddings.length < texts.indexOf(text) + 1) { + embeddings.push([]) + console.error(`Failed to embed text after ${MAX_RETRIES} attempts:`, lastError) + } + } + + return { + embeddings, + usage: { + promptTokens, + totalTokens, + }, + } + } catch (error) { + TelemetryService.instance.captureEvent(TelemetryEventName.CODE_INDEX_ERROR, { + error: error instanceof Error ? error.message : String(error), + stack: error instanceof Error ? error.stack : undefined, + location: "WatsonxEmbedder:createEmbeddings", + }) + throw error + } + } + + /** + * Validates the watsonx embedder configuration by testing the API key and connection + * @returns Promise resolving to validation result with success status and optional error message + */ + async validateConfiguration(): Promise<{ valid: boolean; error?: string }> { + try { + const testText = "test" + + console.log("Testing IBM watsonx.ai configuration with model:", this.modelId) + + const response = await this.watsonxClient.embedText({ + modelId: this.modelId, + inputs: [testText], + projectId: this.projectId, + parameters: { + truncate_input_tokens: MAX_ITEM_TOKENS, + return_options: { + input_text: true, + }, + }, + }) + + if (!response?.result?.results || response.result.results.length === 0) { + console.error("IBM watsonx validation failed: Invalid response format", response) + return { + valid: false, + error: "embeddings:validation.invalidResponse", + } + } + + console.log("IBM watsonx configuration validated successfully") + return { valid: true } + } catch (error) { + console.error("IBM watsonx validation error:", error) + TelemetryService.instance.captureEvent(TelemetryEventName.CODE_INDEX_ERROR, { + error: error instanceof Error ? error.message : String(error), + stack: error instanceof Error ? error.stack : undefined, + location: "WatsonxEmbedder:validateConfiguration", + }) + + let errorMessage = "embeddings:validation.unknownError" + let errorDetails = "" + + if (error instanceof Error) { + errorDetails = error.message + if (error.message.includes("401") || error.message.includes("unauthorized")) { + errorMessage = "embeddings:validation.invalidApiKey" + } else if (error.message.includes("404") || error.message.includes("not found")) { + errorMessage = "embeddings:validation.endpointNotFound" + } else if (error.message.includes("timeout") || error.message.includes("ECONNREFUSED")) { + errorMessage = "embeddings:validation.connectionTimeout" + } else if (error.message.includes("project")) { + errorMessage = "embeddings:validation.invalidProjectId" + } else if (error.message.includes("model")) { + errorMessage = "embeddings:validation.invalidModelId" + } + } + + return { + valid: false, + error: `${errorMessage} (${errorDetails})`, + } + } + } + + /** + * Fetches available embedding models from the IBM watsonx API + * @returns Promise resolving to an object with model IDs as keys and model info as values + */ + async getAvailableModels(): Promise> { + try { + console.log("Fetching available IBM watsonx embedding models...") + + const knownModels: Record = { + "ibm/slate-125m-english-rtrvr-v2": { dimension: 1536 }, + } + + try { + const response = await this.watsonxClient.listFoundationModelSpecs() + + console.log( + "IBM watsonx API response structure:", + Object.keys(response || {}).join(", "), + Object.keys(response?.result || {}).join(", "), + ) + + if (response && response.result) { + const result = response.result as any + + const modelsList = result.models || result.resources || result.foundation_models || [] + + if (Array.isArray(modelsList)) { + for (const model of modelsList) { + const modelId = model.id || model.name || model.model_id + const modelInfo = JSON.stringify(model).toLowerCase() + if ( + modelId && + (modelInfo.includes("embed") || + modelInfo.includes("rtrvr") || + modelInfo.includes("retriev")) + ) { + const dimension = model.dimension || model.vector_size || model.embedding_size || 1536 + knownModels[modelId] = { dimension } + } + } + } + } + } catch (apiError) { + console.warn("Error fetching models from IBM watsonx API:", apiError) + } + + console.log(`Found ${Object.keys(knownModels).length} IBM watsonx embedding models`) + return knownModels + } catch (error) { + console.error("Error in getAvailableModels:", error) + return { + "ibm/slate-125m-english-rtrvr-v2": { dimension: 768 }, + } + } + } + + /** + * Returns information about this embedder + */ + get embedderInfo(): EmbedderInfo { + return { + name: "watsonx", + } + } +} diff --git a/src/services/code-index/interfaces/config.ts b/src/services/code-index/interfaces/config.ts index 9098a60091..18e31ceedf 100644 --- a/src/services/code-index/interfaces/config.ts +++ b/src/services/code-index/interfaces/config.ts @@ -14,6 +14,10 @@ export interface CodeIndexConfig { openAiCompatibleOptions?: { baseUrl: string; apiKey: string } geminiOptions?: { apiKey: string } mistralOptions?: { apiKey: string } + watsonxOptions?: { + codebaseIndexWatsonxApiKey: string + codebaseIndexWatsonxProjectId?: string + } qdrantUrl?: string qdrantApiKey?: string searchMinScore?: number @@ -35,6 +39,8 @@ export type PreviousConfigSnapshot = { openAiCompatibleApiKey?: string geminiApiKey?: string mistralApiKey?: string + codebaseIndexWatsonxApiKey?: string + codebaseIndexWatsonxProjectId?: string qdrantUrl?: string qdrantApiKey?: string } diff --git a/src/services/code-index/interfaces/embedder.ts b/src/services/code-index/interfaces/embedder.ts index c5653ea2b7..48fbb6af83 100644 --- a/src/services/code-index/interfaces/embedder.ts +++ b/src/services/code-index/interfaces/embedder.ts @@ -28,7 +28,7 @@ export interface EmbeddingResponse { } } -export type AvailableEmbedders = "openai" | "ollama" | "openai-compatible" | "gemini" | "mistral" +export type AvailableEmbedders = "openai" | "ollama" | "openai-compatible" | "gemini" | "mistral" | "watsonx" export interface EmbedderInfo { name: AvailableEmbedders diff --git a/src/services/code-index/interfaces/manager.ts b/src/services/code-index/interfaces/manager.ts index fd3b2bfdda..a442d99791 100644 --- a/src/services/code-index/interfaces/manager.ts +++ b/src/services/code-index/interfaces/manager.ts @@ -70,7 +70,7 @@ export interface ICodeIndexManager { } export type IndexingState = "Standby" | "Indexing" | "Indexed" | "Error" -export type EmbedderProvider = "openai" | "ollama" | "openai-compatible" | "gemini" | "mistral" +export type EmbedderProvider = "openai" | "ollama" | "openai-compatible" | "gemini" | "mistral" | "watsonx" export interface IndexProgressUpdate { systemStatus: IndexingState diff --git a/src/services/code-index/service-factory.ts b/src/services/code-index/service-factory.ts index 68b0f5c0bc..bddc9d28d1 100644 --- a/src/services/code-index/service-factory.ts +++ b/src/services/code-index/service-factory.ts @@ -4,6 +4,7 @@ import { CodeIndexOllamaEmbedder } from "./embedders/ollama" import { OpenAICompatibleEmbedder } from "./embedders/openai-compatible" import { GeminiEmbedder } from "./embedders/gemini" import { MistralEmbedder } from "./embedders/mistral" +import { WatsonxEmbedder } from "./embedders/watsonx" import { EmbedderProvider, getDefaultModelId, getModelDimension } from "../../shared/embeddingModels" import { QdrantVectorStore } from "./vector-store/qdrant-client" import { codeParser, DirectoryScanner, FileWatcher } from "./processors" @@ -70,6 +71,15 @@ export class CodeIndexServiceFactory { throw new Error(t("embeddings:serviceFactory.mistralConfigMissing")) } return new MistralEmbedder(config.mistralOptions.apiKey, config.modelId) + } else if (provider === "watsonx") { + if (!config.watsonxOptions?.codebaseIndexWatsonxApiKey) { + throw new Error(t("embeddings:serviceFactory.watsonxConfigMissing")) + } + return new WatsonxEmbedder( + config.watsonxOptions.codebaseIndexWatsonxApiKey, + config.modelId, + config.watsonxOptions.codebaseIndexWatsonxProjectId, + ) } throw new Error( diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 67f8782e19..76f69a2298 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -76,6 +76,7 @@ export interface ExtensionMessage { | "ollamaModels" | "lmStudioModels" | "vsCodeLmModels" + | "watsonxModels" | "huggingFaceModels" | "vsCodeLmApiAvailable" | "updatePrompt" @@ -148,6 +149,7 @@ export interface ExtensionMessage { ollamaModels?: string[] lmStudioModels?: string[] vsCodeLmModels?: { vendor?: string; family?: string; version?: string; id?: string }[] + watsonxModels?: Record huggingFaceModels?: Array<{ id: string object: string diff --git a/src/shared/ProfileValidator.ts b/src/shared/ProfileValidator.ts index 9fc527c15a..79adf7eb78 100644 --- a/src/shared/ProfileValidator.ts +++ b/src/shared/ProfileValidator.ts @@ -84,6 +84,8 @@ export class ProfileValidator { return profile.ollamaModelId case "requesty": return profile.requestyModelId + case "watsonx": + return profile.watsonxModelId case "human-relay": case "fake-ai": default: diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index a91d1af7ba..db95977426 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -67,6 +67,7 @@ export interface WebviewMessage { | "requestOllamaModels" | "requestLmStudioModels" | "requestVsCodeLmModels" + | "requestWatsonxModels" | "requestHuggingFaceModels" | "openImage" | "saveImage" @@ -269,6 +270,8 @@ export interface WebviewMessage { codebaseIndexOpenAiCompatibleApiKey?: string codebaseIndexGeminiApiKey?: string codebaseIndexMistralApiKey?: string + codebaseIndexWatsonxApiKey?: string + codebaseIndexWatsonxProjectId?: string } } diff --git a/src/shared/embeddingModels.ts b/src/shared/embeddingModels.ts index a3cd61e659..b3172814c9 100644 --- a/src/shared/embeddingModels.ts +++ b/src/shared/embeddingModels.ts @@ -2,7 +2,7 @@ * Defines profiles for different embedding models, including their dimensions. */ -export type EmbedderProvider = "openai" | "ollama" | "openai-compatible" | "gemini" | "mistral" // Add other providers as needed +export type EmbedderProvider = "openai" | "ollama" | "openai-compatible" | "gemini" | "mistral" | "watsonx" // Add other providers as needed export interface EmbeddingModelProfile { dimension: number @@ -53,6 +53,9 @@ export const EMBEDDING_MODEL_PROFILES: EmbeddingModelProfiles = { mistral: { "codestral-embed-2505": { dimension: 1536, scoreThreshold: 0.4 }, }, + watsonx: { + "ibm/slate-125m-english-rtrvr-v2": { dimension: 768, scoreThreshold: 0.4 }, + }, } /** @@ -143,6 +146,9 @@ export function getDefaultModelId(provider: EmbedderProvider): string { case "mistral": return "codestral-embed-2505" + case "watsonx": + return "ibm/slate-125m-english-rtrvr-v2" + default: // Fallback for unknown providers console.warn(`Unknown provider for default model ID: ${provider}. Falling back to OpenAI default.`) diff --git a/webview-ui/src/components/chat/CodeIndexPopover.tsx b/webview-ui/src/components/chat/CodeIndexPopover.tsx index c85aaf6ea5..be39c0a173 100644 --- a/webview-ui/src/components/chat/CodeIndexPopover.tsx +++ b/webview-ui/src/components/chat/CodeIndexPopover.tsx @@ -70,6 +70,8 @@ interface LocalCodeIndexSettings { codebaseIndexOpenAiCompatibleApiKey?: string codebaseIndexGeminiApiKey?: string codebaseIndexMistralApiKey?: string + codebaseIndexWatsonxApiKey?: string + codebaseIndexWatsonxProjectId?: string } // Validation schema for codebase index settings @@ -136,6 +138,15 @@ const createValidationSchema = (provider: EmbedderProvider, t: any) => { .min(1, t("settings:codeIndex.validation.modelSelectionRequired")), }) + case "watsonx": + return baseSchema.extend({ + codebaseIndexWatsonxApiKey: z.string().min(1, t("settings:codeIndex.validation.watsonxApiKeyRequired")), + codebaseIndexWatsonxProjectId: z.string().optional(), + codebaseIndexEmbedderModelId: z + .string() + .min(1, t("settings:codeIndex.validation.modelSelectionRequired")), + }) + default: return baseSchema } @@ -180,6 +191,8 @@ export const CodeIndexPopover: React.FC = ({ codebaseIndexOpenAiCompatibleApiKey: "", codebaseIndexGeminiApiKey: "", codebaseIndexMistralApiKey: "", + codebaseIndexWatsonxApiKey: "", + codebaseIndexWatsonxProjectId: "", }) // Initial settings state - stores the settings when popover opens @@ -214,6 +227,8 @@ export const CodeIndexPopover: React.FC = ({ codebaseIndexOpenAiCompatibleApiKey: "", codebaseIndexGeminiApiKey: "", codebaseIndexMistralApiKey: "", + codebaseIndexWatsonxApiKey: "", + codebaseIndexWatsonxProjectId: "", } setInitialSettings(settings) setCurrentSettings(settings) @@ -231,11 +246,32 @@ export const CodeIndexPopover: React.FC = ({ } }, [open]) + // Request WatsonX models when provider is selected and API key is available + useEffect(() => { + if ( + currentSettings.codebaseIndexEmbedderProvider === "watsonx" && + currentSettings.codebaseIndexWatsonxApiKey && + currentSettings.codebaseIndexWatsonxApiKey !== SECRET_PLACEHOLDER + ) { + vscode.postMessage({ + type: "requestWatsonxModels", + values: { + apiKey: currentSettings.codebaseIndexWatsonxApiKey, + projectId: currentSettings.codebaseIndexWatsonxProjectId, + }, + }) + } + }, [ + currentSettings.codebaseIndexEmbedderProvider, + currentSettings.codebaseIndexWatsonxApiKey, + currentSettings.codebaseIndexWatsonxProjectId, + ]) + // Use a ref to capture current settings for the save handler const currentSettingsRef = useRef(currentSettings) currentSettingsRef.current = currentSettings - // Listen for indexing status updates and save responses + // Listen for indexing status updates, save responses, and watsonx models useEffect(() => { const handleMessage = (event: MessageEvent) => { if (event.data.type === "indexingStatusUpdate") { @@ -268,6 +304,10 @@ export const CodeIndexPopover: React.FC = ({ setSaveStatus("idle") setSaveError(null) } + } else if (event.data.type === "watsonxModels" && event.data.watsonxModels) { + // Update the extension state context with the watsonx models + // The models will be automatically available through the codebaseIndexModels context + console.log("Received WatsonX models:", event.data.watsonxModels) } } @@ -308,6 +348,17 @@ export const CodeIndexPopover: React.FC = ({ if (!prev.codebaseIndexMistralApiKey || prev.codebaseIndexMistralApiKey === SECRET_PLACEHOLDER) { updated.codebaseIndexMistralApiKey = secretStatus.hasMistralApiKey ? SECRET_PLACEHOLDER : "" } + if (!prev.codebaseIndexWatsonxApiKey || prev.codebaseIndexWatsonxApiKey === SECRET_PLACEHOLDER) { + updated.codebaseIndexWatsonxApiKey = secretStatus.hasWatsonxApiKey ? SECRET_PLACEHOLDER : "" + } + if ( + !prev.codebaseIndexWatsonxProjectId || + prev.codebaseIndexWatsonxProjectId === SECRET_PLACEHOLDER + ) { + updated.codebaseIndexWatsonxProjectId = secretStatus.hasWatsonxProjectId + ? SECRET_PLACEHOLDER + : "" + } return updated } @@ -380,7 +431,8 @@ export const CodeIndexPopover: React.FC = ({ key === "codeIndexOpenAiKey" || key === "codebaseIndexOpenAiCompatibleApiKey" || key === "codebaseIndexGeminiApiKey" || - key === "codebaseIndexMistralApiKey" + key === "codebaseIndexMistralApiKey" || + key === "codebaseIndexWatsonxApiKey" ) { dataToValidate[key] = "placeholder-valid" } @@ -490,6 +542,12 @@ export const CodeIndexPopover: React.FC = ({ const transformStyleString = `translateX(-${100 - progressPercentage}%)` + // Helper function to safely access models for any provider + const getProviderModels = (provider: EmbedderProvider) => { + if (!codebaseIndexModels) return {} + return (codebaseIndexModels as any)[provider] || {} + } + const getAvailableModels = () => { if (!codebaseIndexModels) return [] @@ -628,6 +686,9 @@ export const CodeIndexPopover: React.FC = ({ {t("settings:codeIndex.mistralProvider")} + + {t("settings:codeIndex.watsonxProvider")} + @@ -673,10 +734,10 @@ export const CodeIndexPopover: React.FC = ({ {t("settings:codeIndex.selectModel")} {getAvailableModels().map((modelId) => { - const model = - codebaseIndexModels?.[ - currentSettings.codebaseIndexEmbedderProvider - ]?.[modelId] + const providerModels = getProviderModels( + currentSettings.codebaseIndexEmbedderProvider, + ) + const model = providerModels[modelId] return ( {modelId}{" "} @@ -930,10 +991,10 @@ export const CodeIndexPopover: React.FC = ({ {t("settings:codeIndex.selectModel")} {getAvailableModels().map((modelId) => { - const model = - codebaseIndexModels?.[ - currentSettings.codebaseIndexEmbedderProvider - ]?.[modelId] + const providerModels = getProviderModels( + currentSettings.codebaseIndexEmbedderProvider, + ) + const model = providerModels[modelId] return ( {modelId}{" "} @@ -995,10 +1056,99 @@ export const CodeIndexPopover: React.FC = ({ {t("settings:codeIndex.selectModel")} {getAvailableModels().map((modelId) => { - const model = - codebaseIndexModels?.[ - currentSettings.codebaseIndexEmbedderProvider - ]?.[modelId] + const providerModels = getProviderModels( + currentSettings.codebaseIndexEmbedderProvider, + ) + const model = providerModels[modelId] + return ( + + {modelId}{" "} + {model + ? t("settings:codeIndex.modelDimensions", { + dimension: model.dimension, + }) + : ""} + + ) + })} + + {formErrors.codebaseIndexEmbedderModelId && ( +

+ {formErrors.codebaseIndexEmbedderModelId} +

+ )} + + + )} + + {currentSettings.codebaseIndexEmbedderProvider === "watsonx" && ( + <> +
+ + + updateSetting("codebaseIndexWatsonxApiKey", e.target.value) + } + placeholder={t("settings:codeIndex.watsonxApiKeyPlaceholder")} + className={cn("w-full", { + "border-red-500": formErrors.watsonxApiKey, + })} + /> + {formErrors.watsonxApiKey && ( +

+ {formErrors.watsonxApiKey} +

+ )} +
+ +
+ + + updateSetting("codebaseIndexWatsonxProjectId", e.target.value) + } + placeholder={ + t("settings:codeIndex.watsonxProjectIdPlaceholder") || + "Optional IBM Cloud project ID" + } + className={cn("w-full", { + "border-red-500": formErrors.watsonxProjectId, + })} + /> + {formErrors.watsonxProjectId && ( +

+ {formErrors.watsonxProjectId} +

+ )} +
+ +
+ + + updateSetting("codebaseIndexEmbedderModelId", e.target.value) + } + className={cn("w-full", { + "border-red-500": formErrors.codebaseIndexEmbedderModelId, + })}> + + {t("settings:codeIndex.selectModel")} + + {getAvailableModels().map((modelId) => { + const providerModels = getProviderModels( + currentSettings.codebaseIndexEmbedderProvider, + ) + const model = providerModels[modelId] return ( {modelId}{" "} diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index 977822cac3..e89d5072e7 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -25,6 +25,7 @@ import { chutesDefaultModelId, bedrockDefaultModelId, vertexDefaultModelId, + watsonxAiDefaultModelId, } from "@roo-code/types" import { vscode } from "@src/utils/vscode" @@ -72,6 +73,7 @@ import { Unbound, Vertex, VSCodeLM, + WatsonxAI, XAI, } from "./providers" @@ -300,6 +302,7 @@ const ApiOptions = ({ openai: { field: "openAiModelId" }, ollama: { field: "ollamaModelId" }, lmstudio: { field: "lmStudioModelId" }, + watsonx: { field: "apiModelId", default: watsonxAiDefaultModelId }, } const config = PROVIDER_MODEL_CONFIG[value] @@ -509,6 +512,10 @@ const ApiOptions = ({ /> )} + {selectedProvider === "watsonx" && ( + + )} + {selectedProvider === "human-relay" && ( <>
diff --git a/webview-ui/src/components/settings/constants.ts b/webview-ui/src/components/settings/constants.ts index 995f591034..f64ade1b52 100644 --- a/webview-ui/src/components/settings/constants.ts +++ b/webview-ui/src/components/settings/constants.ts @@ -13,6 +13,7 @@ import { xaiModels, groqModels, chutesModels, + watsonxAiModels, } from "@roo-code/types" export const MODELS_BY_PROVIDER: Partial>> = { @@ -28,6 +29,7 @@ export const MODELS_BY_PROVIDER: Partial a.label.localeCompare(b.label)) diff --git a/webview-ui/src/components/settings/providers/WatsonxAI.tsx b/webview-ui/src/components/settings/providers/WatsonxAI.tsx new file mode 100644 index 0000000000..5c44223c77 --- /dev/null +++ b/webview-ui/src/components/settings/providers/WatsonxAI.tsx @@ -0,0 +1,86 @@ +import { useCallback } from "react" +import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react" + +import type { ProviderSettings } from "@roo-code/types" +import { watsonxAiDefaultModelId, watsonxAiModels } from "@roo-code/types" + +import { useAppTranslation } from "@src/i18n/TranslationContext" +import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink" + +import { inputEventTransform } from "../transforms" + +type WatsonxAIProps = { + apiConfiguration: ProviderSettings + setApiConfigurationField: (field: K, value: ProviderSettings[K]) => void +} + +export const WatsonxAI = ({ apiConfiguration, setApiConfigurationField }: WatsonxAIProps) => { + const { t } = useAppTranslation() + + const handleInputChange = useCallback( + (field: keyof ProviderSettings, transform: (event: E) => any = inputEventTransform) => + (event: E | Event) => { + setApiConfigurationField(field, transform(event as E)) + }, + [setApiConfigurationField], + ) + + const defaultModel = watsonxAiDefaultModelId + const modelInfo = watsonxAiModels[defaultModel] || {} + const defaultModelDescription = + typeof modelInfo === "object" && "contextWindow" in modelInfo + ? `Context window: ${modelInfo.contextWindow} tokens` + : "IBM watsonx model" + + return ( + <> + + + +
+ {t("settings:providers.apiKeyStorageNotice")} +
+ {!apiConfiguration?.watsonxApiKey && ( + + Get WatsonX API Key + + )} + + + + +
+ Project ID is required for IBM watsonx integration +
+ + + + +
+ Default: https://us-south.ml.cloud.ibm.com +

Default Model Information

+
+
+ Model ID: {defaultModel} +
+
+ Description: {defaultModelDescription} +
+
+
+ + ) +} diff --git a/webview-ui/src/components/settings/providers/index.ts b/webview-ui/src/components/settings/providers/index.ts index 6c6fdddaee..fcd1fa6c0c 100644 --- a/webview-ui/src/components/settings/providers/index.ts +++ b/webview-ui/src/components/settings/providers/index.ts @@ -20,3 +20,4 @@ export { Vertex } from "./Vertex" export { VSCodeLM } from "./VSCodeLM" export { XAI } from "./XAI" export { LiteLLM } from "./LiteLLM" +export { WatsonxAI } from "./WatsonxAI" diff --git a/webview-ui/src/components/ui/hooks/useSelectedModel.ts b/webview-ui/src/components/ui/hooks/useSelectedModel.ts index 8dceb6e117..fe5f842831 100644 --- a/webview-ui/src/components/ui/hooks/useSelectedModel.ts +++ b/webview-ui/src/components/ui/hooks/useSelectedModel.ts @@ -34,6 +34,8 @@ import { litellmDefaultModelId, claudeCodeDefaultModelId, claudeCodeModels, + watsonxAiModels, + watsonxAiDefaultModelId, } from "@roo-code/types" import type { RouterModels } from "@roo/api" @@ -224,6 +226,14 @@ function getSelectedModel({ const info = claudeCodeModels[id as keyof typeof claudeCodeModels] return { id, info: { ...openAiModelInfoSaneDefaults, ...info } } } + case "watsonx": { + const id = apiConfiguration.apiModelId ?? watsonxAiDefaultModelId + const info = watsonxAiModels[id as keyof typeof watsonxAiModels] + return { + id, + info: info || undefined, + } + } // case "anthropic": // case "human-relay": // case "fake-ai": diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index 7c58e679c6..6f31b5e3a8 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -55,6 +55,11 @@ "mistralProvider": "Mistral", "mistralApiKeyLabel": "API Key:", "mistralApiKeyPlaceholder": "Enter your Mistral API key", + "watsonxProvider": "IBM watsonx", + "watsonxApiKeyLabel": "API Key", + "watsonxApiKeyPlaceholder": "Enter your IBM watsonx API key", + "watsonxProjectIdLabel": "Project ID", + "watsonxProjectIdPlaceholder": "Enter your IBM watsonx project ID", "openaiCompatibleProvider": "OpenAI Compatible", "openAiKeyLabel": "OpenAI API Key", "openAiKeyPlaceholder": "Enter your OpenAI API key", @@ -122,7 +127,8 @@ "mistralApiKeyRequired": "Mistral API key is required", "ollamaBaseUrlRequired": "Ollama base URL is required", "baseUrlRequired": "Base URL is required", - "modelDimensionMinValue": "Model dimension must be greater than 0" + "modelDimensionMinValue": "Model dimension must be greater than 0", + "watsonxApiKeyRequired": "IBM watsonx API key is required" } }, "autoApprove": { diff --git a/webview-ui/src/utils/validate.ts b/webview-ui/src/utils/validate.ts index ed546cccc7..785a149adb 100644 --- a/webview-ui/src/utils/validate.ts +++ b/webview-ui/src/utils/validate.ts @@ -110,6 +110,14 @@ function validateModelsAndKeysProvided(apiConfiguration: ProviderSettings): stri return i18next.t("settings:validation.modelId") } break + case "watsonx": + if (!apiConfiguration.watsonxApiKey) { + return i18next.t("settings:validation.apiKey") + } + if (!apiConfiguration.watsonxProjectId) { + return i18next.t("settings:validation.projectId") + } + break } return undefined From 0362aaecdb0d74581fbf7fc450d74317b329c4ad Mon Sep 17 00:00:00 2001 From: Prasang-A-Prajapati Date: Tue, 26 Aug 2025 11:40:22 -0400 Subject: [PATCH 03/20] add watsonx missing provider in the typs --- packages/types/src/provider-settings.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/types/src/provider-settings.ts b/packages/types/src/provider-settings.ts index c783c2cc99..85e22f22f6 100644 --- a/packages/types/src/provider-settings.ts +++ b/packages/types/src/provider-settings.ts @@ -25,6 +25,7 @@ import { vscodeLlmModels, xaiModels, internationalZAiModels, + watsonxAiModels, } from "./providers/index.js" /** @@ -541,6 +542,11 @@ export const MODELS_BY_PROVIDER: Record< label: "VS Code LM API", models: Object.keys(vscodeLlmModels), }, + watsonx: { + id: "watsonx", + label: "IBM watsonx", + models: Object.keys(watsonxAiModels), + }, xai: { id: "xai", label: "xAI (Grok)", models: Object.keys(xaiModels) }, zai: { id: "zai", label: "Zai", models: Object.keys(internationalZAiModels) }, From 408b554e84032f0b1dbdf319107356b4c0fd9cf9 Mon Sep 17 00:00:00 2001 From: Prasang Prajapati Date: Tue, 26 Aug 2025 12:07:49 -0400 Subject: [PATCH 04/20] fix lock file --- pnpm-lock.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 31aea6f423..7101b9230d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8366,6 +8366,9 @@ packages: quansync@0.2.11: resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} + querystringify@2.2.0: + resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} + queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -18679,6 +18682,8 @@ snapshots: quansync@0.2.11: {} + querystringify@2.2.0: {} + queue-microtask@1.2.3: {} randombytes@2.1.0: From 2129d11634e1c4c94a93e90706889092eec2f19d Mon Sep 17 00:00:00 2001 From: Prasang Prajapati Date: Tue, 26 Aug 2025 12:38:04 -0400 Subject: [PATCH 05/20] fix providers issue --- packages/types/src/provider-settings.ts | 2 ++ src/shared/api.ts | 2 ++ .../settings/__tests__/ApiOptions.provider-filtering.spec.tsx | 1 + webview-ui/src/utils/__tests__/validate.test.ts | 1 + 4 files changed, 6 insertions(+) diff --git a/packages/types/src/provider-settings.ts b/packages/types/src/provider-settings.ts index 85e22f22f6..956ea3e6b2 100644 --- a/packages/types/src/provider-settings.ts +++ b/packages/types/src/provider-settings.ts @@ -436,6 +436,7 @@ export const MODEL_ID_KEYS: Partial[] = [ "litellmModelId", "huggingFaceModelId", "ioIntelligenceModelId", + "watsonxModelId", ] export const getModelId = (settings: ProviderSettings): string | undefined => { @@ -566,6 +567,7 @@ export const dynamicProviders = [ "openrouter", "requesty", "unbound", + "watsonx", ] as const satisfies readonly ProviderName[] export type DynamicProvider = (typeof dynamicProviders)[number] diff --git a/src/shared/api.ts b/src/shared/api.ts index 3dde992c6f..061a604faa 100644 --- a/src/shared/api.ts +++ b/src/shared/api.ts @@ -27,6 +27,7 @@ const routerNames = [ "ollama", "lmstudio", "io-intelligence", + "watsonx", ] as const export type RouterName = (typeof routerNames)[number] @@ -151,3 +152,4 @@ export type GetModelsOptions = | { provider: "ollama"; baseUrl?: string } | { provider: "lmstudio"; baseUrl?: string } | { provider: "io-intelligence"; apiKey: string } + | { provider: "watsonx"; apiKey: string } diff --git a/webview-ui/src/components/settings/__tests__/ApiOptions.provider-filtering.spec.tsx b/webview-ui/src/components/settings/__tests__/ApiOptions.provider-filtering.spec.tsx index 52048de95d..4686337d21 100644 --- a/webview-ui/src/components/settings/__tests__/ApiOptions.provider-filtering.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/ApiOptions.provider-filtering.spec.tsx @@ -159,6 +159,7 @@ describe("ApiOptions Provider Filtering", () => { expect(providerValues).toContain("unbound") expect(providerValues).toContain("requesty") expect(providerValues).toContain("io-intelligence") + expect(providerValues).toContain("watsonx") }) it("should filter static providers based on organization allow list", () => { diff --git a/webview-ui/src/utils/__tests__/validate.test.ts b/webview-ui/src/utils/__tests__/validate.test.ts index f14fb89200..6ee84c8f77 100644 --- a/webview-ui/src/utils/__tests__/validate.test.ts +++ b/webview-ui/src/utils/__tests__/validate.test.ts @@ -41,6 +41,7 @@ describe("Model Validation Functions", () => { ollama: {}, lmstudio: {}, "io-intelligence": {}, + watsonx: {}, } const allowAllOrganization: OrganizationAllowList = { From dd2eb49e11d4f4b5d351d6dae830ab09332347c3 Mon Sep 17 00:00:00 2001 From: Prasang Prajapati Date: Tue, 26 Aug 2025 12:39:28 -0400 Subject: [PATCH 06/20] fix providers issue --- src/api/providers/fetchers/modelCache.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/api/providers/fetchers/modelCache.ts b/src/api/providers/fetchers/modelCache.ts index f4c240a61c..4e296302d7 100644 --- a/src/api/providers/fetchers/modelCache.ts +++ b/src/api/providers/fetchers/modelCache.ts @@ -18,6 +18,7 @@ import { GetModelsOptions } from "../../../shared/api" import { getOllamaModels } from "./ollama" import { getLMStudioModels } from "./lmstudio" import { getIOIntelligenceModels } from "./io-intelligence" +import { getWatsonxModels } from "./watsonx" const memoryCache = new NodeCache({ stdTTL: 5 * 60, checkperiod: 5 * 60 }) async function writeModels(router: RouterName, data: ModelRecord) { @@ -81,6 +82,9 @@ export const getModels = async (options: GetModelsOptions): Promise case "io-intelligence": models = await getIOIntelligenceModels(options.apiKey) break + case "watsonx": + models = await getWatsonxModels(options.apiKey) + break default: { // Ensures router is exhaustively checked if RouterName is a strict union const exhaustiveCheck: never = provider From 13f49fecedb60aad71032c005fed4ecd7115b4e2 Mon Sep 17 00:00:00 2001 From: Prasang Prajapati Date: Tue, 26 Aug 2025 12:43:28 -0400 Subject: [PATCH 07/20] fix providers issue --- src/shared/api.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shared/api.ts b/src/shared/api.ts index 061a604faa..ff08984fae 100644 --- a/src/shared/api.ts +++ b/src/shared/api.ts @@ -152,4 +152,4 @@ export type GetModelsOptions = | { provider: "ollama"; baseUrl?: string } | { provider: "lmstudio"; baseUrl?: string } | { provider: "io-intelligence"; apiKey: string } - | { provider: "watsonx"; apiKey: string } + | { provider: "watsonx"; apiKey: string; baseUrl: string } From 55743512e0b4571b777d5e72e9370b65c2e350a8 Mon Sep 17 00:00:00 2001 From: Prasang Prajapati Date: Tue, 26 Aug 2025 15:16:15 -0400 Subject: [PATCH 08/20] add refresh models button to get the models dynamically --- packages/types/src/provider-settings.ts | 7 +- src/api/providers/fetchers/watsonx.ts | 8 +- src/core/webview/webviewMessageHandler.ts | 19 ++ .../src/components/settings/ApiOptions.tsx | 16 +- .../src/components/settings/ModelPicker.tsx | 1 + .../settings/providers/WatsonxAI.tsx | 205 +++++++++++++++--- .../components/ui/hooks/useSelectedModel.ts | 9 +- webview-ui/src/utils/validate.ts | 5 + 8 files changed, 222 insertions(+), 48 deletions(-) diff --git a/packages/types/src/provider-settings.ts b/packages/types/src/provider-settings.ts index 956ea3e6b2..4960f7ef03 100644 --- a/packages/types/src/provider-settings.ts +++ b/packages/types/src/provider-settings.ts @@ -25,7 +25,6 @@ import { vscodeLlmModels, xaiModels, internationalZAiModels, - watsonxAiModels, } from "./providers/index.js" /** @@ -543,11 +542,6 @@ export const MODELS_BY_PROVIDER: Record< label: "VS Code LM API", models: Object.keys(vscodeLlmModels), }, - watsonx: { - id: "watsonx", - label: "IBM watsonx", - models: Object.keys(watsonxAiModels), - }, xai: { id: "xai", label: "xAI (Grok)", models: Object.keys(xaiModels) }, zai: { id: "zai", label: "Zai", models: Object.keys(internationalZAiModels) }, @@ -558,6 +552,7 @@ export const MODELS_BY_PROVIDER: Record< openrouter: { id: "openrouter", label: "OpenRouter", models: [] }, requesty: { id: "requesty", label: "Requesty", models: [] }, unbound: { id: "unbound", label: "Unbound", models: [] }, + watsonx: { id: "watsonx", label: "IBM watsonx", models: [] }, } export const dynamicProviders = [ diff --git a/src/api/providers/fetchers/watsonx.ts b/src/api/providers/fetchers/watsonx.ts index dcf049eb29..98da1003a7 100644 --- a/src/api/providers/fetchers/watsonx.ts +++ b/src/api/providers/fetchers/watsonx.ts @@ -24,7 +24,6 @@ export async function getWatsonxModels( }), }) - await service.getAuthenticator().authenticate() let knownModels: Record = {} try { @@ -37,12 +36,7 @@ export async function getWatsonxModels( for (const model of modelsList) { const modelId = model.id || model.name || model.model_id const modelInfo = JSON.stringify(model).toLowerCase() - if ( - modelId && - !modelInfo.includes("embed") && - !modelInfo.includes("rtrvr") && - !modelInfo.includes("retriev") - ) { + if (modelId && !modelInfo.includes("embed") && !modelInfo.includes("rtrvr")) { const contextWindow = model.context_length || model.max_input_tokens || 8192 const maxTokens = model.max_output_tokens || Math.floor(contextWindow / 2) diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 23710cd805..89352df249 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -549,6 +549,7 @@ export const webviewMessageHandler = async ( litellm: {}, ollama: {}, lmstudio: {}, + watsonx: {}, } const safeGetModels = async (options: GetModelsOptions): Promise => { @@ -574,6 +575,14 @@ export const webviewMessageHandler = async ( }, }, { key: "glama", options: { provider: "glama" } }, + { + key: "watsonx", + options: { + provider: "watsonx", + apiKey: apiConfiguration.watsonxApiKey!, + baseUrl: apiConfiguration.watsonxBaseUrl!, + }, + }, { key: "unbound", options: { provider: "unbound", apiKey: apiConfiguration.unboundApiKey } }, ] @@ -598,6 +607,16 @@ export const webviewMessageHandler = async ( }) } + const watsonxApiKey = apiConfiguration.watsonxApiKey + const watsonxBaseUrl = apiConfiguration.watsonxBaseUrl + + if (watsonxApiKey && watsonxBaseUrl) { + modelFetchPromises.push({ + key: "watsonx", + options: { provider: "watsonx", apiKey: watsonxApiKey, baseUrl: watsonxBaseUrl }, + }) + } + const results = await Promise.allSettled( modelFetchPromises.map(async ({ key, options }) => { const models = await safeGetModels(options) diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index 158c6000c6..e0366db203 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -226,6 +226,8 @@ const ApiOptions = ({ vscode.postMessage({ type: "requestVsCodeLmModels" }) } else if (selectedProvider === "litellm") { vscode.postMessage({ type: "requestRouterModels" }) + } else if (selectedProvider === "watsonx") { + vscode.postMessage({ type: "requestWatsonxModels" }) } }, 250, @@ -238,6 +240,9 @@ const ApiOptions = ({ apiConfiguration?.lmStudioBaseUrl, apiConfiguration?.litellmBaseUrl, apiConfiguration?.litellmApiKey, + apiConfiguration.watsonxApiKey, + apiConfiguration.watsonxProjectId, + apiConfiguration.watsonxBaseUrl, customHeaders, ], ) @@ -340,7 +345,7 @@ const ApiOptions = ({ openai: { field: "openAiModelId" }, ollama: { field: "ollamaModelId" }, lmstudio: { field: "lmStudioModelId" }, - watsonx: { field: "apiModelId", default: watsonxAiDefaultModelId }, + watsonx: { field: "watsonxModelId", default: watsonxAiDefaultModelId }, } const config = PROVIDER_MODEL_CONFIG[value] @@ -594,7 +599,12 @@ const ApiOptions = ({ )} {selectedProvider === "watsonx" && ( - + )} {selectedProvider === "sambanova" && ( @@ -652,7 +662,7 @@ const ApiOptions = ({ )} - {selectedProviderModels.length > 0 && ( + {selectedProviderModels.length > 0 && selectedProvider !== "watsonx" && ( <>
diff --git a/webview-ui/src/components/settings/ModelPicker.tsx b/webview-ui/src/components/settings/ModelPicker.tsx index 0753f9fc2b..dc051d9f35 100644 --- a/webview-ui/src/components/settings/ModelPicker.tsx +++ b/webview-ui/src/components/settings/ModelPicker.tsx @@ -37,6 +37,7 @@ type ModelIdKey = keyof Pick< | "openAiModelId" | "litellmModelId" | "ioIntelligenceModelId" + | "watsonxModelId" > interface ModelPickerProps { diff --git a/webview-ui/src/components/settings/providers/WatsonxAI.tsx b/webview-ui/src/components/settings/providers/WatsonxAI.tsx index 5c44223c77..2258e698cc 100644 --- a/webview-ui/src/components/settings/providers/WatsonxAI.tsx +++ b/webview-ui/src/components/settings/providers/WatsonxAI.tsx @@ -1,21 +1,112 @@ -import { useCallback } from "react" +import { useCallback, useState, useEffect, useRef } from "react" import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react" -import type { ProviderSettings } from "@roo-code/types" -import { watsonxAiDefaultModelId, watsonxAiModels } from "@roo-code/types" +import { watsonxAiDefaultModelId, type ProviderSettings } from "@roo-code/types" import { useAppTranslation } from "@src/i18n/TranslationContext" import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink" +import { vscode } from "@src/utils/vscode" +import { Button, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@src/components/ui" +import { ExtensionMessage } from "@roo/ExtensionMessage" import { inputEventTransform } from "../transforms" +import { OrganizationAllowList } from "@roo/cloud" +import { useExtensionState } from "@src/context/ExtensionStateContext" +import { RouterName } from "@roo/api" +import { ModelPicker } from "../ModelPicker" + +// Define the available regions +const WATSONX_REGIONS = { + "us-south": "Dallas (us-south.ml.cloud.ibm.com)", + "eu-de": "Frankfurt (eu-de.ml.cloud.ibm.com)", + "eu-gb": "London (eu-gb.ml.cloud.ibm.com)", + "jp-tok": "Tokyo (jp-tok.ml.cloud.ibm.com)", + "au-syd": "Sydney (au-syd.ml.cloud.ibm.com)", + "ca-tor": "Toronto (ca-tor.ml.cloud.ibm.com)", + "ap-south-1": "Mumbai (ap-south-1.aws.wxai.ibm.com)", +} + +// Map region codes to full URLs +const REGION_TO_URL = { + "us-south": "https://us-south.ml.cloud.ibm.com", + "eu-de": "https://eu-de.ml.cloud.ibm.com", + "eu-gb": "https://eu-gb.ml.cloud.ibm.com", + "jp-tok": "https://jp-tok.ml.cloud.ibm.com", + "au-syd": "https://au-syd.ml.cloud.ibm.com", + "ca-tor": "https://ca-tor.ml.cloud.ibm.com", + "ap-south-1": "https://ap-south-1.aws.wxai.ibm.com", + custom: "", // For custom URL input +} type WatsonxAIProps = { apiConfiguration: ProviderSettings - setApiConfigurationField: (field: K, value: ProviderSettings[K]) => void + setApiConfigurationField: (field: keyof ProviderSettings, value: ProviderSettings[keyof ProviderSettings]) => void + organizationAllowList: OrganizationAllowList + modelValidationError?: string } -export const WatsonxAI = ({ apiConfiguration, setApiConfigurationField }: WatsonxAIProps) => { +export const WatsonxAI = ({ + apiConfiguration, + setApiConfigurationField, + organizationAllowList, + modelValidationError, +}: WatsonxAIProps) => { const { t } = useAppTranslation() + const { routerModels } = useExtensionState() + const [refreshStatus, setRefreshStatus] = useState<"idle" | "loading" | "success" | "error">("idle") + const [refreshError, setRefreshError] = useState() + const watsonxErrorJustReceived = useRef(false) + + // Determine the current region based on the base URL + const getCurrentRegion = () => { + const baseUrl = apiConfiguration?.watsonxBaseUrl || "" + + // Find the region that matches the current base URL + const regionEntry = Object.entries(REGION_TO_URL).find(([_, url]) => url === baseUrl) + + // Return the region code or 'us-south' as default if not found + return regionEntry ? regionEntry[0] : "us-south" + } + + const [selectedRegion, setSelectedRegion] = useState(getCurrentRegion()) + + // Handle region selection + const handleRegionSelect = useCallback( + (region: string) => { + setSelectedRegion(region) + + // Update the base URL in the API configuration + const baseUrl = REGION_TO_URL[region as keyof typeof REGION_TO_URL] || "" + setApiConfigurationField("watsonxBaseUrl", baseUrl) + }, + [setApiConfigurationField], + ) + + useEffect(() => { + const handleMessage = (event: MessageEvent) => { + const message = event.data + if (message.type === "singleRouterModelFetchResponse" && !message.success) { + const providerName = message.values?.provider as RouterName + if (providerName === "watsonx") { + watsonxErrorJustReceived.current = true + setRefreshStatus("error") + setRefreshError(message.error) + } + } else if (message.type === "routerModels") { + // When router models are updated, update the refresh status + if (refreshStatus === "loading") { + if (!watsonxErrorJustReceived.current) { + setRefreshStatus("success") + } + } + } + } + + window.addEventListener("message", handleMessage) + return () => { + window.removeEventListener("message", handleMessage) + } + }, [refreshStatus, refreshError, t]) const handleInputChange = useCallback( (field: keyof ProviderSettings, transform: (event: E) => any = inputEventTransform) => @@ -25,12 +116,29 @@ export const WatsonxAI = ({ apiConfiguration, setApiConfigurationField }: Watson [setApiConfigurationField], ) - const defaultModel = watsonxAiDefaultModelId - const modelInfo = watsonxAiModels[defaultModel] || {} - const defaultModelDescription = - typeof modelInfo === "object" && "contextWindow" in modelInfo - ? `Context window: ${modelInfo.contextWindow} tokens` - : "IBM watsonx model" + const handleRefreshModels = useCallback(() => { + setRefreshStatus("loading") + setRefreshError(undefined) + + const apiKey = apiConfiguration.watsonxApiKey + const projectId = apiConfiguration.watsonxProjectId + const baseUrl = REGION_TO_URL[selectedRegion as keyof typeof REGION_TO_URL] + + if (!apiKey) { + setRefreshStatus("error") + setRefreshError(t("settings:providers.refreshModels.missingConfig")) + return + } + + vscode.postMessage({ + type: "requestRouterModels", + values: { + watsonxApiKey: apiKey, + watsonxProjectId: projectId, + watsonxBaseUrl: baseUrl, + }, + }) + }, [apiConfiguration, setRefreshStatus, setRefreshError, t, selectedRegion]) return ( <> @@ -62,25 +170,66 @@ export const WatsonxAI = ({ apiConfiguration, setApiConfigurationField }: Watson Project ID is required for IBM watsonx integration
- - - -
- Default: https://us-south.ml.cloud.ibm.com -

Default Model Information

-
-
- Model ID: {defaultModel} -
-
- Description: {defaultModelDescription} -
+
+ + +
+ Selected endpoint: {REGION_TO_URL[selectedRegion as keyof typeof REGION_TO_URL]}
+ + + {refreshStatus === "loading" && ( +
+ {t("settings:providers.refreshModels.loading") || "Loading models..."} +
+ )} + {refreshStatus === "success" && ( +
+ {t("settings:providers.refreshModels.success") || "Models refreshed successfully"} +
+ )} + {refreshStatus === "error" && ( +
+ {refreshError || t("settings:providers.refreshModels.error") || "Failed to refresh models"} +
+ )} + + ) } diff --git a/webview-ui/src/components/ui/hooks/useSelectedModel.ts b/webview-ui/src/components/ui/hooks/useSelectedModel.ts index 87548008d5..5588a1c1c8 100644 --- a/webview-ui/src/components/ui/hooks/useSelectedModel.ts +++ b/webview-ui/src/components/ui/hooks/useSelectedModel.ts @@ -36,8 +36,6 @@ import { litellmDefaultModelId, claudeCodeDefaultModelId, claudeCodeModels, - watsonxAiModels, - watsonxAiDefaultModelId, sambaNovaModels, sambaNovaDefaultModelId, doubaoModels, @@ -57,6 +55,8 @@ import { qwenCodeDefaultModelId, qwenCodeModels, BEDROCK_CLAUDE_SONNET_4_MODEL_ID, + watsonxAiDefaultModelId, + watsonxAiModels, } from "@roo-code/types" import type { ModelRecord, RouterModels } from "@roo/api" @@ -71,6 +71,7 @@ export const useSelectedModel = (apiConfiguration?: ProviderSettings) => { const lmStudioModelId = provider === "lmstudio" ? apiConfiguration?.lmStudioModelId : undefined const routerModels = useRouterModels() + const openRouterModelProviders = useOpenRouterModelProviders(openRouterModelId) const lmStudioModels = useLmStudioModels(lmStudioModelId) @@ -284,11 +285,11 @@ function getSelectedModel({ return { id, info: { ...openAiModelInfoSaneDefaults, ...info } } } case "watsonx": { - const id = apiConfiguration.apiModelId ?? watsonxAiDefaultModelId + const id = apiConfiguration.watsonxModelId ?? watsonxAiDefaultModelId const info = watsonxAiModels[id as keyof typeof watsonxAiModels] return { id, - info: info || undefined, + info: info, } } case "cerebras": { diff --git a/webview-ui/src/utils/validate.ts b/webview-ui/src/utils/validate.ts index fedd3c9df2..6a3ca214d6 100644 --- a/webview-ui/src/utils/validate.ts +++ b/webview-ui/src/utils/validate.ts @@ -212,6 +212,8 @@ function getModelIdForProvider(apiConfiguration: ProviderSettings, provider: str return apiConfiguration.huggingFaceModelId case "io-intelligence": return apiConfiguration.ioIntelligenceModelId + case "watsonx": + return apiConfiguration.watsonxModelId default: return apiConfiguration.apiModelId } @@ -282,6 +284,9 @@ export function validateModelId(apiConfiguration: ProviderSettings, routerModels case "litellm": modelId = apiConfiguration.litellmModelId break + case "watsonx": + modelId = apiConfiguration.watsonxModelId + break case "io-intelligence": modelId = apiConfiguration.ioIntelligenceModelId break From 4d9fbb014b51a60366411f478a7a97dfb3bde87a Mon Sep 17 00:00:00 2001 From: Prasang Prajapati Date: Tue, 26 Aug 2025 15:27:41 -0400 Subject: [PATCH 09/20] allow any type to handle watsonx --- src/core/webview/ClineProvider.ts | 2 +- src/extension.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index b6c00ee379..c69e00b72e 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -2213,7 +2213,7 @@ export class ClineProvider await ExtensionBridgeService.handleRemoteControlState( userInfo, enabled, - { ...bridgeConfig, provider: this, sessionId: vscode.env.sessionId }, + { ...bridgeConfig, provider: this as any, sessionId: vscode.env.sessionId }, (message: string) => this.log(message), ) diff --git a/src/extension.ts b/src/extension.ts index 6cb6ea4b07..4c7ff64b60 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -144,7 +144,7 @@ export async function activate(context: vscode.ExtensionContext) { ExtensionBridgeService.handleRemoteControlState( userInfo, contextProxy.getValue("remoteControlEnabled"), - { ...bridgeConfig, provider, sessionId: vscode.env.sessionId }, + { ...bridgeConfig, provider: provider as any, sessionId: vscode.env.sessionId }, (message: string) => outputChannel.appendLine(message), ) }) From 37c55843c4d52acfaee33454c7d9250203551e6b Mon Sep 17 00:00:00 2001 From: Prasang Prajapati Date: Wed, 27 Aug 2025 12:43:50 -0400 Subject: [PATCH 10/20] add support for IBM Cloud Pak for Data --- packages/types/src/provider-settings.ts | 5 + src/api/providers/watsonx.ts | 60 +++- .../settings/providers/WatsonxAI.tsx | 271 ++++++++++++++---- webview-ui/src/i18n/locales/en/settings.json | 10 +- webview-ui/src/utils/validate.ts | 26 +- 5 files changed, 304 insertions(+), 68 deletions(-) diff --git a/packages/types/src/provider-settings.ts b/packages/types/src/provider-settings.ts index 4960f7ef03..7b997ae19a 100644 --- a/packages/types/src/provider-settings.ts +++ b/packages/types/src/provider-settings.ts @@ -289,10 +289,15 @@ const litellmSchema = baseProviderSettingsSchema.extend({ }) const watsonxSchema = baseProviderSettingsSchema.extend({ + watsonxPlatform: z.string().optional(), watsonxBaseUrl: z.string().optional(), watsonxApiKey: z.string().optional(), watsonxProjectId: z.string().optional(), watsonxModelId: z.string().optional(), + watsonxUsername: z.string().optional(), + watsonxAuthType: z.string().optional(), + watsonxPassword: z.string().optional(), + watsonxRegion: z.string().optional(), }) const cerebrasSchema = apiModelIdProviderModelSchema.extend({ diff --git a/src/api/providers/watsonx.ts b/src/api/providers/watsonx.ts index 94691da4b2..4b1ec48d06 100644 --- a/src/api/providers/watsonx.ts +++ b/src/api/providers/watsonx.ts @@ -2,7 +2,7 @@ import * as vscode from "vscode" import { Anthropic } from "@anthropic-ai/sdk" import { ModelInfo, watsonxAiDefaultModelId, watsonxAiModels, WatsonxAIModelId } from "@roo-code/types" import type { ApiHandlerOptions } from "../../shared/api" -import { IamAuthenticator } from "ibm-cloud-sdk-core" +import { IamAuthenticator, CloudPakForDataAuthenticator } from "ibm-cloud-sdk-core" import { ApiStream } from "../transform/stream" import { BaseProvider } from "./base-provider" import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index" @@ -17,26 +17,66 @@ export class WatsonxAIHandler extends BaseProvider implements SingleCompletionHa constructor(options: ApiHandlerOptions) { super() this.options = options + this.projectId = (this.options as any).watsonxProjectId if (!this.projectId) { throw new Error("You must provide a valid IBM watsonx project ID.") } - const apiKey = (this.options as any).watsonxApiKey - if (!apiKey) { - throw new Error("You must provide a valid IBM watsonx API key.") - } - const serviceUrl = (this.options as any).watsonxBaseUrl || "https://us-south.ml.cloud.ibm.com" + + const serviceUrl = (this.options as any).watsonxBaseUrl + const platform = (this.options as any).watsonxPlatform try { const serviceOptions: any = { version: "2024-05-31", serviceUrl: serviceUrl, - authenticator: new IamAuthenticator({ - apikey: apiKey, - }), } - this.service = WatsonXAI.newInstance(serviceOptions) + // Choose authenticator based on platform + if (platform === "cloudPak") { + const username = this.options.watsonxUsername + if (!username) { + throw new Error("You must provide a valid username for IBM Cloud Pak for Data.") + } + + const authType = this.options.watsonxAuthType + + if (authType === "apiKey") { + const apiKey = this.options.watsonxApiKey + if (!apiKey) { + throw new Error("You must provide a valid API key for IBM Cloud Pak for Data.") + } + + serviceOptions.authenticator = new CloudPakForDataAuthenticator({ + username: username, + apikey: apiKey, + url: serviceUrl, + }) + } else { + const password = this.options.watsonxPassword + if (!password) { + throw new Error("You must provide a valid password for IBM Cloud Pak for Data.") + } + + serviceOptions.authenticator = new CloudPakForDataAuthenticator({ + username: username, + password: password, + url: serviceUrl, + }) + } + } else { + // Default to IBM Cloud with IAM authentication + const apiKey = this.options.watsonxApiKey + if (!apiKey) { + throw new Error("You must provide a valid IBM watsonx API key.") + } + + serviceOptions.authenticator = new IamAuthenticator({ + apikey: apiKey, + }) + } + + this.service = WatsonXAI.newInstance(serviceOptions) this.service.getAuthenticator().authenticate() } catch (error) { throw new Error( diff --git a/webview-ui/src/components/settings/providers/WatsonxAI.tsx b/webview-ui/src/components/settings/providers/WatsonxAI.tsx index 2258e698cc..4ed447454b 100644 --- a/webview-ui/src/components/settings/providers/WatsonxAI.tsx +++ b/webview-ui/src/components/settings/providers/WatsonxAI.tsx @@ -15,7 +15,6 @@ import { useExtensionState } from "@src/context/ExtensionStateContext" import { RouterName } from "@roo/api" import { ModelPicker } from "../ModelPicker" -// Define the available regions const WATSONX_REGIONS = { "us-south": "Dallas (us-south.ml.cloud.ibm.com)", "eu-de": "Frankfurt (eu-de.ml.cloud.ibm.com)", @@ -26,7 +25,6 @@ const WATSONX_REGIONS = { "ap-south-1": "Mumbai (ap-south-1.aws.wxai.ibm.com)", } -// Map region codes to full URLs const REGION_TO_URL = { "us-south": "https://us-south.ml.cloud.ibm.com", "eu-de": "https://eu-de.ml.cloud.ibm.com", @@ -35,7 +33,7 @@ const REGION_TO_URL = { "au-syd": "https://au-syd.ml.cloud.ibm.com", "ca-tor": "https://ca-tor.ml.cloud.ibm.com", "ap-south-1": "https://ap-south-1.aws.wxai.ibm.com", - custom: "", // For custom URL input + custom: "", } type WatsonxAIProps = { @@ -57,27 +55,60 @@ export const WatsonxAI = ({ const [refreshError, setRefreshError] = useState() const watsonxErrorJustReceived = useRef(false) - // Determine the current region based on the base URL + useEffect(() => { + if (!apiConfiguration.watsonxPlatform) { + setApiConfigurationField("watsonxPlatform", "ibmCloud") + } + }, [apiConfiguration.watsonxPlatform, setApiConfigurationField]) + const getCurrentRegion = () => { const baseUrl = apiConfiguration?.watsonxBaseUrl || "" - - // Find the region that matches the current base URL const regionEntry = Object.entries(REGION_TO_URL).find(([_, url]) => url === baseUrl) - - // Return the region code or 'us-south' as default if not found return regionEntry ? regionEntry[0] : "us-south" } const [selectedRegion, setSelectedRegion] = useState(getCurrentRegion()) - // Handle region selection const handleRegionSelect = useCallback( (region: string) => { setSelectedRegion(region) - - // Update the base URL in the API configuration const baseUrl = REGION_TO_URL[region as keyof typeof REGION_TO_URL] || "" setApiConfigurationField("watsonxBaseUrl", baseUrl) + setApiConfigurationField("watsonxRegion", region) + }, + [setApiConfigurationField], + ) + + const handlePlatformChange = useCallback( + (newPlatform: "ibmCloud" | "cloudPak") => { + setApiConfigurationField("watsonxPlatform", newPlatform) + + if (newPlatform === "ibmCloud") { + const defaultRegion = "us-south" + setSelectedRegion(defaultRegion) + setApiConfigurationField("watsonxRegion", defaultRegion) + setApiConfigurationField("watsonxBaseUrl", REGION_TO_URL[defaultRegion]) + setApiConfigurationField("watsonxUsername", "") + setApiConfigurationField("watsonxPassword", "") + setApiConfigurationField("watsonxAuthType", "apiKey") + } else { + setSelectedRegion("custom") + setApiConfigurationField("watsonxBaseUrl", "") + setApiConfigurationField("watsonxAuthType", "apiKey") + setApiConfigurationField("watsonxRegion", "") + } + }, + [setApiConfigurationField], + ) + + const handleAuthTypeChange = useCallback( + (newAuthType: "apiKey" | "password") => { + setApiConfigurationField("watsonxAuthType", newAuthType) + if (newAuthType === "apiKey") { + setApiConfigurationField("watsonxPassword", "") + } else { + setApiConfigurationField("watsonxApiKey", "") + } }, [setApiConfigurationField], ) @@ -93,7 +124,6 @@ export const WatsonxAI = ({ setRefreshError(message.error) } } else if (message.type === "routerModels") { - // When router models are updated, update the refresh status if (refreshStatus === "loading") { if (!watsonxErrorJustReceived.current) { setRefreshStatus("success") @@ -121,44 +151,190 @@ export const WatsonxAI = ({ setRefreshError(undefined) const apiKey = apiConfiguration.watsonxApiKey - const projectId = apiConfiguration.watsonxProjectId - const baseUrl = REGION_TO_URL[selectedRegion as keyof typeof REGION_TO_URL] + const platform = apiConfiguration.watsonxPlatform + const customUrl = apiConfiguration.watsonxBaseUrl || "" + const username = apiConfiguration.watsonxUsername + const authType = apiConfiguration.watsonxAuthType + const password = apiConfiguration.watsonxPassword - if (!apiKey) { + let baseUrl = "" + if (platform === "ibmCloud") { + baseUrl = REGION_TO_URL[selectedRegion as keyof typeof REGION_TO_URL] + } else { + baseUrl = customUrl + setApiConfigurationField("watsonxBaseUrl", baseUrl) + } + + if (platform === "ibmCloud" && (!apiKey || !baseUrl)) { setRefreshStatus("error") setRefreshError(t("settings:providers.refreshModels.missingConfig")) return } + if (platform === "cloudPak") { + if (!baseUrl) { + setRefreshStatus("error") + setRefreshError("URL is required for IBM Cloud Pak for Data") + return + } + + if (!username) { + setRefreshStatus("error") + setRefreshError("Username is required for IBM Cloud Pak for Data") + return + } + + if (authType === "apiKey" && !apiKey) { + setRefreshStatus("error") + setRefreshError("API Key is required for IBM Cloud Pak for Data") + return + } + + if (authType === "password" && !password) { + setRefreshStatus("error") + setRefreshError("Password is required for IBM Cloud Pak for Data") + return + } + } + vscode.postMessage({ type: "requestRouterModels", values: { - watsonxApiKey: apiKey, - watsonxProjectId: projectId, - watsonxBaseUrl: baseUrl, + watsonxPlatform: apiConfiguration.watsonxPlatform, + watsonxBaseUrl: apiConfiguration.watsonxBaseUrl, + watsonxApiKey: apiConfiguration.watsonxApiKey, + watsonxProjectId: apiConfiguration.watsonxProjectId, + watsonxModelId: apiConfiguration.watsonxModelId, + watsonxUsername: apiConfiguration.watsonxUsername, + watsonxAuthType: apiConfiguration.watsonxAuthType, + watsonxPassword: apiConfiguration.watsonxPassword, + watsonxRegion: apiConfiguration.watsonxRegion, }, }) - }, [apiConfiguration, setRefreshStatus, setRefreshError, t, selectedRegion]) + }, [apiConfiguration, setRefreshStatus, setRefreshError, t, selectedRegion, setApiConfigurationField]) return ( <> - - - -
- {t("settings:providers.apiKeyStorageNotice")} + {/* Platform Selection */} +
+ +
- {!apiConfiguration?.watsonxApiKey && ( - - Get WatsonX API Key - + + {/* IBM Cloud specific fields */} + {apiConfiguration.watsonxPlatform === "ibmCloud" && ( + <> + + + +
+ {t("settings:providers.apiKeyStorageNotice")} +
+ {!apiConfiguration?.watsonxApiKey && ( + + Get WatsonX API Key + + )} + +
+ + +
+ Selected endpoint: {REGION_TO_URL[selectedRegion as keyof typeof REGION_TO_URL]} +
+
+ )} + {/* IBM Cloud Pak for Data specific fields */} + {apiConfiguration.watsonxPlatform === "cloudPak" && ( + <> + + + +
+ Enter the full URL of your IBM Cloud Pak for Data instance +
+ + + + + +
+ + +
+ + {apiConfiguration.watsonxAuthType === "apiKey" ? ( + + + + ) : ( + + + + )} +
+ {t("settings:providers.apiKeyStorageNotice")} +
+ + )} + + {/* Common fields for both platforms */} -
- - -
- Selected endpoint: {REGION_TO_URL[selectedRegion as keyof typeof REGION_TO_URL]} -
-
-
- {refreshStatus === "loading" && (
- {t("settings:providers.refreshModels.loading") || "Loading models..."} + {t("settings:providers.refreshModels.loading")}
)} {refreshStatus === "success" && ( -
- {t("settings:providers.refreshModels.success") || "Models refreshed successfully"} -
+
{"Models retrieved successfully"}
)} {refreshStatus === "error" && ( -
- {refreshError || t("settings:providers.refreshModels.error") || "Failed to refresh models"} -
+
{refreshError || "Failed to retrieve models"}
)} 0 ? watsonxModels : {}} modelIdKey="watsonxModelId" - serviceName="IBM watsonx" - serviceUrl="https://cloud.ibm.com/apidocs/watsonx-ai#list-foundation-model-specs" + serviceName="" + serviceUrl="" setApiConfigurationField={setApiConfigurationField} organizationAllowList={organizationAllowList} errorMessage={modelValidationError} /> + +
+ + ), + }} + values={{ serviceName: "IBM watsonx" }} + /> +
) } diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index bc2915769a..4f50b84535 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -239,6 +239,7 @@ "openRouterApiKey": "OpenRouter API Key", "getOpenRouterApiKey": "Get OpenRouter API Key", "apiKeyStorageNotice": "API keys are stored securely in VSCode's Secret Storage", + "passwordStorageNotice": "Passwords are stored securely in VSCode's Secret Storage", "glamaApiKey": "Glama API Key", "getGlamaApiKey": "Get Glama API Key", "useCustomBaseUrl": "Use custom base URL", @@ -468,6 +469,9 @@ "placeholder": "Default: claude", "maxTokensLabel": "Max Output Tokens", "maxTokensDescription": "Maximum number of output tokens for Claude Code responses. Default is 8000." + }, + "watsonx": { + "description": "The extension automatically fetches the latest list of models available on {{serviceName}}." } }, "browser": { From ea8e61c9e25a99385eee6b80f68b4f9c23ab67b6 Mon Sep 17 00:00:00 2001 From: Prasang Prajapati Date: Tue, 9 Sep 2025 16:06:26 -0400 Subject: [PATCH 15/20] pass empty model id for default --- src/api/providers/watsonx.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/api/providers/watsonx.ts b/src/api/providers/watsonx.ts index 4b1ec48d06..425a366817 100644 --- a/src/api/providers/watsonx.ts +++ b/src/api/providers/watsonx.ts @@ -197,9 +197,7 @@ export class WatsonxAIHandler extends BaseProvider implements SingleCompletionHa override getModel(): { id: string; info: ModelInfo } { return { id: (this.options as any).watsonxModelId || watsonxAiDefaultModelId, - info: - watsonxAiModels[(this.options as any).watsonxModelId as WatsonxAIModelId] || - watsonxAiModels[watsonxAiDefaultModelId], + info: watsonxAiModels[(this.options as any).watsonxModelId as WatsonxAIModelId] || "", } } } From 1341c439bf88691e552a8a9a0909a312b305e7fe Mon Sep 17 00:00:00 2001 From: Prasang Prajapati Date: Tue, 9 Sep 2025 17:55:11 -0400 Subject: [PATCH 16/20] add timestamp script to generate vsix with timestamp added --- package.json | 2 ++ scripts/update-version-timestamp.js | 40 +++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100755 scripts/update-version-timestamp.js diff --git a/package.json b/package.json index cdc8a1abca..04d0ee2fd1 100644 --- a/package.json +++ b/package.json @@ -18,8 +18,10 @@ "bundle:nightly": "turbo bundle:nightly --log-order grouped --output-logs new-only", "vsix": "turbo vsix --log-order grouped --output-logs new-only", "vsix:nightly": "turbo vsix:nightly --log-order grouped --output-logs new-only", + "vsix:timestamp": "node scripts/update-version-timestamp.js && turbo vsix --log-order grouped --output-logs new-only", "clean": "turbo clean --log-order grouped --output-logs new-only && rimraf dist out bin .vite-port .turbo", "install:vsix": "pnpm install --frozen-lockfile && pnpm clean && pnpm vsix && node scripts/install-vsix.js", + "install:vsix:timestamp": "pnpm install --frozen-lockfile && pnpm clean && pnpm vsix:timestamp && node scripts/install-vsix.js", "changeset:version": "cp CHANGELOG.md src/CHANGELOG.md && changeset version && cp -vf src/CHANGELOG.md .", "knip": "knip --include files", "update-contributors": "node scripts/update-contributors.js", diff --git a/scripts/update-version-timestamp.js b/scripts/update-version-timestamp.js new file mode 100755 index 0000000000..d5ef9ee2f0 --- /dev/null +++ b/scripts/update-version-timestamp.js @@ -0,0 +1,40 @@ +#!/usr/bin/env node + +const fs = require("fs") +const path = require("path") + +/** + * Updates the version in package.json with a timestamp suffix + */ +function updateVersionWithTimestamp() { + const packageJsonPath = path.join(__dirname, "../src/package.json") + + try { + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")) + const currentVersion = packageJson.version + const versionMatch = currentVersion.match(/^(\d+\.\d+\.\d+)/) + const baseVersion = versionMatch ? versionMatch[1] : currentVersion + const now = new Date() + const month = String(now.getMonth() + 1).padStart(2, "0") + const day = String(now.getDate()).padStart(2, "0") + const year = now.getFullYear() + const hours = String(now.getHours()).padStart(2, "0") + const minutes = String(now.getMinutes()).padStart(2, "0") + const seconds = String(now.getSeconds()).padStart(2, "0") + const timestamp = `${month}${day}${year}-${hours}${minutes}${seconds}` + const newVersion = `${baseVersion}-${timestamp}` + packageJson.version = newVersion + fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, "\t")) + console.log(`Version updated from ${currentVersion} to ${newVersion}`) + return newVersion + } catch (error) { + console.error("Error updating version with timestamp:", error) + process.exit(1) + } +} + +if (require.main === module) { + updateVersionWithTimestamp() +} + +module.exports = { updateVersionWithTimestamp } From 99bdf522630091a5957ed1d29654c6841f2d4038 Mon Sep 17 00:00:00 2001 From: Prasang Prajapati Date: Thu, 11 Sep 2025 18:19:27 -0400 Subject: [PATCH 17/20] fix margins and text --- packages/types/src/providers/watsonx.ts | 86 ++-------------- src/api/providers/fetchers/watsonx.ts | 53 +++++----- src/api/providers/watsonx.ts | 9 +- .../settings/providers/WatsonxAI.tsx | 98 ++++++++++--------- webview-ui/src/i18n/locales/en/settings.json | 4 +- 5 files changed, 96 insertions(+), 154 deletions(-) diff --git a/packages/types/src/providers/watsonx.ts b/packages/types/src/providers/watsonx.ts index f4998b87a0..5846125c65 100644 --- a/packages/types/src/providers/watsonx.ts +++ b/packages/types/src/providers/watsonx.ts @@ -5,93 +5,21 @@ export const watsonxAiDefaultModelId = "" // Common model properties export const baseModelInfo: ModelInfo = { - maxTokens: 4096, - contextWindow: 128000, + maxTokens: 131072, + contextWindow: 131072, supportsImages: false, - supportsPromptCache: true, - supportsReasoningEffort: false, + supportsPromptCache: false, + supportsReasoningEffort: true, supportsReasoningBudget: false, requiredReasoningBudget: false, - inputPrice: 0, - outputPrice: 0, + inputPrice: 5.22, + outputPrice: 5.22, } export const watsonxAiModels = { // IBM Granite model "ibm/granite-3-3-8b-instruct": { ...baseModelInfo, - description: "Granite 3.3 8b Instruct - IBM-trained, dense decoder-only model", - }, - "ibm/granite-3-2-8b-instruct": { - ...baseModelInfo, - description: "Granite 3.2 8b Instruct - Text-only model capable of reasoning", - }, - "ibm/granite-3-2b-instruct": { - ...baseModelInfo, - description: "Granite 3 2b Instruct - IBM-trained, dense decoder-only model", - }, - "ibm/granite-3-8b-instruct": { - ...baseModelInfo, - description: "Granite 3 8b Instruct - IBM-trained, dense decoder-only model", - }, - "ibm/granite-guardian-3-2b": { - ...baseModelInfo, - description: "Granite Guardian 3 2b - IBM-trained, dense decoder-only model", - }, - "ibm/granite-guardian-3-8b": { - ...baseModelInfo, - description: "Granite Guardian 3 8b - IBM-trained, dense decoder-only model", - }, - "ibm/granite-vision-3-2-2b": { - ...baseModelInfo, - supportsImages: true, - description: "Granite 3 Vision - Image-text, text-out model capable of understanding images", - }, - // Meta Llama models - "meta-llama/llama-3-2-11b-vision-instruct": { - ...baseModelInfo, - supportsImages: true, - description: "Llama 3 2 11b Vision Instruct - Auto-regressive language model with transformer architecture", - }, - "meta-llama/llama-3-2-1b-instruct": { - ...baseModelInfo, - description: "Llama 3 2 1b Instruct - Auto-regressive language model with transformer architecture", - }, - "meta-llama/llama-3-2-3b-instruct": { - ...baseModelInfo, - description: "Llama 3 2 3b Instruct - Auto-regressive language model with transformer architecture", - }, - "meta-llama/llama-3-2-90b-vision-instruct": { - ...baseModelInfo, - supportsImages: true, - description: "Llama 3 2 90b Vision Instruct - Auto-regressive language model with transformer architecture", - }, - "meta-llama/llama-3-3-70b-instruct": { - ...baseModelInfo, - description: "Llama 3 3 70b Instruct - FP8 quantized version of the original FP16 weights", - }, - "meta-llama/llama-3-405b-instruct": { - ...baseModelInfo, - contextWindow: 128000, - description: "Llama 3 405b Instruct - Meta's largest open-source foundation model with 405 billion parameters", - }, - "meta-llama/llama-4-maverick-17b-1-0": { - ...baseModelInfo, - contextWindow: 128000, - description: "Llama 4 Maverick - 17 billion active parameter model with 128 experts", - }, - "meta-llama/llama-guard-3-11b-vision": { - ...baseModelInfo, - supportsImages: true, - description: "Llama Guard 3 11b Vision - Auto-regressive language model with transformer architecture", - }, - // Mistral AI models - "mistralai/mistral-medium-2505": { - ...baseModelInfo, - description: "Mistral Medium - Latest iteration of the Mistral Medium model family", - }, - "mistralai/mistral-small-3-1-24b-instruct-2503": { - ...baseModelInfo, - description: "Mistral Small 3.1 24B Base 2503 - Instruction-finetuned version of Mistral Small", + description: "", }, } as const satisfies Record diff --git a/src/api/providers/fetchers/watsonx.ts b/src/api/providers/fetchers/watsonx.ts index 3087dd1bca..e9255dbdbf 100644 --- a/src/api/providers/fetchers/watsonx.ts +++ b/src/api/providers/fetchers/watsonx.ts @@ -63,35 +63,42 @@ export async function getWatsonxModels( let knownModels: Record = {} try { - const response = await service.listFoundationModelSpecs() + let response + if (embedded) { + response = await service.listFoundationModelSpecs({ filters: "function_embedding" }) + } else { + response = await service.listFoundationModelSpecs({ filters: "!function_embedding" }) + } if (response && response.result) { const result = response.result as any - const modelsList = result.models || result.resources || result.foundation_models || [] - if (Array.isArray(modelsList)) { + const modelsList = result.resources + if (Array.isArray(modelsList) && modelsList.length > 0) { for (const model of modelsList) { const modelId = model.id || model.name || model.model_id - const modelInfo = JSON.stringify(model).toLowerCase() - if (modelId && !embedded && !modelInfo.includes("embed") && !modelInfo.includes("rtrvr")) { - const contextWindow = model.context_length || model.max_input_tokens || 8192 - const maxTokens = model.max_output_tokens || Math.floor(contextWindow / 2) + const contextWindow = model.context_length || model.max_input_tokens || 8192 + const maxTokens = model.max_output_tokens || Math.floor(contextWindow / 2) - knownModels[modelId] = { - contextWindow, - maxTokens, - supportsPromptCache: false, - } - } else { - if (modelId && embedded && modelInfo.includes("embed") && modelInfo.includes("rtrvr")) { - const contextWindow = model.context_length || model.max_input_tokens || 8192 - const maxTokens = model.max_output_tokens || Math.floor(contextWindow / 2) - - knownModels[modelId] = { - contextWindow, - maxTokens, - supportsPromptCache: false, - } - } + let description = "" + if (model.long_description) { + description = model.long_description + } else if (model.short_description) { + description = model.short_description + } + const supportsImages = model.modality === "multimodal" || model.modality === "vision" || false + const inputPrice = model.pricing?.input_price || 0 + const outputPrice = model.pricing?.output_price || 0 + knownModels[modelId] = { + contextWindow, + maxTokens, + supportsPromptCache: false, + supportsImages, + supportsReasoningEffort: false, + supportsReasoningBudget: false, + requiredReasoningBudget: false, + inputPrice, + outputPrice, + description, } } } diff --git a/src/api/providers/watsonx.ts b/src/api/providers/watsonx.ts index 425a366817..5ddb197828 100644 --- a/src/api/providers/watsonx.ts +++ b/src/api/providers/watsonx.ts @@ -8,6 +8,7 @@ import { BaseProvider } from "./base-provider" import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index" import { WatsonXAI } from "@ibm-cloud/watsonx-ai" import { convertToWatsonxAiMessages } from "../transform/watsonxai-format" +import { calculateApiCostOpenAI } from "../../shared/cost" export class WatsonxAIHandler extends BaseProvider implements SingleCompletionHandler { private options: ApiHandlerOptions @@ -145,11 +146,15 @@ export class WatsonxAIHandler extends BaseProvider implements SingleCompletionHa usageInfo = response.result.usage || {} const outputTokens = usageInfo.completion_tokens + const inputTokens = usageInfo?.prompt_tokens || 0 + const modelInfo = this.getModel().info + const totalCost = calculateApiCostOpenAI(modelInfo, inputTokens, outputTokens) + yield { type: "usage", - inputTokens: usageInfo?.prompt_tokens, + inputTokens: inputTokens, outputTokens, - totalCost: 0, // Actual cost calculation could be added if available + totalCost: totalCost, } } catch (error) { await vscode.window.showErrorMessage(error.message) diff --git a/webview-ui/src/components/settings/providers/WatsonxAI.tsx b/webview-ui/src/components/settings/providers/WatsonxAI.tsx index c372133f7c..7bd5564a23 100644 --- a/webview-ui/src/components/settings/providers/WatsonxAI.tsx +++ b/webview-ui/src/components/settings/providers/WatsonxAI.tsx @@ -248,7 +248,7 @@ export const WatsonxAI = ({ return ( <> {/* Platform Selection */} -
+
- - - - - {Object.entries(WATSONX_REGIONS).map(([regionCode, regionName]) => ( - - {regionName} - - ))} - - -
- Selected endpoint: {REGION_TO_URL[selectedRegion as keyof typeof REGION_TO_URL]} -
+
+ + +
+ Selected endpoint: {REGION_TO_URL[selectedRegion as keyof typeof REGION_TO_URL]}
- +
)} {/* IBM Cloud Pak for Data specific fields */} {apiConfiguration.watsonxPlatform === "cloudPak" && ( - <> +
-
+
Enter the full URL of your IBM Cloud Pak for Data instance
- +
)} -
+
{apiConfiguration.watsonxPlatform === "ibmCloud" && ( - <> +
-
+
{t("settings:providers.apiKeyStorageNotice")}
- +
)} {apiConfiguration.watsonxPlatform === "cloudPak" && ( <> - - - +
+ + + +
-
+