Merge branch 'main' into support_project_mcp

This commit is contained in:
aheizi 2025-03-14 20:48:47 +08:00
commit 5d8a456374
19 changed files with 2728 additions and 1208 deletions

View file

@ -27,8 +27,8 @@ jobs:
node-version: 18
- run: |
git config user.name github-actions
git config user.email github-actions@github.com
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
- name: Install Dependencies
run: |

View file

@ -0,0 +1,59 @@
name: Update Contributors
on:
push:
branches:
- main
workflow_dispatch: # Allows manual triggering
jobs:
update-contributors:
runs-on: ubuntu-latest
permissions:
contents: write # Needed for pushing changes
pull-requests: write # Needed for creating PRs
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Disable Husky
run: |
echo "HUSKY=0" >> $GITHUB_ENV
git config --global core.hooksPath /dev/null
- name: Install dependencies
run: npm ci
- name: Update contributors and format
run: |
npm run update-contributors
npx prettier --write README.md
if git diff --quiet README.md; then echo "changes=false" >> $GITHUB_OUTPUT; else echo "changes=true" >> $GITHUB_OUTPUT; fi
id: check-changes
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Create Pull Request
if: steps.check-changes.outputs.changes == 'true'
uses: peter-evans/create-pull-request@v5
with:
token: ${{ secrets.GITHUB_TOKEN }}
add-paths: |
README.md
commit-message: "docs: update contributors list [skip ci]"
committer: "github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>"
branch: update-contributors
branch-suffix: timestamp
delete-branch: true
title: "Update contributors list"
body: |
Automated update of contributors list in README.md
This PR was created automatically by a GitHub Action workflow.
base: main

View file

@ -1,5 +1,9 @@
# Roo Code Changelog
## [3.8.6] - 2025-03-13
- Revert SSE MCP support while we debug some config validation issues
## [3.8.5] - 2025-03-12
- Refactor terminal architecture to address critical issues with the current design (thanks @KJ7LNW!)

View file

@ -170,6 +170,30 @@ We love community contributions! Heres how to get involved:
---
<!-- START CONTRIBUTORS SECTION - AUTO-GENERATED, DO NOT EDIT MANUALLY -->
## Contributors
Thanks to all our contributors who have helped make Roo Code better!
| <a href="https://github.com/mrubens"><img src="https://avatars.githubusercontent.com/u/2600?v=4" width="100" height="100" alt="mrubens"/><br /><sub><b>mrubens</b></sub></a> | <a href="https://github.com/saoudrizwan"><img src="https://avatars.githubusercontent.com/u/7799382?v=4" width="100" height="100" alt="saoudrizwan"/><br /><sub><b>saoudrizwan</b></sub></a> | <a href="https://github.com/cte"><img src="https://avatars.githubusercontent.com/u/16332?v=4" width="100" height="100" alt="cte"/><br /><sub><b>cte</b></sub></a> | <a href="https://github.com/samhvw8"><img src="https://avatars.githubusercontent.com/u/12538214?v=4" width="100" height="100" alt="samhvw8"/><br /><sub><b>samhvw8</b></sub></a> | <a href="https://github.com/daniel-lxs"><img src="https://avatars.githubusercontent.com/u/57051444?v=4" width="100" height="100" alt="daniel-lxs"/><br /><sub><b>daniel-lxs</b></sub></a> | <a href="https://github.com/a8trejo"><img src="https://avatars.githubusercontent.com/u/62401433?v=4" width="100" height="100" alt="a8trejo"/><br /><sub><b>a8trejo</b></sub></a> |
| :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: |
| <a href="https://github.com/ColemanRoo"><img src="https://avatars.githubusercontent.com/u/117104599?v=4" width="100" height="100" alt="ColemanRoo"/><br /><sub><b>ColemanRoo</b></sub></a> | <a href="https://github.com/stea9499"><img src="https://avatars.githubusercontent.com/u/4163795?v=4" width="100" height="100" alt="stea9499"/><br /><sub><b>stea9499</b></sub></a> | <a href="https://github.com/joemanley201"><img src="https://avatars.githubusercontent.com/u/8299960?v=4" width="100" height="100" alt="joemanley201"/><br /><sub><b>joemanley201</b></sub></a> | <a href="https://github.com/System233"><img src="https://avatars.githubusercontent.com/u/20336040?v=4" width="100" height="100" alt="System233"/><br /><sub><b>System233</b></sub></a> | <a href="https://github.com/jquanton"><img src="https://avatars.githubusercontent.com/u/88576563?v=4" width="100" height="100" alt="jquanton"/><br /><sub><b>jquanton</b></sub></a> | <a href="https://github.com/nissa-seru"><img src="https://avatars.githubusercontent.com/u/119150866?v=4" width="100" height="100" alt="nissa-seru"/><br /><sub><b>nissa-seru</b></sub></a> |
| <a href="https://github.com/hannesrudolph"><img src="https://avatars.githubusercontent.com/u/49103247?v=4" width="100" height="100" alt="hannesrudolph"/><br /><sub><b>hannesrudolph</b></sub></a> | <a href="https://github.com/MuriloFP"><img src="https://avatars.githubusercontent.com/u/50873657?v=4" width="100" height="100" alt="MuriloFP"/><br /><sub><b>MuriloFP</b></sub></a> | <a href="https://github.com/NyxJae"><img src="https://avatars.githubusercontent.com/u/52313587?v=4" width="100" height="100" alt="NyxJae"/><br /><sub><b>NyxJae</b></sub></a> | <a href="https://github.com/punkpeye"><img src="https://avatars.githubusercontent.com/u/108313943?v=4" width="100" height="100" alt="punkpeye"/><br /><sub><b>punkpeye</b></sub></a> | <a href="https://github.com/d-oit"><img src="https://avatars.githubusercontent.com/u/6849456?v=4" width="100" height="100" alt="d-oit"/><br /><sub><b>d-oit</b></sub></a> | <a href="https://github.com/monotykamary"><img src="https://avatars.githubusercontent.com/u/1130103?v=4" width="100" height="100" alt="monotykamary"/><br /><sub><b>monotykamary</b></sub></a> |
| <a href="https://github.com/lloydchang"><img src="https://avatars.githubusercontent.com/u/1329685?v=4" width="100" height="100" alt="lloydchang"/><br /><sub><b>lloydchang</b></sub></a> | <a href="https://github.com/vigneshsubbiah16"><img src="https://avatars.githubusercontent.com/u/51325334?v=4" width="100" height="100" alt="vigneshsubbiah16"/><br /><sub><b>vigneshsubbiah16</b></sub></a> | <a href="https://github.com/Szpadel"><img src="https://avatars.githubusercontent.com/u/1857251?v=4" width="100" height="100" alt="Szpadel"/><br /><sub><b>Szpadel</b></sub></a> | <a href="https://github.com/lupuletic"><img src="https://avatars.githubusercontent.com/u/105351510?v=4" width="100" height="100" alt="lupuletic"/><br /><sub><b>lupuletic</b></sub></a> | <a href="https://github.com/cannuri"><img src="https://avatars.githubusercontent.com/u/91494156?v=4" width="100" height="100" alt="cannuri"/><br /><sub><b>cannuri</b></sub></a> | <a href="https://github.com/Smartsheet-JB-Brown"><img src="https://avatars.githubusercontent.com/u/171734120?v=4" width="100" height="100" alt="Smartsheet-JB-Brown"/><br /><sub><b>Smartsheet-JB-Brown</b></sub></a> |
| <a href="https://github.com/Premshay"><img src="https://avatars.githubusercontent.com/u/28099628?v=4" width="100" height="100" alt="Premshay"/><br /><sub><b>Premshay</b></sub></a> | <a href="https://github.com/psv2522"><img src="https://avatars.githubusercontent.com/u/87223770?v=4" width="100" height="100" alt="psv2522"/><br /><sub><b>psv2522</b></sub></a> | <a href="https://github.com/olweraltuve"><img src="https://avatars.githubusercontent.com/u/39308405?v=4" width="100" height="100" alt="olweraltuve"/><br /><sub><b>olweraltuve</b></sub></a> | <a href="https://github.com/RaySinner"><img src="https://avatars.githubusercontent.com/u/118297374?v=4" width="100" height="100" alt="RaySinner"/><br /><sub><b>RaySinner</b></sub></a> | <a href="https://github.com/qdaxb"><img src="https://avatars.githubusercontent.com/u/4157870?v=4" width="100" height="100" alt="qdaxb"/><br /><sub><b>qdaxb</b></sub></a> | <a href="https://github.com/afshawnlotfi"><img src="https://avatars.githubusercontent.com/u/6283745?v=4" width="100" height="100" alt="afshawnlotfi"/><br /><sub><b>afshawnlotfi</b></sub></a> |
| <a href="https://github.com/emshvac"><img src="https://avatars.githubusercontent.com/u/121588911?v=4" width="100" height="100" alt="emshvac"/><br /><sub><b>emshvac</b></sub></a> | <a href="https://github.com/Lunchb0ne"><img src="https://avatars.githubusercontent.com/u/22198661?v=4" width="100" height="100" alt="Lunchb0ne"/><br /><sub><b>Lunchb0ne</b></sub></a> | <a href="https://github.com/sammcj"><img src="https://avatars.githubusercontent.com/u/862951?v=4" width="100" height="100" alt="sammcj"/><br /><sub><b>sammcj</b></sub></a> | <a href="https://github.com/dtrugman"><img src="https://avatars.githubusercontent.com/u/2451669?v=4" width="100" height="100" alt="dtrugman"/><br /><sub><b>dtrugman</b></sub></a> | <a href="https://github.com/aitoroses"><img src="https://avatars.githubusercontent.com/u/1699368?v=4" width="100" height="100" alt="aitoroses"/><br /><sub><b>aitoroses</b></sub></a> | <a href="https://github.com/yt3trees"><img src="https://avatars.githubusercontent.com/u/57471763?v=4" width="100" height="100" alt="yt3trees"/><br /><sub><b>yt3trees</b></sub></a> |
| <a href="https://github.com/yongjer"><img src="https://avatars.githubusercontent.com/u/54315206?v=4" width="100" height="100" alt="yongjer"/><br /><sub><b>yongjer</b></sub></a> | <a href="https://github.com/vincentsong"><img src="https://avatars.githubusercontent.com/u/2343574?v=4" width="100" height="100" alt="vincentsong"/><br /><sub><b>vincentsong</b></sub></a> | <a href="https://github.com/pugazhendhi-m"><img src="https://avatars.githubusercontent.com/u/132246623?v=4" width="100" height="100" alt="pugazhendhi-m"/><br /><sub><b>pugazhendhi-m</b></sub></a> | <a href="https://github.com/eonghk"><img src="https://avatars.githubusercontent.com/u/139964?v=4" width="100" height="100" alt="eonghk"/><br /><sub><b>eonghk</b></sub></a> | <a href="https://github.com/philfung"><img src="https://avatars.githubusercontent.com/u/1054593?v=4" width="100" height="100" alt="philfung"/><br /><sub><b>philfung</b></sub></a> | <a href="https://github.com/pdecat"><img src="https://avatars.githubusercontent.com/u/318490?v=4" width="100" height="100" alt="pdecat"/><br /><sub><b>pdecat</b></sub></a> |
| <a href="https://github.com/napter"><img src="https://avatars.githubusercontent.com/u/6260841?v=4" width="100" height="100" alt="napter"/><br /><sub><b>napter</b></sub></a> | <a href="https://github.com/mdp"><img src="https://avatars.githubusercontent.com/u/2868?v=4" width="100" height="100" alt="mdp"/><br /><sub><b>mdp</b></sub></a> | <a href="https://github.com/jcbdev"><img src="https://avatars.githubusercontent.com/u/17152092?v=4" width="100" height="100" alt="jcbdev"/><br /><sub><b>jcbdev</b></sub></a> | <a href="https://github.com/anton-otee"><img src="https://avatars.githubusercontent.com/u/149477749?v=4" width="100" height="100" alt="anton-otee"/><br /><sub><b>anton-otee</b></sub></a> | <a href="https://github.com/AMHesch"><img src="https://avatars.githubusercontent.com/u/4777192?v=4" width="100" height="100" alt="AMHesch"/><br /><sub><b>AMHesch</b></sub></a> | <a href="https://github.com/bannzai"><img src="https://avatars.githubusercontent.com/u/10897361?v=4" width="100" height="100" alt="bannzai"/><br /><sub><b>bannzai</b></sub></a> |
| <a href="https://github.com/dairui1"><img src="https://avatars.githubusercontent.com/u/183250644?v=4" width="100" height="100" alt="dairui1"/><br /><sub><b>dairui1</b></sub></a> | <a href="https://github.com/dqroid"><img src="https://avatars.githubusercontent.com/u/192424994?v=4" width="100" height="100" alt="dqroid"/><br /><sub><b>dqroid</b></sub></a> | <a href="https://github.com/kinandan"><img src="https://avatars.githubusercontent.com/u/186135699?v=4" width="100" height="100" alt="kinandan"/><br /><sub><b>kinandan</b></sub></a> | <a href="https://github.com/kohii"><img src="https://avatars.githubusercontent.com/u/6891780?v=4" width="100" height="100" alt="kohii"/><br /><sub><b>kohii</b></sub></a> | <a href="https://github.com/lightrabbit"><img src="https://avatars.githubusercontent.com/u/1521765?v=4" width="100" height="100" alt="lightrabbit"/><br /><sub><b>lightrabbit</b></sub></a> | <a href="https://github.com/olup"><img src="https://avatars.githubusercontent.com/u/13785588?v=4" width="100" height="100" alt="olup"/><br /><sub><b>olup</b></sub></a> |
| <a href="https://github.com/moqimoqidea"><img src="https://avatars.githubusercontent.com/u/39821951?v=4" width="100" height="100" alt="moqimoqidea"/><br /><sub><b>moqimoqidea</b></sub></a> | <a href="https://github.com/mosleyit"><img src="https://avatars.githubusercontent.com/u/189396442?v=4" width="100" height="100" alt="mosleyit"/><br /><sub><b>mosleyit</b></sub></a> | <a href="https://github.com/oprstchn"><img src="https://avatars.githubusercontent.com/u/16177972?v=4" width="100" height="100" alt="oprstchn"/><br /><sub><b>oprstchn</b></sub></a> | <a href="https://github.com/philipnext"><img src="https://avatars.githubusercontent.com/u/81944499?v=4" width="100" height="100" alt="philipnext"/><br /><sub><b>philipnext</b></sub></a> | <a href="https://github.com/refactorthis"><img src="https://avatars.githubusercontent.com/u/3012240?v=4" width="100" height="100" alt="refactorthis"/><br /><sub><b>refactorthis</b></sub></a> | <a href="https://github.com/samir-nimbly"><img src="https://avatars.githubusercontent.com/u/112695483?v=4" width="100" height="100" alt="samir-nimbly"/><br /><sub><b>samir-nimbly</b></sub></a> |
| <a href="https://github.com/shaybc"><img src="https://avatars.githubusercontent.com/u/8535905?v=4" width="100" height="100" alt="shaybc"/><br /><sub><b>shaybc</b></sub></a> | <a href="https://github.com/shohei-ihaya"><img src="https://avatars.githubusercontent.com/u/25131938?v=4" width="100" height="100" alt="shohei-ihaya"/><br /><sub><b>shohei-ihaya</b></sub></a> | <a href="https://github.com/student20880"><img src="https://avatars.githubusercontent.com/u/74263488?v=4" width="100" height="100" alt="student20880"/><br /><sub><b>student20880</b></sub></a> | <a href="https://github.com/PretzelVector"><img src="https://avatars.githubusercontent.com/u/95664360?v=4" width="100" height="100" alt="PretzelVector"/><br /><sub><b>PretzelVector</b></sub></a> | <a href="https://github.com/adamwlarson"><img src="https://avatars.githubusercontent.com/u/1392315?v=4" width="100" height="100" alt="adamwlarson"/><br /><sub><b>adamwlarson</b></sub></a> | <a href="https://github.com/alarno"><img src="https://avatars.githubusercontent.com/u/4355547?v=4" width="100" height="100" alt="alarno"/><br /><sub><b>alarno</b></sub></a> |
| <a href="https://github.com/andreastempsch"><img src="https://avatars.githubusercontent.com/u/117991125?v=4" width="100" height="100" alt="andreastempsch"/><br /><sub><b>andreastempsch</b></sub></a> | <a href="https://github.com/Atlogit"><img src="https://avatars.githubusercontent.com/u/86947554?v=4" width="100" height="100" alt="Atlogit"/><br /><sub><b>Atlogit</b></sub></a> | <a href="https://github.com/dleen"><img src="https://avatars.githubusercontent.com/u/1297964?v=4" width="100" height="100" alt="dleen"/><br /><sub><b>dleen</b></sub></a> | <a href="https://github.com/dbasclpy"><img src="https://avatars.githubusercontent.com/u/139889137?v=4" width="100" height="100" alt="dbasclpy"/><br /><sub><b>dbasclpy</b></sub></a> | <a href="https://github.com/celestial-vault"><img src="https://avatars.githubusercontent.com/u/58194240?v=4" width="100" height="100" alt="celestial-vault"/><br /><sub><b>celestial-vault</b></sub></a> | <a href="https://github.com/DeXtroTip"><img src="https://avatars.githubusercontent.com/u/21011087?v=4" width="100" height="100" alt="DeXtroTip"/><br /><sub><b>DeXtroTip</b></sub></a> |
| <a href="https://github.com/hesara"><img src="https://avatars.githubusercontent.com/u/1335918?v=4" width="100" height="100" alt="hesara"/><br /><sub><b>hesara</b></sub></a> | <a href="https://github.com/eltociear"><img src="https://avatars.githubusercontent.com/u/22633385?v=4" width="100" height="100" alt="eltociear"/><br /><sub><b>eltociear</b></sub></a> | <a href="https://github.com/libertyteeth"><img src="https://avatars.githubusercontent.com/u/32841567?v=4" width="100" height="100" alt="libertyteeth"/><br /><sub><b>libertyteeth</b></sub></a> | <a href="https://github.com/mamertofabian"><img src="https://avatars.githubusercontent.com/u/7698436?v=4" width="100" height="100" alt="mamertofabian"/><br /><sub><b>mamertofabian</b></sub></a> | <a href="https://github.com/marvijo-code"><img src="https://avatars.githubusercontent.com/u/82562019?v=4" width="100" height="100" alt="marvijo-code"/><br /><sub><b>marvijo-code</b></sub></a> | <a href="https://github.com/Sarke"><img src="https://avatars.githubusercontent.com/u/2719310?v=4" width="100" height="100" alt="Sarke"/><br /><sub><b>Sarke</b></sub></a> |
| <a href="https://github.com/tgfjt"><img src="https://avatars.githubusercontent.com/u/2628239?v=4" width="100" height="100" alt="tgfjt"/><br /><sub><b>tgfjt</b></sub></a> | <a href="https://github.com/vladstudio"><img src="https://avatars.githubusercontent.com/u/914320?v=4" width="100" height="100" alt="vladstudio"/><br /><sub><b>vladstudio</b></sub></a> | <a href="https://github.com/ashktn"><img src="https://avatars.githubusercontent.com/u/6723913?v=4" width="100" height="100" alt="ashktn"/><br /><sub><b>ashktn</b></sub></a> | | | |
<!-- END CONTRIBUTORS SECTION -->
## License
[Apache 2.0 © 2025 Roo Veterinary, Inc.](./LICENSE)

View file

@ -72,14 +72,6 @@ const extensionConfig = {
copyWasmFiles,
/* add to the end of plugins array */
esbuildProblemMatcherPlugin,
{
name: "alias-plugin",
setup(build) {
build.onResolve({ filter: /^pkce-challenge$/ }, (args) => {
return { path: require.resolve("pkce-challenge/dist/index.browser.js") }
})
},
},
],
entryPoints: ["src/extension.ts"],
format: "cjs",

797
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -3,7 +3,7 @@
"displayName": "Roo Code (prev. Roo Cline)",
"description": "A whole dev team of AI agents in your editor.",
"publisher": "RooVeterinaryInc",
"version": "3.8.5",
"version": "3.8.6",
"icon": "assets/icons/rocket.png",
"galleryBanner": {
"color": "#617A91",
@ -307,7 +307,8 @@
"watch:tsc": "tsc --noEmit --watch --project tsconfig.json",
"watch-tests": "tsc -p . -w --outDir out",
"changeset": "changeset",
"knip": "knip --include files"
"knip": "knip --include files",
"update-contributors": "node scripts/update-contributors.js"
},
"dependencies": {
"@anthropic-ai/bedrock-sdk": "^0.10.2",
@ -317,7 +318,7 @@
"@google-cloud/vertexai": "^1.9.3",
"@google/generative-ai": "^0.18.0",
"@mistralai/mistralai": "^1.3.6",
"@modelcontextprotocol/sdk": "^1.7.0",
"@modelcontextprotocol/sdk": "^1.0.1",
"@types/clone-deep": "^4.0.4",
"@types/pdf-parse": "^1.1.4",
"@types/tmp": "^0.2.6",
@ -344,12 +345,10 @@
"os-name": "^6.0.0",
"p-wait-for": "^5.0.2",
"pdf-parse": "^1.1.1",
"pkce-challenge": "^4.1.0",
"posthog-node": "^4.7.0",
"pretty-bytes": "^6.1.1",
"puppeteer-chromium-resolver": "^23.0.0",
"puppeteer-core": "^23.4.0",
"reconnecting-eventsource": "^1.6.4",
"serialize-error": "^11.0.3",
"simple-git": "^3.27.0",
"sound-play": "^1.1.0",

208
scripts/update-contributors.js Executable file
View file

@ -0,0 +1,208 @@
#!/usr/bin/env node
/**
* This script fetches contributor data from GitHub and updates the README.md file
* with a contributors section showing avatars and usernames.
*/
const https = require("https")
const fs = require("fs")
const path = require("path")
// GitHub API URL for fetching contributors
const GITHUB_API_URL = "https://api.github.com/repos/RooVetGit/Roo-Code/contributors?per_page=100"
const README_PATH = path.join(__dirname, "..", "README.md")
// Sentinel markers for contributors section
const START_MARKER = "<!-- START CONTRIBUTORS SECTION - AUTO-GENERATED, DO NOT EDIT MANUALLY -->"
const END_MARKER = "<!-- END CONTRIBUTORS SECTION -->"
// HTTP options for GitHub API request
const options = {
headers: {
"User-Agent": "Roo-Code-Contributors-Script",
},
}
// Add GitHub token for authentication if available
if (process.env.GITHUB_TOKEN) {
options.headers.Authorization = `token ${process.env.GITHUB_TOKEN}`
console.log("Using GitHub token from environment variable")
}
/**
* Fetches contributors data from GitHub API
* @returns {Promise<Array>} Array of contributor objects
*/
function fetchContributors() {
return new Promise((resolve, reject) => {
https
.get(GITHUB_API_URL, options, (res) => {
if (res.statusCode !== 200) {
reject(new Error(`GitHub API request failed with status code: ${res.statusCode}`))
return
}
let data = ""
res.on("data", (chunk) => {
data += chunk
})
res.on("end", () => {
try {
const contributors = JSON.parse(data)
resolve(contributors)
} catch (error) {
reject(new Error(`Failed to parse GitHub API response: ${error.message}`))
}
})
})
.on("error", (error) => {
reject(new Error(`GitHub API request failed: ${error.message}`))
})
})
}
/**
* Reads the README.md file
* @returns {Promise<string>} README content
*/
function readReadme() {
return new Promise((resolve, reject) => {
fs.readFile(README_PATH, "utf8", (err, data) => {
if (err) {
reject(new Error(`Failed to read README.md: ${err.message}`))
return
}
resolve(data)
})
})
}
/**
* Creates HTML for the contributors section
* @param {Array} contributors Array of contributor objects from GitHub API
* @returns {string} HTML for contributors section
*/
function formatContributorsSection(contributors) {
// Filter out GitHub Actions bot
const filteredContributors = contributors.filter((c) => !c.login.includes("[bot]") && !c.login.includes("R00-B0T"))
// Start building with Markdown table format
let markdown = `${START_MARKER}
## Contributors
Thanks to all our contributors who have helped make Roo Code better!
`
// Number of columns in the table
const COLUMNS = 6
// Create contributor cell HTML
const createCell = (contributor) => {
return `<a href="${contributor.html_url}"><img src="${contributor.avatar_url}" width="100" height="100" alt="${contributor.login}"/><br /><sub><b>${contributor.login}</b></sub></a>`
}
if (filteredContributors.length > 0) {
// Table header is the first row of contributors
const headerCells = filteredContributors.slice(0, COLUMNS).map(createCell)
// Fill any empty cells in header row
while (headerCells.length < COLUMNS) {
headerCells.push(" ")
}
// Add header row
markdown += `|${headerCells.join("|")}|\n`
// Add alignment row
markdown += "|"
for (let i = 0; i < COLUMNS; i++) {
markdown += ":---:|"
}
markdown += "\n"
// Add remaining contributor rows starting with the second batch
for (let i = COLUMNS; i < filteredContributors.length; i += COLUMNS) {
const rowContributors = filteredContributors.slice(i, i + COLUMNS)
// Create cells for each contributor in this row
const cells = rowContributors.map(createCell)
// Fill any empty cells to maintain table structure
while (cells.length < COLUMNS) {
cells.push(" ")
}
// Add row to the table
markdown += `|${cells.join("|")}|\n`
}
}
markdown += `${END_MARKER}`
return markdown
}
/**
* Updates the README.md file with contributors section
* @param {string} readmeContent Original README content
* @param {string} contributorsSection HTML for contributors section
* @returns {Promise<void>}
*/
function updateReadme(readmeContent, contributorsSection) {
// Find existing contributors section markers
const startPos = readmeContent.indexOf(START_MARKER)
const endPos = readmeContent.indexOf(END_MARKER)
if (startPos === -1 || endPos === -1) {
console.warn("Warning: Could not find contributors section markers in README.md")
console.warn("Skipping update - please add markers to enable automatic updates.")
return
}
// Replace existing section, trimming whitespace at section boundaries
const beforeSection = readmeContent.substring(0, startPos).trimEnd()
const afterSection = readmeContent.substring(endPos + END_MARKER.length).trimStart()
// Ensure single newline separators between sections
const updatedContent = beforeSection + "\n\n" + contributorsSection.trim() + "\n\n" + afterSection
return writeReadme(updatedContent)
}
/**
* Writes updated content to README.md
* @param {string} content Updated README content
* @returns {Promise<void>}
*/
function writeReadme(content) {
return new Promise((resolve, reject) => {
fs.writeFile(README_PATH, content, "utf8", (err) => {
if (err) {
reject(new Error(`Failed to write updated README.md: ${err.message}`))
return
}
resolve()
})
})
}
/**
* Main function that orchestrates the update process
*/
async function main() {
try {
const contributors = await fetchContributors()
const readmeContent = await readReadme()
const contributorsSection = formatContributorsSection(contributors)
await updateReadme(readmeContent, contributorsSection)
} catch (error) {
console.error(`Error: ${error.message}`)
process.exit(1)
}
}
// Run the script
main()

View file

@ -1,14 +0,0 @@
class SSEClientTransport {
constructor(url, options = {}) {
this.url = url
this.options = options
this.onerror = null
this.connect = jest.fn().mockResolvedValue()
this.close = jest.fn().mockResolvedValue()
this.start = jest.fn().mockResolvedValue()
}
}
module.exports = {
SSEClientTransport,
}

View file

@ -0,0 +1,313 @@
// Mock AWS SDK credential providers
jest.mock("@aws-sdk/credential-providers", () => ({
fromIni: jest.fn().mockReturnValue({
accessKeyId: "profile-access-key",
secretAccessKey: "profile-secret-key",
}),
}))
import { AwsBedrockHandler, StreamEvent } from "../bedrock"
import { ApiHandlerOptions } from "../../../shared/api"
import { BedrockRuntimeClient } from "@aws-sdk/client-bedrock-runtime"
describe("AwsBedrockHandler with invokedModelId", () => {
let mockSend: jest.SpyInstance
beforeEach(() => {
// Mock the BedrockRuntimeClient.prototype.send method
mockSend = jest.spyOn(BedrockRuntimeClient.prototype, "send").mockImplementation(async () => {
return {
stream: createMockStream([]),
}
})
})
afterEach(() => {
mockSend.mockRestore()
})
// Helper function to create a mock async iterable stream
function createMockStream(events: StreamEvent[]) {
return {
[Symbol.asyncIterator]: async function* () {
for (const event of events) {
yield event
}
// Always yield a metadata event at the end
yield {
metadata: {
usage: {
inputTokens: 100,
outputTokens: 200,
},
},
}
},
}
}
it("should update costModelConfig when invokedModelId is present in the stream", async () => {
// Create a handler with a custom ARN
const mockOptions: ApiHandlerOptions = {
// apiModelId: "anthropic.claude-3-5-sonnet-20241022-v2:0",
awsAccessKey: "test-access-key",
awsSecretKey: "test-secret-key",
awsRegion: "us-east-1",
awsCustomArn: "arn:aws:bedrock:us-west-2:699475926481:default-prompt-router/anthropic.claude:1",
}
const handler = new AwsBedrockHandler(mockOptions)
// Create a spy on the getModel method before mocking it
const getModelSpy = jest.spyOn(handler, "getModelByName")
// Mock the stream to include an event with invokedModelId and usage metadata
mockSend.mockImplementationOnce(async () => {
return {
stream: createMockStream([
// First event with invokedModelId and usage metadata
{
trace: {
promptRouter: {
invokedModelId:
"arn:aws:bedrock:us-west-2:699475926481:inference-profile/us.anthropic.claude-3-5-sonnet-20240620-v1:0",
usage: {
inputTokens: 150,
outputTokens: 250,
},
},
},
// Some content events
},
{
contentBlockStart: {
start: {
text: "Hello",
},
contentBlockIndex: 0,
},
},
{
contentBlockDelta: {
delta: {
text: ", world!",
},
contentBlockIndex: 0,
},
},
]),
}
})
// Create a message generator
const messageGenerator = handler.createMessage("system prompt", [{ role: "user", content: "user message" }])
// Collect all yielded events to verify usage events
const events = []
for await (const event of messageGenerator) {
events.push(event)
}
// Verify that getModel was called with the correct model name
expect(getModelSpy).toHaveBeenCalledWith("anthropic.claude-3-5-sonnet-20240620-v1:0")
// Verify that getModel returns the updated model info
const costModel = handler.getModel()
expect(costModel.id).toBe("anthropic.claude-3-5-sonnet-20240620-v1:0")
expect(costModel.info.inputPrice).toBe(3)
// Verify that a usage event was emitted after updating the costModelConfig
const usageEvents = events.filter((event) => event.type === "usage")
expect(usageEvents.length).toBeGreaterThanOrEqual(1)
// The last usage event should have the token counts from the metadata
const lastUsageEvent = usageEvents[usageEvents.length - 1]
expect(lastUsageEvent).toEqual({
type: "usage",
inputTokens: 100,
outputTokens: 200,
})
})
it("should not update costModelConfig when invokedModelId is not present", async () => {
// Create a handler with default settings
const mockOptions: ApiHandlerOptions = {
apiModelId: "anthropic.claude-3-5-sonnet-20241022-v2:0",
awsAccessKey: "test-access-key",
awsSecretKey: "test-secret-key",
awsRegion: "us-east-1",
}
const handler = new AwsBedrockHandler(mockOptions)
// Mock the stream without an invokedModelId event
mockSend.mockImplementationOnce(async () => {
return {
stream: createMockStream([
// Some content events but no invokedModelId
{
contentBlockStart: {
start: {
text: "Hello",
},
contentBlockIndex: 0,
},
},
{
contentBlockDelta: {
delta: {
text: ", world!",
},
contentBlockIndex: 0,
},
},
]),
}
})
// Mock getModel to return expected values
const getModelSpy = jest.spyOn(handler, "getModel").mockReturnValue({
id: "anthropic.claude-3-5-sonnet-20241022-v2:0",
info: {
maxTokens: 4096,
contextWindow: 128_000,
supportsPromptCache: false,
supportsImages: true,
},
})
// Create a message generator
const messageGenerator = handler.createMessage("system prompt", [{ role: "user", content: "user message" }])
// Consume the generator
for await (const _ of messageGenerator) {
// Just consume the messages
}
// Verify that getModel returns the original model info
const costModel = handler.getModel()
expect(costModel.id).toBe("anthropic.claude-3-5-sonnet-20241022-v2:0")
// Verify getModel was not called with a model name parameter
expect(getModelSpy).not.toHaveBeenCalledWith(expect.any(String))
})
it("should handle invalid invokedModelId format gracefully", async () => {
// Create a handler with default settings
const mockOptions: ApiHandlerOptions = {
apiModelId: "anthropic.claude-3-5-sonnet-20241022-v2:0",
awsAccessKey: "test-access-key",
awsSecretKey: "test-secret-key",
awsRegion: "us-east-1",
}
const handler = new AwsBedrockHandler(mockOptions)
// Mock the stream with an invalid invokedModelId
mockSend.mockImplementationOnce(async () => {
return {
stream: createMockStream([
// Event with invalid invokedModelId format
{
trace: {
promptRouter: {
invokedModelId: "invalid-format-not-an-arn",
},
},
},
// Some content events
{
contentBlockStart: {
start: {
text: "Hello",
},
contentBlockIndex: 0,
},
},
]),
}
})
// Mock getModel to return expected values
const getModelSpy = jest.spyOn(handler, "getModel").mockReturnValue({
id: "anthropic.claude-3-5-sonnet-20241022-v2:0",
info: {
maxTokens: 4096,
contextWindow: 128_000,
supportsPromptCache: false,
supportsImages: true,
},
})
// Create a message generator
const messageGenerator = handler.createMessage("system prompt", [{ role: "user", content: "user message" }])
// Consume the generator
for await (const _ of messageGenerator) {
// Just consume the messages
}
// Verify that getModel returns the original model info
const costModel = handler.getModel()
expect(costModel.id).toBe("anthropic.claude-3-5-sonnet-20241022-v2:0")
})
it("should handle errors during invokedModelId processing", async () => {
// Create a handler with default settings
const mockOptions: ApiHandlerOptions = {
apiModelId: "anthropic.claude-3-5-sonnet-20241022-v2:0",
awsAccessKey: "test-access-key",
awsSecretKey: "test-secret-key",
awsRegion: "us-east-1",
}
const handler = new AwsBedrockHandler(mockOptions)
// Mock the stream with a valid invokedModelId
mockSend.mockImplementationOnce(async () => {
return {
stream: createMockStream([
// Event with valid invokedModelId
{
trace: {
promptRouter: {
invokedModelId:
"arn:aws:bedrock:us-east-1:123456789:foundation-model/anthropic.claude-3-sonnet-20240229-v1:0",
},
},
},
]),
}
})
// Mock getModel to throw an error when called with the model name
jest.spyOn(handler, "getModel").mockImplementation((modelName?: string) => {
if (modelName === "anthropic.claude-3-sonnet-20240229-v1:0") {
throw new Error("Test error during model lookup")
}
// Default return value for initial call
return {
id: "anthropic.claude-3-5-sonnet-20241022-v2:0",
info: {
maxTokens: 4096,
contextWindow: 128_000,
supportsPromptCache: false,
supportsImages: true,
},
}
})
// Create a message generator
const messageGenerator = handler.createMessage("system prompt", [{ role: "user", content: "user message" }])
// Consume the generator
for await (const _ of messageGenerator) {
// Just consume the messages
}
// Verify that getModel returns the original model info
const costModel = handler.getModel()
expect(costModel.id).toBe("anthropic.claude-3-5-sonnet-20241022-v2:0")
})
})

View file

@ -327,10 +327,36 @@ describe("AwsBedrockHandler", () => {
const modelInfo = customArnHandler.getModel()
expect(modelInfo.id).toBe("arn:aws:bedrock:us-east-1::foundation-model/custom-model")
expect(modelInfo.info.maxTokens).toBe(4096)
expect(modelInfo.info.contextWindow).toBe(128_000)
expect(modelInfo.info.contextWindow).toBe(200_000)
expect(modelInfo.info.supportsPromptCache).toBe(false)
})
it("should correctly identify model info from inference profile ARN", () => {
//this test intentionally uses a model that has different maxTokens, contextWindow and other values than the fall back option in the code
const customArnHandler = new AwsBedrockHandler({
apiModelId: "meta.llama3-8b-instruct-v1:0", // This will be ignored when awsCustomArn is provided
awsAccessKey: "test-access-key",
awsSecretKey: "test-secret-key",
awsRegion: "us-west-2",
awsCustomArn:
"arn:aws:bedrock:us-west-2:699475926481:inference-profile/us.meta.llama3-8b-instruct-v1:0",
})
const modelInfo = customArnHandler.getModel()
// Verify the ARN is used as the model ID
expect(modelInfo.id).toBe(
"arn:aws:bedrock:us-west-2:699475926481:inference-profile/us.meta.llama3-8b-instruct-v1:0",
)
//these should not be the default fall back. they should be Llama's config
expect(modelInfo.info.maxTokens).toBe(2048)
expect(modelInfo.info.contextWindow).toBe(4_000)
expect(modelInfo.info.supportsImages).toBe(false)
expect(modelInfo.info.supportsPromptCache).toBe(false)
// This test highlights that the regex in getModel needs to be updated to handle inference-profile ARNs
})
it("should use default model when custom-arn is selected but no ARN is provided", () => {
const customArnHandler = new AwsBedrockHandler({
apiModelId: "custom-arn",
@ -345,4 +371,163 @@ describe("AwsBedrockHandler", () => {
expect(modelInfo.info).toBeDefined()
})
})
describe("invokedModelId handling", () => {
it("should update costModelConfig when invokedModelId is present in custom ARN scenario", async () => {
const customArnHandler = new AwsBedrockHandler({
apiModelId: "anthropic.claude-3-5-sonnet-20241022-v2:0",
awsAccessKey: "test-access-key",
awsSecretKey: "test-secret-key",
awsRegion: "us-east-1",
awsCustomArn: "arn:aws:bedrock:us-east-1:123456789:foundation-model/custom-model",
})
const mockStreamEvent = {
trace: {
promptRouter: {
invokedModelId: "arn:aws:bedrock:us-east-1:123456789:foundation-model/custom-model:0",
},
},
}
jest.spyOn(customArnHandler, "getModel").mockReturnValue({
id: "custom-model",
info: {
maxTokens: 4096,
contextWindow: 128_000,
supportsPromptCache: false,
supportsImages: true,
},
})
await customArnHandler.createMessage("system prompt", [{ role: "user", content: "user message" }]).next()
expect(customArnHandler.getModel()).toEqual({
id: "custom-model",
info: {
maxTokens: 4096,
contextWindow: 128_000,
supportsPromptCache: false,
supportsImages: true,
},
})
})
it("should update costModelConfig when invokedModelId is present in default model scenario", async () => {
handler = new AwsBedrockHandler({
apiModelId: "anthropic.claude-3-5-sonnet-20241022-v2:0",
awsAccessKey: "test-access-key",
awsSecretKey: "test-secret-key",
awsRegion: "us-east-1",
})
const mockStreamEvent = {
trace: {
promptRouter: {
invokedModelId: "arn:aws:bedrock:us-east-1:123456789:foundation-model/default-model:0",
},
},
}
jest.spyOn(handler, "getModel").mockReturnValue({
id: "default-model",
info: {
maxTokens: 4096,
contextWindow: 128_000,
supportsPromptCache: false,
supportsImages: true,
},
})
await handler.createMessage("system prompt", [{ role: "user", content: "user message" }]).next()
expect(handler.getModel()).toEqual({
id: "default-model",
info: {
maxTokens: 4096,
contextWindow: 128_000,
supportsPromptCache: false,
supportsImages: true,
},
})
})
it("should not update costModelConfig when invokedModelId is not present", async () => {
handler = new AwsBedrockHandler({
apiModelId: "anthropic.claude-3-5-sonnet-20241022-v2:0",
awsAccessKey: "test-access-key",
awsSecretKey: "test-secret-key",
awsRegion: "us-east-1",
})
const mockStreamEvent = {
trace: {
promptRouter: {
// No invokedModelId present
},
},
}
jest.spyOn(handler, "getModel").mockReturnValue({
id: "default-model",
info: {
maxTokens: 4096,
contextWindow: 128_000,
supportsPromptCache: false,
supportsImages: true,
},
})
await handler.createMessage("system prompt", [{ role: "user", content: "user message" }]).next()
expect(handler.getModel()).toEqual({
id: "default-model",
info: {
maxTokens: 4096,
contextWindow: 128_000,
supportsPromptCache: false,
supportsImages: true,
},
})
})
it("should not update costModelConfig when invokedModelId cannot be parsed", async () => {
handler = new AwsBedrockHandler({
apiModelId: "anthropic.claude-3-5-sonnet-20241022-v2:0",
awsAccessKey: "test-access-key",
awsSecretKey: "test-secret-key",
awsRegion: "us-east-1",
})
const mockStreamEvent = {
trace: {
promptRouter: {
invokedModelId: "invalid-arn",
},
},
}
jest.spyOn(handler, "getModel").mockReturnValue({
id: "default-model",
info: {
maxTokens: 4096,
contextWindow: 128_000,
supportsPromptCache: false,
supportsImages: true,
},
})
await handler.createMessage("system prompt", [{ role: "user", content: "user message" }]).next()
expect(handler.getModel()).toEqual({
id: "default-model",
info: {
maxTokens: 4096,
contextWindow: 128_000,
supportsPromptCache: false,
supportsImages: true,
},
})
})
})
})

View file

@ -3,11 +3,19 @@ import {
ConverseStreamCommand,
ConverseCommand,
BedrockRuntimeClientConfig,
ConverseStreamCommandOutput,
} from "@aws-sdk/client-bedrock-runtime"
import { fromIni } from "@aws-sdk/credential-providers"
import { Anthropic } from "@anthropic-ai/sdk"
import { SingleCompletionHandler } from "../"
import { ApiHandlerOptions, BedrockModelId, ModelInfo, bedrockDefaultModelId, bedrockModels } from "../../shared/api"
import {
ApiHandlerOptions,
BedrockModelId,
ModelInfo,
bedrockDefaultModelId,
bedrockModels,
bedrockDefaultPromptRouterModelId,
} from "../../shared/api"
import { ApiStream } from "../transform/stream"
import { convertToBedrockConverseMessages } from "../transform/bedrock-converse-format"
import { BaseProvider } from "./base-provider"
@ -21,7 +29,8 @@ import { logger } from "../../utils/logging"
*/
function validateBedrockArn(arn: string, region?: string) {
// Validate ARN format
const arnRegex = /^arn:aws:bedrock:([^:]+):(\d+):(foundation-model|provisioned-model|default-prompt-router)\/(.+)$/
const arnRegex =
/^arn:aws:bedrock:([^:]+):(\d+):(foundation-model|provisioned-model|default-prompt-router|prompt-router)\/(.+)$/
const match = arn.match(arnRegex)
if (!match) {
@ -86,12 +95,27 @@ export interface StreamEvent {
latencyMs: number
}
}
trace?: {
promptRouter?: {
invokedModelId?: string
usage?: {
inputTokens: number
outputTokens: number
totalTokens?: number // Made optional since we don't use it
}
}
}
}
export class AwsBedrockHandler extends BaseProvider implements SingleCompletionHandler {
protected options: ApiHandlerOptions
private client: BedrockRuntimeClient
private costModelConfig: { id: BedrockModelId | string; info: ModelInfo } = {
id: "",
info: { maxTokens: 0, contextWindow: 0, supportsPromptCache: false, supportsImages: false },
}
constructor(options: ApiHandlerOptions) {
super()
this.options = options
@ -141,8 +165,7 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH
}
override async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const modelConfig = this.getModel()
let modelConfig = this.getModel()
// Handle cross-region inference
let modelId: string
@ -250,8 +273,8 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH
continue
}
// Handle metadata events first
if (streamEvent.metadata?.usage) {
// Handle metadata events first.
if (streamEvent?.metadata?.usage) {
yield {
type: "usage",
inputTokens: streamEvent.metadata.usage.inputTokens || 0,
@ -260,6 +283,37 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH
continue
}
if (streamEvent?.trace?.promptRouter?.invokedModelId) {
try {
const invokedModelId = streamEvent.trace.promptRouter.invokedModelId
const modelMatch = invokedModelId.match(/\/([^\/]+)(?::|$)/)
if (modelMatch && modelMatch[1]) {
let modelName = modelMatch[1]
// Get a new modelConfig from getModel() using invokedModelId.. remove the region first
let region = modelName.slice(0, 3)
if (region === "us." || region === "eu.") modelName = modelName.slice(3)
this.costModelConfig = this.getModelByName(modelName)
}
// Handle metadata events for the promptRouter.
if (streamEvent?.trace?.promptRouter?.usage) {
yield {
type: "usage",
inputTokens: streamEvent?.trace?.promptRouter?.usage?.inputTokens || 0,
outputTokens: streamEvent?.trace?.promptRouter?.usage?.outputTokens || 0,
}
continue
}
} catch (error) {
logger.error("Error handling Bedrock invokedModelId", {
ctx: "bedrock",
error: error instanceof Error ? error : String(error),
})
}
}
// Handle message start
if (streamEvent.messageStart) {
continue
@ -282,7 +336,6 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH
}
continue
}
// Handle message stop
if (streamEvent.messageStop) {
continue
@ -428,122 +481,75 @@ Please check:
}
}
override getModel(): { id: BedrockModelId | string; info: ModelInfo } {
// If custom ARN is provided, use it
if (this.options.awsCustomArn) {
// Custom ARNs should not be modified with region prefixes
// as they already contain the full resource path
//Prompt Router responses come back in a different sequence and the yield calls are not resulting in costs getting updated
getModelByName(modelName: string): { id: BedrockModelId | string; info: ModelInfo } {
// Try to find the model in bedrockModels
if (modelName in bedrockModels) {
const id = modelName as BedrockModelId
// Check if the ARN contains information about the model type
// This helps set appropriate token limits for models behind prompt routers
const arnLower = this.options.awsCustomArn.toLowerCase()
// Determine model info based on ARN content
let modelInfo: ModelInfo
if (arnLower.includes("claude-3-7-sonnet") || arnLower.includes("claude-3.7-sonnet")) {
// Claude 3.7 Sonnet has 8192 tokens in Bedrock
modelInfo = {
maxTokens: 8192,
contextWindow: 200_000,
supportsPromptCache: false,
supportsImages: true,
supportsComputerUse: true,
}
} else if (arnLower.includes("claude-3-5-sonnet") || arnLower.includes("claude-3.5-sonnet")) {
// Claude 3.5 Sonnet has 8192 tokens in Bedrock
modelInfo = {
maxTokens: 8192,
contextWindow: 200_000,
supportsPromptCache: false,
supportsImages: true,
supportsComputerUse: true,
}
} else if (arnLower.includes("claude-3-opus") || arnLower.includes("claude-3.0-opus")) {
// Claude 3 Opus has 4096 tokens in Bedrock
modelInfo = {
maxTokens: 4096,
contextWindow: 200_000,
supportsPromptCache: false,
supportsImages: true,
}
} else if (arnLower.includes("claude-3-haiku") || arnLower.includes("claude-3.0-haiku")) {
// Claude 3 Haiku has 4096 tokens in Bedrock
modelInfo = {
maxTokens: 4096,
contextWindow: 200_000,
supportsPromptCache: false,
supportsImages: true,
}
} else if (arnLower.includes("claude-3-5-haiku") || arnLower.includes("claude-3.5-haiku")) {
// Claude 3.5 Haiku has 8192 tokens in Bedrock
modelInfo = {
maxTokens: 8192,
contextWindow: 200_000,
supportsPromptCache: false,
supportsImages: false,
}
} else if (arnLower.includes("claude")) {
// Generic Claude model with conservative token limit
modelInfo = {
maxTokens: 4096,
contextWindow: 128_000,
supportsPromptCache: false,
supportsImages: true,
}
} else if (arnLower.includes("llama3") || arnLower.includes("llama-3")) {
// Llama 3 models typically have 8192 tokens in Bedrock
modelInfo = {
maxTokens: 8192,
contextWindow: 128_000,
supportsPromptCache: false,
supportsImages: arnLower.includes("90b") || arnLower.includes("11b"),
}
} else if (arnLower.includes("nova-pro")) {
// Amazon Nova Pro
modelInfo = {
maxTokens: 5000,
contextWindow: 300_000,
supportsPromptCache: false,
supportsImages: true,
}
} else {
// Default for unknown models or prompt routers
modelInfo = {
maxTokens: 4096,
contextWindow: 128_000,
supportsPromptCache: false,
supportsImages: true,
}
}
//Do a deep copy of the model info so that later in the code the model id and maxTokens can be set.
// The bedrockModels array is a constant and updating the model ID from the returned invokedModelID value
// in a prompt router response isn't possible on the constant.
let model = JSON.parse(JSON.stringify(bedrockModels[id]))
// If modelMaxTokens is explicitly set in options, override the default
if (this.options.modelMaxTokens && this.options.modelMaxTokens > 0) {
modelInfo.maxTokens = this.options.modelMaxTokens
model.maxTokens = this.options.modelMaxTokens
}
return {
id: this.options.awsCustomArn,
info: modelInfo,
}
return { id, info: model }
}
const modelId = this.options.apiModelId
if (modelId) {
// Special case for custom ARN option
if (modelId === "custom-arn") {
// This should not happen as we should have awsCustomArn set
// but just in case, return a default model
return {
id: bedrockDefaultModelId,
info: bedrockModels[bedrockDefaultModelId],
return { id: bedrockDefaultModelId, info: bedrockModels[bedrockDefaultModelId] }
}
override getModel(): { id: BedrockModelId | string; info: ModelInfo } {
if (this.costModelConfig.id.trim().length > 0) {
return this.costModelConfig
}
// If custom ARN is provided, use it
if (this.options.awsCustomArn) {
// Extract the model name from the ARN
const arnMatch = this.options.awsCustomArn.match(
/^arn:aws:bedrock:([^:]+):(\d+):(inference-profile|foundation-model|provisioned-model)\/(.+)$/,
)
let modelName = arnMatch ? arnMatch[4] : ""
if (modelName) {
let region = modelName.slice(0, 3)
if (region === "us." || region === "eu.") modelName = modelName.slice(3)
let modelData = this.getModelByName(modelName)
modelData.id = this.options.awsCustomArn
if (modelData) {
return modelData
}
}
// For tests, allow any model ID
// An ARN was used, but no model info match found, use default values based on common patterns
let model = this.getModelByName(bedrockDefaultPromptRouterModelId)
// For custom ARNs, always return the specific values expected by tests
return {
id: this.options.awsCustomArn,
info: model.info,
}
}
if (this.options.apiModelId) {
// Special case for custom ARN option
if (this.options.apiModelId === "custom-arn") {
// This should not happen as we should have awsCustomArn set
// but just in case, return a default model
return this.getModelByName(bedrockDefaultModelId)
}
// For tests, allow any model ID (but not custom ARNs, which are handled above)
if (process.env.NODE_ENV === "test") {
return {
id: modelId,
id: this.options.apiModelId,
info: {
maxTokens: 5000,
contextWindow: 128_000,
@ -552,15 +558,9 @@ Please check:
}
}
// For production, validate against known models
if (modelId in bedrockModels) {
const id = modelId as BedrockModelId
return { id, info: bedrockModels[id] }
}
}
return {
id: bedrockDefaultModelId,
info: bedrockModels[bedrockDefaultModelId],
return this.getModelByName(this.options.apiModelId)
}
return this.getModelByName(bedrockDefaultModelId)
}
async completePrompt(prompt: string): Promise<string> {
@ -573,10 +573,6 @@ Please check:
// For custom ARNs, use the ARN directly without modification
if (this.options.awsCustomArn) {
modelId = modelConfig.id
logger.debug("Using custom ARN in completePrompt", {
ctx: "bedrock",
customArn: this.options.awsCustomArn,
})
// Validate ARN format and check region match
const clientRegion = this.client.config.region as string

File diff suppressed because it is too large Load diff

View file

@ -250,51 +250,6 @@ describe("SYSTEM_PROMPT", () => {
true, // enableMcpServerCreation
)
// Verify basic MCP server info is included
expect(prompt).toContain("MCP SERVERS")
expect(prompt).toContain("The Model Context Protocol (MCP) enables communication")
// Verify server types are described
expect(prompt).toContain("Local (Stdio-based) servers")
expect(prompt).toContain("Remote (SSE-based) servers")
// Verify connected servers section exists
expect(prompt).toContain("Connected MCP Servers")
expect(prompt).toContain("When a server is connected")
// Verify server creation info is included
expect(prompt).toContain("Creating an MCP Server")
expect(prompt).toContain("MCP Server Types and Configuration")
// Verify configuration examples are included
expect(prompt).toContain("Local (Stdio) Server Configuration")
expect(prompt).toContain("Remote (SSE) Server Configuration")
expect(prompt).toContain("Common configuration options for both types")
// Verify example server info is included
expect(prompt).toContain("Example Local MCP Server")
expect(prompt).toContain("create-typescript-server")
})
it("should include MCP server creation info when enabled", async () => {
const mockMcpHub = createMockMcpHub()
const prompt = await SYSTEM_PROMPT(
mockContext,
"/test/path",
false,
mockMcpHub,
undefined,
undefined,
defaultModeSlug,
undefined,
undefined,
undefined,
undefined,
experiments,
true,
)
expect(prompt).toMatchSnapshot()
})
@ -315,10 +270,7 @@ describe("SYSTEM_PROMPT", () => {
true, // enableMcpServerCreation
)
// Should not contain any MCP server related content since mcpHub is undefined
expect(prompt).not.toContain("MCP SERVERS")
expect(prompt).not.toContain("Connected MCP Servers")
expect(prompt).not.toContain("Creating an MCP Server")
expect(prompt).toMatchSnapshot()
})
it("should handle different browser viewport sizes", async () => {
@ -781,18 +733,8 @@ describe("addCustomInstructions", () => {
true, // enableMcpServerCreation
)
// Verify server creation info is included
expect(prompt).toContain("Creating an MCP Server")
expect(prompt).toContain("MCP Server Types and Configuration")
// Verify configuration examples are included
expect(prompt).toContain("Local (Stdio) Server Configuration")
expect(prompt).toContain("Remote (SSE) Server Configuration")
expect(prompt).toContain("Common configuration options for both types")
// Verify example server info is included
expect(prompt).toContain("Example Local MCP Server")
expect(prompt).toContain("create-typescript-server")
expect(prompt).toMatchSnapshot()
})
it("should exclude MCP server creation info when disabled", async () => {
@ -815,23 +757,12 @@ describe("addCustomInstructions", () => {
)
expect(prompt).not.toContain("Creating an MCP Server")
expect(prompt).not.toContain("MCP Server Types and Configuration")
expect(prompt).toContain("MCP SERVERS")
expect(prompt).toContain("Connected MCP Servers")
expect(prompt).toMatchSnapshot()
})
it("should prioritize mode-specific rules for code mode", async () => {
const instructions = await addCustomInstructions("", "", "/test/path", defaultModeSlug)
const rulesSection = instructions.split("\n\n").find((section) => section.startsWith("Rules:"))
expect(rulesSection).toBeDefined()
// Get the rules as separate lines and filter for header lines only
const ruleLines = rulesSection?.split("\n").filter((line) => line.startsWith("#")) ?? []
// Verify we have both rule headers
expect(ruleLines).toHaveLength(2)
expect(ruleLines[0]).toContain(`Rules from .clinerules-${defaultModeSlug}`)
expect(ruleLines[1]).toContain("Rules from .clinerules")
expect(instructions).toMatchSnapshot()
})
it("should prioritize mode-specific rules for ask mode", async () => {

View file

@ -49,10 +49,7 @@ export async function getMcpServersSection(
const baseSection = `MCP SERVERS
The Model Context Protocol (MCP) enables communication between the system and MCP servers that provide additional tools and resources to extend your capabilities. MCP servers can be one of two types:
1. Local (Stdio-based) servers: These run locally on the user's machine and communicate via standard input/output
2. Remote (SSE-based) servers: These run on remote machines and communicate via Server-Sent Events (SSE) over HTTP/HTTPS
The Model Context Protocol (MCP) enables communication between the system and locally running MCP servers that provide additional tools and resources to extend your capabilities.
# Connected MCP Servers
@ -74,51 +71,13 @@ The user may ask you something along the lines of "add a tool" that does some fu
When creating MCP servers, it's important to understand that they operate in a non-interactive environment. The server cannot initiate OAuth flows, open browser windows, or prompt for user input during runtime. All credentials and authentication tokens must be provided upfront through environment variables in the MCP settings configuration. For example, Spotify's API uses OAuth to get a refresh token for the user, but the MCP server cannot initiate this flow. While you can walk the user through obtaining an application client ID and secret, you may have to create a separate one-time setup script (like get-refresh-token.js) that captures and logs the final piece of the puzzle: the user's refresh token (i.e. you might run the script using execute_command which would open a browser for authentication, and then log the refresh token so that you can see it in the command output for you to use in the MCP settings configuration).
Unless the user specifies otherwise, new local MCP servers should be created in: ${await mcpHub.getMcpServersPath()}
Unless the user specifies otherwise, new MCP servers should be created in: ${await mcpHub.getMcpServersPath()}
### MCP Server Types and Configuration
MCP servers can be configured in two ways in the MCP settings file:
1. Local (Stdio) Server Configuration:
\`\`\`json
{
"mcpServers": {
"local-weather": {
"command": "node",
"args": ["/path/to/weather-server/build/index.js"],
"env": {
"OPENWEATHER_API_KEY": "your-api-key"
}
}
}
}
\`\`\`
2. Remote (SSE) Server Configuration:
\`\`\`json
{
"mcpServers": {
"remote-weather": {
"url": "https://api.example.com/mcp",
"headers": {
"Authorization": "Bearer your-api-key"
}
}
}
}
\`\`\`
Common configuration options for both types:
- \`disabled\`: (optional) Set to true to temporarily disable the server
- \`timeout\`: (optional) Maximum time in seconds to wait for server responses (default: 60)
- \`alwaysAllow\`: (optional) Array of tool names that don't require user confirmation
### Example Local MCP Server
### Example MCP Server
For example, if the user wanted to give you the ability to retrieve weather information, you could create an MCP server that uses the OpenWeather API to get weather information, add it to the MCP settings configuration file, and then notice that you now have access to new tools and resources in the system prompt that you might use to show the user your new capabilities.
The following example demonstrates how to build a local MCP server that provides weather data functionality using the Stdio transport. While this example shows how to implement resources, resource templates, and tools, in practice you should prefer using tools since they are more flexible and can handle dynamic parameters. The resource and resource template implementations are included here mainly for demonstration purposes of the different MCP capabilities, but a real weather server would likely just expose tools for fetching weather data. (The following steps are for macOS)
The following example demonstrates how to build an MCP server that provides weather data functionality. While this example shows how to implement resources, resource templates, and tools, in practice you should prefer using tools since they are more flexible and can handle dynamic parameters. The resource and resource template implementations are included here mainly for demonstration purposes of the different MCP capabilities, but a real weather server would likely just expose tools for fetching weather data. (The following steps are for macOS)
1. Use the \`create-typescript-server\` tool to bootstrap a new project in the default MCP servers directory:

View file

@ -1,7 +1,5 @@
import { Client } from "@modelcontextprotocol/sdk/client/index.js"
import { StdioClientTransport, StdioServerParameters } from "@modelcontextprotocol/sdk/client/stdio.js"
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"
import ReconnectingEventSource from "reconnecting-eventsource"
import {
CallToolResultSchema,
ListResourcesResultSchema,
@ -33,40 +31,23 @@ import { arePathsEqual } from "../../utils/path"
export type McpConnection = {
server: McpServer
client: Client
transport: StdioClientTransport | SSEClientTransport
transport: StdioClientTransport
}
// Base configuration schema for common settings
const BaseConfigSchema = z.object({
// StdioServerParameters
const AlwaysAllowSchema = z.array(z.string()).default([])
export const StdioConfigSchema = z.object({
command: z.string(),
args: z.array(z.string()).optional(),
env: z.record(z.string()).optional(),
alwaysAllow: AlwaysAllowSchema.optional(),
disabled: z.boolean().optional(),
timeout: z.number().min(1).max(3600).optional().default(60),
alwaysAllow: z.array(z.string()).default([]),
})
// Server configuration schema with automatic type inference
export const ServerConfigSchema = z.union([
// Stdio config (has command field)
BaseConfigSchema.extend({
command: z.string(),
args: z.array(z.string()).optional(),
env: z.record(z.string()).optional(),
}).transform((data) => ({
...data,
type: "stdio" as const,
})),
// SSE config (has url field)
BaseConfigSchema.extend({
url: z.string().url(),
headers: z.record(z.string()).optional(),
}).transform((data) => ({
...data,
type: "sse" as const,
})),
])
// Settings schema
const McpSettingsSchema = z.object({
mcpServers: z.record(ServerConfigSchema),
mcpServers: z.record(StdioConfigSchema),
})
export class McpHub {
@ -75,7 +56,6 @@ export class McpHub {
private settingsWatcher?: vscode.FileSystemWatcher
private projectMcpWatcher?: vscode.FileSystemWatcher
private fileWatchers: Map<string, FSWatcher> = new Map()
private isDisposed: boolean = false
connections: McpConnection[] = []
isConnecting: boolean = false
@ -267,10 +247,11 @@ export class McpHub {
config: z.infer<typeof ServerConfigSchema>,
source: "global" | "project" = "global",
): Promise<void> {
// Remove existing connection if it exists
await this.deleteConnection(name)
// Remove existing connection if it exists (should never happen, the connection should be deleted beforehand)
this.connections = this.connections.filter((conn) => conn.server.name !== name)
try {
// Each MCP server requires its own transport connection and has unique capabilities, configurations, and error handling. Having separate clients also allows proper scoping of resources/tools and independent server management like reconnection.
const client = new Client(
{
name: "Roo Code",
@ -281,96 +262,60 @@ export class McpHub {
},
)
let transport: StdioClientTransport | SSEClientTransport
const transport = new StdioClientTransport({
command: config.command,
args: config.args,
env: {
...config.env,
...(process.env.PATH ? { PATH: process.env.PATH } : {}),
// ...(process.env.NODE_PATH ? { NODE_PATH: process.env.NODE_PATH } : {}),
},
stderr: "pipe", // necessary for stderr to be available
})
if (config.type === "stdio") {
transport = new StdioClientTransport({
command: config.command,
args: config.args,
env: {
...config.env,
...(process.env.PATH ? { PATH: process.env.PATH } : {}),
},
stderr: "pipe",
})
// Set up stdio specific error handling
transport.onerror = async (error) => {
console.error(`Transport error for "${name}":`, error)
const connection = this.connections.find((conn) => conn.server.name === name)
if (connection) {
connection.server.status = "disconnected"
this.appendErrorMessage(connection, error.message)
}
await this.notifyWebviewOfServerChanges()
}
transport.onclose = async () => {
const connection = this.connections.find((conn) => conn.server.name === name)
if (connection) {
connection.server.status = "disconnected"
}
await this.notifyWebviewOfServerChanges()
}
// transport.stderr is only available after the process has been started. However we can't start it separately from the .connect() call because it also starts the transport. And we can't place this after the connect call since we need to capture the stderr stream before the connection is established, in order to capture errors during the connection process.
// As a workaround, we start the transport ourselves, and then monkey-patch the start method to no-op so that .connect() doesn't try to start it again.
await transport.start()
const stderrStream = transport.stderr
if (stderrStream) {
stderrStream.on("data", async (data: Buffer) => {
const errorOutput = data.toString()
console.error(`Server "${name}" stderr:`, errorOutput)
const connection = this.connections.find((conn) => conn.server.name === name)
if (connection) {
// NOTE: we do not set server status to "disconnected" because stderr logs do not necessarily mean the server crashed or disconnected, it could just be informational. In fact when the server first starts up, it immediately logs "<name> server running on stdio" to stderr.
this.appendErrorMessage(connection, errorOutput)
// Only need to update webview right away if it's already disconnected
if (connection.server.status === "disconnected") {
await this.notifyWebviewOfServerChanges()
}
}
})
} else {
console.error(`No stderr stream for ${name}`)
}
transport.start = async () => {} // No-op now, .connect() won't fail
} else {
// SSE connection
const sseOptions = {
requestInit: {
headers: config.headers,
},
}
// Configure ReconnectingEventSource options
const reconnectingEventSourceOptions = {
max_retry_time: 5000, // Maximum retry time in milliseconds
withCredentials: config.headers?.["Authorization"] ? true : false, // Enable credentials if Authorization header exists
}
global.EventSource = ReconnectingEventSource
transport = new SSEClientTransport(new URL(config.url), {
...sseOptions,
eventSourceInit: reconnectingEventSourceOptions,
})
// Set up SSE specific error handling
transport.onerror = async (error) => {
console.error(`Transport error for "${name}":`, error)
const connection = this.connections.find((conn) => conn.server.name === name)
if (connection) {
connection.server.status = "disconnected"
this.appendErrorMessage(connection, error.message)
}
await this.notifyWebviewOfServerChanges()
transport.onerror = async (error) => {
console.error(`Transport error for "${name}":`, error)
const connection = this.connections.find((conn) => conn.server.name === name)
if (connection) {
connection.server.status = "disconnected"
this.appendErrorMessage(connection, error.message)
}
await this.notifyWebviewOfServerChanges()
}
transport.onclose = async () => {
const connection = this.connections.find((conn) => conn.server.name === name)
if (connection) {
connection.server.status = "disconnected"
}
await this.notifyWebviewOfServerChanges()
}
// If the config is invalid, show an error
if (!StdioConfigSchema.safeParse(config).success) {
console.error(`Invalid config for "${name}": missing or invalid parameters`)
const connection: McpConnection = {
server: {
name,
config: JSON.stringify(config),
status: "disconnected",
error: "Invalid config: missing or invalid parameters",
},
client,
transport,
}
this.connections.push(connection)
return
}
// valid schema
const parsedConfig = StdioConfigSchema.parse(config)
const connection: McpConnection = {
server: {
name,
config: JSON.stringify(config),
status: "connecting",
disabled: config.disabled,
disabled: parsedConfig.disabled,
source,
projectPath: source === "project" ? vscode.workspace.workspaceFolders?.[0]?.uri.fsPath : undefined,
},
@ -379,7 +324,30 @@ export class McpHub {
}
this.connections.push(connection)
// Connect (this will automatically start the transport)
// transport.stderr is only available after the process has been started. However we can't start it separately from the .connect() call because it also starts the transport. And we can't place this after the connect call since we need to capture the stderr stream before the connection is established, in order to capture errors during the connection process.
// As a workaround, we start the transport ourselves, and then monkey-patch the start method to no-op so that .connect() doesn't try to start it again.
await transport.start()
const stderrStream = transport.stderr
if (stderrStream) {
stderrStream.on("data", async (data: Buffer) => {
const errorOutput = data.toString()
console.error(`Server "${name}" stderr:`, errorOutput)
const connection = this.connections.find((conn) => conn.server.name === name)
if (connection) {
// NOTE: we do not set server status to "disconnected" because stderr logs do not necessarily mean the server crashed or disconnected, it could just be informational. In fact when the server first starts up, it immediately logs "<name> server running on stdio" to stderr.
this.appendErrorMessage(connection, errorOutput)
// Only need to update webview right away if it's already disconnected
if (connection.server.status === "disconnected") {
await this.notifyWebviewOfServerChanges()
}
}
})
} else {
console.error(`No stderr stream for ${name}`)
}
transport.start = async () => {} // No-op now, .connect() won't fail
// Connect
await client.connect(transport)
connection.server.status = "connected"
connection.server.error = ""
@ -807,7 +775,7 @@ export class McpHub {
let timeout: number
try {
const parsedConfig = ServerConfigSchema.parse(JSON.parse(connection.server.config))
const parsedConfig = StdioConfigSchema.parse(JSON.parse(connection.server.config))
timeout = (parsedConfig.timeout ?? 60) * 1000
} catch (error) {
console.error("Failed to parse server config for timeout:", error)
@ -869,7 +837,6 @@ export class McpHub {
}
async dispose(): Promise<void> {
this.isDisposed = true
this.removeAllFileWatchers()
for (const connection of this.connections) {
try {

View file

@ -2,7 +2,7 @@ import type { McpHub as McpHubType } from "../McpHub"
import type { ClineProvider } from "../../../core/webview/ClineProvider"
import type { ExtensionContext, Uri } from "vscode"
import type { McpConnection } from "../McpHub"
import { ServerConfigSchema } from "../McpHub"
import { StdioConfigSchema } from "../McpHub"
const fs = require("fs/promises")
const { McpHub } = require("../McpHub")
@ -71,7 +71,6 @@ describe("McpHub", () => {
JSON.stringify({
mcpServers: {
"test-server": {
type: "stdio",
command: "node",
args: ["test.js"],
alwaysAllow: ["allowed-tool"],
@ -88,7 +87,6 @@ describe("McpHub", () => {
const mockConfig = {
mcpServers: {
"test-server": {
type: "stdio",
command: "node",
args: ["test.js"],
alwaysAllow: [],
@ -111,7 +109,6 @@ describe("McpHub", () => {
const mockConfig = {
mcpServers: {
"test-server": {
type: "stdio",
command: "node",
args: ["test.js"],
alwaysAllow: ["existing-tool"],
@ -134,7 +131,6 @@ describe("McpHub", () => {
const mockConfig = {
mcpServers: {
"test-server": {
type: "stdio",
command: "node",
args: ["test.js"],
},
@ -159,7 +155,6 @@ describe("McpHub", () => {
const mockConfig = {
mcpServers: {
"test-server": {
type: "stdio",
command: "node",
args: ["test.js"],
disabled: false,
@ -299,21 +294,20 @@ describe("McpHub", () => {
it("should validate timeout values", () => {
// Test valid timeout values
const validConfig = {
type: "stdio",
command: "test",
timeout: 60,
}
expect(() => ServerConfigSchema.parse(validConfig)).not.toThrow()
expect(() => StdioConfigSchema.parse(validConfig)).not.toThrow()
// Test invalid timeout values
const invalidConfigs = [
{ type: "stdio", command: "test", timeout: 0 }, // Too low
{ type: "stdio", command: "test", timeout: 3601 }, // Too high
{ type: "stdio", command: "test", timeout: -1 }, // Negative
{ command: "test", timeout: 0 }, // Too low
{ command: "test", timeout: 3601 }, // Too high
{ command: "test", timeout: -1 }, // Negative
]
invalidConfigs.forEach((config) => {
expect(() => ServerConfigSchema.parse(config)).toThrow()
expect(() => StdioConfigSchema.parse(config)).toThrow()
})
})
@ -321,7 +315,7 @@ describe("McpHub", () => {
const mockConnection: McpConnection = {
server: {
name: "test-server",
config: JSON.stringify({ type: "stdio", command: "test" }), // No timeout specified
config: JSON.stringify({ command: "test" }), // No timeout specified
status: "connected",
},
client: {
@ -344,7 +338,7 @@ describe("McpHub", () => {
const mockConnection: McpConnection = {
server: {
name: "test-server",
config: JSON.stringify({ type: "stdio", command: "test", timeout: 120 }), // 2 minutes
config: JSON.stringify({ command: "test", timeout: 120 }), // 2 minutes
status: "connected",
},
client: {
@ -369,7 +363,6 @@ describe("McpHub", () => {
const mockConfig = {
mcpServers: {
"test-server": {
type: "stdio",
command: "node",
args: ["test.js"],
timeout: 60,
@ -392,7 +385,6 @@ describe("McpHub", () => {
const mockConfig = {
mcpServers: {
"test-server": {
type: "stdio",
command: "node",
args: ["test.js"],
timeout: 60,
@ -414,7 +406,6 @@ describe("McpHub", () => {
server: {
name: "test-server",
config: JSON.stringify({
type: "stdio",
command: "node",
args: ["test.js"],
timeout: 3601, // Invalid timeout
@ -444,7 +435,6 @@ describe("McpHub", () => {
const mockConfig = {
mcpServers: {
"test-server": {
type: "stdio",
command: "node",
args: ["test.js"],
timeout: 60,
@ -468,7 +458,6 @@ describe("McpHub", () => {
const mockConfig = {
mcpServers: {
"test-server": {
type: "stdio",
command: "node",
args: ["test.js"],
timeout: 60,

View file

@ -246,6 +246,8 @@ export interface MessageContent {
export type BedrockModelId = keyof typeof bedrockModels
export const bedrockDefaultModelId: BedrockModelId = "anthropic.claude-3-7-sonnet-20250219-v1:0"
export const bedrockDefaultPromptRouterModelId: BedrockModelId = "anthropic.claude-3-sonnet-20240229-v1:0"
// March, 12 2025 - updated prices to match US-West-2 list price shown at https://aws.amazon.com/bedrock/pricing/
// including older models that are part of the default prompt routers AWS enabled for GA of the promot router feature
export const bedrockModels = {

View file

@ -267,10 +267,11 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
<div
style={{
cursor: "pointer",
color: "var(--vscode-textLink-foreground)",
color: "var(--vscode-badge-foreground)",
fontSize: "11px",
marginLeft: "auto",
textAlign: "right",
paddingRight: 2,
paddingRight: 8,
}}
onClick={() => setIsTextExpanded(!isTextExpanded)}>
See less