mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
Merge main into fix/deepseek-v3.1-terminus-reasoning
Resolved merge conflict by combining: - DeepSeek V3.1 Terminus chat_template_kwargs support from PR - Tool metadata support from main branch
This commit is contained in:
commit
4eb98a7285
381 changed files with 14643 additions and 12799 deletions
|
|
@ -18,14 +18,17 @@ fi
|
|||
|
||||
$pnpm_cmd run check-types
|
||||
|
||||
# Load .env.local if it exists
|
||||
# Use dotenvx to securely load .env.local and run commands that depend on it
|
||||
if [ -f ".env.local" ]; then
|
||||
export $(grep -v '^#' .env.local | xargs)
|
||||
fi
|
||||
|
||||
# Run tests if RUN_TESTS_ON_PUSH is set to true
|
||||
if [ "$RUN_TESTS_ON_PUSH" = "true" ]; then
|
||||
$pnpm_cmd run test
|
||||
# Check if RUN_TESTS_ON_PUSH is set to true and run tests with dotenvx
|
||||
if npx dotenvx get RUN_TESTS_ON_PUSH -f .env.local 2>/dev/null | grep -q "^true$"; then
|
||||
npx dotenvx run -f .env.local -- $pnpm_cmd run test
|
||||
fi
|
||||
else
|
||||
# Fallback: run tests if RUN_TESTS_ON_PUSH is set in regular environment
|
||||
if [ "$RUN_TESTS_ON_PUSH" = "true" ]; then
|
||||
$pnpm_cmd run test
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check for new changesets.
|
||||
|
|
|
|||
|
|
@ -16,14 +16,14 @@ argument-hint: patch | minor | major
|
|||
[list of changes]
|
||||
```
|
||||
|
||||
- Always include contributor attribution using format: (thanks @username!)
|
||||
- For PRs that close issues, also include the issue number and reporter: "- Fix: Description (#123 by @reporter, PR by @contributor)"
|
||||
- For PRs without linked issues, use the standard format: "- Add support for feature (thanks @contributor!)"
|
||||
- Always include contributor attribution and the PR number: use "(PR #<prNumber> by @username)".
|
||||
- For PRs that close issues, include both the issue number and the PR number and authors: "- Fix: Description (#123 by @reporter, PR #456 by @contributor)"
|
||||
- For PRs without linked issues, include the PR number and author: "- Add support for feature (PR #456 by @contributor)"
|
||||
- Provide brief descriptions of each item to explain the change
|
||||
- Order the list from most important to least important
|
||||
- Example formats:
|
||||
- With issue: "- Fix: Resolve memory leak in extension (#456 by @issueReporter, PR by @prAuthor)"
|
||||
- Without issue: "- Add support for Gemini 2.5 Pro caching (thanks @contributor!)"
|
||||
- With issue: "- Fix: Resolve memory leak in extension (#456 by @issueReporter, PR #789 by @prAuthor)"
|
||||
- Without issue: "- Add support for Gemini 2.5 Pro caching (PR #789 by @contributor)"
|
||||
- CRITICAL: Include EVERY SINGLE PR in the changeset - don't assume you know which ones are important. Count the total PRs to verify completeness and cross-reference the list to ensure nothing is missed.
|
||||
|
||||
6. If the generate_image tool is available, create a release image at `releases/[version]-release.png`
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
Then retrieve the issue:
|
||||
|
||||
<execute_command>
|
||||
<command>gh issue view [issue-number] --repo [owner]/[repo] --json number,title,body,state,labels,assignees,milestone,createdAt,updatedAt,closedAt,author</command>
|
||||
<command>gh api repos/[owner]/[repo]/issues/[issue-number] --jq '{number,title,body,state,labels,assignees,milestone,createdAt:.created_at,updatedAt:.updated_at,closedAt:.closed_at,author:.user.login}'</command>
|
||||
</execute_command>
|
||||
|
||||
If the command fails with an authentication error (e.g., "gh: Not authenticated" or "HTTP 401"), ask the user to authenticate:
|
||||
|
|
@ -49,7 +49,7 @@
|
|||
- Any decisions or changes to requirements
|
||||
|
||||
<execute_command>
|
||||
<command>gh issue view [issue number] --repo [owner]/[repo] --comments</command>
|
||||
<command>gh api repos/[owner]/[repo]/issues/[issue-number]/comments --paginate --jq '.[].body'</command>
|
||||
</execute_command>
|
||||
|
||||
Also check for:
|
||||
|
|
|
|||
|
|
@ -29,23 +29,23 @@
|
|||
|
||||
<primary_commands>
|
||||
<command name="gh_issue_view">
|
||||
<purpose>Retrieve the issue details at the start</purpose>
|
||||
<purpose>Retrieve the issue details at the start using the REST Issues API.</purpose>
|
||||
<when>Always use first to get the full issue content</when>
|
||||
<syntax>gh issue view [issue-number] --repo [owner]/[repo] --json number,title,body,state,labels,assignees,milestone,createdAt,updatedAt,closedAt,author</syntax>
|
||||
<syntax>gh api repos/[owner]/[repo]/issues/[issue-number] --jq '{number,title,body,state,labels,assignees,milestone,createdAt:.created_at,updatedAt:.updated_at,closedAt:.closed_at,author:.user.login}'</syntax>
|
||||
<example>
|
||||
<execute_command>
|
||||
<command>gh issue view 123 --repo octocat/hello-world --json number,title,body,state,labels,assignees,milestone,createdAt,updatedAt,closedAt,author</command>
|
||||
<command>gh api repos/octocat/hello-world/issues/123 --jq '{number,title,body,state,labels,assignees,milestone,createdAt:.created_at,updatedAt:.updated_at,closedAt:.closed_at,author:.user.login}'</command>
|
||||
</execute_command>
|
||||
</example>
|
||||
</command>
|
||||
|
||||
<command name="gh_issue_comments">
|
||||
<purpose>Get additional context and requirements from issue comments</purpose>
|
||||
<purpose>Get additional context and requirements from issue comments.</purpose>
|
||||
<when>Always use after viewing issue to see full discussion</when>
|
||||
<syntax>gh issue view [issue-number] --repo [owner]/[repo] --comments</syntax>
|
||||
<syntax>gh api repos/[owner]/[repo]/issues/[issue-number]/comments --paginate --jq '.[].body'</syntax>
|
||||
<example>
|
||||
<execute_command>
|
||||
<command>gh issue view 123 --repo octocat/hello-world --comments</command>
|
||||
<command>gh api repos/octocat/hello-world/issues/123/comments --paginate --jq '.[].body'</command>
|
||||
</execute_command>
|
||||
</example>
|
||||
</command>
|
||||
|
|
@ -109,6 +109,30 @@
|
|||
</command>
|
||||
</optional_commands>
|
||||
|
||||
<projects_v2_commands>
|
||||
<command name="gh_projects_v2_for_issue">
|
||||
<purpose>Inspect associations with GitHub Projects (new Projects experience) for a given issue</purpose>
|
||||
<when>Use when project context is relevant to understanding priority, ownership, or workflow</when>
|
||||
<syntax>gh api graphql -f query='
|
||||
query($owner:String!, $repo:String!, $number:Int!) {
|
||||
repository(owner:$owner, name:$repo) {
|
||||
issue(number:$number) {
|
||||
projectsV2(first:20) {
|
||||
nodes {
|
||||
title
|
||||
url
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
' -F owner=[owner] -F repo=[repo] -F number=[issue-number]</syntax>
|
||||
<note>
|
||||
This uses the projectsV2 field from the new GitHub Projects experience for issue-level project context.
|
||||
</note>
|
||||
</command>
|
||||
</projects_v2_commands>
|
||||
|
||||
<pull_request_commands>
|
||||
<command name="gh_pr_create">
|
||||
<purpose>Create a pull request</purpose>
|
||||
|
|
|
|||
71
CHANGELOG.md
71
CHANGELOG.md
|
|
@ -1,5 +1,76 @@
|
|||
# Roo Code Changelog
|
||||
|
||||
## [3.32.1] - 2025-11-14
|
||||
|
||||

|
||||
|
||||
- Fix: Add abort controller for request cancellation in OpenAI native protocol (PR #9276 by @daniel-lxs)
|
||||
- Fix: Resolve duplicate tool blocks causing 'tool has already been used' error in native protocol mode (PR #9275 by @daniel-lxs)
|
||||
- Fix: Prevent duplicate tool_result blocks in native protocol mode for read_file (PR #9272 by @daniel-lxs)
|
||||
- Fix: Correct OpenAI Native handling of encrypted reasoning blocks to prevent errors during condensing (PR #9263 by @hannesrudolph)
|
||||
- Fix: Disable XML parser for native tool protocol to prevent parsing conflicts (PR #9277 by @daniel-lxs)
|
||||
|
||||
## [3.32.0] - 2025-11-14
|
||||
|
||||

|
||||
|
||||
- Feature: Add GPT-5.1 models to OpenAI provider (PR #9252 by @hannesrudolph)
|
||||
- Feature: Support for OpenAI Responses 24 hour prompt caching (PR #9259 by @hannesrudolph)
|
||||
- Fix: Repair the share button in the UI (PR #9253 by @hannesrudolph)
|
||||
- Docs: Include PR numbers in the release guide to improve traceability (PR #9236 by @hannesrudolph)
|
||||
|
||||
## [3.31.3] - 2025-11-13
|
||||
|
||||

|
||||
|
||||
- Fix: OpenAI Native encrypted_content handling and remove gpt-5-chat-latest verbosity flag (#9225 by @politsin, PR by @hannesrudolph)
|
||||
- Fix: Roo Code Cloud provider Anthropic input token normalization to avoid double-counting (thanks @hannesrudolph!)
|
||||
- Refactor: Rename sliding-window to context-management and truncateConversationIfNeeded to manageContext (thanks @hannesrudolph!)
|
||||
|
||||
## [3.31.2] - 2025-11-12
|
||||
|
||||
- Fix: Apply updated API profile settings when provider/model unchanged (#9208 by @hannesrudolph, PR by @hannesrudolph)
|
||||
- Migrate conversation continuity to plugin-side encrypted reasoning items using Responses API for improved reliability (thanks @hannesrudolph!)
|
||||
- Fix: Include mcpServers in getState() for auto-approval (#9190 by @bozoweed, PR by @daniel-lxs)
|
||||
- Batch settings updates from the webview to the extension host for improved performance (thanks @cte!)
|
||||
- Fix: Replace rate-limited badges with badgen.net to improve README reliability (thanks @daniel-lxs!)
|
||||
|
||||
## [3.31.1] - 2025-11-11
|
||||
|
||||

|
||||
|
||||
- Fix: Prevent command_output ask from blocking in cloud/headless environments (thanks @daniel-lxs!)
|
||||
- Add IPC command for sending messages to the current task (thanks @mrubens!)
|
||||
- Fix: Model switch re-applies selected profile, ensuring task configuration stays in sync (#9179 by @hannesrudolph, PR by @hannesrudolph)
|
||||
- Move auto-approval logic from `ChatView` to `Task` for better architecture (thanks @cte!)
|
||||
- Add custom Button component with variant system (thanks @brunobergher!)
|
||||
|
||||
## [3.31.0] - 2025-11-07
|
||||
|
||||

|
||||
|
||||
- Improvements to to-do lists and task headers (thanks @brunobergher!)
|
||||
- Fix: Prevent crash when streaming chunks have null choices array (thanks @daniel-lxs!)
|
||||
- Fix: Prevent context condensing on settings save when provider/model unchanged (#4430 by @hannesrudolph, PR by @daniel-lxs)
|
||||
- Fix: Respect custom OpenRouter URL for all API operations (#8947 by @sstraus, PR by @roomote)
|
||||
- Add comprehensive error logging to Roo Cloud provider (thanks @daniel-lxs!)
|
||||
- UX: Less caffeinated kangaroo (thanks @brunobergher!)
|
||||
|
||||
## [3.30.3] - 2025-11-06
|
||||
|
||||

|
||||
|
||||
- Feat: Add kimi-k2-thinking model to Moonshot provider (thanks @daniel-lxs!)
|
||||
- Fix: Auto-retry on empty assistant response to prevent task failures (#9076 by @Akillatech, PR by @daniel-lxs)
|
||||
- Fix: Use system role for OpenAI Compatible provider when streaming is disabled (#8215 by @whitfin, PR by @roomote)
|
||||
- Fix: Prevent notification sound on attempt_completion with queued messages (#8537 by @hannesrudolph, PR by @roomote)
|
||||
- Feat: Auto-switch to imported mode with architect fallback for better mode detection (#8239 by @hannesrudolph, PR by @daniel-lxs)
|
||||
- Feat: Add MiniMax-M2-Stable model and enable prompt caching (#9070 by @nokaka, PR by @roomote)
|
||||
- Feat: Improve diff appearance in main chat view (thanks @hannesrudolph!)
|
||||
- UX: Home screen visuals (thanks @brunobergher!)
|
||||
- Docs: Clarify that setting 0 disables Error & Repetition Limit (thanks @roomote!)
|
||||
- Chore: Update dependency @changesets/cli to v2.29.7 (thanks @renovate!)
|
||||
|
||||
## [3.30.2] - 2025-11-05
|
||||
|
||||

|
||||
|
|
|
|||
16
README.md
16
README.md
|
|
@ -1,5 +1,7 @@
|
|||
<p align="center">
|
||||
<a href="https://marketplace.visualstudio.com/items?itemName=RooVeterinaryInc.roo-cline"><img src="https://img.shields.io/visual-studio-marketplace/v/RooVeterinaryInc.roo-cline.svg?label=VS%20Code&color=%23007ACC&style=flat&logo=visualstudiocode&logoColor=white" alt="VS Code"></a>
|
||||
<a href="https://marketplace.visualstudio.com/items?itemName=RooVeterinaryInc.roo-cline"><img src="https://badgen.net/vs-marketplace/v/RooVeterinaryInc.roo-cline?label=VS%20Code&color=007ACC" alt="VS Code"></a>
|
||||
<a href="https://marketplace.visualstudio.com/items?itemName=RooVeterinaryInc.roo-cline"><img src="https://badgen.net/vs-marketplace/i/RooVeterinaryInc.roo-cline?label=Installs&color=007ACC" alt="Installs"></a>
|
||||
<a href="https://marketplace.visualstudio.com/items?itemName=RooVeterinaryInc.roo-cline"><img src="https://badgen.net/vs-marketplace/rating/RooVeterinaryInc.roo-cline?label=Rating&color=007ACC" alt="Rating"></a>
|
||||
<a href="https://x.com/roocode"><img src="https://img.shields.io/badge/roocode-000000?style=flat&logo=x&logoColor=white" alt="X"></a>
|
||||
<a href="https://youtube.com/@roocodeyt?feature=shared"><img src="https://img.shields.io/badge/YouTube-FF0000?style=flat&logo=youtube&logoColor=white" alt="YouTube"></a>
|
||||
<a href="https://discord.gg/roocode"><img src="https://img.shields.io/badge/Join%20Discord-5865F2?style=flat&logo=discord&logoColor=white" alt="Join Discord"></a>
|
||||
|
|
@ -35,7 +37,7 @@
|
|||
- [简体中文](locales/zh-CN/README.md)
|
||||
- [繁體中文](locales/zh-TW/README.md)
|
||||
- ...
|
||||
</details>
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -167,16 +169,6 @@ We love community contributions! Get started by reading our [CONTRIBUTING.md](CO
|
|||
|
||||
---
|
||||
|
||||
## Contributors
|
||||
|
||||
Thanks to all our contributors who have helped make Roo Code better!
|
||||
|
||||
<!-- START CONTRIBUTORS SECTION - AUTO-GENERATED, DO NOT EDIT MANUALLY -->
|
||||
|
||||
[](https://github.com/RooCodeInc/roo-code/graphs/contributors)
|
||||
|
||||
<!-- END CONTRIBUTORS SECTION -->
|
||||
|
||||
## License
|
||||
|
||||
[Apache 2.0 © 2025 Roo Code, Inc.](./LICENSE)
|
||||
|
|
|
|||
|
|
@ -10,10 +10,10 @@ export const content: AgentPageContent = {
|
|||
agentName: "PR Reviewer",
|
||||
hero: {
|
||||
icon: "GitPullRequest",
|
||||
heading: "Get comprehensive code reviews that save you time, not tokens.",
|
||||
heading: "Code reviews that catch what other AI tools (and most humans) miss.",
|
||||
paragraphs: [
|
||||
"Regular AI code review tools cap model usage to protect their margins from fixed monthly prices. That leads to shallow prompts, limited context, and missed issues.",
|
||||
"Roo Code's PR Reviewer flips the script: you bring your own key and leverage it to the max – to find real issues, increase code quality and keep your pull request queue moving.",
|
||||
"Run-of-the-mill, token-saving AI code review tools will surely catch syntax errors and style issues, but they'll usually miss the bugs that actually matter: logic flaws, security vulnerabilities, and misunderstood requirements.",
|
||||
"Roo Code's PR Reviewer uses advanced reasoning models and full repository context to find the issues that slip through—before they reach production.",
|
||||
],
|
||||
image: {
|
||||
url: hero.src,
|
||||
|
|
@ -60,34 +60,33 @@ export const content: AgentPageContent = {
|
|||
],
|
||||
},
|
||||
whyBetter: {
|
||||
heading: "Why Roo's PR Reviewer is so much better",
|
||||
heading: "Why Roo's PR Reviewer is different",
|
||||
features: [
|
||||
{
|
||||
title: "Our agents, your provider keys",
|
||||
title: "Bring your own key, get uncompromised reviews",
|
||||
paragraphs: [
|
||||
"We orchestrate the review, optimize the hell out of the prompts, integrate with GitHub, keep you properly posted.",
|
||||
"We're thoughtful about token usage, but not incentivized to skimp to grow our margins.",
|
||||
"Most AI review tools use fixed pricing, which means they skimp on tokens to protect their margins. That leads to shallow analysis and missed issues.",
|
||||
"With Roo, you bring your own API key. We optimize prompts for depth, not cost-cutting, so reviews focus on real problems like business logic, security vulnerabilities, and architectural issues.",
|
||||
],
|
||||
icon: "Blocks",
|
||||
},
|
||||
{
|
||||
title: "Advanced reasoning and workflows",
|
||||
title: "Advanced reasoning that understands what matters",
|
||||
description:
|
||||
"We optimize for state-of-the-art reasoning models and leverage powerful workflows (Diff analysis → Context Gathering → Impact Mapping → Contract checks) to produce crisp, actionable comments at the right level.",
|
||||
"We leverage state-of-the-art reasoning models with sophisticated workflows: diff analysis, context gathering, impact mapping, and contract validation. This catches the subtle bugs that surface-level tools miss—misunderstood requirements, edge cases, and integration risks.",
|
||||
icon: "ListChecks",
|
||||
},
|
||||
{
|
||||
title: "Fully repository-aware",
|
||||
title: "Repository-aware, not snippet-aware",
|
||||
description:
|
||||
"Reviews traverse code ownership, dependency graphs, and historical patterns to surface risk and deviations, not noise.",
|
||||
"Roo analyzes your entire codebase context—dependency graphs, code ownership, team conventions, and historical patterns. It understands how changes interact with existing systems, not just whether individual lines look correct.",
|
||||
icon: "BookMarked",
|
||||
},
|
||||
],
|
||||
},
|
||||
cta: {
|
||||
heading: "Stop wasting time.",
|
||||
description:
|
||||
"Give Roo Code's PR Reviewer your model key and turn painful reviews into a tangible quality advantage.",
|
||||
heading: "Ready for better code reviews?",
|
||||
description: "Start finding the issues that matter with AI-powered reviews built for depth, not cost-cutting.",
|
||||
buttonText: "Start 14-day Free Trial",
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ import { Button } from "@/components/ui"
|
|||
import { AnimatedBackground } from "@/components/homepage"
|
||||
import { AgentCarousel } from "@/components/reviewer/agent-carousel"
|
||||
import { EXTERNAL_LINKS } from "@/lib/constants"
|
||||
import { trackGoogleAdsConversion } from "@/lib/analytics/google-ads"
|
||||
import { type AgentPageContent, type IconName } from "./agent-page-content"
|
||||
|
||||
/**
|
||||
|
|
@ -47,13 +46,13 @@ export function AgentLandingContent({ content }: { content: AgentPageContent })
|
|||
return (
|
||||
<>
|
||||
{/* Hero Section */}
|
||||
<section className="relative flex md:h-[calc(70vh-theme(spacing.12))] items-center overflow-hidden">
|
||||
<section className="relative flex min-h-screen md:min-h-[calc(70vh-theme(spacing.12))] items-center overflow-hidden py-12 md:py-0">
|
||||
<AnimatedBackground />
|
||||
<div className="container relative flex items-center h-full z-10 mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="grid h-full relative gap-4 md:gap-20 lg:grid-cols-2">
|
||||
<div className="flex flex-col px-4 justify-center space-y-6 sm:space-y-8">
|
||||
<div className="grid h-full relative gap-8 md:gap-12 lg:gap-20 lg:grid-cols-2">
|
||||
<div className="flex flex-col justify-center space-y-6 sm:space-y-8">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight mt-8 md:text-left md:text-4xl lg:text-5xl lg:mt-0">
|
||||
<h1 className="text-3xl font-bold tracking-tight md:text-left md:text-4xl lg:text-5xl">
|
||||
{content.hero.icon &&
|
||||
(() => {
|
||||
const Icon = getIcon(content.hero.icon)
|
||||
|
|
@ -62,7 +61,7 @@ export function AgentLandingContent({ content }: { content: AgentPageContent })
|
|||
{content.hero.heading}
|
||||
</h1>
|
||||
|
||||
<div className="mt-4 max-w-lg space-y-4 text-base text-muted-foreground md:text-left sm:mt-6">
|
||||
<div className="mt-4 max-w-full lg:max-w-lg space-y-4 text-base text-muted-foreground md:text-left sm:mt-6">
|
||||
{content.hero.paragraphs.map((paragraph, index) => (
|
||||
<p key={index}>{paragraph}</p>
|
||||
))}
|
||||
|
|
@ -97,7 +96,6 @@ export function AgentLandingContent({ content }: { content: AgentPageContent })
|
|||
href={EXTERNAL_LINKS.CLOUD_APP_SIGNUP_PRO}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={trackGoogleAdsConversion}
|
||||
className="flex w-full items-center justify-center">
|
||||
{content.hero.cta.buttonText}
|
||||
<ArrowRight className="ml-2" />
|
||||
|
|
@ -110,23 +108,16 @@ export function AgentLandingContent({ content }: { content: AgentPageContent })
|
|||
</div>
|
||||
|
||||
{content.hero.image && (
|
||||
<div className="flex items-center justify-end mx-auto h-full mt-8 lg:mt-0">
|
||||
<div
|
||||
className="relative overflow-clip"
|
||||
style={{
|
||||
width: `${content.hero.image.width}px`,
|
||||
height: `${content.hero.image.height}px`,
|
||||
maxWidth: "100%",
|
||||
}}>
|
||||
<div className="block">
|
||||
<Image
|
||||
src={content.hero.image.url}
|
||||
alt={content.hero.image.alt || "Hero image"}
|
||||
className="max-w-full h-auto"
|
||||
width={content.hero.image.width}
|
||||
height={content.hero.image.height}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-center lg:justify-end mx-auto h-full w-full">
|
||||
<div className="relative w-full max-w-full overflow-hidden rounded-lg">
|
||||
<Image
|
||||
src={content.hero.image.url}
|
||||
alt={content.hero.image.alt || "Hero image"}
|
||||
className="w-full h-auto"
|
||||
width={content.hero.image.width}
|
||||
height={content.hero.image.height}
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -226,7 +217,6 @@ export function AgentLandingContent({ content }: { content: AgentPageContent })
|
|||
href={EXTERNAL_LINKS.CLOUD_APP_SIGNUP_PRO}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={trackGoogleAdsConversion}
|
||||
className="flex items-center justify-center">
|
||||
{content.cta.buttonText}
|
||||
<ArrowRight className="ml-2 h-4 w-4" />
|
||||
|
|
|
|||
|
|
@ -1,60 +0,0 @@
|
|||
"use client"
|
||||
|
||||
import { useEffect, useState } from "react"
|
||||
import Script from "next/script"
|
||||
import { hasConsent, onConsentChange } from "@/lib/analytics/consent-manager"
|
||||
|
||||
// Google Tag Manager ID
|
||||
const GTM_ID = "AW-17391954825"
|
||||
|
||||
/**
|
||||
* Google Analytics Provider
|
||||
* Implements Google's standard gtag.js loading pattern
|
||||
*/
|
||||
export function GoogleAnalyticsProvider({ children }: { children: React.ReactNode }) {
|
||||
const [shouldLoad, setShouldLoad] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
// Check initial consent status
|
||||
if (hasConsent()) {
|
||||
setShouldLoad(true)
|
||||
}
|
||||
|
||||
// Listen for consent changes
|
||||
const unsubscribe = onConsentChange((consented) => {
|
||||
if (consented) {
|
||||
setShouldLoad(true)
|
||||
}
|
||||
})
|
||||
|
||||
return unsubscribe
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
{shouldLoad && (
|
||||
<>
|
||||
{/* Google tag (gtag.js) */}
|
||||
<Script src={`https://www.googletagmanager.com/gtag/js?id=${GTM_ID}`} strategy="afterInteractive" />
|
||||
<Script id="google-analytics" strategy="afterInteractive">
|
||||
{`
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
gtag('config', '${GTM_ID}');
|
||||
`}
|
||||
</Script>
|
||||
</>
|
||||
)}
|
||||
{children}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
// Declare global types for TypeScript
|
||||
declare global {
|
||||
interface Window {
|
||||
dataLayer: unknown[]
|
||||
gtag: (...args: unknown[]) => void
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
"use client"
|
||||
|
||||
import { useEffect, useState } from "react"
|
||||
import Script from "next/script"
|
||||
import { hasConsent, onConsentChange } from "@/lib/analytics/consent-manager"
|
||||
|
||||
// Google Tag Manager Container ID
|
||||
const GTM_ID = "GTM-M2JZHV8N"
|
||||
|
||||
/**
|
||||
* Google Tag Manager Provider
|
||||
* Loads GTM only after user consent is given, following GDPR requirements
|
||||
*/
|
||||
export function GoogleTagManagerProvider({ children }: { children: React.ReactNode }) {
|
||||
const [shouldLoad, setShouldLoad] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
// Check initial consent status
|
||||
if (hasConsent()) {
|
||||
setShouldLoad(true)
|
||||
}
|
||||
|
||||
// Listen for consent changes
|
||||
const unsubscribe = onConsentChange((consented) => {
|
||||
if (consented) {
|
||||
setShouldLoad(true)
|
||||
}
|
||||
})
|
||||
|
||||
return unsubscribe
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
{shouldLoad && (
|
||||
<>
|
||||
{/* Google Tag Manager Script */}
|
||||
<Script
|
||||
id="google-tag-manager"
|
||||
strategy="afterInteractive"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
|
||||
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
|
||||
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
|
||||
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
|
||||
})(window,document,'script','dataLayer','${GTM_ID}');
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
{/* Google Tag Manager (noscript) */}
|
||||
<noscript>
|
||||
<iframe
|
||||
src={`https://www.googletagmanager.com/ns.html?id=${GTM_ID}`}
|
||||
height="0"
|
||||
width="0"
|
||||
style={{ display: "none", visibility: "hidden" }}
|
||||
/>
|
||||
</noscript>
|
||||
</>
|
||||
)}
|
||||
{children}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -3,21 +3,21 @@
|
|||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
|
||||
import { ThemeProvider } from "next-themes"
|
||||
|
||||
import { GoogleTagManagerProvider } from "./google-tag-manager-provider"
|
||||
import { PostHogProvider } from "./posthog-provider"
|
||||
import { GoogleAnalyticsProvider } from "./google-analytics-provider"
|
||||
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
export const Providers = ({ children }: { children: React.ReactNode }) => {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<GoogleAnalyticsProvider>
|
||||
<GoogleTagManagerProvider>
|
||||
<PostHogProvider>
|
||||
<ThemeProvider attribute="class" defaultTheme="dark" enableSystem={false}>
|
||||
{children}
|
||||
</ThemeProvider>
|
||||
</PostHogProvider>
|
||||
</GoogleAnalyticsProvider>
|
||||
</GoogleTagManagerProvider>
|
||||
</QueryClientProvider>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +0,0 @@
|
|||
/**
|
||||
* Google Ads conversion tracking utilities
|
||||
*/
|
||||
|
||||
/**
|
||||
* Track a Google Ads conversion event
|
||||
* This should only be called after user consent has been given
|
||||
*/
|
||||
export function trackGoogleAdsConversion() {
|
||||
if (typeof window !== "undefined" && window.gtag) {
|
||||
window.gtag("event", "conversion", {
|
||||
send_to: "AW-17391954825/VtOZCJe_77MbEInXkOVA",
|
||||
value: 10.0,
|
||||
currency: "USD",
|
||||
})
|
||||
}
|
||||
}
|
||||
12
locales/ca/README.md
generated
12
locales/ca/README.md
generated
|
|
@ -35,7 +35,7 @@
|
|||
- [简体中文](../zh-CN/README.md)
|
||||
- [繁體中文](../zh-TW/README.md)
|
||||
- ...
|
||||
</details>
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -167,16 +167,6 @@ Ens encanten les contribucions de la comunitat! Comença llegint el nostre [CONT
|
|||
|
||||
---
|
||||
|
||||
## Col·laboradors
|
||||
|
||||
Gràcies a tots els nostres col·laboradors que han ajudat a millorar Roo Code!
|
||||
|
||||
<!-- START CONTRIBUTORS SECTION - AUTO-GENERATED, DO NOT EDIT MANUALLY -->
|
||||
|
||||
[](https://github.com/RooCodeInc/roo-code/graphs/contributors)
|
||||
|
||||
<!-- END CONTRIBUTORS SECTION -->
|
||||
|
||||
## Llicència
|
||||
|
||||
[Apache 2.0 © 2025 Roo Code, Inc.](../../LICENSE)
|
||||
|
|
|
|||
12
locales/de/README.md
generated
12
locales/de/README.md
generated
|
|
@ -35,7 +35,7 @@
|
|||
- [简体中文](../zh-CN/README.md)
|
||||
- [繁體中文](../zh-TW/README.md)
|
||||
- ...
|
||||
</details>
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -167,16 +167,6 @@ Wir lieben Community-Beiträge! Lies unsere [CONTRIBUTING.md](CONTRIBUTING.md),
|
|||
|
||||
---
|
||||
|
||||
## Mitwirkende
|
||||
|
||||
Danke an alle unsere Mitwirkenden, die geholfen haben, Roo Code besser zu machen!
|
||||
|
||||
<!-- START CONTRIBUTORS SECTION - AUTO-GENERATED, DO NOT EDIT MANUALLY -->
|
||||
|
||||
[](https://github.com/RooCodeInc/roo-code/graphs/contributors)
|
||||
|
||||
<!-- END CONTRIBUTORS SECTION -->
|
||||
|
||||
## Lizenz
|
||||
|
||||
[Apache 2.0 © 2025 Roo Code, Inc.](../../LICENSE)
|
||||
|
|
|
|||
12
locales/es/README.md
generated
12
locales/es/README.md
generated
|
|
@ -35,7 +35,7 @@
|
|||
- [简体中文](../zh-CN/README.md)
|
||||
- [繁體中文](../zh-TW/README.md)
|
||||
- ...
|
||||
</details>
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -167,16 +167,6 @@ Usamos [changesets](https://github.com/changesets/changesets) para el versionado
|
|||
|
||||
---
|
||||
|
||||
## Colaboradores
|
||||
|
||||
¡Gracias a todos nuestros colaboradores que han ayudado a mejorar Roo Code!
|
||||
|
||||
<!-- START CONTRIBUTORS SECTION - AUTO-GENERATED, DO NOT EDIT MANUALLY -->
|
||||
|
||||
[](https://github.com/RooCodeInc/roo-code/graphs/contributors)
|
||||
|
||||
<!-- END CONTRIBUTORS SECTION -->
|
||||
|
||||
## Licencia
|
||||
|
||||
[Apache 2.0 © 2025 Roo Code, Inc.](../../LICENSE)
|
||||
|
|
|
|||
12
locales/fr/README.md
generated
12
locales/fr/README.md
generated
|
|
@ -35,7 +35,7 @@
|
|||
- [简体中文](../zh-CN/README.md)
|
||||
- [繁體中文](../zh-TW/README.md)
|
||||
- ...
|
||||
</details>
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -167,16 +167,6 @@ Nous adorons les contributions de la communauté ! Commencez par lire notre [CON
|
|||
|
||||
---
|
||||
|
||||
## Contributeurs
|
||||
|
||||
Merci à tous nos contributeurs qui ont aidé à améliorer Roo Code !
|
||||
|
||||
<!-- START CONTRIBUTORS SECTION - AUTO-GENERATED, DO NOT EDIT MANUALLY -->
|
||||
|
||||
[](https://github.com/RooCodeInc/roo-code/graphs/contributors)
|
||||
|
||||
<!-- END CONTRIBUTORS SECTION -->
|
||||
|
||||
## Licence
|
||||
|
||||
[Apache 2.0 © 2025 Roo Code, Inc.](../../LICENSE)
|
||||
|
|
|
|||
12
locales/hi/README.md
generated
12
locales/hi/README.md
generated
|
|
@ -35,7 +35,7 @@
|
|||
- [简体中文](../zh-CN/README.md)
|
||||
- [繁體中文](../zh-TW/README.md)
|
||||
- ...
|
||||
</details>
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -167,16 +167,6 @@ pnpm install:vsix [-y] [--editor=<command>]
|
|||
|
||||
---
|
||||
|
||||
## योगदानकर्ता
|
||||
|
||||
हमारे सभी योगदानकर्ताओं को धन्यवाद जिन्होंने Roo Code को बेहतर बनाने में मदद की है!
|
||||
|
||||
<!-- START CONTRIBUTORS SECTION - AUTO-GENERATED, DO NOT EDIT MANUALLY -->
|
||||
|
||||
[](https://github.com/RooCodeInc/roo-code/graphs/contributors)
|
||||
|
||||
<!-- END CONTRIBUTORS SECTION -->
|
||||
|
||||
## लाइसेंस
|
||||
|
||||
[Apache 2.0 © 2025 Roo Code, Inc.](../../LICENSE)
|
||||
|
|
|
|||
12
locales/id/README.md
generated
12
locales/id/README.md
generated
|
|
@ -35,7 +35,7 @@
|
|||
- [简体中文](../zh-CN/README.md)
|
||||
- [繁體中文](../zh-TW/README.md)
|
||||
- ...
|
||||
</details>
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -167,16 +167,6 @@ Kami menyukai kontribusi komunitas! Mulailah dengan membaca [CONTRIBUTING.md](CO
|
|||
|
||||
---
|
||||
|
||||
## Kontributor
|
||||
|
||||
Terima kasih kepada semua kontributor kami yang telah membantu membuat Roo Code lebih baik!
|
||||
|
||||
<!-- START CONTRIBUTORS SECTION - AUTO-GENERATED, DO NOT EDIT MANUALLY -->
|
||||
|
||||
[](https://github.com/RooCodeInc/roo-code/graphs/contributors)
|
||||
|
||||
<!-- END CONTRIBUTORS SECTION -->
|
||||
|
||||
## Lisensi
|
||||
|
||||
[Apache 2.0 © 2025 Roo Code, Inc.](../../LICENSE)
|
||||
|
|
|
|||
12
locales/it/README.md
generated
12
locales/it/README.md
generated
|
|
@ -35,7 +35,7 @@
|
|||
- [简体中文](../zh-CN/README.md)
|
||||
- [繁體中文](../zh-TW/README.md)
|
||||
- ...
|
||||
</details>
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -167,16 +167,6 @@ Adoriamo i contributi della community! Inizia leggendo il nostro [CONTRIBUTING.m
|
|||
|
||||
---
|
||||
|
||||
## Contributori
|
||||
|
||||
Grazie a tutti i nostri contributori che hanno aiutato a migliorare Roo Code!
|
||||
|
||||
<!-- START CONTRIBUTORS SECTION - AUTO-GENERATED, DO NOT EDIT MANUALLY -->
|
||||
|
||||
[](https://github.com/RooCodeInc/roo-code/graphs/contributors)
|
||||
|
||||
<!-- END CONTRIBUTORS SECTION -->
|
||||
|
||||
## Licenza
|
||||
|
||||
[Apache 2.0 © 2025 Roo Code, Inc.](../../LICENSE)
|
||||
|
|
|
|||
12
locales/ja/README.md
generated
12
locales/ja/README.md
generated
|
|
@ -35,7 +35,7 @@
|
|||
- [简体中文](../zh-CN/README.md)
|
||||
- [繁體中文](../zh-TW/README.md)
|
||||
- ...
|
||||
</details>
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -167,16 +167,6 @@ VSIXパッケージを手動でインストールしたい場合:
|
|||
|
||||
---
|
||||
|
||||
## 貢献者
|
||||
|
||||
Roo Codeをより良くするために協力してくれたすべての貢献者に感謝します!
|
||||
|
||||
<!-- START CONTRIBUTORS SECTION - AUTO-GENERATED, DO NOT EDIT MANUALLY -->
|
||||
|
||||
[](https://github.com/RooCodeInc/roo-code/graphs/contributors)
|
||||
|
||||
<!-- END CONTRIBUTORS SECTION -->
|
||||
|
||||
## ライセンス
|
||||
|
||||
[Apache 2.0 © 2025 Roo Code, Inc.](../../LICENSE)
|
||||
|
|
|
|||
12
locales/ko/README.md
generated
12
locales/ko/README.md
generated
|
|
@ -35,7 +35,7 @@
|
|||
- [简体中文](../zh-CN/README.md)
|
||||
- [繁體中文](../zh-TW/README.md)
|
||||
- ...
|
||||
</details>
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -167,16 +167,6 @@ VSIX 패키지를 수동으로 설치하려면:
|
|||
|
||||
---
|
||||
|
||||
## 기여자
|
||||
|
||||
Roo Code를 더 좋게 만드는 데 도움을 준 모든 기여자들에게 감사합니다!
|
||||
|
||||
<!-- START CONTRIBUTORS SECTION - AUTO-GENERATED, DO NOT EDIT MANUALLY -->
|
||||
|
||||
[](https://github.com/RooCodeInc/roo-code/graphs/contributors)
|
||||
|
||||
<!-- END CONTRIBUTORS SECTION -->
|
||||
|
||||
## 라이선스
|
||||
|
||||
[Apache 2.0 © 2025 Roo Code, Inc.](../../LICENSE)
|
||||
|
|
|
|||
12
locales/nl/README.md
generated
12
locales/nl/README.md
generated
|
|
@ -35,7 +35,7 @@
|
|||
- [简体中文](../zh-CN/README.md)
|
||||
- [繁體中文](../zh-TW/README.md)
|
||||
- ...
|
||||
</details>
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -167,16 +167,6 @@ We houden van bijdragen uit de community! Begin met het lezen van onze [CONTRIBU
|
|||
|
||||
---
|
||||
|
||||
## Bijdragers
|
||||
|
||||
Dank aan al onze bijdragers die hebben geholpen Roo Code beter te maken!
|
||||
|
||||
<!-- START CONTRIBUTORS SECTION - AUTO-GENERATED, DO NOT EDIT MANUALLY -->
|
||||
|
||||
[](https://github.com/RooCodeInc/roo-code/graphs/contributors)
|
||||
|
||||
<!-- END CONTRIBUTORS SECTION -->
|
||||
|
||||
## Licentie
|
||||
|
||||
[Apache 2.0 © 2025 Roo Code, Inc.](../../LICENSE)
|
||||
|
|
|
|||
12
locales/pl/README.md
generated
12
locales/pl/README.md
generated
|
|
@ -35,7 +35,7 @@
|
|||
- [简体中文](../zh-CN/README.md)
|
||||
- [繁體中文](../zh-TW/README.md)
|
||||
- ...
|
||||
</details>
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -167,16 +167,6 @@ Uwielbiamy wkłady społeczności! Zacznij od przeczytania naszego pliku [CONTRI
|
|||
|
||||
---
|
||||
|
||||
## Współtwórcy
|
||||
|
||||
Dziękujemy wszystkim naszym współtwórcom, którzy pomogli ulepszyć Roo Code!
|
||||
|
||||
<!-- START CONTRIBUTORS SECTION - AUTO-GENERATED, DO NOT EDIT MANUALLY -->
|
||||
|
||||
[](https://github.com/RooCodeInc/roo-code/graphs/contributors)
|
||||
|
||||
<!-- END CONTRIBUTORS SECTION -->
|
||||
|
||||
## Licencja
|
||||
|
||||
[Apache 2.0 © 2025 Roo Code, Inc.](../../LICENSE)
|
||||
|
|
|
|||
12
locales/pt-BR/README.md
generated
12
locales/pt-BR/README.md
generated
|
|
@ -35,7 +35,7 @@
|
|||
- [简体中文](../zh-CN/README.md)
|
||||
- [繁體中文](../zh-TW/README.md)
|
||||
- ...
|
||||
</details>
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -167,16 +167,6 @@ Adoramos contribuições da comunidade! Comece lendo nosso [CONTRIBUTING.md](CON
|
|||
|
||||
---
|
||||
|
||||
## Contribuidores
|
||||
|
||||
Obrigado a todos os nossos contribuidores que ajudaram a tornar o Roo Code melhor!
|
||||
|
||||
<!-- START CONTRIBUTORS SECTION - AUTO-GENERATED, DO NOT EDIT MANUALLY -->
|
||||
|
||||
[](https://github.com/RooCodeInc/roo-code/graphs/contributors)
|
||||
|
||||
<!-- END CONTRIBUTORS SECTION -->
|
||||
|
||||
## Licença
|
||||
|
||||
[Apache 2.0 © 2025 Roo Code, Inc.](../../LICENSE)
|
||||
|
|
|
|||
12
locales/ru/README.md
generated
12
locales/ru/README.md
generated
|
|
@ -35,7 +35,7 @@
|
|||
- [简体中文](../zh-CN/README.md)
|
||||
- [繁體中文](../zh-TW/README.md)
|
||||
- ...
|
||||
</details>
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -167,16 +167,6 @@ pnpm install:vsix [-y] [--editor=<command>]
|
|||
|
||||
---
|
||||
|
||||
## Участники
|
||||
|
||||
Спасибо всем нашим участникам, которые помогли сделать Roo Code лучше!
|
||||
|
||||
<!-- START CONTRIBUTORS SECTION - AUTO-GENERATED, DO NOT EDIT MANUALLY -->
|
||||
|
||||
[](https://github.com/RooCodeInc/roo-code/graphs/contributors)
|
||||
|
||||
<!-- END CONTRIBUTORS SECTION -->
|
||||
|
||||
## Лицензия
|
||||
|
||||
[Apache 2.0 © 2025 Roo Code, Inc.](../../LICENSE)
|
||||
|
|
|
|||
12
locales/tr/README.md
generated
12
locales/tr/README.md
generated
|
|
@ -35,7 +35,7 @@
|
|||
- [简体中文](../zh-CN/README.md)
|
||||
- [繁體中文](../zh-TW/README.md)
|
||||
- ...
|
||||
</details>
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -167,16 +167,6 @@ Topluluk katkılarını çok seviyoruz! [CONTRIBUTING.md](CONTRIBUTING.md) dosya
|
|||
|
||||
---
|
||||
|
||||
## Katkıda Bulunanlar
|
||||
|
||||
Roo Code'u daha iyi hale getirmemize yardımcı olan tüm katkıda bulunanlarımıza teşekkür ederiz!
|
||||
|
||||
<!-- START CONTRIBUTORS SECTION - AUTO-GENERATED, DO NOT EDIT MANUALLY -->
|
||||
|
||||
[](https://github.com/RooCodeInc/roo-code/graphs/contributors)
|
||||
|
||||
<!-- END CONTRIBUTORS SECTION -->
|
||||
|
||||
## Lisans
|
||||
|
||||
[Apache 2.0 © 2025 Roo Code, Inc.](../../LICENSE)
|
||||
|
|
|
|||
12
locales/vi/README.md
generated
12
locales/vi/README.md
generated
|
|
@ -35,7 +35,7 @@
|
|||
- [简体中文](../zh-CN/README.md)
|
||||
- [繁體中文](../zh-TW/README.md)
|
||||
- ...
|
||||
</details>
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -167,16 +167,6 @@ Chúng tôi yêu thích những đóng góp của cộng đồng! Bắt đầu b
|
|||
|
||||
---
|
||||
|
||||
## Những người đóng góp
|
||||
|
||||
Cảm ơn tất cả những người đóng góp đã giúp Roo Code trở nên tốt hơn!
|
||||
|
||||
<!-- START CONTRIBUTORS SECTION - AUTO-GENERATED, DO NOT EDIT MANUALLY -->
|
||||
|
||||
[](https://github.com/RooCodeInc/roo-code/graphs/contributors)
|
||||
|
||||
<!-- END CONTRIBUTORS SECTION -->
|
||||
|
||||
## Giấy phép
|
||||
|
||||
[Apache 2.0 © 2025 Roo Code, Inc.](../../LICENSE)
|
||||
|
|
|
|||
12
locales/zh-CN/README.md
generated
12
locales/zh-CN/README.md
generated
|
|
@ -35,7 +35,7 @@
|
|||
- [简体中文](../zh-CN/README.md)
|
||||
- [繁體中文](../zh-TW/README.md)
|
||||
- ...
|
||||
</details>
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -167,16 +167,6 @@ pnpm install:vsix [-y] [--editor=<command>]
|
|||
|
||||
---
|
||||
|
||||
## 贡献者
|
||||
|
||||
感谢所有帮助改进 Roo Code 的贡献者!
|
||||
|
||||
<!-- START CONTRIBUTORS SECTION - AUTO-GENERATED, DO NOT EDIT MANUALLY -->
|
||||
|
||||
[](https://github.com/RooCodeInc/roo-code/graphs/contributors)
|
||||
|
||||
<!-- END CONTRIBUTORS SECTION -->
|
||||
|
||||
## 许可证
|
||||
|
||||
[Apache 2.0 © 2025 Roo Code, Inc.](../../LICENSE)
|
||||
|
|
|
|||
12
locales/zh-TW/README.md
generated
12
locales/zh-TW/README.md
generated
|
|
@ -35,7 +35,7 @@
|
|||
- [简体中文](../zh-CN/README.md)
|
||||
- [繁體中文](../zh-TW/README.md)
|
||||
- ...
|
||||
</details>
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -167,16 +167,6 @@ pnpm install:vsix [-y] [--editor=<command>]
|
|||
|
||||
---
|
||||
|
||||
## 貢獻者
|
||||
|
||||
感謝所有幫助改進 Roo Code 的貢獻者!
|
||||
|
||||
<!-- START CONTRIBUTORS SECTION - AUTO-GENERATED, DO NOT EDIT MANUALLY -->
|
||||
|
||||
[](https://github.com/RooCodeInc/roo-code/graphs/contributors)
|
||||
|
||||
<!-- END CONTRIBUTORS SECTION -->
|
||||
|
||||
## 授權
|
||||
|
||||
[Apache 2.0 © 2025 Roo Code, Inc.](../../LICENSE)
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@
|
|||
"install:vsix": "pnpm install --frozen-lockfile && pnpm clean && pnpm vsix && 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",
|
||||
"evals": "dotenvx run -f packages/evals/.env.development packages/evals/.env.local -- docker compose -f packages/evals/docker-compose.yml --profile server --profile runner up --build --scale runner=0",
|
||||
"npm:publish:types": "pnpm --filter @roo-code/types npm:publish"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -100,6 +100,12 @@ describe("generatePackageJson", () => {
|
|||
default: "",
|
||||
description: "%settings.customStoragePath.description%",
|
||||
},
|
||||
"roo-cline.toolProtocol": {
|
||||
type: "string",
|
||||
enum: ["xml", "native"],
|
||||
default: "xml",
|
||||
description: "%settings.toolProtocol.description%",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -213,6 +219,12 @@ describe("generatePackageJson", () => {
|
|||
default: "",
|
||||
description: "%settings.customStoragePath.description%",
|
||||
},
|
||||
"roo-code-nightly.toolProtocol": {
|
||||
type: "string",
|
||||
enum: ["xml", "native"],
|
||||
default: "xml",
|
||||
description: "%settings.toolProtocol.description%",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -86,6 +86,7 @@ const configurationPropertySchema = z.object({
|
|||
})
|
||||
.optional(),
|
||||
properties: z.record(z.string(), z.any()).optional(),
|
||||
enum: z.array(z.any()).optional(),
|
||||
default: z.any().optional(),
|
||||
description: z.string(),
|
||||
})
|
||||
|
|
|
|||
|
|
@ -704,7 +704,13 @@ export class WebAuthService extends EventEmitter<AuthServiceEvents> implements A
|
|||
signal: AbortSignal.timeout(10000),
|
||||
})
|
||||
|
||||
return clerkOrganizationMembershipsSchema.parse(await response.json()).response
|
||||
if (response.ok) {
|
||||
return clerkOrganizationMembershipsSchema.parse(await response.json()).response
|
||||
}
|
||||
|
||||
const errorMessage = `Failed to get organization memberships: ${response.status} ${response.statusText}`
|
||||
this.log(`[auth] ${errorMessage}`)
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
|
||||
private async getOrganizationMetadata(
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import {
|
|||
type IpcMessage,
|
||||
IpcOrigin,
|
||||
IpcMessageType,
|
||||
TaskCommandName,
|
||||
ipcMessageSchema,
|
||||
} from "@roo-code/types"
|
||||
|
||||
|
|
@ -98,6 +99,13 @@ export class IpcClient extends EventEmitter<IpcClientEvents> {
|
|||
this.sendMessage(message)
|
||||
}
|
||||
|
||||
public sendTaskMessage(text?: string, images?: string[]) {
|
||||
this.sendCommand({
|
||||
commandName: TaskCommandName.SendMessage,
|
||||
data: { text, images },
|
||||
})
|
||||
}
|
||||
|
||||
public sendMessage(message: IpcMessage) {
|
||||
ipc.of[this._id]?.emit("message", message)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -83,7 +83,11 @@ export class IpcServer extends EventEmitter<IpcServerEvents> implements RooCodeI
|
|||
const result = ipcMessageSchema.safeParse(data)
|
||||
|
||||
if (!result.success) {
|
||||
this.log("[server#onMessage] invalid payload", result.error.format(), data)
|
||||
this.log(
|
||||
"[server#onMessage] invalid paylooooad",
|
||||
JSON.stringify(result.error.format()),
|
||||
JSON.stringify(data),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@roo-code/types",
|
||||
"version": "1.83.0",
|
||||
"version": "1.85.0",
|
||||
"description": "TypeScript type definitions for Roo Code.",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
|
|
|
|||
14
packages/types/src/__tests__/message.test.ts
Normal file
14
packages/types/src/__tests__/message.test.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
// pnpm --filter @roo-code/types test src/__tests__/message.test.ts
|
||||
|
||||
import { clineAsks, isIdleAsk, isInteractiveAsk, isResumableAsk, isNonBlockingAsk } from "../message.js"
|
||||
|
||||
describe("ask messages", () => {
|
||||
test("all ask messages are classified", () => {
|
||||
for (const ask of clineAsks) {
|
||||
expect(
|
||||
isIdleAsk(ask) || isInteractiveAsk(ask) || isResumableAsk(ask) || isNonBlockingAsk(ask),
|
||||
`${ask} is not classified`,
|
||||
).toBe(true)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
@ -20,6 +20,7 @@ export * from "./todo.js"
|
|||
export * from "./telemetry.js"
|
||||
export * from "./terminal.js"
|
||||
export * from "./tool.js"
|
||||
export * from "./tool-params.js"
|
||||
export * from "./type-fu.js"
|
||||
export * from "./vscode.js"
|
||||
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ export enum TaskCommandName {
|
|||
CancelTask = "CancelTask",
|
||||
CloseTask = "CloseTask",
|
||||
ResumeTask = "ResumeTask",
|
||||
SendMessage = "SendMessage",
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -73,6 +74,13 @@ export const taskCommandSchema = z.discriminatedUnion("commandName", [
|
|||
commandName: z.literal(TaskCommandName.ResumeTask),
|
||||
data: z.string(),
|
||||
}),
|
||||
z.object({
|
||||
commandName: z.literal(TaskCommandName.SendMessage),
|
||||
data: z.object({
|
||||
text: z.string().optional(),
|
||||
images: z.array(z.string()).optional(),
|
||||
}),
|
||||
}),
|
||||
])
|
||||
|
||||
export type TaskCommand = z.infer<typeof taskCommandSchema>
|
||||
|
|
|
|||
|
|
@ -43,11 +43,6 @@ export const clineAsks = [
|
|||
export const clineAskSchema = z.enum(clineAsks)
|
||||
|
||||
export type ClineAsk = z.infer<typeof clineAskSchema>
|
||||
|
||||
// Needs classification:
|
||||
// - `followup`
|
||||
// - `command_output
|
||||
|
||||
/**
|
||||
* IdleAsk
|
||||
*
|
||||
|
|
@ -102,6 +97,21 @@ export function isInteractiveAsk(ask: ClineAsk): ask is InteractiveAsk {
|
|||
return (interactiveAsks as readonly ClineAsk[]).includes(ask)
|
||||
}
|
||||
|
||||
/**
|
||||
* NonBlockingAsk
|
||||
*
|
||||
* Asks that are not associated with an actual approval, and are only used
|
||||
* to update chat messages.
|
||||
*/
|
||||
|
||||
export const nonBlockingAsks = ["command_output"] as const satisfies readonly ClineAsk[]
|
||||
|
||||
export type NonBlockingAsk = (typeof nonBlockingAsks)[number]
|
||||
|
||||
export function isNonBlockingAsk(ask: ClineAsk): ask is NonBlockingAsk {
|
||||
return (nonBlockingAsks as readonly ClineAsk[]).includes(ask)
|
||||
}
|
||||
|
||||
/**
|
||||
* ClineSay
|
||||
*/
|
||||
|
|
@ -216,15 +226,6 @@ export const clineMessageSchema = z.object({
|
|||
isProtected: z.boolean().optional(),
|
||||
apiProtocol: z.union([z.literal("openai"), z.literal("anthropic")]).optional(),
|
||||
isAnswered: z.boolean().optional(),
|
||||
metadata: z
|
||||
.object({
|
||||
gpt5: z
|
||||
.object({
|
||||
previous_response_id: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
|
||||
export type ClineMessage = z.infer<typeof clineMessageSchema>
|
||||
|
|
|
|||
|
|
@ -18,6 +18,22 @@ export const reasoningEffortWithMinimalSchema = z.union([reasoningEffortsSchema,
|
|||
|
||||
export type ReasoningEffortWithMinimal = z.infer<typeof reasoningEffortWithMinimalSchema>
|
||||
|
||||
/**
|
||||
* Extended Reasoning Effort (includes "none" and "minimal")
|
||||
* Note: "disable" is a UI/control value, not a value sent as effort
|
||||
*/
|
||||
export const reasoningEffortsExtended = ["none", "minimal", "low", "medium", "high"] as const
|
||||
|
||||
export const reasoningEffortExtendedSchema = z.enum(reasoningEffortsExtended)
|
||||
|
||||
export type ReasoningEffortExtended = z.infer<typeof reasoningEffortExtendedSchema>
|
||||
|
||||
/**
|
||||
* Reasoning Effort user setting (includes "disable")
|
||||
*/
|
||||
export const reasoningEffortSettingValues = ["disable", "none", "minimal", "low", "medium", "high"] as const
|
||||
export const reasoningEffortSettingSchema = z.enum(reasoningEffortSettingValues)
|
||||
|
||||
/**
|
||||
* Verbosity
|
||||
*/
|
||||
|
|
@ -58,6 +74,10 @@ export const modelInfoSchema = z.object({
|
|||
contextWindow: z.number(),
|
||||
supportsImages: z.boolean().optional(),
|
||||
supportsPromptCache: z.boolean(),
|
||||
// Optional default prompt cache retention policy for providers that support it.
|
||||
// When set to "24h", extended prompt caching will be requested; when omitted
|
||||
// or set to "in_memory", the default in‑memory cache is used.
|
||||
promptCacheRetention: z.enum(["in_memory", "24h"]).optional(),
|
||||
// Capability flag to indicate whether the model supports an output verbosity parameter
|
||||
supportsVerbosity: z.boolean().optional(),
|
||||
supportsReasoningBudget: z.boolean().optional(),
|
||||
|
|
@ -67,7 +87,9 @@ export const modelInfoSchema = z.object({
|
|||
supportsTemperature: z.boolean().optional(),
|
||||
defaultTemperature: z.number().optional(),
|
||||
requiredReasoningBudget: z.boolean().optional(),
|
||||
supportsReasoningEffort: z.boolean().optional(),
|
||||
supportsReasoningEffort: z
|
||||
.union([z.boolean(), z.array(z.enum(["disable", "none", "minimal", "low", "medium", "high"]))])
|
||||
.optional(),
|
||||
requiredReasoningEffort: z.boolean().optional(),
|
||||
preserveReasoning: z.boolean().optional(),
|
||||
supportedParameters: z.array(modelParametersSchema).optional(),
|
||||
|
|
@ -76,7 +98,8 @@ export const modelInfoSchema = z.object({
|
|||
cacheWritesPrice: z.number().optional(),
|
||||
cacheReadsPrice: z.number().optional(),
|
||||
description: z.string().optional(),
|
||||
reasoningEffort: reasoningEffortsSchema.optional(),
|
||||
// Default effort value for models that support reasoning effort
|
||||
reasoningEffort: reasoningEffortExtendedSchema.optional(),
|
||||
minTokensPerCachePoint: z.number().optional(),
|
||||
maxCachePoints: z.number().optional(),
|
||||
cachableFields: z.array(z.string()).optional(),
|
||||
|
|
@ -84,6 +107,8 @@ export const modelInfoSchema = z.object({
|
|||
deprecated: z.boolean().optional(),
|
||||
// Flag to indicate if the model is free (no cost)
|
||||
isFree: z.boolean().optional(),
|
||||
// Flag to indicate if the model supports native tool calling (OpenAI-style function calling)
|
||||
supportsNativeTools: z.boolean().optional(),
|
||||
/**
|
||||
* Service tiers with pricing information.
|
||||
* Each tier can have a name (for OpenAI service tiers) and pricing overrides.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { z } from "zod"
|
||||
|
||||
import { modelInfoSchema, reasoningEffortWithMinimalSchema, verbosityLevelsSchema, serviceTierSchema } from "./model.js"
|
||||
import { modelInfoSchema, reasoningEffortSettingSchema, verbosityLevelsSchema, serviceTierSchema } from "./model.js"
|
||||
import { codebaseIndexProviderSchema } from "./codebase-index.js"
|
||||
import {
|
||||
anthropicModels,
|
||||
|
|
@ -176,7 +176,7 @@ const baseProviderSettingsSchema = z.object({
|
|||
|
||||
// Model reasoning.
|
||||
enableReasoningEffort: z.boolean().optional(),
|
||||
reasoningEffort: reasoningEffortWithMinimalSchema.optional(),
|
||||
reasoningEffort: reasoningEffortSettingSchema.optional(),
|
||||
modelMaxTokens: z.number().optional(),
|
||||
modelMaxThinkingTokens: z.number().optional(),
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import type { ModelInfo } from "../model.js"
|
||||
|
||||
// Minimax
|
||||
// https://www.minimax.io/platform/document/text_api_intro
|
||||
// https://www.minimax.io/platform/document/pricing
|
||||
// https://platform.minimax.io/docs/guides/pricing
|
||||
// https://platform.minimax.io/docs/api-reference/text-openai-api
|
||||
// https://platform.minimax.io/docs/api-reference/text-anthropic-api
|
||||
export type MinimaxModelId = keyof typeof minimaxModels
|
||||
export const minimaxDefaultModelId: MinimaxModelId = "MiniMax-M2"
|
||||
|
||||
|
|
@ -11,15 +12,28 @@ export const minimaxModels = {
|
|||
maxTokens: 16_384,
|
||||
contextWindow: 192_000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 0.3,
|
||||
outputPrice: 1.2,
|
||||
cacheWritesPrice: 0,
|
||||
cacheReadsPrice: 0,
|
||||
cacheWritesPrice: 0.375,
|
||||
cacheReadsPrice: 0.03,
|
||||
preserveReasoning: true,
|
||||
description:
|
||||
"MiniMax M2, a model born for Agents and code, featuring Top-tier Coding Capabilities, Powerful Agentic Performance, and Ultimate Cost-Effectiveness & Speed.",
|
||||
},
|
||||
"MiniMax-M2-Stable": {
|
||||
maxTokens: 16_384,
|
||||
contextWindow: 192_000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 0.3,
|
||||
outputPrice: 1.2,
|
||||
cacheWritesPrice: 0.375,
|
||||
cacheReadsPrice: 0.03,
|
||||
preserveReasoning: true,
|
||||
description:
|
||||
"MiniMax M2 Stable (High Concurrency, Commercial Use), a model born for Agents and code, featuring Top-tier Coding Capabilities, Powerful Agentic Performance, and Ultimate Cost-Effectiveness & Speed.",
|
||||
},
|
||||
} as const satisfies Record<string, ModelInfo>
|
||||
|
||||
export const MINIMAX_DEFAULT_TEMPERATURE = 1.0
|
||||
|
|
|
|||
|
|
@ -3,86 +3,131 @@ import type { ModelInfo } from "../model.js"
|
|||
// https://openai.com/api/pricing/
|
||||
export type OpenAiNativeModelId = keyof typeof openAiNativeModels
|
||||
|
||||
export const openAiNativeDefaultModelId: OpenAiNativeModelId = "gpt-5-2025-08-07"
|
||||
export const openAiNativeDefaultModelId: OpenAiNativeModelId = "gpt-5.1"
|
||||
|
||||
export const openAiNativeModels = {
|
||||
"gpt-5-chat-latest": {
|
||||
"gpt-5.1": {
|
||||
maxTokens: 128000,
|
||||
contextWindow: 400000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
supportsReasoningEffort: false,
|
||||
inputPrice: 1.25,
|
||||
outputPrice: 10.0,
|
||||
cacheReadsPrice: 0.13,
|
||||
description: "GPT-5 Chat Latest: Optimized for conversational AI and non-reasoning tasks",
|
||||
supportsVerbosity: true,
|
||||
},
|
||||
"gpt-5-2025-08-07": {
|
||||
maxTokens: 128000,
|
||||
contextWindow: 400000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
supportsReasoningEffort: true,
|
||||
promptCacheRetention: "24h",
|
||||
supportsReasoningEffort: ["none", "low", "medium", "high"],
|
||||
reasoningEffort: "medium",
|
||||
inputPrice: 1.25,
|
||||
outputPrice: 10.0,
|
||||
cacheReadsPrice: 0.13,
|
||||
description: "GPT-5: The best model for coding and agentic tasks across domains",
|
||||
// supportsVerbosity is a new capability; ensure ModelInfo includes it
|
||||
cacheReadsPrice: 0.125,
|
||||
supportsVerbosity: true,
|
||||
supportsTemperature: false,
|
||||
tiers: [
|
||||
{ name: "flex", contextWindow: 400000, inputPrice: 0.625, outputPrice: 5.0, cacheReadsPrice: 0.0625 },
|
||||
{ name: "priority", contextWindow: 400000, inputPrice: 2.5, outputPrice: 20.0, cacheReadsPrice: 0.25 },
|
||||
],
|
||||
description: "GPT-5.1: The best model for coding and agentic tasks across domains",
|
||||
},
|
||||
"gpt-5-mini-2025-08-07": {
|
||||
"gpt-5.1-codex": {
|
||||
maxTokens: 128000,
|
||||
contextWindow: 400000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
supportsReasoningEffort: true,
|
||||
promptCacheRetention: "24h",
|
||||
supportsReasoningEffort: ["low", "medium", "high"],
|
||||
reasoningEffort: "medium",
|
||||
inputPrice: 1.25,
|
||||
outputPrice: 10.0,
|
||||
cacheReadsPrice: 0.125,
|
||||
supportsTemperature: false,
|
||||
tiers: [{ name: "priority", contextWindow: 400000, inputPrice: 2.5, outputPrice: 20.0, cacheReadsPrice: 0.25 }],
|
||||
description: "GPT-5.1 Codex: A version of GPT-5.1 optimized for agentic coding in Codex",
|
||||
},
|
||||
"gpt-5.1-codex-mini": {
|
||||
maxTokens: 128000,
|
||||
contextWindow: 400000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
promptCacheRetention: "24h",
|
||||
supportsReasoningEffort: ["low", "medium", "high"],
|
||||
reasoningEffort: "medium",
|
||||
inputPrice: 0.25,
|
||||
outputPrice: 2.0,
|
||||
cacheReadsPrice: 0.03,
|
||||
description: "GPT-5 Mini: A faster, more cost-efficient version of GPT-5 for well-defined tasks",
|
||||
cacheReadsPrice: 0.025,
|
||||
supportsTemperature: false,
|
||||
description: "GPT-5.1 Codex mini: A version of GPT-5.1 optimized for agentic coding in Codex",
|
||||
},
|
||||
"gpt-5": {
|
||||
maxTokens: 128000,
|
||||
contextWindow: 400000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
supportsReasoningEffort: ["minimal", "low", "medium", "high"],
|
||||
reasoningEffort: "medium",
|
||||
inputPrice: 1.25,
|
||||
outputPrice: 10.0,
|
||||
cacheReadsPrice: 0.125,
|
||||
supportsVerbosity: true,
|
||||
supportsTemperature: false,
|
||||
tiers: [
|
||||
{ name: "flex", contextWindow: 400000, inputPrice: 0.625, outputPrice: 5.0, cacheReadsPrice: 0.0625 },
|
||||
{ name: "priority", contextWindow: 400000, inputPrice: 2.5, outputPrice: 20.0, cacheReadsPrice: 0.25 },
|
||||
],
|
||||
description: "GPT-5: The best model for coding and agentic tasks across domains",
|
||||
},
|
||||
"gpt-5-mini": {
|
||||
maxTokens: 128000,
|
||||
contextWindow: 400000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
supportsReasoningEffort: ["minimal", "low", "medium", "high"],
|
||||
reasoningEffort: "medium",
|
||||
inputPrice: 0.25,
|
||||
outputPrice: 2.0,
|
||||
cacheReadsPrice: 0.025,
|
||||
supportsVerbosity: true,
|
||||
supportsTemperature: false,
|
||||
tiers: [
|
||||
{ name: "flex", contextWindow: 400000, inputPrice: 0.125, outputPrice: 1.0, cacheReadsPrice: 0.0125 },
|
||||
{ name: "priority", contextWindow: 400000, inputPrice: 0.45, outputPrice: 3.6, cacheReadsPrice: 0.045 },
|
||||
],
|
||||
},
|
||||
"gpt-5-nano-2025-08-07": {
|
||||
maxTokens: 128000,
|
||||
contextWindow: 400000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
supportsReasoningEffort: true,
|
||||
reasoningEffort: "medium",
|
||||
inputPrice: 0.05,
|
||||
outputPrice: 0.4,
|
||||
cacheReadsPrice: 0.01,
|
||||
description: "GPT-5 Nano: Fastest, most cost-efficient version of GPT-5",
|
||||
supportsVerbosity: true,
|
||||
supportsTemperature: false,
|
||||
tiers: [{ name: "flex", contextWindow: 400000, inputPrice: 0.025, outputPrice: 0.2, cacheReadsPrice: 0.0025 }],
|
||||
description: "GPT-5 Mini: A faster, more cost-efficient version of GPT-5 for well-defined tasks",
|
||||
},
|
||||
"gpt-5-codex": {
|
||||
maxTokens: 128000,
|
||||
contextWindow: 400000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
supportsReasoningEffort: true,
|
||||
supportsReasoningEffort: ["low", "medium", "high"],
|
||||
reasoningEffort: "medium",
|
||||
inputPrice: 1.25,
|
||||
outputPrice: 10.0,
|
||||
cacheReadsPrice: 0.13,
|
||||
cacheReadsPrice: 0.125,
|
||||
supportsTemperature: false,
|
||||
tiers: [{ name: "priority", contextWindow: 400000, inputPrice: 2.5, outputPrice: 20.0, cacheReadsPrice: 0.25 }],
|
||||
description: "GPT-5-Codex: A version of GPT-5 optimized for agentic coding in Codex",
|
||||
},
|
||||
"gpt-5-nano": {
|
||||
maxTokens: 128000,
|
||||
contextWindow: 400000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
supportsReasoningEffort: ["minimal", "low", "medium", "high"],
|
||||
reasoningEffort: "medium",
|
||||
inputPrice: 0.05,
|
||||
outputPrice: 0.4,
|
||||
cacheReadsPrice: 0.005,
|
||||
supportsVerbosity: true,
|
||||
supportsTemperature: false,
|
||||
tiers: [{ name: "flex", contextWindow: 400000, inputPrice: 0.025, outputPrice: 0.2, cacheReadsPrice: 0.0025 }],
|
||||
description: "GPT-5 Nano: Fastest, most cost-efficient version of GPT-5",
|
||||
},
|
||||
"gpt-5-chat-latest": {
|
||||
maxTokens: 128000,
|
||||
contextWindow: 400000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 1.25,
|
||||
outputPrice: 10.0,
|
||||
cacheReadsPrice: 0.125,
|
||||
description: "GPT-5 Chat: Optimized for conversational AI and non-reasoning tasks",
|
||||
},
|
||||
"gpt-4.1": {
|
||||
maxTokens: 32_768,
|
||||
|
|
@ -131,7 +176,7 @@ export const openAiNativeModels = {
|
|||
inputPrice: 2.0,
|
||||
outputPrice: 8.0,
|
||||
cacheReadsPrice: 0.5,
|
||||
supportsReasoningEffort: true,
|
||||
supportsReasoningEffort: ["low", "medium", "high"],
|
||||
reasoningEffort: "medium",
|
||||
supportsTemperature: false,
|
||||
tiers: [
|
||||
|
|
@ -169,7 +214,7 @@ export const openAiNativeModels = {
|
|||
inputPrice: 1.1,
|
||||
outputPrice: 4.4,
|
||||
cacheReadsPrice: 0.275,
|
||||
supportsReasoningEffort: true,
|
||||
supportsReasoningEffort: ["low", "medium", "high"],
|
||||
reasoningEffort: "medium",
|
||||
supportsTemperature: false,
|
||||
tiers: [
|
||||
|
|
@ -207,7 +252,7 @@ export const openAiNativeModels = {
|
|||
inputPrice: 1.1,
|
||||
outputPrice: 4.4,
|
||||
cacheReadsPrice: 0.55,
|
||||
supportsReasoningEffort: true,
|
||||
supportsReasoningEffort: ["low", "medium", "high"],
|
||||
reasoningEffort: "medium",
|
||||
supportsTemperature: false,
|
||||
},
|
||||
|
|
@ -296,11 +341,63 @@ export const openAiNativeModels = {
|
|||
supportsPromptCache: false,
|
||||
inputPrice: 1.5,
|
||||
outputPrice: 6,
|
||||
cacheReadsPrice: 0,
|
||||
cacheReadsPrice: 0.375,
|
||||
supportsTemperature: false,
|
||||
description:
|
||||
"Codex Mini: Cloud-based software engineering agent powered by codex-1, a version of o3 optimized for coding tasks. Trained with reinforcement learning to generate human-style code, adhere to instructions, and iteratively run tests.",
|
||||
},
|
||||
// Dated clones (snapshots) preserved for backward compatibility
|
||||
"gpt-5-2025-08-07": {
|
||||
maxTokens: 128000,
|
||||
contextWindow: 400000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
supportsReasoningEffort: ["minimal", "low", "medium", "high"],
|
||||
reasoningEffort: "medium",
|
||||
inputPrice: 1.25,
|
||||
outputPrice: 10.0,
|
||||
cacheReadsPrice: 0.125,
|
||||
supportsVerbosity: true,
|
||||
supportsTemperature: false,
|
||||
tiers: [
|
||||
{ name: "flex", contextWindow: 400000, inputPrice: 0.625, outputPrice: 5.0, cacheReadsPrice: 0.0625 },
|
||||
{ name: "priority", contextWindow: 400000, inputPrice: 2.5, outputPrice: 20.0, cacheReadsPrice: 0.25 },
|
||||
],
|
||||
description: "GPT-5: The best model for coding and agentic tasks across domains",
|
||||
},
|
||||
"gpt-5-mini-2025-08-07": {
|
||||
maxTokens: 128000,
|
||||
contextWindow: 400000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
supportsReasoningEffort: ["minimal", "low", "medium", "high"],
|
||||
reasoningEffort: "medium",
|
||||
inputPrice: 0.25,
|
||||
outputPrice: 2.0,
|
||||
cacheReadsPrice: 0.025,
|
||||
supportsVerbosity: true,
|
||||
supportsTemperature: false,
|
||||
tiers: [
|
||||
{ name: "flex", contextWindow: 400000, inputPrice: 0.125, outputPrice: 1.0, cacheReadsPrice: 0.0125 },
|
||||
{ name: "priority", contextWindow: 400000, inputPrice: 0.45, outputPrice: 3.6, cacheReadsPrice: 0.045 },
|
||||
],
|
||||
description: "GPT-5 Mini: A faster, more cost-efficient version of GPT-5 for well-defined tasks",
|
||||
},
|
||||
"gpt-5-nano-2025-08-07": {
|
||||
maxTokens: 128000,
|
||||
contextWindow: 400000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
supportsReasoningEffort: ["minimal", "low", "medium", "high"],
|
||||
reasoningEffort: "medium",
|
||||
inputPrice: 0.05,
|
||||
outputPrice: 0.4,
|
||||
cacheReadsPrice: 0.005,
|
||||
supportsVerbosity: true,
|
||||
supportsTemperature: false,
|
||||
tiers: [{ name: "flex", contextWindow: 400000, inputPrice: 0.025, outputPrice: 0.2, cacheReadsPrice: 0.0025 }],
|
||||
description: "GPT-5 Nano: Fastest, most cost-efficient version of GPT-5",
|
||||
},
|
||||
} as const satisfies Record<string, ModelInfo>
|
||||
|
||||
export const openAiModelInfoSaneDefaults: ModelInfo = {
|
||||
|
|
@ -317,6 +414,5 @@ export const openAiModelInfoSaneDefaults: ModelInfo = {
|
|||
export const azureOpenAiDefaultApiVersion = "2024-08-01-preview"
|
||||
|
||||
export const OPENAI_NATIVE_DEFAULT_TEMPERATURE = 0
|
||||
export const GPT5_DEFAULT_TEMPERATURE = 1.0
|
||||
|
||||
export const OPENAI_AZURE_AI_INFERENCE_PATH = "/models/chat/completions"
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ export const openRouterDefaultModelInfo: ModelInfo = {
|
|||
contextWindow: 200_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
supportsNativeTools: true,
|
||||
inputPrice: 3.0,
|
||||
outputPrice: 15.0,
|
||||
cacheWritesPrice: 3.75,
|
||||
|
|
|
|||
37
packages/types/src/tool-params.ts
Normal file
37
packages/types/src/tool-params.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
/**
|
||||
* Tool parameter type definitions for native protocol
|
||||
*/
|
||||
|
||||
export interface LineRange {
|
||||
start: number
|
||||
end: number
|
||||
}
|
||||
|
||||
export interface FileEntry {
|
||||
path: string
|
||||
lineRanges?: LineRange[]
|
||||
}
|
||||
|
||||
export interface Coordinate {
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
|
||||
export interface Size {
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
export interface BrowserActionParams {
|
||||
action: "launch" | "click" | "hover" | "type" | "scroll_down" | "scroll_up" | "resize" | "close"
|
||||
url?: string
|
||||
coordinate?: Coordinate
|
||||
size?: Size
|
||||
text?: string
|
||||
}
|
||||
|
||||
export interface GenerateImageParams {
|
||||
prompt: string
|
||||
path: string
|
||||
image?: string
|
||||
}
|
||||
|
|
@ -54,3 +54,38 @@ export const toolUsageSchema = z.record(
|
|||
)
|
||||
|
||||
export type ToolUsage = z.infer<typeof toolUsageSchema>
|
||||
|
||||
/**
|
||||
* Tool protocol constants
|
||||
*/
|
||||
export const TOOL_PROTOCOL = {
|
||||
XML: "xml",
|
||||
NATIVE: "native",
|
||||
} as const
|
||||
|
||||
/**
|
||||
* Tool protocol type for system prompt generation
|
||||
* Derived from TOOL_PROTOCOL constants to ensure type safety
|
||||
*/
|
||||
export type ToolProtocol = (typeof TOOL_PROTOCOL)[keyof typeof TOOL_PROTOCOL]
|
||||
|
||||
/**
|
||||
* Checks if the protocol is native (non-XML).
|
||||
*
|
||||
* @param protocol - The tool protocol to check
|
||||
* @returns True if protocol is native
|
||||
*/
|
||||
export function isNativeProtocol(protocol: ToolProtocol): boolean {
|
||||
return protocol === TOOL_PROTOCOL.NATIVE
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the effective protocol from settings or falls back to the default XML.
|
||||
* This function is safe to use in webview-accessible code as it doesn't depend on vscode module.
|
||||
*
|
||||
* @param toolProtocol - Optional tool protocol from settings
|
||||
* @returns The effective tool protocol (defaults to "xml")
|
||||
*/
|
||||
export function getEffectiveProtocol(toolProtocol?: ToolProtocol): ToolProtocol {
|
||||
return toolProtocol || TOOL_PROTOCOL.XML
|
||||
}
|
||||
|
|
|
|||
114
pnpm-lock.yaml
generated
114
pnpm-lock.yaml
generated
|
|
@ -18,7 +18,7 @@ importers:
|
|||
devDependencies:
|
||||
'@changesets/cli':
|
||||
specifier: ^2.27.10
|
||||
version: 2.29.6(@types/node@24.2.1)
|
||||
version: 2.29.7(@types/node@24.2.1)
|
||||
'@dotenvx/dotenvx':
|
||||
specifier: ^1.34.0
|
||||
version: 1.44.2
|
||||
|
|
@ -782,6 +782,9 @@ importers:
|
|||
serialize-error:
|
||||
specifier: ^12.0.0
|
||||
version: 12.0.0
|
||||
shell-quote:
|
||||
specifier: ^1.8.2
|
||||
version: 1.8.3
|
||||
simple-git:
|
||||
specifier: ^3.27.0
|
||||
version: 3.27.0
|
||||
|
|
@ -879,6 +882,9 @@ importers:
|
|||
'@types/ps-tree':
|
||||
specifier: ^1.1.6
|
||||
version: 1.1.6
|
||||
'@types/shell-quote':
|
||||
specifier: ^1.7.5
|
||||
version: 1.7.5
|
||||
'@types/stream-json':
|
||||
specifier: ^1.7.8
|
||||
version: 1.7.8
|
||||
|
|
@ -1020,6 +1026,9 @@ importers:
|
|||
debounce:
|
||||
specifier: ^2.1.1
|
||||
version: 2.2.0
|
||||
diff:
|
||||
specifier: ^5.2.0
|
||||
version: 5.2.0
|
||||
fast-deep-equal:
|
||||
specifier: ^3.1.3
|
||||
version: 3.1.3
|
||||
|
|
@ -1153,6 +1162,9 @@ importers:
|
|||
'@testing-library/user-event':
|
||||
specifier: ^14.6.1
|
||||
version: 14.6.1(@testing-library/dom@10.4.0)
|
||||
'@types/diff':
|
||||
specifier: ^5.2.1
|
||||
version: 5.2.3
|
||||
'@types/jest':
|
||||
specifier: ^29.0.0
|
||||
version: 29.5.14
|
||||
|
|
@ -1543,8 +1555,8 @@ packages:
|
|||
'@braintree/sanitize-url@7.1.1':
|
||||
resolution: {integrity: sha512-i1L7noDNxtFyL5DmZafWy1wRVhGehQmzZaz1HiN5e7iylJMSZR7ekOV7NsIqa5qBldlLrsKv4HbgFUVlQrz8Mw==}
|
||||
|
||||
'@changesets/apply-release-plan@7.0.12':
|
||||
resolution: {integrity: sha512-EaET7As5CeuhTzvXTQCRZeBUcisoYPDDcXvgTE/2jmmypKp0RC7LxKj/yzqeh/1qFTZI7oDGFcL1PHRuQuketQ==}
|
||||
'@changesets/apply-release-plan@7.0.13':
|
||||
resolution: {integrity: sha512-BIW7bofD2yAWoE8H4V40FikC+1nNFEKBisMECccS16W1rt6qqhNTBDmIw5HaqmMgtLNz9e7oiALiEUuKrQ4oHg==}
|
||||
|
||||
'@changesets/assemble-release-plan@6.0.9':
|
||||
resolution: {integrity: sha512-tPgeeqCHIwNo8sypKlS3gOPmsS3wP0zHt67JDuL20P4QcXiw/O4Hl7oXiuLnP9yg+rXLQ2sScdV1Kkzde61iSQ==}
|
||||
|
|
@ -1552,8 +1564,8 @@ packages:
|
|||
'@changesets/changelog-git@0.2.1':
|
||||
resolution: {integrity: sha512-x/xEleCFLH28c3bQeQIyeZf8lFXyDFVn1SgcBiR2Tw/r4IAWlk1fzxCEZ6NxQAjF2Nwtczoen3OA2qR+UawQ8Q==}
|
||||
|
||||
'@changesets/cli@2.29.6':
|
||||
resolution: {integrity: sha512-6qCcVsIG1KQLhpQ5zE8N0PckIx4+9QlHK3z6/lwKnw7Tir71Bjw8BeOZaxA/4Jt00pcgCnCSWZnyuZf5Il05QQ==}
|
||||
'@changesets/cli@2.29.7':
|
||||
resolution: {integrity: sha512-R7RqWoaksyyKXbKXBTbT4REdy22yH81mcFK6sWtqSanxUCbUi9Uf+6aqxZtDQouIqPdem2W56CdxXgsxdq7FLQ==}
|
||||
hasBin: true
|
||||
|
||||
'@changesets/config@3.1.1':
|
||||
|
|
@ -2044,8 +2056,8 @@ packages:
|
|||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@inquirer/external-editor@1.0.1':
|
||||
resolution: {integrity: sha512-Oau4yL24d2B5IL4ma4UpbQigkVhzPDXLoqy1ggK4gnHg/stmkffJE4oOXHXF3uz0UEpywG68KcyXsyYpA1Re/Q==}
|
||||
'@inquirer/external-editor@1.0.2':
|
||||
resolution: {integrity: sha512-yy9cOoBnx58TlsPrIxauKIFQTiyH+0MK4e97y4sV9ERbI+zDxw7i2hxHLCIEGIE/8PPvDxGhgzIOTSOWcs6/MQ==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
'@types/node': '>=18'
|
||||
|
|
@ -2473,8 +2485,8 @@ packages:
|
|||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@petamoriken/float16@3.9.2':
|
||||
resolution: {integrity: sha512-VgffxawQde93xKxT3qap3OH+meZf7VaSB5Sqd4Rqc+FP5alWbpOyan/7tRbOAvynjpG3GpdtAuGU/NdhQpmrog==}
|
||||
'@petamoriken/float16@3.9.3':
|
||||
resolution: {integrity: sha512-8awtpHXCx/bNpFt4mt2xdkgtgVvKqty8VbjHI/WWWQuEw+KLzFot3f4+LkQY9YmOtq7A5GdOnqoIC8Pdygjk2g==}
|
||||
|
||||
'@pkgjs/parseargs@0.11.0':
|
||||
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
|
||||
|
|
@ -6474,8 +6486,8 @@ packages:
|
|||
resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==}
|
||||
engines: {node: '>= 14'}
|
||||
|
||||
human-id@4.1.1:
|
||||
resolution: {integrity: sha512-3gKm/gCSUipeLsRYZbbdA1BD83lBoWUkZ7G9VFrhWPAU76KwYo5KR8V28bpoPm/ygy0x5/GCbpRQdY7VLYCoIg==}
|
||||
human-id@4.1.2:
|
||||
resolution: {integrity: sha512-v/J+4Z/1eIJovEBdlV5TYj1IR+ZiohcYGRY+qN/oC9dAfKzVT023N/Bgw37hrKCoVRBvk3bqyzpr2PP5YeTMSg==}
|
||||
hasBin: true
|
||||
|
||||
human-signals@2.1.0:
|
||||
|
|
@ -6516,6 +6528,10 @@ packages:
|
|||
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
iconv-lite@0.7.0:
|
||||
resolution: {integrity: sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
identity-obj-proxy@3.0.0:
|
||||
resolution: {integrity: sha512-00n6YnVHKrinT9t0d9+5yZC6UBNJANpYEQvL2LlX6Ab9lnmxzIRcEmTPuyGScvl1+jKuCICX1Z0Ab1pPKKdikA==}
|
||||
engines: {node: '>=4'}
|
||||
|
|
@ -8047,8 +8063,8 @@ packages:
|
|||
package-manager-detector@0.2.11:
|
||||
resolution: {integrity: sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ==}
|
||||
|
||||
package-manager-detector@1.3.0:
|
||||
resolution: {integrity: sha512-ZsEbbZORsyHuO00lY1kV3/t72yp6Ysay6Pd17ZAlNGuGwmWDLCJxFpRs0IzfXfj1o4icJOkUEioexFHzyPurSQ==}
|
||||
package-manager-detector@1.5.0:
|
||||
resolution: {integrity: sha512-uBj69dVlYe/+wxj8JOpr97XfsxH/eumMt6HqjNTmJDf/6NO9s+0uxeOneIz3AsPt2m6y9PqzDzd3ATcU17MNfw==}
|
||||
|
||||
pako@0.2.9:
|
||||
resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==}
|
||||
|
|
@ -8840,6 +8856,11 @@ packages:
|
|||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
semver@7.7.3:
|
||||
resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==}
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
send@1.2.0:
|
||||
resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==}
|
||||
engines: {node: '>= 18'}
|
||||
|
|
@ -10276,7 +10297,7 @@ snapshots:
|
|||
|
||||
'@antfu/install-pkg@1.1.0':
|
||||
dependencies:
|
||||
package-manager-detector: 1.3.0
|
||||
package-manager-detector: 1.5.0
|
||||
tinyexec: 1.0.1
|
||||
|
||||
'@antfu/utils@8.1.1': {}
|
||||
|
|
@ -11049,7 +11070,7 @@ snapshots:
|
|||
|
||||
'@braintree/sanitize-url@7.1.1': {}
|
||||
|
||||
'@changesets/apply-release-plan@7.0.12':
|
||||
'@changesets/apply-release-plan@7.0.13':
|
||||
dependencies:
|
||||
'@changesets/config': 3.1.1
|
||||
'@changesets/get-version-range-type': 0.4.0
|
||||
|
|
@ -11063,7 +11084,7 @@ snapshots:
|
|||
outdent: 0.5.0
|
||||
prettier: 2.8.8
|
||||
resolve-from: 5.0.0
|
||||
semver: 7.7.2
|
||||
semver: 7.7.3
|
||||
|
||||
'@changesets/assemble-release-plan@6.0.9':
|
||||
dependencies:
|
||||
|
|
@ -11072,15 +11093,15 @@ snapshots:
|
|||
'@changesets/should-skip-package': 0.1.2
|
||||
'@changesets/types': 6.1.0
|
||||
'@manypkg/get-packages': 1.1.3
|
||||
semver: 7.7.2
|
||||
semver: 7.7.3
|
||||
|
||||
'@changesets/changelog-git@0.2.1':
|
||||
dependencies:
|
||||
'@changesets/types': 6.1.0
|
||||
|
||||
'@changesets/cli@2.29.6(@types/node@24.2.1)':
|
||||
'@changesets/cli@2.29.7(@types/node@24.2.1)':
|
||||
dependencies:
|
||||
'@changesets/apply-release-plan': 7.0.12
|
||||
'@changesets/apply-release-plan': 7.0.13
|
||||
'@changesets/assemble-release-plan': 6.0.9
|
||||
'@changesets/changelog-git': 0.2.1
|
||||
'@changesets/config': 3.1.1
|
||||
|
|
@ -11094,7 +11115,7 @@ snapshots:
|
|||
'@changesets/should-skip-package': 0.1.2
|
||||
'@changesets/types': 6.1.0
|
||||
'@changesets/write': 0.4.0
|
||||
'@inquirer/external-editor': 1.0.1(@types/node@24.2.1)
|
||||
'@inquirer/external-editor': 1.0.2(@types/node@24.2.1)
|
||||
'@manypkg/get-packages': 1.1.3
|
||||
ansi-colors: 4.1.3
|
||||
ci-info: 3.9.0
|
||||
|
|
@ -11105,7 +11126,7 @@ snapshots:
|
|||
package-manager-detector: 0.2.11
|
||||
picocolors: 1.1.1
|
||||
resolve-from: 5.0.0
|
||||
semver: 7.7.2
|
||||
semver: 7.7.3
|
||||
spawndamnit: 3.0.1
|
||||
term-size: 2.2.1
|
||||
transitivePeerDependencies:
|
||||
|
|
@ -11130,7 +11151,7 @@ snapshots:
|
|||
'@changesets/types': 6.1.0
|
||||
'@manypkg/get-packages': 1.1.3
|
||||
picocolors: 1.1.1
|
||||
semver: 7.7.2
|
||||
semver: 7.7.3
|
||||
|
||||
'@changesets/get-release-plan@4.0.13':
|
||||
dependencies:
|
||||
|
|
@ -11190,7 +11211,7 @@ snapshots:
|
|||
dependencies:
|
||||
'@changesets/types': 6.1.0
|
||||
fs-extra: 7.0.1
|
||||
human-id: 4.1.1
|
||||
human-id: 4.1.2
|
||||
prettier: 2.8.8
|
||||
|
||||
'@chevrotain/cst-dts-gen@11.0.3':
|
||||
|
|
@ -11570,10 +11591,10 @@ snapshots:
|
|||
'@img/sharp-win32-x64@0.33.5':
|
||||
optional: true
|
||||
|
||||
'@inquirer/external-editor@1.0.1(@types/node@24.2.1)':
|
||||
'@inquirer/external-editor@1.0.2(@types/node@24.2.1)':
|
||||
dependencies:
|
||||
chardet: 2.1.0
|
||||
iconv-lite: 0.6.3
|
||||
iconv-lite: 0.7.0
|
||||
optionalDependencies:
|
||||
'@types/node': 24.2.1
|
||||
|
||||
|
|
@ -11729,14 +11750,14 @@ snapshots:
|
|||
|
||||
'@manypkg/find-root@1.1.0':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.28.3
|
||||
'@babel/runtime': 7.28.4
|
||||
'@types/node': 12.20.55
|
||||
find-up: 4.1.0
|
||||
fs-extra: 8.1.0
|
||||
|
||||
'@manypkg/get-packages@1.1.3':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.28.3
|
||||
'@babel/runtime': 7.28.4
|
||||
'@changesets/types': 4.1.0
|
||||
'@manypkg/find-root': 1.1.0
|
||||
fs-extra: 8.1.0
|
||||
|
|
@ -11982,7 +12003,7 @@ snapshots:
|
|||
'@oxc-resolver/binding-win32-x64-msvc@11.2.0':
|
||||
optional: true
|
||||
|
||||
'@petamoriken/float16@3.9.2':
|
||||
'@petamoriken/float16@3.9.3':
|
||||
optional: true
|
||||
|
||||
'@pkgjs/parseargs@0.11.0':
|
||||
|
|
@ -11996,7 +12017,7 @@ snapshots:
|
|||
extract-zip: 2.0.1
|
||||
progress: 2.0.3
|
||||
proxy-agent: 6.5.0
|
||||
semver: 7.7.2
|
||||
semver: 7.7.3
|
||||
tar-fs: 3.0.9
|
||||
yargs: 17.7.2
|
||||
transitivePeerDependencies:
|
||||
|
|
@ -12009,7 +12030,7 @@ snapshots:
|
|||
extract-zip: 2.0.1
|
||||
progress: 2.0.3
|
||||
proxy-agent: 6.5.0
|
||||
semver: 7.7.2
|
||||
semver: 7.7.3
|
||||
tar-fs: 3.0.9
|
||||
unbzip2-stream: 1.4.3
|
||||
yargs: 17.7.2
|
||||
|
|
@ -13910,7 +13931,7 @@ snapshots:
|
|||
fast-glob: 3.3.3
|
||||
is-glob: 4.0.3
|
||||
minimatch: 9.0.5
|
||||
semver: 7.7.2
|
||||
semver: 7.7.3
|
||||
ts-api-utils: 2.1.0(typescript@5.8.3)
|
||||
typescript: 5.8.3
|
||||
transitivePeerDependencies:
|
||||
|
|
@ -14020,7 +14041,7 @@ snapshots:
|
|||
sirv: 3.0.1
|
||||
tinyglobby: 0.2.14
|
||||
tinyrainbow: 2.0.0
|
||||
vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.2.1)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0)
|
||||
vitest: 3.2.4(@types/debug@4.1.12)(@types/node@20.17.50)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0)
|
||||
|
||||
'@vitest/utils@3.2.4':
|
||||
dependencies:
|
||||
|
|
@ -15253,7 +15274,7 @@ snapshots:
|
|||
|
||||
dom-helpers@5.2.1:
|
||||
dependencies:
|
||||
'@babel/runtime': 7.28.3
|
||||
'@babel/runtime': 7.28.4
|
||||
csstype: 3.1.3
|
||||
|
||||
dom-serializer@2.0.0:
|
||||
|
|
@ -16160,10 +16181,10 @@ snapshots:
|
|||
|
||||
gel@2.1.0:
|
||||
dependencies:
|
||||
'@petamoriken/float16': 3.9.2
|
||||
'@petamoriken/float16': 3.9.3
|
||||
debug: 4.4.3
|
||||
env-paths: 3.0.0
|
||||
semver: 7.7.2
|
||||
semver: 7.7.3
|
||||
shell-quote: 1.8.3
|
||||
which: 4.0.0
|
||||
transitivePeerDependencies:
|
||||
|
|
@ -16545,7 +16566,7 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
human-id@4.1.1: {}
|
||||
human-id@4.1.2: {}
|
||||
|
||||
human-signals@2.1.0: {}
|
||||
|
||||
|
|
@ -16577,6 +16598,10 @@ snapshots:
|
|||
dependencies:
|
||||
safer-buffer: 2.1.2
|
||||
|
||||
iconv-lite@0.7.0:
|
||||
dependencies:
|
||||
safer-buffer: 2.1.2
|
||||
|
||||
identity-obj-proxy@3.0.0:
|
||||
dependencies:
|
||||
harmony-reflect: 1.6.2
|
||||
|
|
@ -17421,7 +17446,7 @@ snapshots:
|
|||
|
||||
make-dir@4.0.0:
|
||||
dependencies:
|
||||
semver: 7.7.2
|
||||
semver: 7.7.3
|
||||
|
||||
mammoth@1.9.1:
|
||||
dependencies:
|
||||
|
|
@ -18146,7 +18171,7 @@ snapshots:
|
|||
minimatch: 10.0.1
|
||||
pidtree: 0.6.0
|
||||
read-package-json-fast: 4.0.0
|
||||
shell-quote: 1.8.2
|
||||
shell-quote: 1.8.3
|
||||
which: 5.0.0
|
||||
|
||||
npm-run-path@4.0.1:
|
||||
|
|
@ -18392,7 +18417,7 @@ snapshots:
|
|||
dependencies:
|
||||
quansync: 0.2.11
|
||||
|
||||
package-manager-detector@1.3.0: {}
|
||||
package-manager-detector@1.5.0: {}
|
||||
|
||||
pako@0.2.9: {}
|
||||
|
||||
|
|
@ -18902,7 +18927,7 @@ snapshots:
|
|||
|
||||
react-transition-group@4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.28.3
|
||||
'@babel/runtime': 7.28.4
|
||||
dom-helpers: 5.2.1
|
||||
loose-envify: 1.4.0
|
||||
prop-types: 15.8.1
|
||||
|
|
@ -19240,7 +19265,7 @@ snapshots:
|
|||
|
||||
rtl-css-js@1.16.1:
|
||||
dependencies:
|
||||
'@babel/runtime': 7.28.3
|
||||
'@babel/runtime': 7.28.4
|
||||
|
||||
run-applescript@7.0.0: {}
|
||||
|
||||
|
|
@ -19326,6 +19351,8 @@ snapshots:
|
|||
|
||||
semver@7.7.2: {}
|
||||
|
||||
semver@7.7.3: {}
|
||||
|
||||
send@1.2.0:
|
||||
dependencies:
|
||||
debug: 4.4.1(supports-color@8.1.1)
|
||||
|
|
@ -19399,7 +19426,7 @@ snapshots:
|
|||
dependencies:
|
||||
color: 4.2.3
|
||||
detect-libc: 2.0.4
|
||||
semver: 7.7.2
|
||||
semver: 7.7.3
|
||||
optionalDependencies:
|
||||
'@img/sharp-darwin-arm64': 0.33.5
|
||||
'@img/sharp-darwin-x64': 0.33.5
|
||||
|
|
@ -19430,8 +19457,7 @@ snapshots:
|
|||
|
||||
shell-quote@1.8.2: {}
|
||||
|
||||
shell-quote@1.8.3:
|
||||
optional: true
|
||||
shell-quote@1.8.3: {}
|
||||
|
||||
shiki@3.4.1:
|
||||
dependencies:
|
||||
|
|
|
|||
BIN
releases/3.30.3-release.png
Normal file
BIN
releases/3.30.3-release.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1,023 KiB |
BIN
releases/3.31.0-release.png
Normal file
BIN
releases/3.31.0-release.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1 MiB |
BIN
releases/3.31.1-release.png
Normal file
BIN
releases/3.31.1-release.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.6 MiB |
BIN
releases/3.31.3-release.png
Normal file
BIN
releases/3.31.3-release.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
BIN
releases/3.32.0-release.png
Normal file
BIN
releases/3.32.0-release.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
BIN
releases/3.32.1-release.png
Normal file
BIN
releases/3.32.1-release.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2 MiB |
|
|
@ -1,384 +0,0 @@
|
|||
#!/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.
|
||||
* It also updates all localized README files in the locales directory.
|
||||
*/
|
||||
|
||||
const https = require("https")
|
||||
const fs = require("fs")
|
||||
const { promisify } = require("util")
|
||||
const path = require("path")
|
||||
|
||||
// Promisify filesystem operations
|
||||
const readFileAsync = promisify(fs.readFile)
|
||||
const writeFileAsync = promisify(fs.writeFile)
|
||||
|
||||
// GitHub API URL for fetching contributors
|
||||
const GITHUB_API_URL = "https://api.github.com/repos/RooCodeInc/Roo-Code/contributors?per_page=100"
|
||||
const README_PATH = path.join(__dirname, "..", "README.md")
|
||||
const LOCALES_DIR = path.join(__dirname, "..", "locales")
|
||||
|
||||
// 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")
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the GitHub API Link header to extract pagination URLs
|
||||
* Based on RFC 5988 format for the Link header
|
||||
* @param {string} header The Link header from GitHub API response
|
||||
* @returns {Object} Object containing URLs for next, prev, first, last pages (if available)
|
||||
*/
|
||||
function parseLinkHeader(header) {
|
||||
// Return empty object if no header is provided
|
||||
if (!header || header.trim() === "") return {}
|
||||
|
||||
// Initialize links object
|
||||
const links = {}
|
||||
|
||||
// Split the header into individual link entries
|
||||
// Example: <https://api.github.com/...?page=2>; rel="next", <https://api.github.com/...?page=5>; rel="last"
|
||||
const entries = header.split(/,\s*/)
|
||||
|
||||
// Process each link entry
|
||||
for (const entry of entries) {
|
||||
// Extract the URL (between < and >) and the parameters (after >)
|
||||
const segments = entry.split(";")
|
||||
if (segments.length < 2) continue
|
||||
|
||||
// Extract URL from the first segment, removing < and >
|
||||
const urlMatch = segments[0].match(/<(.+)>/)
|
||||
if (!urlMatch) continue
|
||||
const url = urlMatch[1]
|
||||
|
||||
// Find the rel="value" parameter
|
||||
let rel = null
|
||||
for (let i = 1; i < segments.length; i++) {
|
||||
const relMatch = segments[i].match(/\s*rel\s*=\s*"?([^"]+)"?/)
|
||||
if (relMatch) {
|
||||
rel = relMatch[1]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Only add to links if both URL and rel were found
|
||||
if (rel) {
|
||||
links[rel] = url
|
||||
}
|
||||
}
|
||||
|
||||
return links
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs an HTTP GET request and returns the response
|
||||
* @param {string} url The URL to fetch
|
||||
* @param {Object} options Request options
|
||||
* @returns {Promise<Object>} Response object with status, headers and body
|
||||
*/
|
||||
function httpGet(url, options) {
|
||||
return new Promise((resolve, reject) => {
|
||||
https
|
||||
.get(url, options, (res) => {
|
||||
let data = ""
|
||||
res.on("data", (chunk) => {
|
||||
data += chunk
|
||||
})
|
||||
|
||||
res.on("end", () => {
|
||||
resolve({
|
||||
statusCode: res.statusCode,
|
||||
headers: res.headers,
|
||||
body: data,
|
||||
})
|
||||
})
|
||||
})
|
||||
.on("error", (error) => {
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches a single page of contributors from GitHub API
|
||||
* @param {string} url The API URL to fetch
|
||||
* @returns {Promise<Object>} Object containing contributors and pagination links
|
||||
*/
|
||||
async function fetchContributorsPage(url) {
|
||||
try {
|
||||
// Make the HTTP request
|
||||
const response = await httpGet(url, options)
|
||||
|
||||
// Check for successful response
|
||||
if (response.statusCode !== 200) {
|
||||
throw new Error(`GitHub API request failed with status code: ${response.statusCode}`)
|
||||
}
|
||||
|
||||
// Parse the Link header for pagination
|
||||
const linkHeader = response.headers.link
|
||||
const links = parseLinkHeader(linkHeader)
|
||||
|
||||
// Parse the JSON response
|
||||
const contributors = JSON.parse(response.body)
|
||||
|
||||
return { contributors, links }
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to fetch contributors page: ${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches all contributors data from GitHub API (handling pagination)
|
||||
* @returns {Promise<Array>} Array of all contributor objects
|
||||
*/
|
||||
async function fetchContributors() {
|
||||
let allContributors = []
|
||||
let currentUrl = GITHUB_API_URL
|
||||
let pageCount = 1
|
||||
|
||||
// Loop through all pages of contributors
|
||||
while (currentUrl) {
|
||||
console.log(`Fetching contributors page ${pageCount}...`)
|
||||
const { contributors, links } = await fetchContributorsPage(currentUrl)
|
||||
|
||||
allContributors = allContributors.concat(contributors)
|
||||
|
||||
// Move to the next page if it exists
|
||||
currentUrl = links.next
|
||||
pageCount++
|
||||
}
|
||||
|
||||
console.log(`Fetched ${allContributors.length} contributors from ${pageCount - 1} pages`)
|
||||
return allContributors
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the README.md file
|
||||
* @returns {Promise<string>} README content
|
||||
*/
|
||||
async function readReadme() {
|
||||
try {
|
||||
return await readFileAsync(README_PATH, "utf8")
|
||||
} catch (err) {
|
||||
throw new Error(`Failed to read README.md: ${err.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates HTML for the contributors section
|
||||
* @param {Array} contributors Array of contributor objects from GitHub API
|
||||
* @returns {string} HTML for contributors section
|
||||
*/
|
||||
const EXCLUDED_LOGIN_SUBSTRINGS = ['[bot]', 'R00-B0T'];
|
||||
const EXCLUDED_LOGIN_EXACTS = ['cursor', 'roomote'];
|
||||
|
||||
function formatContributorsSection(contributors) {
|
||||
// Filter out GitHub Actions bot, cursor, and roomote
|
||||
const filteredContributors = contributors.filter((c) =>
|
||||
!EXCLUDED_LOGIN_SUBSTRINGS.some(sub => c.login.includes(sub)) &&
|
||||
!EXCLUDED_LOGIN_EXACTS.includes(c.login)
|
||||
)
|
||||
|
||||
// Start building with Markdown table format
|
||||
let markdown = `${START_MARKER}
|
||||
`
|
||||
// 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>}
|
||||
*/
|
||||
async 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
|
||||
|
||||
await writeReadme(updatedContent)
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes updated content to README.md
|
||||
* @param {string} content Updated README content
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function writeReadme(content) {
|
||||
try {
|
||||
await writeFileAsync(README_PATH, content, "utf8")
|
||||
} catch (err) {
|
||||
throw new Error(`Failed to write updated README.md: ${err.message}`)
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Finds all localized README files in the locales directory
|
||||
* @returns {Promise<string[]>} Array of README file paths
|
||||
*/
|
||||
async function findLocalizedReadmes() {
|
||||
const readmeFiles = []
|
||||
|
||||
// Check if locales directory exists
|
||||
if (!fs.existsSync(LOCALES_DIR)) {
|
||||
// No localized READMEs found
|
||||
return readmeFiles
|
||||
}
|
||||
|
||||
// Get all language subdirectories
|
||||
const languageDirs = fs
|
||||
.readdirSync(LOCALES_DIR, { withFileTypes: true })
|
||||
.filter((dirent) => dirent.isDirectory())
|
||||
.map((dirent) => dirent.name)
|
||||
|
||||
// Add all localized READMEs to the list
|
||||
for (const langDir of languageDirs) {
|
||||
const readmePath = path.join(LOCALES_DIR, langDir, "README.md")
|
||||
if (fs.existsSync(readmePath)) {
|
||||
readmeFiles.push(readmePath)
|
||||
}
|
||||
}
|
||||
|
||||
return readmeFiles
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a localized README file with contributors section
|
||||
* @param {string} filePath Path to the README file
|
||||
* @param {string} contributorsSection HTML for contributors section
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function updateLocalizedReadme(filePath, contributorsSection) {
|
||||
try {
|
||||
// Read the file content
|
||||
const readmeContent = await readFileAsync(filePath, "utf8")
|
||||
|
||||
// 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 ${filePath}`)
|
||||
console.warn(`Skipping update for ${filePath}`)
|
||||
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
|
||||
|
||||
// Write the updated content
|
||||
await writeFileAsync(filePath, updatedContent, "utf8")
|
||||
console.log(`Updated ${filePath}`)
|
||||
} catch (err) {
|
||||
console.warn(`Warning: Could not update ${filePath}: ${err.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main function that orchestrates the update process
|
||||
*/
|
||||
async function main() {
|
||||
try {
|
||||
// Fetch contributors from GitHub (now handles pagination)
|
||||
const contributors = await fetchContributors()
|
||||
console.log(`Total contributors: ${contributors.length}`)
|
||||
|
||||
// Generate contributors section
|
||||
const contributorsSection = formatContributorsSection(contributors)
|
||||
|
||||
// Update main README
|
||||
const readmeContent = await readReadme()
|
||||
await updateReadme(readmeContent, contributorsSection)
|
||||
console.log(`Updated ${README_PATH}`)
|
||||
|
||||
// Find and update all localized README files
|
||||
const localizedReadmes = await findLocalizedReadmes()
|
||||
console.log(`Found ${localizedReadmes.length} localized README files`)
|
||||
|
||||
// Update each localized README
|
||||
for (const readmePath of localizedReadmes) {
|
||||
await updateLocalizedReadme(readmePath, contributorsSection)
|
||||
}
|
||||
|
||||
console.log("Contributors section update complete")
|
||||
} catch (error) {
|
||||
console.error(`Error: ${error.message}`)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// Run the script
|
||||
main()
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import OpenAI from "openai"
|
||||
|
||||
import type { ProviderSettings, ModelInfo } from "@roo-code/types"
|
||||
import type { ProviderSettings, ModelInfo, ToolProtocol } from "@roo-code/types"
|
||||
|
||||
import { ApiStream } from "./transform/stream"
|
||||
|
||||
|
|
@ -49,14 +50,20 @@ export interface SingleCompletionHandler {
|
|||
}
|
||||
|
||||
export interface ApiHandlerCreateMessageMetadata {
|
||||
mode?: string
|
||||
taskId: string
|
||||
previousResponseId?: string
|
||||
/**
|
||||
* When true, the provider must NOT fall back to internal continuity state
|
||||
* (e.g., lastResponseId) if previousResponseId is absent.
|
||||
* Used to enforce "skip once" after a condense operation.
|
||||
* Task ID used for tracking and provider-specific features:
|
||||
* - DeepInfra: Used as prompt_cache_key for caching
|
||||
* - Roo: Sent as X-Roo-Task-ID header
|
||||
* - Requesty: Sent as trace_id
|
||||
* - Unbound: Sent in unbound_metadata
|
||||
*/
|
||||
taskId: string
|
||||
/**
|
||||
* Current mode slug for provider-specific tracking:
|
||||
* - Requesty: Sent in extra metadata
|
||||
* - Unbound: Sent in unbound_metadata
|
||||
*/
|
||||
mode?: string
|
||||
suppressPreviousResponseId?: boolean
|
||||
/**
|
||||
* Controls whether the response should be stored for 30 days in OpenAI's Responses API.
|
||||
|
|
@ -66,6 +73,21 @@ export interface ApiHandlerCreateMessageMetadata {
|
|||
* @default true
|
||||
*/
|
||||
store?: boolean
|
||||
/**
|
||||
* Optional array of tool definitions to pass to the model.
|
||||
* For OpenAI-compatible providers, these are ChatCompletionTool definitions.
|
||||
*/
|
||||
tools?: OpenAI.Chat.ChatCompletionTool[]
|
||||
/**
|
||||
* Controls which (if any) tool is called by the model.
|
||||
* Can be "none", "auto", "required", or a specific tool choice.
|
||||
*/
|
||||
tool_choice?: OpenAI.Chat.ChatCompletionCreateParams["tool_choice"]
|
||||
/**
|
||||
* The tool protocol being used (XML or Native).
|
||||
* Used by providers to determine whether to include native tool definitions.
|
||||
*/
|
||||
toolProtocol?: ToolProtocol
|
||||
}
|
||||
|
||||
export interface ApiHandler {
|
||||
|
|
|
|||
|
|
@ -82,7 +82,25 @@ describe("MiniMaxHandler", () => {
|
|||
expect(model.info).toEqual(minimaxModels[testModelId])
|
||||
expect(model.info.contextWindow).toBe(192_000)
|
||||
expect(model.info.maxTokens).toBe(16_384)
|
||||
expect(model.info.supportsPromptCache).toBe(false)
|
||||
expect(model.info.supportsPromptCache).toBe(true)
|
||||
expect(model.info.cacheWritesPrice).toBe(0.375)
|
||||
expect(model.info.cacheReadsPrice).toBe(0.03)
|
||||
})
|
||||
|
||||
it("should return MiniMax-M2-Stable model with correct configuration", () => {
|
||||
const testModelId: MinimaxModelId = "MiniMax-M2-Stable"
|
||||
const handlerWithModel = new MiniMaxHandler({
|
||||
apiModelId: testModelId,
|
||||
minimaxApiKey: "test-minimax-api-key",
|
||||
})
|
||||
const model = handlerWithModel.getModel()
|
||||
expect(model.id).toBe(testModelId)
|
||||
expect(model.info).toEqual(minimaxModels[testModelId])
|
||||
expect(model.info.contextWindow).toBe(192_000)
|
||||
expect(model.info.maxTokens).toBe(16_384)
|
||||
expect(model.info.supportsPromptCache).toBe(true)
|
||||
expect(model.info.cacheWritesPrice).toBe(0.375)
|
||||
expect(model.info.cacheReadsPrice).toBe(0.03)
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -261,6 +279,34 @@ describe("MiniMaxHandler", () => {
|
|||
undefined,
|
||||
)
|
||||
})
|
||||
|
||||
it("should handle streaming chunks with null choices array", async () => {
|
||||
const testContent = "Content after null choices"
|
||||
|
||||
mockCreate.mockImplementationOnce(() => {
|
||||
return {
|
||||
[Symbol.asyncIterator]: () => ({
|
||||
next: vitest
|
||||
.fn()
|
||||
.mockResolvedValueOnce({
|
||||
done: false,
|
||||
value: { choices: null },
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
done: false,
|
||||
value: { choices: [{ delta: { content: testContent } }] },
|
||||
})
|
||||
.mockResolvedValueOnce({ done: true }),
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
const stream = handler.createMessage("system prompt", [])
|
||||
const firstChunk = await stream.next()
|
||||
|
||||
expect(firstChunk.done).toBe(false)
|
||||
expect(firstChunk.value).toEqual({ type: "text", text: testContent })
|
||||
})
|
||||
})
|
||||
|
||||
describe("Model Configuration", () => {
|
||||
|
|
@ -269,9 +315,23 @@ describe("MiniMaxHandler", () => {
|
|||
expect(model.maxTokens).toBe(16_384)
|
||||
expect(model.contextWindow).toBe(192_000)
|
||||
expect(model.supportsImages).toBe(false)
|
||||
expect(model.supportsPromptCache).toBe(false)
|
||||
expect(model.supportsPromptCache).toBe(true)
|
||||
expect(model.inputPrice).toBe(0.3)
|
||||
expect(model.outputPrice).toBe(1.2)
|
||||
expect(model.cacheWritesPrice).toBe(0.375)
|
||||
expect(model.cacheReadsPrice).toBe(0.03)
|
||||
})
|
||||
|
||||
it("should correctly configure MiniMax-M2-Stable model properties", () => {
|
||||
const model = minimaxModels["MiniMax-M2-Stable"]
|
||||
expect(model.maxTokens).toBe(16_384)
|
||||
expect(model.contextWindow).toBe(192_000)
|
||||
expect(model.supportsImages).toBe(false)
|
||||
expect(model.supportsPromptCache).toBe(true)
|
||||
expect(model.inputPrice).toBe(0.3)
|
||||
expect(model.outputPrice).toBe(1.2)
|
||||
expect(model.cacheWritesPrice).toBe(0.375)
|
||||
expect(model.cacheReadsPrice).toBe(0.03)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -344,6 +344,51 @@ describe("OpenAiNativeHandler - normalizeUsage", () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe("OpenAiNativeHandler - prompt cache retention", () => {
|
||||
let handler: OpenAiNativeHandler
|
||||
|
||||
beforeEach(() => {
|
||||
handler = new OpenAiNativeHandler({
|
||||
openAiNativeApiKey: "test-key",
|
||||
})
|
||||
})
|
||||
|
||||
const buildRequestBodyForModel = (modelId: string) => {
|
||||
// Force the handler to use the requested model ID
|
||||
;(handler as any).options.apiModelId = modelId
|
||||
const model = handler.getModel()
|
||||
// Minimal formatted input/systemPrompt/verbosity/metadata for building the body
|
||||
return (handler as any).buildRequestBody(model, [], "", model.verbosity, undefined, undefined)
|
||||
}
|
||||
|
||||
it("should set prompt_cache_retention=24h for gpt-5.1 models that support prompt caching", () => {
|
||||
const body = buildRequestBodyForModel("gpt-5.1")
|
||||
expect(body.prompt_cache_retention).toBe("24h")
|
||||
|
||||
const codexBody = buildRequestBodyForModel("gpt-5.1-codex")
|
||||
expect(codexBody.prompt_cache_retention).toBe("24h")
|
||||
|
||||
const codexMiniBody = buildRequestBodyForModel("gpt-5.1-codex-mini")
|
||||
expect(codexMiniBody.prompt_cache_retention).toBe("24h")
|
||||
})
|
||||
|
||||
it("should not set prompt_cache_retention for non-gpt-5.1 models even if they support prompt caching", () => {
|
||||
const body = buildRequestBodyForModel("gpt-5")
|
||||
expect(body.prompt_cache_retention).toBeUndefined()
|
||||
|
||||
const fourOBody = buildRequestBodyForModel("gpt-4o")
|
||||
expect(fourOBody.prompt_cache_retention).toBeUndefined()
|
||||
})
|
||||
|
||||
it("should not set prompt_cache_retention when the model does not support prompt caching", () => {
|
||||
const modelId = "codex-mini-latest"
|
||||
expect(openAiNativeModels[modelId as keyof typeof openAiNativeModels].supportsPromptCache).toBe(false)
|
||||
|
||||
const body = buildRequestBodyForModel(modelId)
|
||||
expect(body.prompt_cache_retention).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("cost calculation", () => {
|
||||
it("should pass total input tokens to calculateApiCostOpenAI", () => {
|
||||
const usage = {
|
||||
|
|
|
|||
|
|
@ -159,6 +159,9 @@ describe("OpenAiNativeHandler", () => {
|
|||
},
|
||||
],
|
||||
}),
|
||||
expect.objectContaining({
|
||||
signal: expect.any(Object),
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
|
|
@ -202,7 +205,7 @@ describe("OpenAiNativeHandler", () => {
|
|||
openAiNativeApiKey: "test-api-key",
|
||||
})
|
||||
const modelInfo = handlerWithoutModel.getModel()
|
||||
expect(modelInfo.id).toBe("gpt-5-2025-08-07") // Default model
|
||||
expect(modelInfo.id).toBe("gpt-5.1") // Default model
|
||||
expect(modelInfo.info).toBeDefined()
|
||||
})
|
||||
})
|
||||
|
|
@ -247,7 +250,7 @@ describe("OpenAiNativeHandler", () => {
|
|||
|
||||
handler = new OpenAiNativeHandler({
|
||||
...mockOptions,
|
||||
apiModelId: "gpt-5-2025-08-07",
|
||||
apiModelId: "gpt-5.1",
|
||||
})
|
||||
|
||||
const stream = handler.createMessage(systemPrompt, messages)
|
||||
|
|
@ -271,7 +274,7 @@ describe("OpenAiNativeHandler", () => {
|
|||
)
|
||||
const body1 = (mockFetch.mock.calls[0][1] as any).body as string
|
||||
const parsedBody = JSON.parse(body1)
|
||||
expect(parsedBody.model).toBe("gpt-5-2025-08-07")
|
||||
expect(parsedBody.model).toBe("gpt-5.1")
|
||||
expect(parsedBody.instructions).toBe("You are a helpful assistant.")
|
||||
// Now using structured format with content arrays (no system prompt in input; it's provided via `instructions`)
|
||||
expect(parsedBody.input).toEqual([
|
||||
|
|
@ -399,7 +402,7 @@ describe("OpenAiNativeHandler", () => {
|
|||
|
||||
handler = new OpenAiNativeHandler({
|
||||
...mockOptions,
|
||||
apiModelId: "gpt-5-2025-08-07",
|
||||
apiModelId: "gpt-5.1",
|
||||
verbosity: "low", // Set verbosity through options
|
||||
})
|
||||
|
||||
|
|
@ -442,7 +445,7 @@ describe("OpenAiNativeHandler", () => {
|
|||
|
||||
handler = new OpenAiNativeHandler({
|
||||
...mockOptions,
|
||||
apiModelId: "gpt-5-2025-08-07",
|
||||
apiModelId: "gpt-5.1",
|
||||
reasoningEffort: "minimal" as any, // GPT-5 supports minimal
|
||||
})
|
||||
|
||||
|
|
@ -461,6 +464,44 @@ describe("OpenAiNativeHandler", () => {
|
|||
)
|
||||
})
|
||||
|
||||
it("should omit reasoning when selection is 'disable'", async () => {
|
||||
// Mock fetch for Responses API
|
||||
const mockFetch = vitest.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
body: new ReadableStream({
|
||||
start(controller) {
|
||||
controller.enqueue(
|
||||
new TextEncoder().encode(
|
||||
'data: {"type":"response.output_item.added","item":{"type":"text","text":"No reasoning"}}\n\n',
|
||||
),
|
||||
)
|
||||
controller.enqueue(new TextEncoder().encode("data: [DONE]\n\n"))
|
||||
controller.close()
|
||||
},
|
||||
}),
|
||||
})
|
||||
global.fetch = mockFetch as any
|
||||
|
||||
// Mock SDK to fail
|
||||
mockResponsesCreate.mockRejectedValue(new Error("SDK not available"))
|
||||
|
||||
const handler = new OpenAiNativeHandler({
|
||||
...mockOptions,
|
||||
apiModelId: "gpt-5.1",
|
||||
reasoningEffort: "disable" as any,
|
||||
})
|
||||
|
||||
const stream = handler.createMessage(systemPrompt, messages)
|
||||
for await (const _ of stream) {
|
||||
// drain
|
||||
}
|
||||
|
||||
const bodyStr = (mockFetch.mock.calls[0][1] as any).body as string
|
||||
const parsed = JSON.parse(bodyStr)
|
||||
expect(parsed.reasoning).toBeUndefined()
|
||||
expect(parsed.include).toBeUndefined()
|
||||
})
|
||||
|
||||
it("should support low reasoning effort for GPT-5", async () => {
|
||||
// Mock fetch for Responses API
|
||||
const mockFetch = vitest.fn().mockResolvedValue({
|
||||
|
|
@ -484,7 +525,7 @@ describe("OpenAiNativeHandler", () => {
|
|||
|
||||
handler = new OpenAiNativeHandler({
|
||||
...mockOptions,
|
||||
apiModelId: "gpt-5-2025-08-07",
|
||||
apiModelId: "gpt-5.1",
|
||||
reasoningEffort: "low",
|
||||
})
|
||||
|
||||
|
|
@ -503,7 +544,7 @@ describe("OpenAiNativeHandler", () => {
|
|||
)
|
||||
const body2 = (mockFetch.mock.calls[0][1] as any).body as string
|
||||
const parsedBody = JSON.parse(body2)
|
||||
expect(parsedBody.model).toBe("gpt-5-2025-08-07")
|
||||
expect(parsedBody.model).toBe("gpt-5.1")
|
||||
expect(parsedBody.reasoning?.effort).toBe("low")
|
||||
expect(parsedBody.reasoning?.summary).toBe("auto")
|
||||
expect(parsedBody.text?.verbosity).toBe("medium")
|
||||
|
|
@ -535,7 +576,7 @@ describe("OpenAiNativeHandler", () => {
|
|||
|
||||
handler = new OpenAiNativeHandler({
|
||||
...mockOptions,
|
||||
apiModelId: "gpt-5-2025-08-07",
|
||||
apiModelId: "gpt-5.1",
|
||||
verbosity: "high",
|
||||
reasoningEffort: "minimal" as any,
|
||||
})
|
||||
|
|
@ -555,7 +596,7 @@ describe("OpenAiNativeHandler", () => {
|
|||
)
|
||||
const body3 = (mockFetch.mock.calls[0][1] as any).body as string
|
||||
const parsedBody = JSON.parse(body3)
|
||||
expect(parsedBody.model).toBe("gpt-5-2025-08-07")
|
||||
expect(parsedBody.model).toBe("gpt-5.1")
|
||||
expect(parsedBody.reasoning?.effort).toBe("minimal")
|
||||
expect(parsedBody.reasoning?.summary).toBe("auto")
|
||||
expect(parsedBody.text?.verbosity).toBe("high")
|
||||
|
|
@ -613,7 +654,7 @@ describe("OpenAiNativeHandler", () => {
|
|||
|
||||
handler = new OpenAiNativeHandler({
|
||||
...mockOptions,
|
||||
apiModelId: "gpt-5-2025-08-07",
|
||||
apiModelId: "gpt-5.1",
|
||||
})
|
||||
|
||||
const stream = handler.createMessage(systemPrompt, messages)
|
||||
|
|
@ -669,7 +710,7 @@ describe("OpenAiNativeHandler", () => {
|
|||
|
||||
handler = new OpenAiNativeHandler({
|
||||
...mockOptions,
|
||||
apiModelId: "gpt-5-2025-08-07",
|
||||
apiModelId: "gpt-5.1",
|
||||
})
|
||||
|
||||
const stream = handler.createMessage(systemPrompt, messages)
|
||||
|
|
@ -686,69 +727,6 @@ describe("OpenAiNativeHandler", () => {
|
|||
expect(contentChunks).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should support previous_response_id for conversation continuity", async () => {
|
||||
// Mock fetch for Responses API
|
||||
const mockFetch = vitest.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
body: new ReadableStream({
|
||||
start(controller) {
|
||||
// Include response ID in the response
|
||||
controller.enqueue(
|
||||
new TextEncoder().encode(
|
||||
'data: {"type":"response.created","response":{"id":"resp_123","status":"in_progress"}}\n\n',
|
||||
),
|
||||
)
|
||||
controller.enqueue(
|
||||
new TextEncoder().encode(
|
||||
'data: {"type":"response.output_item.added","item":{"type":"text","text":"Response with ID"}}\n\n',
|
||||
),
|
||||
)
|
||||
controller.enqueue(
|
||||
new TextEncoder().encode(
|
||||
'data: {"type":"response.done","response":{"id":"resp_123","usage":{"prompt_tokens":10,"completion_tokens":3}}}\n\n',
|
||||
),
|
||||
)
|
||||
controller.enqueue(new TextEncoder().encode("data: [DONE]\n\n"))
|
||||
controller.close()
|
||||
},
|
||||
}),
|
||||
})
|
||||
global.fetch = mockFetch as any
|
||||
|
||||
// Mock SDK to fail
|
||||
mockResponsesCreate.mockRejectedValue(new Error("SDK not available"))
|
||||
|
||||
handler = new OpenAiNativeHandler({
|
||||
...mockOptions,
|
||||
apiModelId: "gpt-5-2025-08-07",
|
||||
})
|
||||
|
||||
// First request - should not have previous_response_id
|
||||
const stream1 = handler.createMessage(systemPrompt, messages)
|
||||
const chunks1: any[] = []
|
||||
for await (const chunk of stream1) {
|
||||
chunks1.push(chunk)
|
||||
}
|
||||
|
||||
// Verify first request doesn't include previous_response_id
|
||||
let firstCallBody = JSON.parse(mockFetch.mock.calls[0][1].body)
|
||||
expect(firstCallBody.previous_response_id).toBeUndefined()
|
||||
|
||||
// Second request with metadata - should include previous_response_id
|
||||
const stream2 = handler.createMessage(systemPrompt, messages, {
|
||||
taskId: "test-task",
|
||||
previousResponseId: "resp_456",
|
||||
})
|
||||
const chunks2: any[] = []
|
||||
for await (const chunk of stream2) {
|
||||
chunks2.push(chunk)
|
||||
}
|
||||
|
||||
// Verify second request includes the provided previous_response_id
|
||||
let secondCallBody = JSON.parse(mockFetch.mock.calls[1][1].body)
|
||||
expect(secondCallBody.previous_response_id).toBe("resp_456")
|
||||
})
|
||||
|
||||
it("should handle unhandled stream events gracefully", async () => {
|
||||
// Mock fetch for the fallback SSE path
|
||||
const mockFetch = vitest.fn().mockResolvedValue({
|
||||
|
|
@ -777,7 +755,7 @@ describe("OpenAiNativeHandler", () => {
|
|||
|
||||
handler = new OpenAiNativeHandler({
|
||||
...mockOptions,
|
||||
apiModelId: "gpt-5-2025-08-07",
|
||||
apiModelId: "gpt-5.1",
|
||||
})
|
||||
|
||||
const stream = handler.createMessage(systemPrompt, messages)
|
||||
|
|
@ -798,397 +776,44 @@ describe("OpenAiNativeHandler", () => {
|
|||
expect(textChunks[0].text).toBe("Hello")
|
||||
})
|
||||
|
||||
it("should use stored response ID when metadata doesn't provide one", async () => {
|
||||
// Mock fetch for Responses API
|
||||
const mockFetch = vitest
|
||||
.fn()
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
body: new ReadableStream({
|
||||
start(controller) {
|
||||
// First response with ID
|
||||
controller.enqueue(
|
||||
new TextEncoder().encode(
|
||||
'data: {"type":"response.done","response":{"id":"resp_789","output":[{"type":"text","content":[{"type":"text","text":"First"}]}],"usage":{"prompt_tokens":10,"completion_tokens":1}}}\n\n',
|
||||
),
|
||||
)
|
||||
controller.enqueue(new TextEncoder().encode("data: [DONE]\n\n"))
|
||||
controller.close()
|
||||
},
|
||||
}),
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
body: new ReadableStream({
|
||||
start(controller) {
|
||||
// Second response
|
||||
controller.enqueue(
|
||||
new TextEncoder().encode(
|
||||
'data: {"type":"response.output_item.added","item":{"type":"text","text":"Second"}}\n\n',
|
||||
),
|
||||
)
|
||||
controller.enqueue(new TextEncoder().encode("data: [DONE]\n\n"))
|
||||
controller.close()
|
||||
},
|
||||
}),
|
||||
})
|
||||
global.fetch = mockFetch as any
|
||||
|
||||
// Mock SDK to fail
|
||||
mockResponsesCreate.mockRejectedValue(new Error("SDK not available"))
|
||||
|
||||
handler = new OpenAiNativeHandler({
|
||||
...mockOptions,
|
||||
apiModelId: "gpt-5-2025-08-07",
|
||||
})
|
||||
|
||||
// First request - establishes response ID
|
||||
const stream1 = handler.createMessage(systemPrompt, messages)
|
||||
for await (const chunk of stream1) {
|
||||
// consume stream
|
||||
}
|
||||
|
||||
// Second request without metadata - should use stored response ID
|
||||
const stream2 = handler.createMessage(systemPrompt, messages, { taskId: "test-task" })
|
||||
for await (const chunk of stream2) {
|
||||
// consume stream
|
||||
}
|
||||
|
||||
// Verify second request uses the stored response ID from first request
|
||||
let secondCallBody = JSON.parse(mockFetch.mock.calls[1][1].body)
|
||||
expect(secondCallBody.previous_response_id).toBe("resp_789")
|
||||
})
|
||||
|
||||
it("should retry with full conversation when previous_response_id fails", async () => {
|
||||
// This test verifies the fix for context loss bug when previous_response_id becomes invalid
|
||||
const mockFetch = vitest
|
||||
.fn()
|
||||
// First call: fails with 400 error about invalid previous_response_id
|
||||
.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 400,
|
||||
text: async () => JSON.stringify({ error: { message: "Previous response not found" } }),
|
||||
})
|
||||
// Second call (retry): succeeds
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
body: new ReadableStream({
|
||||
start(controller) {
|
||||
controller.enqueue(
|
||||
new TextEncoder().encode(
|
||||
'data: {"type":"response.output_item.added","item":{"type":"text","text":"Retry successful"}}\n\n',
|
||||
),
|
||||
)
|
||||
controller.enqueue(
|
||||
new TextEncoder().encode(
|
||||
'data: {"type":"response.done","response":{"id":"resp_new","usage":{"prompt_tokens":100,"completion_tokens":2}}}\n\n',
|
||||
),
|
||||
)
|
||||
controller.enqueue(new TextEncoder().encode("data: [DONE]\n\n"))
|
||||
controller.close()
|
||||
},
|
||||
}),
|
||||
})
|
||||
global.fetch = mockFetch as any
|
||||
|
||||
// Mock SDK to fail
|
||||
mockResponsesCreate.mockRejectedValue(new Error("SDK not available"))
|
||||
|
||||
handler = new OpenAiNativeHandler({
|
||||
...mockOptions,
|
||||
apiModelId: "gpt-5-2025-08-07",
|
||||
})
|
||||
|
||||
// Prepare a multi-turn conversation
|
||||
const conversationMessages: Anthropic.Messages.MessageParam[] = [
|
||||
{ role: "user", content: "What is 2+2?" },
|
||||
{ role: "assistant", content: "2+2 equals 4." },
|
||||
{ role: "user", content: "What about 3+3?" },
|
||||
{ role: "assistant", content: "3+3 equals 6." },
|
||||
{ role: "user", content: "And 4+4?" }, // Latest message
|
||||
]
|
||||
|
||||
// Call with a previous_response_id that will fail
|
||||
const stream = handler.createMessage(systemPrompt, conversationMessages, {
|
||||
taskId: "test-task",
|
||||
previousResponseId: "resp_invalid",
|
||||
})
|
||||
|
||||
const chunks: any[] = []
|
||||
for await (const chunk of stream) {
|
||||
chunks.push(chunk)
|
||||
}
|
||||
|
||||
// Verify we got the successful response
|
||||
const textChunks = chunks.filter((c) => c.type === "text")
|
||||
expect(textChunks).toHaveLength(1)
|
||||
expect(textChunks[0].text).toBe("Retry successful")
|
||||
|
||||
// Verify two requests were made
|
||||
expect(mockFetch).toHaveBeenCalledTimes(2)
|
||||
|
||||
// First request: includes previous_response_id and only latest message
|
||||
const firstCallBody = JSON.parse(mockFetch.mock.calls[0][1].body)
|
||||
expect(firstCallBody.previous_response_id).toBe("resp_invalid")
|
||||
expect(firstCallBody.input).toEqual([
|
||||
{
|
||||
role: "user",
|
||||
content: [{ type: "input_text", text: "And 4+4?" }],
|
||||
},
|
||||
])
|
||||
|
||||
// Second request (retry): NO previous_response_id, but FULL conversation history
|
||||
const secondCallBody = JSON.parse(mockFetch.mock.calls[1][1].body)
|
||||
expect(secondCallBody.previous_response_id).toBeUndefined()
|
||||
expect(secondCallBody.instructions).toBe(systemPrompt)
|
||||
// Should include the FULL conversation history
|
||||
expect(secondCallBody.input).toEqual([
|
||||
{
|
||||
role: "user",
|
||||
content: [{ type: "input_text", text: "What is 2+2?" }],
|
||||
},
|
||||
{
|
||||
role: "assistant",
|
||||
content: [{ type: "output_text", text: "2+2 equals 4." }],
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: [{ type: "input_text", text: "What about 3+3?" }],
|
||||
},
|
||||
{
|
||||
role: "assistant",
|
||||
content: [{ type: "output_text", text: "3+3 equals 6." }],
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: [{ type: "input_text", text: "And 4+4?" }],
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it("should retry with full conversation when SDK returns 400 for invalid previous_response_id", async () => {
|
||||
// Test the SDK path (executeRequest method) for handling invalid previous_response_id
|
||||
|
||||
// Mock SDK to return an async iterable that we can control
|
||||
const createMockStream = (chunks: any[]) => {
|
||||
return {
|
||||
async *[Symbol.asyncIterator]() {
|
||||
for (const chunk of chunks) {
|
||||
yield chunk
|
||||
}
|
||||
it("should format full conversation correctly", async () => {
|
||||
const mockFetch = vitest.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
body: new ReadableStream({
|
||||
start(controller) {
|
||||
controller.enqueue(
|
||||
new TextEncoder().encode(
|
||||
'data: {"type":"response.output_item.added","item":{"type":"text","text":"Response"}}\n\n',
|
||||
),
|
||||
)
|
||||
controller.enqueue(new TextEncoder().encode("data: [DONE]\n\n"))
|
||||
controller.close()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// First call: SDK throws 400 error
|
||||
mockResponsesCreate
|
||||
.mockRejectedValueOnce({
|
||||
status: 400,
|
||||
message: "Previous response resp_invalid not found",
|
||||
})
|
||||
// Second call (retry): SDK succeeds with async iterable
|
||||
.mockResolvedValueOnce(
|
||||
createMockStream([
|
||||
{ type: "response.text.delta", delta: "Context" },
|
||||
{ type: "response.text.delta", delta: " preserved!" },
|
||||
{
|
||||
type: "response.done",
|
||||
response: { id: "resp_new", usage: { prompt_tokens: 150, completion_tokens: 2 } },
|
||||
},
|
||||
]),
|
||||
)
|
||||
|
||||
handler = new OpenAiNativeHandler({
|
||||
...mockOptions,
|
||||
apiModelId: "gpt-5-2025-08-07",
|
||||
}),
|
||||
})
|
||||
|
||||
// Prepare a conversation with context
|
||||
const conversationMessages: Anthropic.Messages.MessageParam[] = [
|
||||
{ role: "user", content: "Remember the number 42" },
|
||||
{ role: "assistant", content: "I'll remember 42." },
|
||||
{ role: "user", content: "What number did I ask you to remember?" },
|
||||
]
|
||||
|
||||
// Call with a previous_response_id that will fail
|
||||
const stream = handler.createMessage(systemPrompt, conversationMessages, {
|
||||
taskId: "test-task",
|
||||
previousResponseId: "resp_invalid",
|
||||
})
|
||||
|
||||
const chunks: any[] = []
|
||||
for await (const chunk of stream) {
|
||||
chunks.push(chunk)
|
||||
}
|
||||
|
||||
// Verify we got the successful response
|
||||
const textChunks = chunks.filter((c) => c.type === "text")
|
||||
expect(textChunks).toHaveLength(2)
|
||||
expect(textChunks[0].text).toBe("Context")
|
||||
expect(textChunks[1].text).toBe(" preserved!")
|
||||
|
||||
// Verify two SDK calls were made
|
||||
expect(mockResponsesCreate).toHaveBeenCalledTimes(2)
|
||||
|
||||
// First SDK call: includes previous_response_id and only latest message
|
||||
const firstCallBody = mockResponsesCreate.mock.calls[0][0]
|
||||
expect(firstCallBody.previous_response_id).toBe("resp_invalid")
|
||||
expect(firstCallBody.input).toEqual([
|
||||
{
|
||||
role: "user",
|
||||
content: [{ type: "input_text", text: "What number did I ask you to remember?" }],
|
||||
},
|
||||
])
|
||||
|
||||
// Second SDK call (retry): NO previous_response_id, but FULL conversation history
|
||||
const secondCallBody = mockResponsesCreate.mock.calls[1][0]
|
||||
expect(secondCallBody.previous_response_id).toBeUndefined()
|
||||
expect(secondCallBody.instructions).toBe(systemPrompt)
|
||||
// Should include the FULL conversation history to preserve context
|
||||
expect(secondCallBody.input).toEqual([
|
||||
{
|
||||
role: "user",
|
||||
content: [{ type: "input_text", text: "Remember the number 42" }],
|
||||
},
|
||||
{
|
||||
role: "assistant",
|
||||
content: [{ type: "output_text", text: "I'll remember 42." }],
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: [{ type: "input_text", text: "What number did I ask you to remember?" }],
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it("should only send latest message when using previous_response_id", async () => {
|
||||
// Mock fetch for Responses API
|
||||
const mockFetch = vitest
|
||||
.fn()
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
body: new ReadableStream({
|
||||
start(controller) {
|
||||
// First response with ID
|
||||
controller.enqueue(
|
||||
new TextEncoder().encode(
|
||||
'data: {"type":"response.done","response":{"id":"resp_001","output":[{"type":"text","content":[{"type":"text","text":"First"}]}],"usage":{"prompt_tokens":50,"completion_tokens":1}}}\n\n',
|
||||
),
|
||||
)
|
||||
controller.enqueue(new TextEncoder().encode("data: [DONE]\n\n"))
|
||||
controller.close()
|
||||
},
|
||||
}),
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
body: new ReadableStream({
|
||||
start(controller) {
|
||||
// Second response
|
||||
controller.enqueue(
|
||||
new TextEncoder().encode(
|
||||
'data: {"type":"response.output_item.added","item":{"type":"text","text":"Second"}}\n\n',
|
||||
),
|
||||
)
|
||||
controller.enqueue(
|
||||
new TextEncoder().encode(
|
||||
'data: {"type":"response.done","response":{"id":"resp_002","usage":{"prompt_tokens":10,"completion_tokens":1}}}\n\n',
|
||||
),
|
||||
)
|
||||
controller.enqueue(new TextEncoder().encode("data: [DONE]\n\n"))
|
||||
controller.close()
|
||||
},
|
||||
}),
|
||||
})
|
||||
global.fetch = mockFetch as any
|
||||
|
||||
// Mock SDK to fail
|
||||
mockResponsesCreate.mockRejectedValue(new Error("SDK not available"))
|
||||
|
||||
handler = new OpenAiNativeHandler({
|
||||
...mockOptions,
|
||||
apiModelId: "gpt-5-2025-08-07",
|
||||
})
|
||||
|
||||
// First request with full conversation
|
||||
const firstMessages: Anthropic.Messages.MessageParam[] = [
|
||||
{ role: "user", content: "Hello" },
|
||||
{ role: "assistant", content: "Hi there!" },
|
||||
{ role: "user", content: "How are you?" },
|
||||
]
|
||||
|
||||
const stream1 = handler.createMessage(systemPrompt, firstMessages)
|
||||
for await (const chunk of stream1) {
|
||||
// consume stream
|
||||
}
|
||||
|
||||
// Verify first request sends full conversation in structured format
|
||||
let firstCallBody = JSON.parse(mockFetch.mock.calls[0][1].body)
|
||||
expect(firstCallBody.instructions).toBe(systemPrompt)
|
||||
expect(firstCallBody.input).toEqual([
|
||||
{
|
||||
role: "user",
|
||||
content: [{ type: "input_text", text: "Hello" }],
|
||||
},
|
||||
{
|
||||
role: "assistant",
|
||||
content: [{ type: "output_text", text: "Hi there!" }],
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: [{ type: "input_text", text: "How are you?" }],
|
||||
},
|
||||
])
|
||||
expect(firstCallBody.previous_response_id).toBeUndefined()
|
||||
|
||||
// Second request with previous_response_id - should only send latest message
|
||||
const secondMessages: Anthropic.Messages.MessageParam[] = [
|
||||
{ role: "user", content: "Hello" },
|
||||
{ role: "assistant", content: "Hi there!" },
|
||||
{ role: "user", content: "How are you?" },
|
||||
{ role: "assistant", content: "I'm doing well!" },
|
||||
{ role: "user", content: "What's the weather?" }, // Latest message
|
||||
]
|
||||
|
||||
const stream2 = handler.createMessage(systemPrompt, secondMessages, {
|
||||
taskId: "test-task",
|
||||
previousResponseId: "resp_001",
|
||||
})
|
||||
for await (const chunk of stream2) {
|
||||
// consume stream
|
||||
}
|
||||
|
||||
// Verify second request only sends the latest user message in structured format
|
||||
let secondCallBody = JSON.parse(mockFetch.mock.calls[1][1].body)
|
||||
expect(secondCallBody.input).toEqual([
|
||||
{
|
||||
role: "user",
|
||||
content: [{ type: "input_text", text: "What's the weather?" }],
|
||||
},
|
||||
])
|
||||
expect(secondCallBody.previous_response_id).toBe("resp_001")
|
||||
})
|
||||
|
||||
it("should correctly prepare structured input", () => {
|
||||
const gpt5Handler = new OpenAiNativeHandler({
|
||||
...mockOptions,
|
||||
apiModelId: "gpt-5-2025-08-07",
|
||||
apiModelId: "gpt-5.1",
|
||||
})
|
||||
|
||||
// Test with metadata that has previousResponseId
|
||||
// @ts-expect-error - private method
|
||||
const { formattedInput, previousResponseId } = gpt5Handler.prepareStructuredInput(systemPrompt, messages, {
|
||||
const stream = gpt5Handler.createMessage(systemPrompt, messages, {
|
||||
taskId: "task1",
|
||||
previousResponseId: "resp_123",
|
||||
})
|
||||
for await (const chunk of stream) {
|
||||
// consume
|
||||
}
|
||||
|
||||
expect(previousResponseId).toBe("resp_123")
|
||||
expect(formattedInput).toEqual([
|
||||
const callBody = JSON.parse(mockFetch.mock.calls[0][1].body)
|
||||
expect(callBody.input).toEqual([
|
||||
{
|
||||
role: "user",
|
||||
content: [{ type: "input_text", text: "Hello!" }],
|
||||
},
|
||||
])
|
||||
expect(callBody.previous_response_id).toBeUndefined()
|
||||
})
|
||||
|
||||
it("should provide helpful error messages for different error codes", async () => {
|
||||
|
|
@ -1216,7 +841,7 @@ describe("OpenAiNativeHandler", () => {
|
|||
|
||||
handler = new OpenAiNativeHandler({
|
||||
...mockOptions,
|
||||
apiModelId: "gpt-5-2025-08-07",
|
||||
apiModelId: "gpt-5.1",
|
||||
})
|
||||
|
||||
const stream = handler.createMessage(systemPrompt, messages)
|
||||
|
|
@ -1266,7 +891,7 @@ describe("GPT-5 streaming event coverage (additional)", () => {
|
|||
mockResponsesCreate.mockRejectedValue(new Error("SDK not available"))
|
||||
|
||||
const handler = new OpenAiNativeHandler({
|
||||
apiModelId: "gpt-5-2025-08-07",
|
||||
apiModelId: "gpt-5.1",
|
||||
openAiNativeApiKey: "test-api-key",
|
||||
})
|
||||
|
||||
|
|
@ -1309,7 +934,7 @@ describe("GPT-5 streaming event coverage (additional)", () => {
|
|||
mockResponsesCreate.mockRejectedValue(new Error("SDK not available"))
|
||||
|
||||
const handler = new OpenAiNativeHandler({
|
||||
apiModelId: "gpt-5-2025-08-07",
|
||||
apiModelId: "gpt-5.1",
|
||||
openAiNativeApiKey: "test-api-key",
|
||||
})
|
||||
|
||||
|
|
@ -1358,7 +983,7 @@ describe("GPT-5 streaming event coverage (additional)", () => {
|
|||
mockResponsesCreate.mockRejectedValue(new Error("SDK not available"))
|
||||
|
||||
const handler = new OpenAiNativeHandler({
|
||||
apiModelId: "gpt-5-2025-08-07",
|
||||
apiModelId: "gpt-5.1",
|
||||
openAiNativeApiKey: "test-api-key",
|
||||
})
|
||||
|
||||
|
|
@ -1514,6 +1139,9 @@ describe("GPT-5 streaming event coverage (additional)", () => {
|
|||
stream: false,
|
||||
store: false,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
signal: expect.any(Object),
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -494,7 +494,7 @@ describe("OpenAiHandler", () => {
|
|||
{
|
||||
model: azureOptions.openAiModelId,
|
||||
messages: [
|
||||
{ role: "user", content: systemPrompt },
|
||||
{ role: "system", content: systemPrompt },
|
||||
{ role: "user", content: "Hello!" },
|
||||
],
|
||||
},
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ vitest.mock("../fetchers/modelCache", () => ({
|
|||
contextWindow: 200000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
supportsNativeTools: true,
|
||||
inputPrice: 3,
|
||||
outputPrice: 15,
|
||||
cacheWritesPrice: 3.75,
|
||||
|
|
@ -119,6 +120,7 @@ describe("OpenRouterHandler", () => {
|
|||
const result = await handler.fetchModel()
|
||||
expect(result.id).toBe("anthropic/claude-sonnet-4.5")
|
||||
expect(result.info.supportsPromptCache).toBe(true)
|
||||
expect(result.info.supportsNativeTools).toBe(true)
|
||||
})
|
||||
|
||||
it("honors custom maxTokens for thinking models", async () => {
|
||||
|
|
|
|||
|
|
@ -116,7 +116,15 @@ export abstract class BaseOpenAiCompatibleProvider<ModelName extends string>
|
|||
)
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
// Check for provider-specific error responses (e.g., MiniMax base_resp)
|
||||
const chunkAny = chunk as any
|
||||
if (chunkAny.base_resp?.status_code && chunkAny.base_resp.status_code !== 0) {
|
||||
throw new Error(
|
||||
`${this.providerName} API Error (${chunkAny.base_resp.status_code}): ${chunkAny.base_resp.status_msg || "Unknown error"}`,
|
||||
)
|
||||
}
|
||||
|
||||
const delta = chunk.choices?.[0]?.delta
|
||||
|
||||
if (delta?.content) {
|
||||
for (const processedChunk of matcher.update(delta.content)) {
|
||||
|
|
@ -155,7 +163,15 @@ export abstract class BaseOpenAiCompatibleProvider<ModelName extends string>
|
|||
messages: [{ role: "user", content: prompt }],
|
||||
})
|
||||
|
||||
return response.choices[0]?.message.content || ""
|
||||
// Check for provider-specific error responses (e.g., MiniMax base_resp)
|
||||
const responseAny = response as any
|
||||
if (responseAny.base_resp?.status_code && responseAny.base_resp.status_code !== 0) {
|
||||
throw new Error(
|
||||
`${this.providerName} API Error (${responseAny.base_resp.status_code}): ${responseAny.base_resp.status_msg || "Unknown error"}`,
|
||||
)
|
||||
}
|
||||
|
||||
return response.choices?.[0]?.message.content || ""
|
||||
} catch (error) {
|
||||
throw handleOpenAIError(error, this.providerName)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ describe("OpenRouter API", () => {
|
|||
description: expect.any(String),
|
||||
supportsReasoningBudget: false,
|
||||
supportsReasoningEffort: false,
|
||||
supportsNativeTools: true,
|
||||
supportedParameters: ["max_tokens", "temperature", "reasoning", "include_reasoning"],
|
||||
})
|
||||
|
||||
|
|
@ -44,6 +45,7 @@ describe("OpenRouter API", () => {
|
|||
supportsReasoningBudget: true,
|
||||
requiredReasoningBudget: true,
|
||||
supportsReasoningEffort: true,
|
||||
supportsNativeTools: true,
|
||||
supportedParameters: ["max_tokens", "temperature", "reasoning", "include_reasoning"],
|
||||
})
|
||||
|
||||
|
|
@ -96,6 +98,7 @@ describe("OpenRouter API", () => {
|
|||
cacheReadsPrice: 0.31,
|
||||
description: undefined,
|
||||
supportsReasoningEffort: undefined,
|
||||
supportsNativeTools: undefined,
|
||||
supportedParameters: undefined,
|
||||
},
|
||||
"google-ai-studio": {
|
||||
|
|
@ -110,6 +113,7 @@ describe("OpenRouter API", () => {
|
|||
cacheReadsPrice: 0.31,
|
||||
description: undefined,
|
||||
supportsReasoningEffort: undefined,
|
||||
supportsNativeTools: undefined,
|
||||
supportedParameters: undefined,
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -68,6 +68,7 @@ describe("getRooModels", () => {
|
|||
supportsImages: true,
|
||||
supportsReasoningEffort: true,
|
||||
requiredReasoningEffort: false,
|
||||
supportsNativeTools: false,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 100, // 0.0001 * 1_000_000
|
||||
outputPrice: 200, // 0.0002 * 1_000_000
|
||||
|
|
@ -116,6 +117,7 @@ describe("getRooModels", () => {
|
|||
supportsImages: false,
|
||||
supportsReasoningEffort: true,
|
||||
requiredReasoningEffort: true,
|
||||
supportsNativeTools: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 100, // 0.0001 * 1_000_000
|
||||
outputPrice: 200, // 0.0002 * 1_000_000
|
||||
|
|
@ -162,6 +164,7 @@ describe("getRooModels", () => {
|
|||
supportsImages: false,
|
||||
supportsReasoningEffort: false,
|
||||
requiredReasoningEffort: false,
|
||||
supportsNativeTools: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 100, // 0.0001 * 1_000_000
|
||||
outputPrice: 200, // 0.0002 * 1_000_000
|
||||
|
|
|
|||
|
|
@ -115,7 +115,7 @@ export async function getOpenRouterModels(options?: ApiHandlerOptions): Promise<
|
|||
continue
|
||||
}
|
||||
|
||||
models[id] = parseOpenRouterModel({
|
||||
const parsedModel = parseOpenRouterModel({
|
||||
id,
|
||||
model,
|
||||
inputModality: architecture?.input_modalities,
|
||||
|
|
@ -123,6 +123,8 @@ export async function getOpenRouterModels(options?: ApiHandlerOptions): Promise<
|
|||
maxTokens: top_provider?.max_completion_tokens,
|
||||
supportedParameters: supported_parameters,
|
||||
})
|
||||
|
||||
models[id] = parsedModel
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
|
|
@ -216,6 +218,7 @@ export const parseOpenRouterModel = ({
|
|||
cacheReadsPrice,
|
||||
description: model.description,
|
||||
supportsReasoningEffort: supportedParameters ? supportedParameters.includes("reasoning") : undefined,
|
||||
supportsNativeTools: supportedParameters ? supportedParameters.includes("tools") : undefined,
|
||||
supportedParameters: supportedParameters ? supportedParameters.filter(isModelParameter) : undefined,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,11 @@ import { DEFAULT_HEADERS } from "../constants"
|
|||
* @throws Will throw an error if the request fails or the response is not as expected.
|
||||
*/
|
||||
export async function getRooModels(baseUrl: string, apiKey?: string): Promise<ModelRecord> {
|
||||
// Construct the models endpoint URL early so it's available in catch block for logging
|
||||
// Strip trailing /v1 or /v1/ to avoid /v1/v1/models
|
||||
const normalizedBase = baseUrl.replace(/\/?v1\/?$/, "")
|
||||
const url = `${normalizedBase}/v1/models`
|
||||
|
||||
try {
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/json",
|
||||
|
|
@ -24,11 +29,6 @@ export async function getRooModels(baseUrl: string, apiKey?: string): Promise<Mo
|
|||
headers["Authorization"] = `Bearer ${apiKey}`
|
||||
}
|
||||
|
||||
// Construct the models endpoint URL
|
||||
// Strip trailing /v1 or /v1/ to avoid /v1/v1/models
|
||||
const normalizedBase = baseUrl.replace(/\/?v1\/?$/, "")
|
||||
const url = `${normalizedBase}/v1/models`
|
||||
|
||||
// Use fetch with AbortController for better timeout handling
|
||||
const controller = new AbortController()
|
||||
const timeoutId = setTimeout(() => controller.abort(), 10000)
|
||||
|
|
@ -40,6 +40,21 @@ export async function getRooModels(baseUrl: string, apiKey?: string): Promise<Mo
|
|||
})
|
||||
|
||||
if (!response.ok) {
|
||||
// Log detailed error information
|
||||
let errorBody = ""
|
||||
try {
|
||||
errorBody = await response.text()
|
||||
} catch {
|
||||
errorBody = "(unable to read response body)"
|
||||
}
|
||||
|
||||
console.error(`[getRooModels] HTTP error:`, {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
url,
|
||||
body: errorBody,
|
||||
})
|
||||
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
|
||||
}
|
||||
|
||||
|
|
@ -77,6 +92,9 @@ export async function getRooModels(baseUrl: string, apiKey?: string): Promise<Mo
|
|||
// Determine if the model requires reasoning effort based on tags
|
||||
const requiredReasoningEffort = tags.includes("reasoning-required")
|
||||
|
||||
// Determine if the model supports native tool calling based on tags
|
||||
const supportsNativeTools = tags.includes("tool-use")
|
||||
|
||||
// Parse pricing (API returns strings, convert to numbers)
|
||||
const inputPrice = parseApiPrice(pricing.input)
|
||||
const outputPrice = parseApiPrice(pricing.output)
|
||||
|
|
@ -89,6 +107,7 @@ export async function getRooModels(baseUrl: string, apiKey?: string): Promise<Mo
|
|||
supportsImages,
|
||||
supportsReasoningEffort,
|
||||
requiredReasoningEffort,
|
||||
supportsNativeTools,
|
||||
supportsPromptCache: Boolean(cacheReadPrice !== undefined),
|
||||
inputPrice,
|
||||
outputPrice,
|
||||
|
|
@ -105,7 +124,14 @@ export async function getRooModels(baseUrl: string, apiKey?: string): Promise<Mo
|
|||
clearTimeout(timeoutId)
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("Error fetching Roo Code Cloud models:", error.message ? error.message : error)
|
||||
// Enhanced error logging
|
||||
console.error("[getRooModels] Error fetching Roo Code Cloud models:", {
|
||||
message: error.message || String(error),
|
||||
name: error.name,
|
||||
stack: error.stack,
|
||||
url,
|
||||
hasApiKey: Boolean(apiKey),
|
||||
})
|
||||
|
||||
// Handle abort/timeout
|
||||
if (error.name === "AbortError") {
|
||||
|
|
|
|||
|
|
@ -7,10 +7,9 @@ import {
|
|||
OpenAiNativeModelId,
|
||||
openAiNativeModels,
|
||||
OPENAI_NATIVE_DEFAULT_TEMPERATURE,
|
||||
GPT5_DEFAULT_TEMPERATURE,
|
||||
type ReasoningEffort,
|
||||
type VerbosityLevel,
|
||||
type ReasoningEffortWithMinimal,
|
||||
type ReasoningEffortExtended,
|
||||
type ServiceTier,
|
||||
} from "@roo-code/types"
|
||||
|
||||
|
|
@ -26,19 +25,17 @@ import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from ".
|
|||
|
||||
export type OpenAiNativeModel = ReturnType<OpenAiNativeHandler["getModel"]>
|
||||
|
||||
// GPT-5 specific types
|
||||
|
||||
// Constants for model identification
|
||||
const GPT5_MODEL_PREFIX = "gpt-5"
|
||||
|
||||
export class OpenAiNativeHandler extends BaseProvider implements SingleCompletionHandler {
|
||||
protected options: ApiHandlerOptions
|
||||
private client: OpenAI
|
||||
private lastResponseId: string | undefined
|
||||
private responseIdPromise: Promise<string | undefined> | undefined
|
||||
private responseIdResolver: ((value: string | undefined) => void) | undefined
|
||||
// Resolved service tier from Responses API (actual tier used by OpenAI)
|
||||
private lastServiceTier: ServiceTier | undefined
|
||||
// Complete response output array (includes reasoning items with encrypted_content)
|
||||
private lastResponseOutput: any[] | undefined
|
||||
// Last top-level response id from Responses API (for troubleshooting)
|
||||
private lastResponseId: string | undefined
|
||||
// Abort controller for cancelling ongoing requests
|
||||
private abortController?: AbortController
|
||||
|
||||
// Event types handled by the shared event processor to avoid duplication
|
||||
private readonly coreHandledEventTypes = new Set<string>([
|
||||
|
|
@ -57,9 +54,10 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
|
|||
constructor(options: ApiHandlerOptions) {
|
||||
super()
|
||||
this.options = options
|
||||
// Default to including reasoning.summary: "auto" for GPT‑5 unless explicitly disabled
|
||||
if (this.options.enableGpt5ReasoningSummary === undefined) {
|
||||
this.options.enableGpt5ReasoningSummary = true
|
||||
// Default to including reasoning.summary: "auto" for models that support Responses API
|
||||
// reasoning summaries unless explicitly disabled.
|
||||
if (this.options.enableResponsesReasoningSummary === undefined) {
|
||||
this.options.enableResponsesReasoningSummary = true
|
||||
}
|
||||
const apiKey = this.options.openAiNativeApiKey ?? "not-provided"
|
||||
this.client = new OpenAI({ baseURL: this.options.openAiNativeBaseUrl, apiKey })
|
||||
|
|
@ -126,17 +124,6 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
|
|||
return out
|
||||
}
|
||||
|
||||
private resolveResponseId(responseId: string | undefined): void {
|
||||
if (responseId) {
|
||||
this.lastResponseId = responseId
|
||||
}
|
||||
// Resolve the promise so the next request can use this ID
|
||||
if (this.responseIdResolver) {
|
||||
this.responseIdResolver(responseId)
|
||||
this.responseIdResolver = undefined
|
||||
}
|
||||
}
|
||||
|
||||
override async *createMessage(
|
||||
systemPrompt: string,
|
||||
messages: Anthropic.Messages.MessageParam[],
|
||||
|
|
@ -156,6 +143,10 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
|
|||
): ApiStream {
|
||||
// Reset resolved tier for this request; will be set from response if present
|
||||
this.lastServiceTier = undefined
|
||||
// Reset output array to capture current response output items
|
||||
this.lastResponseOutput = undefined
|
||||
// Reset last response id for this request
|
||||
this.lastResponseId = undefined
|
||||
|
||||
// Use Responses API for ALL models
|
||||
const { verbosity, reasoning } = this.getModel()
|
||||
|
|
@ -163,54 +154,13 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
|
|||
// Resolve reasoning effort for models that support it
|
||||
const reasoningEffort = this.getReasoningEffort(model)
|
||||
|
||||
// Wait for any pending response ID from a previous request to be available
|
||||
// This handles the race condition with fast nano model responses
|
||||
let effectivePreviousResponseId = metadata?.previousResponseId
|
||||
|
||||
// Check if we should suppress previous response ID (e.g., after condense or message edit)
|
||||
if (metadata?.suppressPreviousResponseId) {
|
||||
// Clear the stored lastResponseId to prevent it from being used in future requests
|
||||
this.lastResponseId = undefined
|
||||
effectivePreviousResponseId = undefined
|
||||
} else {
|
||||
// Only try to get fallback response IDs if not suppressing
|
||||
|
||||
// If we have a pending response ID promise, wait for it to resolve
|
||||
if (!effectivePreviousResponseId && this.responseIdPromise) {
|
||||
try {
|
||||
const resolvedId = await Promise.race([
|
||||
this.responseIdPromise,
|
||||
// Timeout after 100ms to avoid blocking too long
|
||||
new Promise<undefined>((resolve) => setTimeout(() => resolve(undefined), 100)),
|
||||
])
|
||||
if (resolvedId) {
|
||||
effectivePreviousResponseId = resolvedId
|
||||
}
|
||||
} catch {
|
||||
// Non-fatal if promise fails
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to the last known response ID if still not available
|
||||
if (!effectivePreviousResponseId && this.lastResponseId) {
|
||||
effectivePreviousResponseId = this.lastResponseId
|
||||
}
|
||||
}
|
||||
|
||||
// Format input and capture continuity id
|
||||
const { formattedInput, previousResponseId } = this.prepareStructuredInput(systemPrompt, messages, metadata)
|
||||
const requestPreviousResponseId = effectivePreviousResponseId || previousResponseId
|
||||
|
||||
// Create a new promise for this request's response ID
|
||||
this.responseIdPromise = new Promise<string | undefined>((resolve) => {
|
||||
this.responseIdResolver = resolve
|
||||
})
|
||||
// Format full conversation (messages already include reasoning items from API history)
|
||||
const formattedInput = this.formatFullConversation(systemPrompt, messages)
|
||||
|
||||
// Build request body
|
||||
const requestBody = this.buildRequestBody(
|
||||
model,
|
||||
formattedInput,
|
||||
requestPreviousResponseId,
|
||||
systemPrompt,
|
||||
verbosity,
|
||||
reasoningEffort,
|
||||
|
|
@ -224,65 +174,72 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
|
|||
private buildRequestBody(
|
||||
model: OpenAiNativeModel,
|
||||
formattedInput: any,
|
||||
requestPreviousResponseId: string | undefined,
|
||||
systemPrompt: string,
|
||||
verbosity: any,
|
||||
reasoningEffort: ReasoningEffortWithMinimal | undefined,
|
||||
reasoningEffort: ReasoningEffortExtended | undefined,
|
||||
metadata?: ApiHandlerCreateMessageMetadata,
|
||||
): any {
|
||||
// Build a request body (also used for fallback)
|
||||
// Ensure we explicitly pass max_output_tokens for GPT‑5 based on Roo's reserved model response calculation
|
||||
// Build a request body for the OpenAI Responses API.
|
||||
// Ensure we explicitly pass max_output_tokens based on Roo's reserved model response calculation
|
||||
// so requests do not default to very large limits (e.g., 120k).
|
||||
interface Gpt5RequestBody {
|
||||
interface ResponsesRequestBody {
|
||||
model: string
|
||||
input: Array<{ role: "user" | "assistant"; content: any[] }>
|
||||
input: Array<{ role: "user" | "assistant"; content: any[] } | { type: string; content: string }>
|
||||
stream: boolean
|
||||
reasoning?: { effort: ReasoningEffortWithMinimal; summary?: "auto" }
|
||||
reasoning?: { effort?: ReasoningEffortExtended; summary?: "auto" }
|
||||
text?: { verbosity: VerbosityLevel }
|
||||
temperature?: number
|
||||
max_output_tokens?: number
|
||||
previous_response_id?: string
|
||||
store?: boolean
|
||||
instructions?: string
|
||||
service_tier?: ServiceTier
|
||||
include?: string[]
|
||||
/** Prompt cache retention policy: "in_memory" (default) or "24h" for extended caching */
|
||||
prompt_cache_retention?: "in_memory" | "24h"
|
||||
}
|
||||
|
||||
// Validate requested tier against model support; if not supported, omit.
|
||||
const requestedTier = (this.options.openAiNativeServiceTier as ServiceTier | undefined) || undefined
|
||||
const allowedTierNames = new Set(model.info.tiers?.map((t) => t.name).filter(Boolean) || [])
|
||||
|
||||
const body: Gpt5RequestBody = {
|
||||
// Decide whether to enable extended prompt cache retention for this request
|
||||
const promptCacheRetention = this.getPromptCacheRetention(model)
|
||||
|
||||
const body: ResponsesRequestBody = {
|
||||
model: model.id,
|
||||
input: formattedInput,
|
||||
stream: true,
|
||||
store: metadata?.store !== false, // Default to true unless explicitly set to false
|
||||
// Always use stateless operation with encrypted reasoning
|
||||
store: false,
|
||||
// Always include instructions (system prompt) for Responses API.
|
||||
// Unlike Chat Completions, system/developer roles in input have no special semantics here.
|
||||
// The official way to set system behavior is the top-level `instructions` field.
|
||||
instructions: systemPrompt,
|
||||
...(reasoningEffort && {
|
||||
reasoning: {
|
||||
effort: reasoningEffort,
|
||||
...(this.options.enableGpt5ReasoningSummary ? { summary: "auto" as const } : {}),
|
||||
},
|
||||
}),
|
||||
// Only include encrypted reasoning content when reasoning effort is set
|
||||
...(reasoningEffort ? { include: ["reasoning.encrypted_content"] } : {}),
|
||||
...(reasoningEffort
|
||||
? {
|
||||
reasoning: {
|
||||
...(reasoningEffort ? { effort: reasoningEffort } : {}),
|
||||
...(this.options.enableResponsesReasoningSummary ? { summary: "auto" as const } : {}),
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
// Only include temperature if the model supports it
|
||||
...(model.info.supportsTemperature !== false && {
|
||||
temperature:
|
||||
this.options.modelTemperature ??
|
||||
(model.id.startsWith(GPT5_MODEL_PREFIX)
|
||||
? GPT5_DEFAULT_TEMPERATURE
|
||||
: OPENAI_NATIVE_DEFAULT_TEMPERATURE),
|
||||
temperature: this.options.modelTemperature ?? OPENAI_NATIVE_DEFAULT_TEMPERATURE,
|
||||
}),
|
||||
// Explicitly include the calculated max output tokens.
|
||||
// Use the per-request reserved output computed by Roo (params.maxTokens from getModelParams).
|
||||
...(model.maxTokens ? { max_output_tokens: model.maxTokens } : {}),
|
||||
...(requestPreviousResponseId && { previous_response_id: requestPreviousResponseId }),
|
||||
// Include tier when selected and supported by the model, or when explicitly "default"
|
||||
...(requestedTier &&
|
||||
(requestedTier === "default" || allowedTierNames.has(requestedTier)) && {
|
||||
service_tier: requestedTier,
|
||||
}),
|
||||
// Enable extended prompt cache retention for models that support it.
|
||||
// This uses the OpenAI Responses API `prompt_cache_retention` parameter.
|
||||
...(promptCacheRetention ? { prompt_cache_retention: promptCacheRetention } : {}),
|
||||
}
|
||||
|
||||
// Include text.verbosity only when the model explicitly supports it
|
||||
|
|
@ -300,9 +257,14 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
|
|||
systemPrompt?: string,
|
||||
messages?: Anthropic.Messages.MessageParam[],
|
||||
): ApiStream {
|
||||
// Create AbortController for cancellation
|
||||
this.abortController = new AbortController()
|
||||
|
||||
try {
|
||||
// Use the official SDK
|
||||
const stream = (await (this.client as any).responses.create(requestBody)) as AsyncIterable<any>
|
||||
const stream = (await (this.client as any).responses.create(requestBody, {
|
||||
signal: this.abortController.signal,
|
||||
})) as AsyncIterable<any>
|
||||
|
||||
if (typeof (stream as any)[Symbol.asyncIterator] !== "function") {
|
||||
throw new Error(
|
||||
|
|
@ -311,72 +273,27 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
|
|||
}
|
||||
|
||||
for await (const event of stream) {
|
||||
// Check if request was aborted
|
||||
if (this.abortController.signal.aborted) {
|
||||
break
|
||||
}
|
||||
|
||||
for await (const outChunk of this.processEvent(event, model)) {
|
||||
yield outChunk
|
||||
}
|
||||
}
|
||||
} catch (sdkErr: any) {
|
||||
// Check if this is a 400 error about previous_response_id not found
|
||||
const errorMessage = sdkErr?.message || sdkErr?.error?.message || ""
|
||||
const is400Error = sdkErr?.status === 400 || sdkErr?.response?.status === 400
|
||||
const isPreviousResponseError =
|
||||
errorMessage.includes("Previous response") || errorMessage.includes("not found")
|
||||
|
||||
if (is400Error && requestBody.previous_response_id && isPreviousResponseError) {
|
||||
// Log the error and retry without the previous_response_id
|
||||
|
||||
// Clear the stored lastResponseId to prevent using it again
|
||||
this.lastResponseId = undefined
|
||||
|
||||
// Re-prepare the full conversation without previous_response_id
|
||||
let retryRequestBody = { ...requestBody }
|
||||
delete retryRequestBody.previous_response_id
|
||||
|
||||
// If we have the original messages, re-prepare the full conversation
|
||||
if (systemPrompt && messages) {
|
||||
const { formattedInput } = this.prepareStructuredInput(systemPrompt, messages, undefined)
|
||||
retryRequestBody.input = formattedInput
|
||||
}
|
||||
|
||||
try {
|
||||
// Retry with the SDK
|
||||
const retryStream = (await (this.client as any).responses.create(
|
||||
retryRequestBody,
|
||||
)) as AsyncIterable<any>
|
||||
|
||||
if (typeof (retryStream as any)[Symbol.asyncIterator] !== "function") {
|
||||
// If SDK fails, fall back to SSE
|
||||
yield* this.makeGpt5ResponsesAPIRequest(
|
||||
retryRequestBody,
|
||||
model,
|
||||
metadata,
|
||||
systemPrompt,
|
||||
messages,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
for await (const event of retryStream) {
|
||||
for await (const outChunk of this.processEvent(event, model)) {
|
||||
yield outChunk
|
||||
}
|
||||
}
|
||||
return
|
||||
} catch (retryErr) {
|
||||
// If retry also fails, fall back to SSE
|
||||
yield* this.makeGpt5ResponsesAPIRequest(retryRequestBody, model, metadata, systemPrompt, messages)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// For other errors, fallback to manual SSE via fetch
|
||||
yield* this.makeGpt5ResponsesAPIRequest(requestBody, model, metadata, systemPrompt, messages)
|
||||
// For errors, fallback to manual SSE via fetch
|
||||
yield* this.makeResponsesApiRequest(requestBody, model, metadata, systemPrompt, messages)
|
||||
} finally {
|
||||
this.abortController = undefined
|
||||
}
|
||||
}
|
||||
|
||||
private formatFullConversation(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): any {
|
||||
// Format the entire conversation history for the Responses API using structured format
|
||||
// This supports both text and images
|
||||
// Messages already include reasoning items from API history, so we just need to format them
|
||||
const formattedMessages: any[] = []
|
||||
|
||||
// Do NOT embed the system prompt as a developer message in the Responses API input.
|
||||
|
|
@ -384,6 +301,13 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
|
|||
|
||||
// Process each message
|
||||
for (const message of messages) {
|
||||
// Check if this is a reasoning item (already formatted in API history)
|
||||
if ((message as any).type === "reasoning") {
|
||||
// Pass through reasoning items as-is
|
||||
formattedMessages.push(message)
|
||||
continue
|
||||
}
|
||||
|
||||
const role = message.role === "user" ? "user" : "assistant"
|
||||
const content: any[] = []
|
||||
|
||||
|
|
@ -421,41 +345,7 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
|
|||
return formattedMessages
|
||||
}
|
||||
|
||||
private formatSingleStructuredMessage(message: Anthropic.Messages.MessageParam): any {
|
||||
// Format a single message for the Responses API when using previous_response_id
|
||||
// When using previous_response_id, we only send the latest user message
|
||||
const role = message.role === "user" ? "user" : "assistant"
|
||||
|
||||
if (typeof message.content === "string") {
|
||||
// For simple string content, return structured format with proper type
|
||||
return {
|
||||
role,
|
||||
content: [{ type: "input_text", text: message.content }],
|
||||
}
|
||||
} else if (Array.isArray(message.content)) {
|
||||
// Extract text and image content from blocks
|
||||
const content: any[] = []
|
||||
|
||||
for (const block of message.content) {
|
||||
if (block.type === "text") {
|
||||
// User messages use input_text
|
||||
content.push({ type: "input_text", text: (block as any).text })
|
||||
} else if (block.type === "image") {
|
||||
const image = block as Anthropic.Messages.ImageBlockParam
|
||||
const imageUrl = `data:${image.source.media_type};base64,${image.source.data}`
|
||||
content.push({ type: "input_image", image_url: imageUrl })
|
||||
}
|
||||
}
|
||||
|
||||
if (content.length > 0) {
|
||||
return { role, content }
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
private async *makeGpt5ResponsesAPIRequest(
|
||||
private async *makeResponsesApiRequest(
|
||||
requestBody: any,
|
||||
model: OpenAiNativeModel,
|
||||
metadata?: ApiHandlerCreateMessageMetadata,
|
||||
|
|
@ -466,6 +356,9 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
|
|||
const baseUrl = this.options.openAiNativeBaseUrl || "https://api.openai.com"
|
||||
const url = `${baseUrl}/v1/responses`
|
||||
|
||||
// Create AbortController for cancellation
|
||||
this.abortController = new AbortController()
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
|
|
@ -475,12 +368,13 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
|
|||
Accept: "text/event-stream",
|
||||
},
|
||||
body: JSON.stringify(requestBody),
|
||||
signal: this.abortController.signal,
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
|
||||
let errorMessage = `GPT-5 API request failed (${response.status})`
|
||||
let errorMessage = `OpenAI Responses API request failed (${response.status})`
|
||||
let errorDetails = ""
|
||||
|
||||
// Try to parse error as JSON for better error messages
|
||||
|
|
@ -498,53 +392,6 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
|
|||
errorDetails = errorText
|
||||
}
|
||||
|
||||
// Check if this is a 400 error about previous_response_id not found
|
||||
const isPreviousResponseError =
|
||||
errorDetails.includes("Previous response") || errorDetails.includes("not found")
|
||||
|
||||
if (response.status === 400 && requestBody.previous_response_id && isPreviousResponseError) {
|
||||
// Log the error and retry without the previous_response_id
|
||||
|
||||
// Clear the stored lastResponseId to prevent using it again
|
||||
this.lastResponseId = undefined
|
||||
// Resolve the promise once to unblock any waiting requests
|
||||
this.resolveResponseId(undefined)
|
||||
|
||||
// Re-prepare the full conversation without previous_response_id
|
||||
let retryRequestBody = { ...requestBody }
|
||||
delete retryRequestBody.previous_response_id
|
||||
|
||||
// If we have the original messages, re-prepare the full conversation
|
||||
if (systemPrompt && messages) {
|
||||
const { formattedInput } = this.prepareStructuredInput(systemPrompt, messages, undefined)
|
||||
retryRequestBody.input = formattedInput
|
||||
}
|
||||
|
||||
// Retry the request with full conversation context
|
||||
const retryResponse = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
Accept: "text/event-stream",
|
||||
},
|
||||
body: JSON.stringify(retryRequestBody),
|
||||
})
|
||||
|
||||
if (!retryResponse.ok) {
|
||||
// If retry also fails, throw the original error
|
||||
throw new Error(`Responses API retry failed (${retryResponse.status})`)
|
||||
}
|
||||
|
||||
if (!retryResponse.body) {
|
||||
throw new Error("Responses API error: No response body from retry request")
|
||||
}
|
||||
|
||||
// Handle the successful retry response
|
||||
yield* this.handleStreamResponse(retryResponse.body, model)
|
||||
return
|
||||
}
|
||||
|
||||
// Provide user-friendly error messages based on status code
|
||||
switch (response.status) {
|
||||
case 400:
|
||||
|
|
@ -597,47 +444,8 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
|
|||
}
|
||||
// Handle non-Error objects
|
||||
throw new Error(`Unexpected error connecting to Responses API`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the input and conversation continuity parameters for a Responses API call.
|
||||
* Decides whether to send full conversation or just the latest message based on previousResponseId.
|
||||
*
|
||||
* - If a `previousResponseId` is available (either from metadata or the handler's state),
|
||||
* it formats only the most recent user message for the input and returns the response ID
|
||||
* to maintain conversation context.
|
||||
* - Otherwise, it formats the entire conversation history (system prompt + messages) for the input.
|
||||
*
|
||||
* @returns An object containing the formatted input and the previous response ID (if used).
|
||||
*/
|
||||
private prepareStructuredInput(
|
||||
systemPrompt: string,
|
||||
messages: Anthropic.Messages.MessageParam[],
|
||||
metadata?: ApiHandlerCreateMessageMetadata,
|
||||
): { formattedInput: any; previousResponseId?: string } {
|
||||
// Note: suppressPreviousResponseId is handled in handleResponsesApiMessage
|
||||
// This method now only handles formatting based on whether we have a previous response ID
|
||||
|
||||
// Check for previous response ID from metadata or fallback to lastResponseId
|
||||
const isFirstMessage = messages.length === 1 && messages[0].role === "user"
|
||||
const previousResponseId = metadata?.previousResponseId ?? (!isFirstMessage ? this.lastResponseId : undefined)
|
||||
|
||||
if (previousResponseId) {
|
||||
// When using previous_response_id, only send the latest user message
|
||||
const lastUserMessage = [...messages].reverse().find((msg) => msg.role === "user")
|
||||
if (lastUserMessage) {
|
||||
const formattedMessage = this.formatSingleStructuredMessage(lastUserMessage)
|
||||
// formatSingleStructuredMessage now always returns an object with role and content
|
||||
if (formattedMessage) {
|
||||
return { formattedInput: [formattedMessage], previousResponseId }
|
||||
}
|
||||
}
|
||||
return { formattedInput: [], previousResponseId }
|
||||
} else {
|
||||
// Format full conversation history (returns an array of structured messages)
|
||||
const formattedInput = this.formatFullConversation(systemPrompt, messages)
|
||||
return { formattedInput }
|
||||
} finally {
|
||||
this.abortController = undefined
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -658,6 +466,11 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
|
|||
|
||||
try {
|
||||
while (true) {
|
||||
// Check if request was aborted
|
||||
if (this.abortController?.signal.aborted) {
|
||||
break
|
||||
}
|
||||
|
||||
const { done, value } = await reader.read()
|
||||
if (done) break
|
||||
|
||||
|
|
@ -675,14 +488,18 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
|
|||
try {
|
||||
const parsed = JSON.parse(data)
|
||||
|
||||
// Store response ID for conversation continuity
|
||||
if (parsed.response?.id) {
|
||||
this.resolveResponseId(parsed.response.id)
|
||||
}
|
||||
// Capture resolved service tier if present
|
||||
if (parsed.response?.service_tier) {
|
||||
this.lastServiceTier = parsed.response.service_tier as ServiceTier
|
||||
}
|
||||
// Capture complete output array (includes reasoning items with encrypted_content)
|
||||
if (parsed.response?.output && Array.isArray(parsed.response.output)) {
|
||||
this.lastResponseOutput = parsed.response.output
|
||||
}
|
||||
// Capture top-level response id
|
||||
if (parsed.response?.id) {
|
||||
this.lastResponseId = parsed.response.id as string
|
||||
}
|
||||
|
||||
// Delegate standard event types to the shared processor to avoid duplication
|
||||
if (parsed?.type && this.coreHandledEventTypes.has(parsed.type)) {
|
||||
|
|
@ -970,14 +787,18 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
|
|||
)
|
||||
}
|
||||
} else if (parsed.type === "response.completed" || parsed.type === "response.done") {
|
||||
// Store response ID for conversation continuity
|
||||
if (parsed.response?.id) {
|
||||
this.resolveResponseId(parsed.response.id)
|
||||
}
|
||||
// Capture resolved service tier if present
|
||||
if (parsed.response?.service_tier) {
|
||||
this.lastServiceTier = parsed.response.service_tier as ServiceTier
|
||||
}
|
||||
// Capture top-level response id
|
||||
if (parsed.response?.id) {
|
||||
this.lastResponseId = parsed.response.id as string
|
||||
}
|
||||
// Capture complete output array (includes reasoning items with encrypted_content)
|
||||
if (parsed.response?.output && Array.isArray(parsed.response.output)) {
|
||||
this.lastResponseOutput = parsed.response.output
|
||||
}
|
||||
|
||||
// Check if the done event contains the complete output (as a fallback)
|
||||
if (
|
||||
|
|
@ -1016,7 +837,7 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
|
|||
}
|
||||
}
|
||||
|
||||
// Usage for done/completed is already handled by processGpt5Event in SDK path.
|
||||
// Usage for done/completed is already handled by processEvent in the SDK path.
|
||||
// For SSE path, usage often arrives separately; avoid double-emitting here.
|
||||
}
|
||||
// These are structural or status events, we can just log them at a lower level or ignore.
|
||||
|
|
@ -1098,14 +919,18 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
|
|||
* Shared processor for Responses API events.
|
||||
*/
|
||||
private async *processEvent(event: any, model: OpenAiNativeModel): ApiStream {
|
||||
// Persist response id for conversation continuity when available
|
||||
if (event?.response?.id) {
|
||||
this.resolveResponseId(event.response.id)
|
||||
}
|
||||
// Capture resolved service tier when available
|
||||
if (event?.response?.service_tier) {
|
||||
this.lastServiceTier = event.response.service_tier as ServiceTier
|
||||
}
|
||||
// Capture complete output array (includes reasoning items with encrypted_content)
|
||||
if (event?.response?.output && Array.isArray(event.response.output)) {
|
||||
this.lastResponseOutput = event.response.output
|
||||
}
|
||||
// Capture top-level response id
|
||||
if (event?.response?.id) {
|
||||
this.lastResponseId = event.response.id as string
|
||||
}
|
||||
|
||||
// Handle known streaming text deltas
|
||||
if (event?.type === "response.text.delta" || event?.type === "response.output_text.delta") {
|
||||
|
|
@ -1180,20 +1005,27 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
|
|||
}
|
||||
}
|
||||
|
||||
private getReasoningEffort(model: OpenAiNativeModel): ReasoningEffortWithMinimal | undefined {
|
||||
const { reasoning, info } = model
|
||||
private getReasoningEffort(model: OpenAiNativeModel): ReasoningEffortExtended | undefined {
|
||||
// Single source of truth: user setting overrides, else model default (from types).
|
||||
const selected = (this.options.reasoningEffort as any) ?? (model.info.reasoningEffort as any)
|
||||
return selected && selected !== "disable" ? (selected as any) : undefined
|
||||
}
|
||||
|
||||
// Check if reasoning effort is configured
|
||||
if (reasoning && "reasoning_effort" in reasoning) {
|
||||
const effort = reasoning.reasoning_effort as string
|
||||
// Support all effort levels
|
||||
if (effort === "minimal" || effort === "low" || effort === "medium" || effort === "high") {
|
||||
return effort as ReasoningEffortWithMinimal
|
||||
}
|
||||
/**
|
||||
* Returns the appropriate prompt cache retention policy for the given model, if any.
|
||||
*
|
||||
* The policy is driven by ModelInfo.promptCacheRetention so that model-specific details
|
||||
* live in the shared types layer rather than this provider. When set to "24h" and the
|
||||
* model supports prompt caching, extended prompt cache retention is requested.
|
||||
*/
|
||||
private getPromptCacheRetention(model: OpenAiNativeModel): "24h" | undefined {
|
||||
if (!model.info.supportsPromptCache) return undefined
|
||||
|
||||
if (model.info.promptCacheRetention === "24h") {
|
||||
return "24h"
|
||||
}
|
||||
|
||||
// Use the model's default from types if available
|
||||
return info.reasoningEffort as ReasoningEffortWithMinimal | undefined
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1231,19 +1063,11 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
|
|||
modelId: id,
|
||||
model: info,
|
||||
settings: this.options,
|
||||
defaultTemperature: id.startsWith(GPT5_MODEL_PREFIX)
|
||||
? GPT5_DEFAULT_TEMPERATURE
|
||||
: OPENAI_NATIVE_DEFAULT_TEMPERATURE,
|
||||
defaultTemperature: OPENAI_NATIVE_DEFAULT_TEMPERATURE,
|
||||
})
|
||||
|
||||
// For models using the Responses API, ensure we support reasoning effort
|
||||
const effort =
|
||||
(this.options.reasoningEffort as ReasoningEffortWithMinimal | undefined) ??
|
||||
(info.reasoningEffort as ReasoningEffortWithMinimal | undefined)
|
||||
|
||||
if (effort) {
|
||||
;(params.reasoning as any) = { reasoning_effort: effort }
|
||||
}
|
||||
// Reasoning effort inclusion is handled by getModelParams/getOpenAiReasoning.
|
||||
// Do not re-compute or filter efforts here.
|
||||
|
||||
// The o3 models are named like "o3-mini-[reasoning-effort]", which are
|
||||
// not valid model ids, so we need to strip the suffix.
|
||||
|
|
@ -1251,24 +1075,35 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
|
|||
}
|
||||
|
||||
/**
|
||||
* Gets the last response ID captured from the Responses API stream.
|
||||
* Used for maintaining conversation continuity across requests.
|
||||
* @returns The response ID, or undefined if not available yet
|
||||
* Extracts encrypted_content and id from the first reasoning item in the output array.
|
||||
* This is the minimal data needed for stateless API continuity.
|
||||
*
|
||||
* @returns Object with encrypted_content and id, or undefined if not available
|
||||
*/
|
||||
getLastResponseId(): string | undefined {
|
||||
getEncryptedContent(): { encrypted_content: string; id?: string } | undefined {
|
||||
if (!this.lastResponseOutput) return undefined
|
||||
|
||||
// Find the first reasoning item with encrypted_content
|
||||
const reasoningItem = this.lastResponseOutput.find(
|
||||
(item) => item.type === "reasoning" && item.encrypted_content,
|
||||
)
|
||||
|
||||
if (!reasoningItem?.encrypted_content) return undefined
|
||||
|
||||
return {
|
||||
encrypted_content: reasoningItem.encrypted_content,
|
||||
...(reasoningItem.id ? { id: reasoningItem.id } : {}),
|
||||
}
|
||||
}
|
||||
|
||||
getResponseId(): string | undefined {
|
||||
return this.lastResponseId
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the last response ID for conversation continuity.
|
||||
* Typically only used in tests or special flows.
|
||||
* @param responseId The response ID to store
|
||||
*/
|
||||
setResponseId(responseId: string): void {
|
||||
this.lastResponseId = responseId
|
||||
}
|
||||
|
||||
async completePrompt(prompt: string): Promise<string> {
|
||||
// Create AbortController for cancellation
|
||||
this.abortController = new AbortController()
|
||||
|
||||
try {
|
||||
const model = this.getModel()
|
||||
const { verbosity, reasoning } = model
|
||||
|
|
@ -1287,6 +1122,8 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
|
|||
],
|
||||
stream: false, // Non-streaming for completePrompt
|
||||
store: false, // Don't store prompt completions
|
||||
// Only include encrypted reasoning content when reasoning effort is set
|
||||
...(reasoningEffort ? { include: ["reasoning.encrypted_content"] } : {}),
|
||||
}
|
||||
|
||||
// Include service tier if selected and supported
|
||||
|
|
@ -1300,17 +1137,13 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
|
|||
if (reasoningEffort) {
|
||||
requestBody.reasoning = {
|
||||
effort: reasoningEffort,
|
||||
...(this.options.enableGpt5ReasoningSummary ? { summary: "auto" as const } : {}),
|
||||
...(this.options.enableResponsesReasoningSummary ? { summary: "auto" as const } : {}),
|
||||
}
|
||||
}
|
||||
|
||||
// Only include temperature if the model supports it
|
||||
if (model.info.supportsTemperature !== false) {
|
||||
requestBody.temperature =
|
||||
this.options.modelTemperature ??
|
||||
(model.id.startsWith(GPT5_MODEL_PREFIX)
|
||||
? GPT5_DEFAULT_TEMPERATURE
|
||||
: OPENAI_NATIVE_DEFAULT_TEMPERATURE)
|
||||
requestBody.temperature = this.options.modelTemperature ?? OPENAI_NATIVE_DEFAULT_TEMPERATURE
|
||||
}
|
||||
|
||||
// Include max_output_tokens if available
|
||||
|
|
@ -1323,8 +1156,16 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
|
|||
requestBody.text = { verbosity: (verbosity || "medium") as VerbosityLevel }
|
||||
}
|
||||
|
||||
// Enable extended prompt cache retention for eligible models
|
||||
const promptCacheRetention = this.getPromptCacheRetention(model)
|
||||
if (promptCacheRetention) {
|
||||
requestBody.prompt_cache_retention = promptCacheRetention
|
||||
}
|
||||
|
||||
// Make the non-streaming request
|
||||
const response = await (this.client as any).responses.create(requestBody)
|
||||
const response = await (this.client as any).responses.create(requestBody, {
|
||||
signal: this.abortController.signal,
|
||||
})
|
||||
|
||||
// Extract text from the response
|
||||
if (response?.output && Array.isArray(response.output)) {
|
||||
|
|
@ -1350,6 +1191,8 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
|
|||
throw new Error(`OpenAI Native completion error: ${error.message}`)
|
||||
}
|
||||
throw error
|
||||
} finally {
|
||||
this.abortController = undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -99,12 +99,12 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
|
|||
return
|
||||
}
|
||||
|
||||
if (this.options.openAiStreamingEnabled ?? true) {
|
||||
let systemMessage: OpenAI.Chat.ChatCompletionSystemMessageParam = {
|
||||
role: "system",
|
||||
content: systemPrompt,
|
||||
}
|
||||
let systemMessage: OpenAI.Chat.ChatCompletionSystemMessageParam = {
|
||||
role: "system",
|
||||
content: systemPrompt,
|
||||
}
|
||||
|
||||
if (this.options.openAiStreamingEnabled ?? true) {
|
||||
let convertedMessages
|
||||
|
||||
if (deepseekReasoner) {
|
||||
|
|
@ -191,7 +191,7 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
|
|||
let lastUsage
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta ?? {}
|
||||
const delta = chunk.choices?.[0]?.delta ?? {}
|
||||
|
||||
if (delta.content) {
|
||||
for (const chunk of matcher.update(delta.content)) {
|
||||
|
|
@ -218,12 +218,6 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
|
|||
yield this.processUsageMetrics(lastUsage, modelInfo)
|
||||
}
|
||||
} else {
|
||||
// o1 for instance doesnt support streaming, non-1 temp, or system prompt
|
||||
const systemMessage: OpenAI.Chat.ChatCompletionUserMessageParam = {
|
||||
role: "user",
|
||||
content: systemPrompt,
|
||||
}
|
||||
|
||||
const requestOptions: OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming = {
|
||||
model: modelId,
|
||||
messages: deepseekReasoner
|
||||
|
|
@ -248,7 +242,7 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
|
|||
|
||||
yield {
|
||||
type: "text",
|
||||
text: response.choices[0]?.message.content || "",
|
||||
text: response.choices?.[0]?.message.content || "",
|
||||
}
|
||||
|
||||
yield this.processUsageMetrics(response.usage, modelInfo)
|
||||
|
|
@ -296,7 +290,7 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
|
|||
throw handleOpenAIError(error, this.providerName)
|
||||
}
|
||||
|
||||
return response.choices[0]?.message.content || ""
|
||||
return response.choices?.[0]?.message.content || ""
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
throw new Error(`${this.providerName} completion error: ${error.message}`)
|
||||
|
|
@ -379,7 +373,7 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
|
|||
|
||||
yield {
|
||||
type: "text",
|
||||
text: response.choices[0]?.message.content || "",
|
||||
text: response.choices?.[0]?.message.content || "",
|
||||
}
|
||||
yield this.processUsageMetrics(response.usage)
|
||||
}
|
||||
|
|
@ -387,7 +381,7 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
|
|||
|
||||
private async *handleStreamResponse(stream: AsyncIterable<OpenAI.Chat.Completions.ChatCompletionChunk>): ApiStream {
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
const delta = chunk.choices?.[0]?.delta
|
||||
if (delta?.content) {
|
||||
yield {
|
||||
type: "text",
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ import { getModelEndpoints } from "./fetchers/modelEndpointCache"
|
|||
|
||||
import { DEFAULT_HEADERS } from "./constants"
|
||||
import { BaseProvider } from "./base-provider"
|
||||
import type { SingleCompletionHandler } from "../index"
|
||||
import type { ApiHandlerCreateMessageMetadata, SingleCompletionHandler } from "../index"
|
||||
import { handleOpenAIError } from "./utils/openai-error-handler"
|
||||
|
||||
// Image generation types
|
||||
|
|
@ -98,6 +98,32 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
|
|||
const apiKey = this.options.openRouterApiKey ?? "not-provided"
|
||||
|
||||
this.client = new OpenAI({ baseURL, apiKey, defaultHeaders: DEFAULT_HEADERS })
|
||||
|
||||
// Load models asynchronously to populate cache before getModel() is called
|
||||
this.loadDynamicModels().catch((error) => {
|
||||
console.error("[OpenRouterHandler] Failed to load dynamic models:", error)
|
||||
})
|
||||
}
|
||||
|
||||
private async loadDynamicModels(): Promise<void> {
|
||||
try {
|
||||
const [models, endpoints] = await Promise.all([
|
||||
getModels({ provider: "openrouter" }),
|
||||
getModelEndpoints({
|
||||
router: "openrouter",
|
||||
modelId: this.options.openRouterModelId,
|
||||
endpoint: this.options.openRouterSpecificProvider,
|
||||
}),
|
||||
])
|
||||
|
||||
this.models = models
|
||||
this.endpoints = endpoints
|
||||
} catch (error) {
|
||||
console.error("[OpenRouterHandler] Error loading dynamic models:", {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -129,6 +155,7 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
|
|||
override async *createMessage(
|
||||
systemPrompt: string,
|
||||
messages: Anthropic.Messages.MessageParam[],
|
||||
metadata?: ApiHandlerCreateMessageMetadata,
|
||||
): AsyncGenerator<ApiStreamChunk> {
|
||||
const model = await this.fetchModel()
|
||||
|
||||
|
|
@ -190,9 +217,12 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
|
|||
allow_fallbacks: false,
|
||||
},
|
||||
}),
|
||||
parallel_tool_calls: false, // Ensure only one tool call at a time
|
||||
...(transforms && { transforms }),
|
||||
...(finalReasoning && { reasoning: finalReasoning }),
|
||||
...(chatTemplateKwargs && { chat_template_kwargs: chatTemplateKwargs }),
|
||||
...(metadata?.tools && { tools: metadata.tools }),
|
||||
...(metadata?.tool_choice && { tool_choice: metadata.tool_choice }),
|
||||
}
|
||||
|
||||
let stream
|
||||
|
|
@ -203,6 +233,7 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
|
|||
}
|
||||
|
||||
let lastUsage: CompletionUsage | undefined = undefined
|
||||
const toolCallAccumulator = new Map<number, { id: string; name: string; arguments: string }>()
|
||||
|
||||
for await (const chunk of stream) {
|
||||
// OpenRouter returns an error object instead of the OpenAI SDK throwing an error.
|
||||
|
|
@ -213,13 +244,52 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
|
|||
}
|
||||
|
||||
const delta = chunk.choices[0]?.delta
|
||||
const finishReason = chunk.choices[0]?.finish_reason
|
||||
|
||||
if ("reasoning" in delta && delta.reasoning && typeof delta.reasoning === "string") {
|
||||
yield { type: "reasoning", text: delta.reasoning }
|
||||
if (delta) {
|
||||
if ("reasoning" in delta && delta.reasoning && typeof delta.reasoning === "string") {
|
||||
yield { type: "reasoning", text: delta.reasoning }
|
||||
}
|
||||
|
||||
// Check for tool calls in delta
|
||||
if ("tool_calls" in delta && Array.isArray(delta.tool_calls)) {
|
||||
for (const toolCall of delta.tool_calls) {
|
||||
const index = toolCall.index
|
||||
const existing = toolCallAccumulator.get(index)
|
||||
|
||||
if (existing) {
|
||||
// Accumulate arguments for existing tool call
|
||||
if (toolCall.function?.arguments) {
|
||||
existing.arguments += toolCall.function.arguments
|
||||
}
|
||||
} else {
|
||||
// Start new tool call accumulation
|
||||
toolCallAccumulator.set(index, {
|
||||
id: toolCall.id || "",
|
||||
name: toolCall.function?.name || "",
|
||||
arguments: toolCall.function?.arguments || "",
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (delta.content) {
|
||||
yield { type: "text", text: delta.content }
|
||||
}
|
||||
}
|
||||
|
||||
if (delta?.content) {
|
||||
yield { type: "text", text: delta.content }
|
||||
// When finish_reason is 'tool_calls', yield all accumulated tool calls
|
||||
if (finishReason === "tool_calls" && toolCallAccumulator.size > 0) {
|
||||
for (const toolCall of toolCallAccumulator.values()) {
|
||||
yield {
|
||||
type: "tool_call",
|
||||
id: toolCall.id,
|
||||
name: toolCall.name,
|
||||
arguments: toolCall.arguments,
|
||||
}
|
||||
}
|
||||
// Clear accumulator after yielding
|
||||
toolCallAccumulator.clear()
|
||||
}
|
||||
|
||||
if (chunk.usage) {
|
||||
|
|
@ -340,7 +410,8 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
|
|||
}
|
||||
|
||||
try {
|
||||
const response = await fetch("https://openrouter.ai/api/v1/chat/completions", {
|
||||
const baseURL = this.options.openRouterBaseUrl || "https://openrouter.ai/api/v1"
|
||||
const response = await fetch(`${baseURL}/chat/completions`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
|
|
|
|||
|
|
@ -28,6 +28,16 @@ interface RequestyUsage extends OpenAI.CompletionUsage {
|
|||
total_cost?: number
|
||||
}
|
||||
|
||||
type RequestyChatCompletionParamsStreaming = OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming & {
|
||||
requesty?: {
|
||||
trace_id?: string
|
||||
extra?: {
|
||||
mode?: string
|
||||
}
|
||||
}
|
||||
thinking?: AnthropicReasoningParams
|
||||
}
|
||||
|
||||
type RequestyChatCompletionParams = OpenAI.Chat.ChatCompletionCreateParams & {
|
||||
requesty?: {
|
||||
trace_id?: string
|
||||
|
|
@ -118,12 +128,17 @@ export class RequestyHandler extends BaseProvider implements SingleCompletionHan
|
|||
...convertToOpenAiMessages(messages),
|
||||
]
|
||||
|
||||
const completionParams: RequestyChatCompletionParams = {
|
||||
// Map extended efforts to OpenAI Chat Completions-accepted values (omit unsupported)
|
||||
const allowedEffort = (["low", "medium", "high"] as const).includes(reasoning_effort as any)
|
||||
? (reasoning_effort as OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming["reasoning_effort"])
|
||||
: undefined
|
||||
|
||||
const completionParams: RequestyChatCompletionParamsStreaming = {
|
||||
messages: openAiMessages,
|
||||
model,
|
||||
max_tokens,
|
||||
temperature,
|
||||
...(reasoning_effort && reasoning_effort !== "minimal" && { reasoning_effort }),
|
||||
...(allowedEffort && { reasoning_effort: allowedEffort }),
|
||||
...(thinking && { thinking }),
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
|
|
@ -132,6 +147,7 @@ export class RequestyHandler extends BaseProvider implements SingleCompletionHan
|
|||
|
||||
let stream
|
||||
try {
|
||||
// With streaming params type, SDK returns an async iterable stream
|
||||
stream = await this.client.chat.completions.create(completionParams)
|
||||
} catch (error) {
|
||||
throw handleOpenAIError(error, this.providerName)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import OpenAI from "openai"
|
||||
|
||||
import { rooDefaultModelId } from "@roo-code/types"
|
||||
import { rooDefaultModelId, getApiProtocol } from "@roo-code/types"
|
||||
import { CloudService } from "@roo-code/cloud"
|
||||
|
||||
import type { ApiHandlerOptions, ModelRecord } from "../../shared/api"
|
||||
|
|
@ -100,6 +100,8 @@ export class RooHandler extends BaseOpenAiCompatibleProvider<string> {
|
|||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
...(reasoning && { reasoning }),
|
||||
...(metadata?.tools && { tools: metadata.tools }),
|
||||
...(metadata?.tool_choice && { tool_choice: metadata.tool_choice }),
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
@ -115,61 +117,124 @@ export class RooHandler extends BaseOpenAiCompatibleProvider<string> {
|
|||
messages: Anthropic.Messages.MessageParam[],
|
||||
metadata?: ApiHandlerCreateMessageMetadata,
|
||||
): ApiStream {
|
||||
const stream = await this.createStream(
|
||||
systemPrompt,
|
||||
messages,
|
||||
metadata,
|
||||
metadata?.taskId ? { headers: { "X-Roo-Task-ID": metadata.taskId } } : undefined,
|
||||
)
|
||||
try {
|
||||
const stream = await this.createStream(
|
||||
systemPrompt,
|
||||
messages,
|
||||
metadata,
|
||||
metadata?.taskId ? { headers: { "X-Roo-Task-ID": metadata.taskId } } : undefined,
|
||||
)
|
||||
|
||||
let lastUsage: RooUsage | undefined = undefined
|
||||
let lastUsage: RooUsage | undefined = undefined
|
||||
// Accumulate tool calls by index - similar to how reasoning accumulates
|
||||
const toolCallAccumulator = new Map<number, { id: string; name: string; arguments: string }>()
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
const finishReason = chunk.choices[0]?.finish_reason
|
||||
|
||||
if (delta) {
|
||||
// Check for reasoning content (similar to OpenRouter)
|
||||
if ("reasoning" in delta && delta.reasoning && typeof delta.reasoning === "string") {
|
||||
yield {
|
||||
type: "reasoning",
|
||||
text: delta.reasoning,
|
||||
if (delta) {
|
||||
// Check for reasoning content (similar to OpenRouter)
|
||||
if ("reasoning" in delta && delta.reasoning && typeof delta.reasoning === "string") {
|
||||
yield {
|
||||
type: "reasoning",
|
||||
text: delta.reasoning,
|
||||
}
|
||||
}
|
||||
|
||||
// Also check for reasoning_content for backward compatibility
|
||||
if ("reasoning_content" in delta && typeof delta.reasoning_content === "string") {
|
||||
yield {
|
||||
type: "reasoning",
|
||||
text: delta.reasoning_content,
|
||||
}
|
||||
}
|
||||
|
||||
// Check for tool calls in delta
|
||||
if ("tool_calls" in delta && Array.isArray(delta.tool_calls)) {
|
||||
for (const toolCall of delta.tool_calls) {
|
||||
const index = toolCall.index
|
||||
const existing = toolCallAccumulator.get(index)
|
||||
|
||||
if (existing) {
|
||||
// Accumulate arguments for existing tool call
|
||||
if (toolCall.function?.arguments) {
|
||||
existing.arguments += toolCall.function.arguments
|
||||
}
|
||||
} else {
|
||||
// Start new tool call accumulation
|
||||
toolCallAccumulator.set(index, {
|
||||
id: toolCall.id || "",
|
||||
name: toolCall.function?.name || "",
|
||||
arguments: toolCall.function?.arguments || "",
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (delta.content) {
|
||||
yield {
|
||||
type: "text",
|
||||
text: delta.content,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Also check for reasoning_content for backward compatibility
|
||||
if ("reasoning_content" in delta && typeof delta.reasoning_content === "string") {
|
||||
yield {
|
||||
type: "reasoning",
|
||||
text: delta.reasoning_content,
|
||||
// When finish_reason is 'tool_calls', yield all accumulated tool calls
|
||||
if (finishReason === "tool_calls" && toolCallAccumulator.size > 0) {
|
||||
for (const [index, toolCall] of toolCallAccumulator.entries()) {
|
||||
yield {
|
||||
type: "tool_call",
|
||||
id: toolCall.id,
|
||||
name: toolCall.name,
|
||||
arguments: toolCall.arguments,
|
||||
}
|
||||
}
|
||||
// Clear accumulator after yielding
|
||||
toolCallAccumulator.clear()
|
||||
}
|
||||
|
||||
if (delta.content) {
|
||||
yield {
|
||||
type: "text",
|
||||
text: delta.content,
|
||||
}
|
||||
if (chunk.usage) {
|
||||
lastUsage = chunk.usage as RooUsage
|
||||
}
|
||||
}
|
||||
|
||||
if (chunk.usage) {
|
||||
lastUsage = chunk.usage as RooUsage
|
||||
}
|
||||
}
|
||||
if (lastUsage) {
|
||||
// Check if the current model is marked as free
|
||||
const model = this.getModel()
|
||||
const isFreeModel = model.info.isFree ?? false
|
||||
|
||||
if (lastUsage) {
|
||||
// Check if the current model is marked as free
|
||||
const model = this.getModel()
|
||||
const isFreeModel = model.info.isFree ?? false
|
||||
// Normalize input tokens based on protocol expectations:
|
||||
// - OpenAI protocol expects TOTAL input tokens (cached + non-cached)
|
||||
// - Anthropic protocol expects NON-CACHED input tokens (caches passed separately)
|
||||
const modelId = model.id
|
||||
const apiProtocol = getApiProtocol("roo", modelId)
|
||||
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: lastUsage.prompt_tokens || 0,
|
||||
outputTokens: lastUsage.completion_tokens || 0,
|
||||
cacheWriteTokens: lastUsage.cache_creation_input_tokens,
|
||||
cacheReadTokens: lastUsage.prompt_tokens_details?.cached_tokens,
|
||||
totalCost: isFreeModel ? 0 : (lastUsage.cost ?? 0),
|
||||
const promptTokens = lastUsage.prompt_tokens || 0
|
||||
const cacheWrite = lastUsage.cache_creation_input_tokens || 0
|
||||
const cacheRead = lastUsage.prompt_tokens_details?.cached_tokens || 0
|
||||
const nonCached = Math.max(0, promptTokens - cacheWrite - cacheRead)
|
||||
|
||||
const inputTokensForDownstream = apiProtocol === "anthropic" ? nonCached : promptTokens
|
||||
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: inputTokensForDownstream,
|
||||
outputTokens: lastUsage.completion_tokens || 0,
|
||||
cacheWriteTokens: cacheWrite,
|
||||
cacheReadTokens: cacheRead,
|
||||
totalCost: isFreeModel ? 0 : (lastUsage.cost ?? 0),
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Log streaming errors with context
|
||||
console.error("[RooHandler] Error during message streaming:", {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
modelId: this.options.apiModelId,
|
||||
hasTaskId: Boolean(metadata?.taskId),
|
||||
})
|
||||
throw error
|
||||
}
|
||||
}
|
||||
override async completePrompt(prompt: string): Promise<string> {
|
||||
|
|
@ -187,7 +252,13 @@ export class RooHandler extends BaseOpenAiCompatibleProvider<string> {
|
|||
apiKey,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("[RooHandler] Error loading dynamic models:", error)
|
||||
// Enhanced error logging with more context
|
||||
console.error("[RooHandler] Error loading dynamic models:", {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
baseURL,
|
||||
hasApiKey: Boolean(apiKey),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -211,6 +282,7 @@ export class RooHandler extends BaseOpenAiCompatibleProvider<string> {
|
|||
supportsImages: false,
|
||||
supportsReasoningEffort: false,
|
||||
supportsPromptCache: true,
|
||||
supportsNativeTools: false,
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -15,6 +15,13 @@ export function handleOpenAIError(error: unknown, providerName: string): Error {
|
|||
if (error instanceof Error) {
|
||||
const msg = error.message || ""
|
||||
|
||||
// Log the original error details for debugging
|
||||
console.error(`[${providerName}] API error:`, {
|
||||
message: msg,
|
||||
name: error.name,
|
||||
stack: error.stack,
|
||||
})
|
||||
|
||||
// Invalid character/ByteString conversion error in API key
|
||||
if (msg.includes("Cannot convert argument to a ByteString")) {
|
||||
return new Error(i18n.t("common:errors.api.invalidKeyInvalidChars"))
|
||||
|
|
@ -25,5 +32,6 @@ export function handleOpenAIError(error: unknown, providerName: string): Error {
|
|||
}
|
||||
|
||||
// Non-Error: wrap with provider-specific prefix
|
||||
console.error(`[${providerName}] Non-Error exception:`, error)
|
||||
return new Error(`${providerName} completion error: ${String(error)}`)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import * as vscode from "vscode"
|
||||
import { Package } from "../../../shared/package"
|
||||
|
||||
/**
|
||||
* Gets the API request timeout from VSCode configuration with validation.
|
||||
|
|
@ -7,7 +8,7 @@ import * as vscode from "vscode"
|
|||
*/
|
||||
export function getApiRequestTimeout(): number {
|
||||
// Get timeout with validation to ensure it's a valid non-negative number
|
||||
const configTimeout = vscode.workspace.getConfiguration("roo-cline").get<number>("apiRequestTimeout", 600)
|
||||
const configTimeout = vscode.workspace.getConfiguration(Package.name).get<number>("apiRequestTimeout", 600)
|
||||
|
||||
// Validate that it's actually a number and not NaN
|
||||
if (typeof configTimeout !== "number" || isNaN(configTimeout)) {
|
||||
|
|
|
|||
|
|
@ -545,6 +545,79 @@ describe("getModelParams", () => {
|
|||
expect(result.reasoning).toEqual({ effort: "medium" })
|
||||
})
|
||||
|
||||
it("should include 'minimal' effort for openai format", () => {
|
||||
const model: ModelInfo = {
|
||||
...baseModel,
|
||||
// Array capability explicitly includes minimal
|
||||
supportsReasoningEffort: ["minimal", "low", "medium", "high"] as any,
|
||||
}
|
||||
|
||||
const result = getModelParams({
|
||||
...openaiParams,
|
||||
settings: { reasoningEffort: "minimal" as any },
|
||||
model,
|
||||
})
|
||||
|
||||
expect(result.reasoningEffort).toBe("minimal")
|
||||
expect(result.reasoning).toEqual({ reasoning_effort: "minimal" })
|
||||
})
|
||||
|
||||
it("should include 'none' effort for openai format", () => {
|
||||
const model: ModelInfo = {
|
||||
...baseModel,
|
||||
// Array capability explicitly includes none
|
||||
supportsReasoningEffort: ["none", "low", "medium", "high"] as any,
|
||||
}
|
||||
|
||||
const result = getModelParams({
|
||||
...openaiParams,
|
||||
settings: { reasoningEffort: "none" as any },
|
||||
model,
|
||||
})
|
||||
|
||||
expect(result.reasoningEffort).toBe("none")
|
||||
expect(result.reasoning).toEqual({ reasoning_effort: "none" })
|
||||
})
|
||||
|
||||
it("should omit reasoning for 'disable' selection", () => {
|
||||
const model: ModelInfo = {
|
||||
...baseModel,
|
||||
supportsReasoningEffort: true,
|
||||
}
|
||||
|
||||
const result = getModelParams({
|
||||
...openaiParams,
|
||||
settings: { reasoningEffort: "disable" as any },
|
||||
model,
|
||||
})
|
||||
|
||||
expect(result.reasoningEffort).toBeUndefined()
|
||||
expect(result.reasoning).toBeUndefined()
|
||||
})
|
||||
|
||||
it("should include 'minimal' and 'none' for openrouter format", () => {
|
||||
const model: ModelInfo = {
|
||||
...baseModel,
|
||||
// Array capability explicitly includes both
|
||||
supportsReasoningEffort: ["none", "minimal", "low", "medium", "high"] as any,
|
||||
}
|
||||
|
||||
const minimalRes = getModelParams({
|
||||
...openrouterParams,
|
||||
settings: { reasoningEffort: "minimal" as any },
|
||||
model,
|
||||
})
|
||||
expect(minimalRes.reasoningEffort).toBe("minimal")
|
||||
expect(minimalRes.reasoning).toEqual({ effort: "minimal" })
|
||||
|
||||
const noneRes = getModelParams({
|
||||
...openrouterParams,
|
||||
settings: { reasoningEffort: "none" as any },
|
||||
model,
|
||||
})
|
||||
expect(noneRes.reasoningEffort).toBe("none")
|
||||
expect(noneRes.reasoning).toEqual({ effort: "none" })
|
||||
})
|
||||
it("should not use reasoning effort for anthropic format", () => {
|
||||
const model: ModelInfo = {
|
||||
...baseModel,
|
||||
|
|
|
|||
|
|
@ -529,7 +529,7 @@ describe("reasoning.ts", () => {
|
|||
|
||||
const result = getOpenAiReasoning(optionsWithoutEffort)
|
||||
|
||||
expect(result).toEqual({ reasoning_effort: undefined })
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
|
||||
it("should handle all reasoning effort values", () => {
|
||||
|
|
@ -829,7 +829,7 @@ describe("reasoning.ts", () => {
|
|||
expect(result).toEqual({ enabled: false })
|
||||
})
|
||||
|
||||
it("should not return reasoning params for minimal effort", () => {
|
||||
it("should omit reasoning params for minimal effort", () => {
|
||||
const modelWithSupported: ModelInfo = {
|
||||
...baseModel,
|
||||
supportsReasoningEffort: true,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import {
|
|||
type ModelInfo,
|
||||
type ProviderSettings,
|
||||
type VerbosityLevel,
|
||||
type ReasoningEffortWithMinimal,
|
||||
type ReasoningEffortExtended,
|
||||
ANTHROPIC_DEFAULT_MAX_TOKENS,
|
||||
} from "@roo-code/types"
|
||||
|
||||
|
|
@ -39,7 +39,7 @@ type GetModelParamsOptions<T extends Format> = {
|
|||
type BaseModelParams = {
|
||||
maxTokens: number | undefined
|
||||
temperature: number | undefined
|
||||
reasoningEffort: ReasoningEffortWithMinimal | undefined
|
||||
reasoningEffort: ReasoningEffortExtended | undefined
|
||||
reasoningBudget: number | undefined
|
||||
verbosity: VerbosityLevel | undefined
|
||||
}
|
||||
|
|
@ -129,8 +129,17 @@ export function getModelParams({
|
|||
temperature = 1.0
|
||||
} else if (shouldUseReasoningEffort({ model, settings })) {
|
||||
// "Traditional" reasoning models use the `reasoningEffort` parameter.
|
||||
const effort = customReasoningEffort ?? model.reasoningEffort
|
||||
reasoningEffort = effort as ReasoningEffortWithMinimal
|
||||
const effort = (customReasoningEffort ?? model.reasoningEffort) as any
|
||||
// Do not propagate "disable" into model params; treat as omission
|
||||
if (effort && effort !== "disable") {
|
||||
if (model.supportsReasoningEffort === true) {
|
||||
// Boolean capability: accept extended efforts; UI still exposes low/medium/high by default
|
||||
reasoningEffort = effort as ReasoningEffortExtended
|
||||
} else {
|
||||
// Array capability: honor exactly what's defined by the model
|
||||
reasoningEffort = effort as ReasoningEffortExtended
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const params: BaseModelParams = { maxTokens, temperature, reasoningEffort, reasoningBudget, verbosity }
|
||||
|
|
|
|||
|
|
@ -2,19 +2,19 @@ import { BetaThinkingConfigParam } from "@anthropic-ai/sdk/resources/beta"
|
|||
import OpenAI from "openai"
|
||||
import type { GenerateContentConfig } from "@google/genai"
|
||||
|
||||
import type { ModelInfo, ProviderSettings, ReasoningEffortWithMinimal } from "@roo-code/types"
|
||||
import type { ModelInfo, ProviderSettings, ReasoningEffortExtended } from "@roo-code/types"
|
||||
|
||||
import { shouldUseReasoningBudget, shouldUseReasoningEffort } from "../../shared/api"
|
||||
|
||||
export type OpenRouterReasoningParams = {
|
||||
effort?: ReasoningEffortWithMinimal
|
||||
effort?: ReasoningEffortExtended
|
||||
max_tokens?: number
|
||||
exclude?: boolean
|
||||
}
|
||||
|
||||
export type RooReasoningParams = {
|
||||
enabled?: boolean
|
||||
effort?: ReasoningEffortWithMinimal
|
||||
effort?: ReasoningEffortExtended
|
||||
}
|
||||
|
||||
export type AnthropicReasoningParams = BetaThinkingConfigParam
|
||||
|
|
@ -26,7 +26,7 @@ export type GeminiReasoningParams = GenerateContentConfig["thinkingConfig"]
|
|||
export type GetModelReasoningOptions = {
|
||||
model: ModelInfo
|
||||
reasoningBudget: number | undefined
|
||||
reasoningEffort: ReasoningEffortWithMinimal | undefined
|
||||
reasoningEffort: ReasoningEffortExtended | "disable" | undefined
|
||||
settings: ProviderSettings
|
||||
}
|
||||
|
||||
|
|
@ -39,8 +39,8 @@ export const getOpenRouterReasoning = ({
|
|||
shouldUseReasoningBudget({ model, settings })
|
||||
? { max_tokens: reasoningBudget }
|
||||
: shouldUseReasoningEffort({ model, settings })
|
||||
? reasoningEffort
|
||||
? { effort: reasoningEffort }
|
||||
? reasoningEffort && reasoningEffort !== "disable"
|
||||
? { effort: reasoningEffort as ReasoningEffortExtended }
|
||||
: undefined
|
||||
: undefined
|
||||
|
||||
|
|
@ -50,28 +50,36 @@ export const getRooReasoning = ({
|
|||
settings,
|
||||
}: GetModelReasoningOptions): RooReasoningParams | undefined => {
|
||||
// Check if model supports reasoning effort
|
||||
if (!model.supportsReasoningEffort) {
|
||||
return undefined
|
||||
}
|
||||
if (!model.supportsReasoningEffort) return undefined
|
||||
|
||||
// If enableReasoningEffort is explicitly false, return enabled: false
|
||||
// Explicit off switch from settings: always send disabled for back-compat and to
|
||||
// prevent automatic reasoning when the toggle is turned off.
|
||||
if (settings.enableReasoningEffort === false) {
|
||||
return { enabled: false }
|
||||
}
|
||||
|
||||
// If reasoning effort is provided, return it with enabled: true
|
||||
if (reasoningEffort && reasoningEffort !== "minimal") {
|
||||
return { enabled: true, effort: reasoningEffort }
|
||||
}
|
||||
|
||||
// If reasoningEffort is explicitly undefined (None selected), disable reasoning
|
||||
// This ensures we explicitly tell the backend not to use reasoning
|
||||
if (reasoningEffort === undefined) {
|
||||
// For Roo models that support reasoning effort, absence of a selection should be
|
||||
// treated as an explicit "off" signal so that the backend does not auto-enable
|
||||
// reasoning. This aligns with the default behavior in tests.
|
||||
if (!reasoningEffort) {
|
||||
return { enabled: false }
|
||||
}
|
||||
|
||||
// Default: no reasoning parameter (reasoning not enabled)
|
||||
return undefined
|
||||
// "disable" is a legacy sentinel that means "omit the reasoning field entirely"
|
||||
// and let the server decide any defaults.
|
||||
if (reasoningEffort === "disable") {
|
||||
return undefined
|
||||
}
|
||||
|
||||
// For Roo, "minimal" is treated as "none" for effort-based reasoning – we omit
|
||||
// the reasoning field entirely instead of sending an explicit effort.
|
||||
if (reasoningEffort === "minimal") {
|
||||
return undefined
|
||||
}
|
||||
|
||||
// When an effort is provided (e.g. "low" | "medium" | "high" | "none"), enable
|
||||
// with the selected effort.
|
||||
return { enabled: true, effort: reasoningEffort as ReasoningEffortExtended }
|
||||
}
|
||||
|
||||
export const getAnthropicReasoning = ({
|
||||
|
|
@ -86,17 +94,11 @@ export const getOpenAiReasoning = ({
|
|||
reasoningEffort,
|
||||
settings,
|
||||
}: GetModelReasoningOptions): OpenAiReasoningParams | undefined => {
|
||||
if (!shouldUseReasoningEffort({ model, settings })) {
|
||||
return undefined
|
||||
}
|
||||
if (!shouldUseReasoningEffort({ model, settings })) return undefined
|
||||
if (reasoningEffort === "disable" || !reasoningEffort) return undefined
|
||||
|
||||
// If model has reasoning effort capability, return object even if effort is undefined
|
||||
// This preserves the reasoning_effort field in the API call
|
||||
if (reasoningEffort === "minimal") {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return { reasoning_effort: reasoningEffort }
|
||||
// Include "none" | "minimal" | "low" | "medium" | "high" literally
|
||||
return { reasoning_effort: reasoningEffort as any }
|
||||
}
|
||||
|
||||
export const getGeminiReasoning = ({
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ export type ApiStreamChunk =
|
|||
| ApiStreamUsageChunk
|
||||
| ApiStreamReasoningChunk
|
||||
| ApiStreamGroundingChunk
|
||||
| ApiStreamToolCallChunk
|
||||
| ApiStreamError
|
||||
|
||||
export interface ApiStreamError {
|
||||
|
|
@ -38,6 +39,13 @@ export interface ApiStreamGroundingChunk {
|
|||
sources: GroundingSource[]
|
||||
}
|
||||
|
||||
export interface ApiStreamToolCallChunk {
|
||||
type: "tool_call"
|
||||
id: string
|
||||
name: string
|
||||
arguments: string
|
||||
}
|
||||
|
||||
export interface GroundingSource {
|
||||
title: string
|
||||
url: string
|
||||
|
|
|
|||
312
src/core/assistant-message/NativeToolCallParser.ts
Normal file
312
src/core/assistant-message/NativeToolCallParser.ts
Normal file
|
|
@ -0,0 +1,312 @@
|
|||
import { type ToolName, toolNames, type FileEntry } from "@roo-code/types"
|
||||
import { type ToolUse, type ToolParamName, toolParamNames, type NativeToolArgs } from "../../shared/tools"
|
||||
|
||||
/**
|
||||
* Helper type to extract properly typed native arguments for a given tool.
|
||||
* Returns the type from NativeToolArgs if the tool is defined there, otherwise never.
|
||||
*/
|
||||
type NativeArgsFor<TName extends ToolName> = TName extends keyof NativeToolArgs ? NativeToolArgs[TName] : never
|
||||
|
||||
/**
|
||||
* Parser for native tool calls (OpenAI-style function calling).
|
||||
* Converts native tool call format to ToolUse format for compatibility
|
||||
* with existing tool execution infrastructure.
|
||||
*
|
||||
* For tools with refactored parsers (e.g., read_file), this parser provides
|
||||
* typed arguments via nativeArgs. Tool-specific handlers should consume
|
||||
* nativeArgs directly rather than relying on synthesized legacy params.
|
||||
*/
|
||||
export class NativeToolCallParser {
|
||||
/**
|
||||
* Convert a native tool call chunk to a ToolUse object.
|
||||
*
|
||||
* @param toolCall - The native tool call from the API stream
|
||||
* @returns A properly typed ToolUse object
|
||||
*/
|
||||
public static parseToolCall<TName extends ToolName>(toolCall: {
|
||||
id: string
|
||||
name: TName
|
||||
arguments: string
|
||||
}): ToolUse<TName> | null {
|
||||
// Check if this is a dynamic MCP tool (mcp_serverName_toolName)
|
||||
if (typeof toolCall.name === "string" && toolCall.name.startsWith("mcp_")) {
|
||||
return this.parseDynamicMcpTool(toolCall) as ToolUse<TName> | null
|
||||
}
|
||||
|
||||
// Validate tool name
|
||||
if (!toolNames.includes(toolCall.name as ToolName)) {
|
||||
console.error(`Invalid tool name: ${toolCall.name}`)
|
||||
console.error(`Valid tool names:`, toolNames)
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
// Parse the arguments JSON string
|
||||
const args = JSON.parse(toolCall.arguments)
|
||||
|
||||
// Build legacy params object for backward compatibility with XML protocol and UI.
|
||||
// Native execution path uses nativeArgs instead, which has proper typing.
|
||||
const params: Partial<Record<ToolParamName, string>> = {}
|
||||
|
||||
for (const [key, value] of Object.entries(args)) {
|
||||
// Skip complex parameters that have been migrated to nativeArgs.
|
||||
// For read_file, the 'files' parameter is a FileEntry[] array that can't be
|
||||
// meaningfully stringified. The properly typed data is in nativeArgs instead.
|
||||
if (toolCall.name === "read_file" && key === "files") {
|
||||
continue
|
||||
}
|
||||
|
||||
// Validate parameter name
|
||||
if (!toolParamNames.includes(key as ToolParamName)) {
|
||||
console.warn(`Unknown parameter '${key}' for tool '${toolCall.name}'`)
|
||||
console.warn(`Valid param names:`, toolParamNames)
|
||||
continue
|
||||
}
|
||||
|
||||
// Convert to string for legacy params format
|
||||
const stringValue = typeof value === "string" ? value : JSON.stringify(value)
|
||||
params[key as ToolParamName] = stringValue
|
||||
}
|
||||
|
||||
// Build typed nativeArgs for tools that support it.
|
||||
// This switch statement serves two purposes:
|
||||
// 1. Validation: Ensures required parameters are present before constructing nativeArgs
|
||||
// 2. Transformation: Converts raw JSON to properly typed structures
|
||||
//
|
||||
// Each case validates the minimum required parameters and constructs a properly typed
|
||||
// nativeArgs object. If validation fails, nativeArgs remains undefined and the tool
|
||||
// will fall back to legacy parameter parsing if supported.
|
||||
let nativeArgs: NativeArgsFor<TName> | undefined = undefined
|
||||
|
||||
switch (toolCall.name) {
|
||||
case "read_file":
|
||||
if (args.files && Array.isArray(args.files)) {
|
||||
nativeArgs = { files: args.files } as NativeArgsFor<TName>
|
||||
}
|
||||
break
|
||||
|
||||
case "attempt_completion":
|
||||
if (args.result) {
|
||||
nativeArgs = { result: args.result } as NativeArgsFor<TName>
|
||||
}
|
||||
break
|
||||
|
||||
case "execute_command":
|
||||
if (args.command) {
|
||||
nativeArgs = {
|
||||
command: args.command,
|
||||
cwd: args.cwd,
|
||||
} as NativeArgsFor<TName>
|
||||
}
|
||||
break
|
||||
|
||||
case "insert_content":
|
||||
if (args.path !== undefined && args.line !== undefined && args.content !== undefined) {
|
||||
nativeArgs = {
|
||||
path: args.path,
|
||||
line: typeof args.line === "number" ? args.line : parseInt(String(args.line), 10),
|
||||
content: args.content,
|
||||
} as NativeArgsFor<TName>
|
||||
}
|
||||
break
|
||||
|
||||
case "apply_diff":
|
||||
if (args.path !== undefined && args.diff !== undefined) {
|
||||
nativeArgs = {
|
||||
path: args.path,
|
||||
diff: args.diff,
|
||||
} as NativeArgsFor<TName>
|
||||
}
|
||||
break
|
||||
|
||||
case "ask_followup_question":
|
||||
if (args.question !== undefined && args.follow_up !== undefined) {
|
||||
nativeArgs = {
|
||||
question: args.question,
|
||||
follow_up: args.follow_up,
|
||||
} as NativeArgsFor<TName>
|
||||
}
|
||||
break
|
||||
|
||||
case "browser_action":
|
||||
if (args.action !== undefined) {
|
||||
nativeArgs = {
|
||||
action: args.action,
|
||||
url: args.url,
|
||||
coordinate: args.coordinate,
|
||||
size: args.size,
|
||||
text: args.text,
|
||||
} as NativeArgsFor<TName>
|
||||
}
|
||||
break
|
||||
|
||||
case "codebase_search":
|
||||
if (args.query !== undefined) {
|
||||
nativeArgs = {
|
||||
query: args.query,
|
||||
path: args.path,
|
||||
} as NativeArgsFor<TName>
|
||||
}
|
||||
break
|
||||
|
||||
case "fetch_instructions":
|
||||
if (args.task !== undefined) {
|
||||
nativeArgs = {
|
||||
task: args.task,
|
||||
} as NativeArgsFor<TName>
|
||||
}
|
||||
break
|
||||
|
||||
case "generate_image":
|
||||
if (args.prompt !== undefined && args.path !== undefined) {
|
||||
nativeArgs = {
|
||||
prompt: args.prompt,
|
||||
path: args.path,
|
||||
image: args.image,
|
||||
} as NativeArgsFor<TName>
|
||||
}
|
||||
break
|
||||
|
||||
case "list_code_definition_names":
|
||||
if (args.path !== undefined) {
|
||||
nativeArgs = {
|
||||
path: args.path,
|
||||
} as NativeArgsFor<TName>
|
||||
}
|
||||
break
|
||||
|
||||
case "run_slash_command":
|
||||
if (args.command !== undefined) {
|
||||
nativeArgs = {
|
||||
command: args.command,
|
||||
args: args.args,
|
||||
} as NativeArgsFor<TName>
|
||||
}
|
||||
break
|
||||
|
||||
case "search_files":
|
||||
if (args.path !== undefined && args.regex !== undefined) {
|
||||
nativeArgs = {
|
||||
path: args.path,
|
||||
regex: args.regex,
|
||||
file_pattern: args.file_pattern,
|
||||
} as NativeArgsFor<TName>
|
||||
}
|
||||
break
|
||||
|
||||
case "switch_mode":
|
||||
if (args.mode_slug !== undefined && args.reason !== undefined) {
|
||||
nativeArgs = {
|
||||
mode_slug: args.mode_slug,
|
||||
reason: args.reason,
|
||||
} as NativeArgsFor<TName>
|
||||
}
|
||||
break
|
||||
|
||||
case "update_todo_list":
|
||||
if (args.todos !== undefined) {
|
||||
nativeArgs = {
|
||||
todos: args.todos,
|
||||
} as NativeArgsFor<TName>
|
||||
}
|
||||
break
|
||||
|
||||
case "write_to_file":
|
||||
if (args.path !== undefined && args.content !== undefined && args.line_count !== undefined) {
|
||||
nativeArgs = {
|
||||
path: args.path,
|
||||
content: args.content,
|
||||
line_count:
|
||||
typeof args.line_count === "number"
|
||||
? args.line_count
|
||||
: parseInt(String(args.line_count), 10),
|
||||
} as NativeArgsFor<TName>
|
||||
}
|
||||
break
|
||||
|
||||
case "use_mcp_tool":
|
||||
if (args.server_name !== undefined && args.tool_name !== undefined) {
|
||||
nativeArgs = {
|
||||
server_name: args.server_name,
|
||||
tool_name: args.tool_name,
|
||||
arguments: args.arguments,
|
||||
} as NativeArgsFor<TName>
|
||||
}
|
||||
break
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
const result: ToolUse<TName> = {
|
||||
type: "tool_use" as const,
|
||||
name: toolCall.name,
|
||||
params,
|
||||
partial: false, // Native tool calls are always complete when yielded
|
||||
nativeArgs,
|
||||
}
|
||||
|
||||
return result
|
||||
} catch (error) {
|
||||
console.error(`Failed to parse tool call arguments:`, error)
|
||||
console.error(`Error details:`, error instanceof Error ? error.message : String(error))
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse dynamic MCP tools (named mcp_serverName_toolName).
|
||||
* These are generated dynamically by getMcpServerTools() and need to be
|
||||
* converted back to use_mcp_tool format.
|
||||
*/
|
||||
private static parseDynamicMcpTool(toolCall: {
|
||||
id: string
|
||||
name: string
|
||||
arguments: string
|
||||
}): ToolUse<"use_mcp_tool"> | null {
|
||||
try {
|
||||
const args = JSON.parse(toolCall.arguments)
|
||||
|
||||
// Extract server_name and tool_name from the arguments
|
||||
// The dynamic tool schema includes these as const properties
|
||||
const serverName = args.server_name
|
||||
const toolName = args.tool_name
|
||||
const toolInputProps = args.toolInputProps
|
||||
|
||||
if (!serverName || !toolName) {
|
||||
console.error(`Missing server_name or tool_name in dynamic MCP tool`)
|
||||
return null
|
||||
}
|
||||
|
||||
// Build params for backward compatibility with XML protocol
|
||||
const params: Partial<Record<string, string>> = {
|
||||
server_name: serverName,
|
||||
tool_name: toolName,
|
||||
}
|
||||
|
||||
if (toolInputProps) {
|
||||
params.arguments = JSON.stringify(toolInputProps)
|
||||
}
|
||||
|
||||
// Build nativeArgs with properly typed structure
|
||||
const nativeArgs: NativeToolArgs["use_mcp_tool"] = {
|
||||
server_name: serverName,
|
||||
tool_name: toolName,
|
||||
arguments: toolInputProps,
|
||||
}
|
||||
|
||||
const result: ToolUse<"use_mcp_tool"> = {
|
||||
type: "tool_use" as const,
|
||||
name: "use_mcp_tool",
|
||||
params,
|
||||
partial: false,
|
||||
nativeArgs,
|
||||
}
|
||||
|
||||
return result
|
||||
} catch (error) {
|
||||
console.error(`Failed to parse dynamic MCP tool:`, error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,41 +1,45 @@
|
|||
import cloneDeep from "clone-deep"
|
||||
import { serializeError } from "serialize-error"
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
|
||||
import type { ToolName, ClineAsk, ToolProgressStatus } from "@roo-code/types"
|
||||
import { TelemetryService } from "@roo-code/telemetry"
|
||||
|
||||
import { defaultModeSlug, getModeBySlug } from "../../shared/modes"
|
||||
import type { ToolParamName, ToolResponse } from "../../shared/tools"
|
||||
import type { ToolParamName, ToolResponse, ToolUse } from "../../shared/tools"
|
||||
import { Package } from "../../shared/package"
|
||||
|
||||
import { fetchInstructionsTool } from "../tools/fetchInstructionsTool"
|
||||
import { listFilesTool } from "../tools/listFilesTool"
|
||||
import { getReadFileToolDescription, readFileTool } from "../tools/readFileTool"
|
||||
import { fetchInstructionsTool } from "../tools/FetchInstructionsTool"
|
||||
import { listFilesTool } from "../tools/ListFilesTool"
|
||||
import { readFileTool } from "../tools/ReadFileTool"
|
||||
import { getSimpleReadFileToolDescription, simpleReadFileTool } from "../tools/simpleReadFileTool"
|
||||
import { shouldUseSingleFileRead } from "@roo-code/types"
|
||||
import { writeToFileTool } from "../tools/writeToFileTool"
|
||||
import { applyDiffTool } from "../tools/multiApplyDiffTool"
|
||||
import { insertContentTool } from "../tools/insertContentTool"
|
||||
import { listCodeDefinitionNamesTool } from "../tools/listCodeDefinitionNamesTool"
|
||||
import { searchFilesTool } from "../tools/searchFilesTool"
|
||||
import { browserActionTool } from "../tools/browserActionTool"
|
||||
import { executeCommandTool } from "../tools/executeCommandTool"
|
||||
import { useMcpToolTool } from "../tools/useMcpToolTool"
|
||||
import { writeToFileTool } from "../tools/WriteToFileTool"
|
||||
import { applyDiffTool } from "../tools/MultiApplyDiffTool"
|
||||
import { insertContentTool } from "../tools/InsertContentTool"
|
||||
import { listCodeDefinitionNamesTool } from "../tools/ListCodeDefinitionNamesTool"
|
||||
import { searchFilesTool } from "../tools/SearchFilesTool"
|
||||
import { browserActionTool } from "../tools/BrowserActionTool"
|
||||
import { executeCommandTool } from "../tools/ExecuteCommandTool"
|
||||
import { useMcpToolTool } from "../tools/UseMcpToolTool"
|
||||
import { accessMcpResourceTool } from "../tools/accessMcpResourceTool"
|
||||
import { askFollowupQuestionTool } from "../tools/askFollowupQuestionTool"
|
||||
import { switchModeTool } from "../tools/switchModeTool"
|
||||
import { attemptCompletionTool } from "../tools/attemptCompletionTool"
|
||||
import { newTaskTool } from "../tools/newTaskTool"
|
||||
import { askFollowupQuestionTool } from "../tools/AskFollowupQuestionTool"
|
||||
import { switchModeTool } from "../tools/SwitchModeTool"
|
||||
import { attemptCompletionTool, AttemptCompletionCallbacks } from "../tools/AttemptCompletionTool"
|
||||
import { newTaskTool } from "../tools/NewTaskTool"
|
||||
|
||||
import { updateTodoListTool } from "../tools/updateTodoListTool"
|
||||
import { runSlashCommandTool } from "../tools/runSlashCommandTool"
|
||||
import { generateImageTool } from "../tools/generateImageTool"
|
||||
import { updateTodoListTool } from "../tools/UpdateTodoListTool"
|
||||
import { runSlashCommandTool } from "../tools/RunSlashCommandTool"
|
||||
import { generateImageTool } from "../tools/GenerateImageTool"
|
||||
|
||||
import { formatResponse } from "../prompts/responses"
|
||||
import { validateToolUse } from "../tools/validateToolUse"
|
||||
import { Task } from "../task/Task"
|
||||
import { codebaseSearchTool } from "../tools/codebaseSearchTool"
|
||||
import { codebaseSearchTool } from "../tools/CodebaseSearchTool"
|
||||
import { experiments, EXPERIMENT_IDS } from "../../shared/experiments"
|
||||
import { applyDiffToolLegacy } from "../tools/applyDiffTool"
|
||||
import { applyDiffTool as applyDiffToolClass } from "../tools/ApplyDiffTool"
|
||||
import { isNativeProtocol } from "@roo-code/types"
|
||||
import { getToolProtocolFromSettings } from "../../utils/toolProtocol"
|
||||
|
||||
/**
|
||||
* Processes and presents assistant message content to the user interface.
|
||||
|
|
@ -80,7 +84,18 @@ export async function presentAssistantMessage(cline: Task) {
|
|||
return
|
||||
}
|
||||
|
||||
const block = cloneDeep(cline.assistantMessageContent[cline.currentStreamingContentIndex]) // need to create copy bc while stream is updating the array, it could be updating the reference block properties too
|
||||
let block: any
|
||||
try {
|
||||
block = cloneDeep(cline.assistantMessageContent[cline.currentStreamingContentIndex]) // need to create copy bc while stream is updating the array, it could be updating the reference block properties too
|
||||
} catch (error) {
|
||||
console.error(`ERROR cloning block:`, error)
|
||||
console.error(
|
||||
`Block content:`,
|
||||
JSON.stringify(cline.assistantMessageContent[cline.currentStreamingContentIndex], null, 2),
|
||||
)
|
||||
cline.presentAssistantMessageLocked = false
|
||||
return
|
||||
}
|
||||
|
||||
switch (block.type) {
|
||||
case "text": {
|
||||
|
|
@ -163,7 +178,12 @@ export async function presentAssistantMessage(cline: Task) {
|
|||
if (shouldUseSingleFileRead(modelId)) {
|
||||
return getSimpleReadFileToolDescription(block.name, block.params)
|
||||
} else {
|
||||
return getReadFileToolDescription(block.name, block.params)
|
||||
// Prefer native typed args when available; fall back to legacy params
|
||||
// Check if nativeArgs exists (native protocol)
|
||||
if (block.nativeArgs) {
|
||||
return readFileTool.getReadFileToolDescription(block.name, block.nativeArgs)
|
||||
}
|
||||
return readFileTool.getReadFileToolDescription(block.name, block.params)
|
||||
}
|
||||
case "fetch_instructions":
|
||||
return `[${block.name} for '${block.params.task}']`
|
||||
|
|
@ -224,6 +244,8 @@ export async function presentAssistantMessage(cline: Task) {
|
|||
return `[${block.name} for '${block.params.command}'${block.params.args ? ` with args: ${block.params.args}` : ""}]`
|
||||
case "generate_image":
|
||||
return `[${block.name} for '${block.params.path}']`
|
||||
default:
|
||||
return `[${block.name}]`
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -255,13 +277,63 @@ export async function presentAssistantMessage(cline: Task) {
|
|||
break
|
||||
}
|
||||
|
||||
const pushToolResult = (content: ToolResponse) => {
|
||||
cline.userMessageContent.push({ type: "text", text: `${toolDescription()} Result:` })
|
||||
// Track if we've already pushed a tool result for this tool call (native protocol only)
|
||||
let hasToolResult = false
|
||||
|
||||
if (typeof content === "string") {
|
||||
cline.userMessageContent.push({ type: "text", text: content || "(tool did not return anything)" })
|
||||
const pushToolResult = (content: ToolResponse) => {
|
||||
// Check if we're using native tool protocol
|
||||
const isNative = isNativeProtocol(getToolProtocolFromSettings())
|
||||
|
||||
// Get the tool call ID if this is a native tool call
|
||||
const toolCallId = (block as any).id
|
||||
|
||||
if (isNative && toolCallId) {
|
||||
// For native protocol, only allow ONE tool_result per tool call
|
||||
if (hasToolResult) {
|
||||
console.warn(
|
||||
`[presentAssistantMessage] Skipping duplicate tool_result for tool_use_id: ${toolCallId}`,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// For native protocol, add as tool_result block
|
||||
let resultContent: string
|
||||
if (typeof content === "string") {
|
||||
resultContent = content || "(tool did not return anything)"
|
||||
} else {
|
||||
// Convert array of content blocks to string for tool result
|
||||
// Tool results in OpenAI format only support strings
|
||||
resultContent = content
|
||||
.map((item) => {
|
||||
if (item.type === "text") {
|
||||
return item.text
|
||||
} else if (item.type === "image") {
|
||||
return "(image content)"
|
||||
}
|
||||
return ""
|
||||
})
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
cline.userMessageContent.push({
|
||||
type: "tool_result",
|
||||
tool_use_id: toolCallId,
|
||||
content: resultContent,
|
||||
} as Anthropic.ToolResultBlockParam)
|
||||
|
||||
hasToolResult = true
|
||||
} else {
|
||||
cline.userMessageContent.push(...content)
|
||||
// For XML protocol, add as text blocks (legacy behavior)
|
||||
cline.userMessageContent.push({ type: "text", text: `${toolDescription()} Result:` })
|
||||
|
||||
if (typeof content === "string") {
|
||||
cline.userMessageContent.push({
|
||||
type: "text",
|
||||
text: content || "(tool did not return anything)",
|
||||
})
|
||||
} else {
|
||||
cline.userMessageContent.push(...content)
|
||||
}
|
||||
}
|
||||
|
||||
// Once a tool result has been collected, ignore all other tool
|
||||
|
|
@ -422,12 +494,35 @@ export async function presentAssistantMessage(cline: Task) {
|
|||
switch (block.name) {
|
||||
case "write_to_file":
|
||||
await checkpointSaveAndMark(cline)
|
||||
await writeToFileTool(cline, block, askApproval, handleError, pushToolResult, removeClosingTag)
|
||||
await writeToFileTool.handle(cline, block as ToolUse<"write_to_file">, {
|
||||
askApproval,
|
||||
handleError,
|
||||
pushToolResult,
|
||||
removeClosingTag,
|
||||
})
|
||||
break
|
||||
case "update_todo_list":
|
||||
await updateTodoListTool(cline, block, askApproval, handleError, pushToolResult, removeClosingTag)
|
||||
await updateTodoListTool.handle(cline, block as ToolUse<"update_todo_list">, {
|
||||
askApproval,
|
||||
handleError,
|
||||
pushToolResult,
|
||||
removeClosingTag,
|
||||
})
|
||||
break
|
||||
case "apply_diff": {
|
||||
await checkpointSaveAndMark(cline)
|
||||
|
||||
// Check if native protocol is enabled - if so, always use single-file class-based tool
|
||||
if (isNativeProtocol(getToolProtocolFromSettings())) {
|
||||
await applyDiffToolClass.handle(cline, block as ToolUse<"apply_diff">, {
|
||||
askApproval,
|
||||
handleError,
|
||||
pushToolResult,
|
||||
removeClosingTag,
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
// Get the provider and state to check experiment settings
|
||||
const provider = cline.providerRef.deref()
|
||||
let isMultiFileApplyDiffEnabled = false
|
||||
|
|
@ -441,24 +536,25 @@ export async function presentAssistantMessage(cline: Task) {
|
|||
}
|
||||
|
||||
if (isMultiFileApplyDiffEnabled) {
|
||||
await checkpointSaveAndMark(cline)
|
||||
await applyDiffTool(cline, block, askApproval, handleError, pushToolResult, removeClosingTag)
|
||||
} else {
|
||||
await checkpointSaveAndMark(cline)
|
||||
await applyDiffToolLegacy(
|
||||
cline,
|
||||
block,
|
||||
await applyDiffToolClass.handle(cline, block as ToolUse<"apply_diff">, {
|
||||
askApproval,
|
||||
handleError,
|
||||
pushToolResult,
|
||||
removeClosingTag,
|
||||
)
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
case "insert_content":
|
||||
await checkpointSaveAndMark(cline)
|
||||
await insertContentTool(cline, block, askApproval, handleError, pushToolResult, removeClosingTag)
|
||||
await insertContentTool.handle(cline, block as ToolUse<"insert_content">, {
|
||||
askApproval,
|
||||
handleError,
|
||||
pushToolResult,
|
||||
removeClosingTag,
|
||||
})
|
||||
break
|
||||
case "read_file":
|
||||
// Check if this model should use the simplified single-file read tool
|
||||
|
|
@ -473,39 +569,78 @@ export async function presentAssistantMessage(cline: Task) {
|
|||
removeClosingTag,
|
||||
)
|
||||
} else {
|
||||
await readFileTool(cline, block, askApproval, handleError, pushToolResult, removeClosingTag)
|
||||
// Type assertion is safe here because we're in the "read_file" case
|
||||
await readFileTool.handle(cline, block as ToolUse<"read_file">, {
|
||||
askApproval,
|
||||
handleError,
|
||||
pushToolResult,
|
||||
removeClosingTag,
|
||||
})
|
||||
}
|
||||
break
|
||||
case "fetch_instructions":
|
||||
await fetchInstructionsTool(cline, block, askApproval, handleError, pushToolResult)
|
||||
break
|
||||
case "list_files":
|
||||
await listFilesTool(cline, block, askApproval, handleError, pushToolResult, removeClosingTag)
|
||||
break
|
||||
case "codebase_search":
|
||||
await codebaseSearchTool(cline, block, askApproval, handleError, pushToolResult, removeClosingTag)
|
||||
break
|
||||
case "list_code_definition_names":
|
||||
await listCodeDefinitionNamesTool(
|
||||
cline,
|
||||
block,
|
||||
await fetchInstructionsTool.handle(cline, block as ToolUse<"fetch_instructions">, {
|
||||
askApproval,
|
||||
handleError,
|
||||
pushToolResult,
|
||||
removeClosingTag,
|
||||
)
|
||||
})
|
||||
break
|
||||
case "list_files":
|
||||
await listFilesTool.handle(cline, block as ToolUse<"list_files">, {
|
||||
askApproval,
|
||||
handleError,
|
||||
pushToolResult,
|
||||
removeClosingTag,
|
||||
})
|
||||
break
|
||||
case "codebase_search":
|
||||
await codebaseSearchTool.handle(cline, block as ToolUse<"codebase_search">, {
|
||||
askApproval,
|
||||
handleError,
|
||||
pushToolResult,
|
||||
removeClosingTag,
|
||||
})
|
||||
break
|
||||
case "list_code_definition_names":
|
||||
await listCodeDefinitionNamesTool.handle(cline, block as ToolUse<"list_code_definition_names">, {
|
||||
askApproval,
|
||||
handleError,
|
||||
pushToolResult,
|
||||
removeClosingTag,
|
||||
})
|
||||
break
|
||||
case "search_files":
|
||||
await searchFilesTool(cline, block, askApproval, handleError, pushToolResult, removeClosingTag)
|
||||
await searchFilesTool.handle(cline, block as ToolUse<"search_files">, {
|
||||
askApproval,
|
||||
handleError,
|
||||
pushToolResult,
|
||||
removeClosingTag,
|
||||
})
|
||||
break
|
||||
case "browser_action":
|
||||
await browserActionTool(cline, block, askApproval, handleError, pushToolResult, removeClosingTag)
|
||||
await browserActionTool.handle(cline, block as ToolUse<"browser_action">, {
|
||||
askApproval,
|
||||
handleError,
|
||||
pushToolResult,
|
||||
removeClosingTag,
|
||||
})
|
||||
break
|
||||
case "execute_command":
|
||||
await executeCommandTool(cline, block, askApproval, handleError, pushToolResult, removeClosingTag)
|
||||
await executeCommandTool.handle(cline, block as ToolUse<"execute_command">, {
|
||||
askApproval,
|
||||
handleError,
|
||||
pushToolResult,
|
||||
removeClosingTag,
|
||||
})
|
||||
break
|
||||
case "use_mcp_tool":
|
||||
await useMcpToolTool(cline, block, askApproval, handleError, pushToolResult, removeClosingTag)
|
||||
await useMcpToolTool.handle(cline, block as ToolUse<"use_mcp_tool">, {
|
||||
askApproval,
|
||||
handleError,
|
||||
pushToolResult,
|
||||
removeClosingTag,
|
||||
})
|
||||
break
|
||||
case "access_mcp_resource":
|
||||
await accessMcpResourceTool(
|
||||
|
|
@ -518,38 +653,61 @@ export async function presentAssistantMessage(cline: Task) {
|
|||
)
|
||||
break
|
||||
case "ask_followup_question":
|
||||
await askFollowupQuestionTool(
|
||||
cline,
|
||||
block,
|
||||
await askFollowupQuestionTool.handle(cline, block as ToolUse<"ask_followup_question">, {
|
||||
askApproval,
|
||||
handleError,
|
||||
pushToolResult,
|
||||
removeClosingTag,
|
||||
)
|
||||
})
|
||||
break
|
||||
case "switch_mode":
|
||||
await switchModeTool(cline, block, askApproval, handleError, pushToolResult, removeClosingTag)
|
||||
break
|
||||
case "new_task":
|
||||
await newTaskTool(cline, block, askApproval, handleError, pushToolResult, removeClosingTag)
|
||||
break
|
||||
case "attempt_completion":
|
||||
await attemptCompletionTool(
|
||||
cline,
|
||||
block,
|
||||
await switchModeTool.handle(cline, block as ToolUse<"switch_mode">, {
|
||||
askApproval,
|
||||
handleError,
|
||||
pushToolResult,
|
||||
removeClosingTag,
|
||||
})
|
||||
break
|
||||
case "new_task":
|
||||
await newTaskTool.handle(cline, block as ToolUse<"new_task">, {
|
||||
askApproval,
|
||||
handleError,
|
||||
pushToolResult,
|
||||
removeClosingTag,
|
||||
})
|
||||
break
|
||||
case "attempt_completion": {
|
||||
const completionCallbacks: AttemptCompletionCallbacks = {
|
||||
askApproval,
|
||||
handleError,
|
||||
pushToolResult,
|
||||
removeClosingTag,
|
||||
toolDescription,
|
||||
askFinishSubTaskApproval,
|
||||
toolDescription,
|
||||
}
|
||||
await attemptCompletionTool.handle(
|
||||
cline,
|
||||
block as ToolUse<"attempt_completion">,
|
||||
completionCallbacks,
|
||||
)
|
||||
break
|
||||
}
|
||||
case "run_slash_command":
|
||||
await runSlashCommandTool(cline, block, askApproval, handleError, pushToolResult, removeClosingTag)
|
||||
await runSlashCommandTool.handle(cline, block as ToolUse<"run_slash_command">, {
|
||||
askApproval,
|
||||
handleError,
|
||||
pushToolResult,
|
||||
removeClosingTag,
|
||||
})
|
||||
break
|
||||
case "generate_image":
|
||||
await generateImageTool(cline, block, askApproval, handleError, pushToolResult, removeClosingTag)
|
||||
await checkpointSaveAndMark(cline)
|
||||
await generateImageTool.handle(cline, block as ToolUse<"generate_image">, {
|
||||
askApproval,
|
||||
handleError,
|
||||
pushToolResult,
|
||||
removeClosingTag,
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
|
|
@ -596,6 +754,12 @@ export async function presentAssistantMessage(cline: Task) {
|
|||
// this function ourselves.
|
||||
presentAssistantMessage(cline)
|
||||
return
|
||||
} else {
|
||||
// CRITICAL FIX: If we're out of bounds and the stream is complete, set userMessageContentReady
|
||||
// This handles the case where assistantMessageContent is empty or becomes empty after processing
|
||||
if (cline.didCompleteReadingStream) {
|
||||
cline.userMessageContentReady = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { GlobalState, ClineMessage, ClineAsk } from "@roo-code/types"
|
||||
|
||||
import { getApiMetrics } from "../../shared/getApiMetrics"
|
||||
import { ClineAskResponse } from "../../shared/WebviewMessage"
|
||||
|
||||
|
|
@ -2,7 +2,6 @@ import { GlobalState, ClineMessage } from "@roo-code/types"
|
|||
|
||||
import { AutoApprovalHandler } from "../AutoApprovalHandler"
|
||||
|
||||
// Mock getApiMetrics
|
||||
vi.mock("../../../shared/getApiMetrics", () => ({
|
||||
getApiMetrics: vi.fn(),
|
||||
}))
|
||||
368
src/core/auto-approval/commands.ts
Normal file
368
src/core/auto-approval/commands.ts
Normal file
|
|
@ -0,0 +1,368 @@
|
|||
import { parseCommand } from "../../shared/parse-command"
|
||||
|
||||
/**
|
||||
* Detect dangerous parameter substitutions that could lead to command execution.
|
||||
* These patterns are never auto-approved and always require explicit user approval.
|
||||
*
|
||||
* Detected patterns:
|
||||
* - ${var@P} - Prompt string expansion (interprets escape sequences and executes embedded commands)
|
||||
* - ${var@Q} - Quote removal
|
||||
* - ${var@E} - Escape sequence expansion
|
||||
* - ${var@A} - Assignment statement
|
||||
* - ${var@a} - Attribute flags
|
||||
* - ${var=value} with escape sequences - Can embed commands via \140 (backtick), \x60, or \u0060
|
||||
* - ${!var} - Indirect variable references
|
||||
* - <<<$(...) or <<<`...` - Here-strings with command substitution
|
||||
* - =(...) - Zsh process substitution that executes commands
|
||||
* - *(e:...:) or similar - Zsh glob qualifiers with code execution
|
||||
*
|
||||
* @param source - The command string to analyze
|
||||
* @returns true if dangerous substitution patterns are detected, false otherwise
|
||||
*/
|
||||
export function containsDangerousSubstitution(source: string): boolean {
|
||||
// Check for dangerous parameter expansion operators that can execute commands
|
||||
// ${var@P} - Prompt string expansion (interprets escape sequences and executes embedded commands)
|
||||
// ${var@Q} - Quote removal
|
||||
// ${var@E} - Escape sequence expansion
|
||||
// ${var@A} - Assignment statement
|
||||
// ${var@a} - Attribute flags
|
||||
const dangerousParameterExpansion = /\$\{[^}]*@[PQEAa][^}]*\}/.test(source)
|
||||
|
||||
// Check for parameter expansions with assignments that could contain escape sequences
|
||||
// ${var=value} or ${var:=value} can embed commands via escape sequences like \140 (backtick)
|
||||
// Also check for ${var+value}, ${var:-value}, ${var:+value}, ${var:?value}
|
||||
const parameterAssignmentWithEscapes =
|
||||
/\$\{[^}]*[=+\-?][^}]*\\[0-7]{3}[^}]*\}/.test(source) || // octal escapes
|
||||
/\$\{[^}]*[=+\-?][^}]*\\x[0-9a-fA-F]{2}[^}]*\}/.test(source) || // hex escapes
|
||||
/\$\{[^}]*[=+\-?][^}]*\\u[0-9a-fA-F]{4}[^}]*\}/.test(source) // unicode escapes
|
||||
|
||||
// Check for indirect variable references that could execute commands
|
||||
// ${!var} performs indirect expansion which can be dangerous with crafted variable names
|
||||
const indirectExpansion = /\$\{![^}]+\}/.test(source)
|
||||
|
||||
// Check for here-strings with command substitution
|
||||
// <<<$(...) or <<<`...` can execute commands
|
||||
const hereStringWithSubstitution = /<<<\s*(\$\(|`)/.test(source)
|
||||
|
||||
// Check for zsh process substitution =(...) which executes commands
|
||||
// =(...) creates a temporary file containing the output of the command, but executes it
|
||||
const zshProcessSubstitution = /=\([^)]+\)/.test(source)
|
||||
|
||||
// Check for zsh glob qualifiers with code execution (e:...:)
|
||||
// Patterns like *(e:whoami:) or ?(e:rm -rf /:) execute commands during glob expansion
|
||||
// This regex matches patterns like *(e:...:), ?(e:...:), +(e:...:), @(e:...:), !(e:...:)
|
||||
const zshGlobQualifier = /[*?+@!]\(e:[^:]+:\)/.test(source)
|
||||
|
||||
// Return true if any dangerous pattern is detected
|
||||
return (
|
||||
dangerousParameterExpansion ||
|
||||
parameterAssignmentWithEscapes ||
|
||||
indirectExpansion ||
|
||||
hereStringWithSubstitution ||
|
||||
zshProcessSubstitution ||
|
||||
zshGlobQualifier
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the longest matching prefix from a list of prefixes for a given command.
|
||||
*
|
||||
* This is the core function that implements the "longest prefix match" strategy.
|
||||
* It searches through all provided prefixes and returns the longest one that
|
||||
* matches the beginning of the command (case-insensitive).
|
||||
*
|
||||
* **Special Cases:**
|
||||
* - Wildcard "*" matches any command but is treated as length 1 for comparison
|
||||
* - Empty command or empty prefixes list returns null
|
||||
* - Matching is case-insensitive and uses startsWith logic
|
||||
*
|
||||
* **Examples:**
|
||||
* ```typescript
|
||||
* findLongestPrefixMatch("git push origin", ["git", "git push"])
|
||||
* // Returns "git push" (longer match)
|
||||
*
|
||||
* findLongestPrefixMatch("npm install", ["*", "npm"])
|
||||
* // Returns "npm" (specific match preferred over wildcard)
|
||||
*
|
||||
* findLongestPrefixMatch("unknown command", ["git", "npm"])
|
||||
* // Returns null (no match found)
|
||||
* ```
|
||||
*
|
||||
* @param command - The command to match against
|
||||
* @param prefixes - List of prefix patterns to search through
|
||||
* @returns The longest matching prefix, or null if no match found
|
||||
*/
|
||||
export function findLongestPrefixMatch(command: string, prefixes: string[]): string | null {
|
||||
if (!command || !prefixes?.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
const trimmedCommand = command.trim().toLowerCase()
|
||||
let longestMatch: string | null = null
|
||||
|
||||
for (const prefix of prefixes) {
|
||||
const lowerPrefix = prefix.toLowerCase()
|
||||
// Handle wildcard "*" - it matches any command
|
||||
if (lowerPrefix === "*" || trimmedCommand.startsWith(lowerPrefix)) {
|
||||
if (!longestMatch || lowerPrefix.length > longestMatch.length) {
|
||||
longestMatch = lowerPrefix
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return longestMatch
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a single command should be auto-approved.
|
||||
* Returns true only for commands that explicitly match the allowlist
|
||||
* and either don't match the denylist or have a longer allowlist match.
|
||||
*
|
||||
* Special handling for wildcards: "*" in allowlist allows any command,
|
||||
* but denylist can still block specific commands.
|
||||
*/
|
||||
export function isAutoApprovedSingleCommand(
|
||||
command: string,
|
||||
allowedCommands: string[],
|
||||
deniedCommands?: string[],
|
||||
): boolean {
|
||||
if (!command) {
|
||||
return true
|
||||
}
|
||||
|
||||
// If no allowlist configured, nothing can be auto-approved
|
||||
if (!allowedCommands?.length) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if wildcard is present in allowlist
|
||||
const hasWildcard = allowedCommands.some((cmd) => cmd.toLowerCase() === "*")
|
||||
|
||||
// If no denylist provided (undefined), use simple allowlist logic
|
||||
if (deniedCommands === undefined) {
|
||||
const trimmedCommand = command.trim().toLowerCase()
|
||||
|
||||
return allowedCommands.some((prefix) => {
|
||||
const lowerPrefix = prefix.toLowerCase()
|
||||
// Handle wildcard "*" - it matches any command
|
||||
return lowerPrefix === "*" || trimmedCommand.startsWith(lowerPrefix)
|
||||
})
|
||||
}
|
||||
|
||||
// Find longest matching prefix in both lists
|
||||
const longestDeniedMatch = findLongestPrefixMatch(command, deniedCommands)
|
||||
const longestAllowedMatch = findLongestPrefixMatch(command, allowedCommands)
|
||||
|
||||
// Special case: if wildcard is present and no denylist match, auto-approve
|
||||
if (hasWildcard && !longestDeniedMatch) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Must have an allowlist match to be auto-approved
|
||||
if (!longestAllowedMatch) {
|
||||
return false
|
||||
}
|
||||
|
||||
// If no denylist match, auto-approve
|
||||
if (!longestDeniedMatch) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Both have matches - allowlist must be longer to auto-approve
|
||||
return longestAllowedMatch.length > longestDeniedMatch.length
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a single command should be auto-denied.
|
||||
* Returns true only for commands that explicitly match the denylist
|
||||
* and either don't match the allowlist or have a longer denylist match.
|
||||
*/
|
||||
export function isAutoDeniedSingleCommand(
|
||||
command: string,
|
||||
allowedCommands: string[],
|
||||
deniedCommands?: string[],
|
||||
): boolean {
|
||||
if (!command) return false
|
||||
|
||||
// If no denylist configured, nothing can be auto-denied
|
||||
if (!deniedCommands?.length) return false
|
||||
|
||||
// Find longest matching prefix in both lists
|
||||
const longestDeniedMatch = findLongestPrefixMatch(command, deniedCommands)
|
||||
const longestAllowedMatch = findLongestPrefixMatch(command, allowedCommands || [])
|
||||
|
||||
// Must have a denylist match to be auto-denied
|
||||
if (!longestDeniedMatch) return false
|
||||
|
||||
// If no allowlist match, auto-deny
|
||||
if (!longestAllowedMatch) return true
|
||||
|
||||
// Both have matches - denylist must be longer or equal to auto-deny
|
||||
return longestDeniedMatch.length >= longestAllowedMatch.length
|
||||
}
|
||||
|
||||
/**
|
||||
* Command approval decision types
|
||||
*/
|
||||
export type CommandDecision = "auto_approve" | "auto_deny" | "ask_user"
|
||||
|
||||
/**
|
||||
* Unified command validation that implements the longest prefix match rule.
|
||||
* Returns a definitive decision for a command based on allowlist and denylist.
|
||||
*
|
||||
* This is the main entry point for command validation in the Command Denylist feature.
|
||||
* It handles complex command chains and applies the longest prefix match strategy
|
||||
* to resolve conflicts between allowlist and denylist patterns.
|
||||
*
|
||||
* **Decision Logic:**
|
||||
* 1. **Dangerous Substitution Protection**: Commands with dangerous parameter expansions are never auto-approved
|
||||
* 2. **Command Parsing**: Split command chains (&&, ||, ;, |, &) into individual commands
|
||||
* 3. **Individual Validation**: For each sub-command, apply longest prefix match rule
|
||||
* 4. **Aggregation**: Combine decisions using "any denial blocks all" principle
|
||||
*
|
||||
* **Return Values:**
|
||||
* - `"auto_approve"`: All sub-commands are explicitly allowed and no dangerous patterns detected
|
||||
* - `"auto_deny"`: At least one sub-command is explicitly denied
|
||||
* - `"ask_user"`: Mixed or no matches found, requires user decision, or contains dangerous patterns
|
||||
*
|
||||
* **Examples:**
|
||||
* ```typescript
|
||||
* // Simple approval
|
||||
* getCommandDecision("git status", ["git"], [])
|
||||
* // Returns "auto_approve"
|
||||
*
|
||||
* // Dangerous pattern - never auto-approved
|
||||
* getCommandDecision('echo "${var@P}"', ["echo"], [])
|
||||
* // Returns "ask_user"
|
||||
*
|
||||
* // Longest prefix match - denial wins
|
||||
* getCommandDecision("git push origin", ["git"], ["git push"])
|
||||
* // Returns "auto_deny"
|
||||
*
|
||||
* // Command chain - any denial blocks all
|
||||
* getCommandDecision("git status && rm file", ["git"], ["rm"])
|
||||
* // Returns "auto_deny"
|
||||
*
|
||||
* // No matches - ask user
|
||||
* getCommandDecision("unknown command", ["git"], ["rm"])
|
||||
* // Returns "ask_user"
|
||||
* ```
|
||||
*
|
||||
* @param command - The full command string to validate
|
||||
* @param allowedCommands - List of allowed command prefixes
|
||||
* @param deniedCommands - Optional list of denied command prefixes
|
||||
* @returns Decision indicating whether to approve, deny, or ask user
|
||||
*/
|
||||
export function getCommandDecision(
|
||||
command: string,
|
||||
allowedCommands: string[],
|
||||
deniedCommands?: string[],
|
||||
): CommandDecision {
|
||||
if (!command?.trim()) {
|
||||
return "auto_approve"
|
||||
}
|
||||
|
||||
// Parse into sub-commands (split by &&, ||, ;, |)
|
||||
const subCommands = parseCommand(command)
|
||||
|
||||
// Check each sub-command and collect decisions
|
||||
const decisions: CommandDecision[] = subCommands.map((cmd) => {
|
||||
// Remove simple PowerShell-like redirections (e.g. 2>&1) before checking
|
||||
const cmdWithoutRedirection = cmd.replace(/\d*>&\d*/, "").trim()
|
||||
|
||||
return getSingleCommandDecision(cmdWithoutRedirection, allowedCommands, deniedCommands)
|
||||
})
|
||||
|
||||
// If any sub-command is denied, deny the whole command
|
||||
if (decisions.includes("auto_deny")) {
|
||||
return "auto_deny"
|
||||
}
|
||||
|
||||
// Require explicit user approval for dangerous patterns
|
||||
if (containsDangerousSubstitution(command)) {
|
||||
return "ask_user"
|
||||
}
|
||||
|
||||
// If all sub-commands are approved, approve the whole command
|
||||
if (decisions.every((decision) => decision === "auto_approve")) {
|
||||
return "auto_approve"
|
||||
}
|
||||
|
||||
// Otherwise, ask user
|
||||
return "ask_user"
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the decision for a single command using longest prefix match rule.
|
||||
*
|
||||
* This is the core logic that implements the conflict resolution between
|
||||
* allowlist and denylist using the "longest prefix match" strategy.
|
||||
*
|
||||
* **Longest Prefix Match Algorithm:**
|
||||
* 1. Find the longest matching prefix in the allowlist
|
||||
* 2. Find the longest matching prefix in the denylist
|
||||
* 3. Compare lengths to determine which rule takes precedence
|
||||
* 4. Longer (more specific) match wins the conflict
|
||||
*
|
||||
* **Decision Matrix:**
|
||||
* | Allowlist Match | Denylist Match | Result | Reason |
|
||||
* |----------------|----------------|---------|---------|
|
||||
* | Yes | No | auto_approve | Only allowlist matches |
|
||||
* | No | Yes | auto_deny | Only denylist matches |
|
||||
* | Yes | Yes (shorter) | auto_approve | Allowlist is more specific |
|
||||
* | Yes | Yes (longer/equal) | auto_deny | Denylist is more specific |
|
||||
* | No | No | ask_user | No rules apply |
|
||||
*
|
||||
* **Examples:**
|
||||
* ```typescript
|
||||
* // Only allowlist matches
|
||||
* getSingleCommandDecision("git status", ["git"], ["npm"])
|
||||
* // Returns "auto_approve"
|
||||
*
|
||||
* // Denylist is more specific
|
||||
* getSingleCommandDecision("git push origin", ["git"], ["git push"])
|
||||
* // Returns "auto_deny" (denylist "git push" > allowlist "git")
|
||||
*
|
||||
* // Allowlist is more specific
|
||||
* getSingleCommandDecision("git push --dry-run", ["git push --dry-run"], ["git push"])
|
||||
* // Returns "auto_approve" (allowlist is longer)
|
||||
*
|
||||
* // No matches
|
||||
* getSingleCommandDecision("unknown", ["git"], ["npm"])
|
||||
* // Returns "ask_user"
|
||||
* ```
|
||||
*
|
||||
* @param command - Single command to validate (no chaining)
|
||||
* @param allowedCommands - List of allowed command prefixes
|
||||
* @param deniedCommands - Optional list of denied command prefixes
|
||||
* @returns Decision for this specific command
|
||||
*/
|
||||
export function getSingleCommandDecision(
|
||||
command: string,
|
||||
allowedCommands: string[],
|
||||
deniedCommands?: string[],
|
||||
): CommandDecision {
|
||||
if (!command) return "auto_approve"
|
||||
|
||||
// Find longest matching prefixes in both lists
|
||||
const longestAllowedMatch = findLongestPrefixMatch(command, allowedCommands || [])
|
||||
const longestDeniedMatch = findLongestPrefixMatch(command, deniedCommands || [])
|
||||
|
||||
// If only allowlist has a match, auto-approve
|
||||
if (longestAllowedMatch && !longestDeniedMatch) {
|
||||
return "auto_approve"
|
||||
}
|
||||
|
||||
// If only denylist has a match, auto-deny
|
||||
if (!longestAllowedMatch && longestDeniedMatch) {
|
||||
return "auto_deny"
|
||||
}
|
||||
|
||||
// Both lists have matches - apply longest prefix match rule
|
||||
if (longestAllowedMatch && longestDeniedMatch) {
|
||||
return longestAllowedMatch.length > longestDeniedMatch.length ? "auto_approve" : "auto_deny"
|
||||
}
|
||||
|
||||
// If neither list has a match, ask user
|
||||
return "ask_user"
|
||||
}
|
||||
189
src/core/auto-approval/index.ts
Normal file
189
src/core/auto-approval/index.ts
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
import { type ClineAsk, type McpServerUse, type FollowUpData, isNonBlockingAsk } from "@roo-code/types"
|
||||
|
||||
import type { ClineSayTool, ExtensionState } from "../../shared/ExtensionMessage"
|
||||
import { ClineAskResponse } from "../../shared/WebviewMessage"
|
||||
|
||||
import { isWriteToolAction, isReadOnlyToolAction } from "./tools"
|
||||
import { isMcpToolAlwaysAllowed } from "./mcp"
|
||||
import { getCommandDecision } from "./commands"
|
||||
|
||||
// We have 10 different actions that can be auto-approved.
|
||||
export type AutoApprovalState =
|
||||
| "alwaysAllowReadOnly"
|
||||
| "alwaysAllowWrite"
|
||||
| "alwaysAllowBrowser"
|
||||
| "alwaysApproveResubmit"
|
||||
| "alwaysAllowMcp"
|
||||
| "alwaysAllowModeSwitch"
|
||||
| "alwaysAllowSubtasks"
|
||||
| "alwaysAllowExecute"
|
||||
| "alwaysAllowFollowupQuestions"
|
||||
| "alwaysAllowUpdateTodoList"
|
||||
|
||||
// Some of these actions have additional settings associated with them.
|
||||
export type AutoApprovalStateOptions =
|
||||
| "autoApprovalEnabled"
|
||||
| "alwaysAllowReadOnlyOutsideWorkspace" // For `alwaysAllowReadOnly`.
|
||||
| "alwaysAllowWriteOutsideWorkspace" // For `alwaysAllowWrite`.
|
||||
| "alwaysAllowWriteProtected"
|
||||
| "followupAutoApproveTimeoutMs" // For `alwaysAllowFollowupQuestions`.
|
||||
| "mcpServers" // For `alwaysAllowMcp`.
|
||||
| "allowedCommands" // For `alwaysAllowExecute`.
|
||||
| "deniedCommands"
|
||||
|
||||
export type CheckAutoApprovalResult =
|
||||
| { decision: "approve" }
|
||||
| { decision: "deny" }
|
||||
| { decision: "ask" }
|
||||
| {
|
||||
decision: "timeout"
|
||||
timeout: number
|
||||
fn: () => { askResponse: ClineAskResponse; text?: string; images?: string[] }
|
||||
}
|
||||
|
||||
export async function checkAutoApproval({
|
||||
state,
|
||||
ask,
|
||||
text,
|
||||
isProtected,
|
||||
}: {
|
||||
state?: Pick<ExtensionState, AutoApprovalState | AutoApprovalStateOptions>
|
||||
ask: ClineAsk
|
||||
text?: string
|
||||
isProtected?: boolean
|
||||
}): Promise<CheckAutoApprovalResult> {
|
||||
if (isNonBlockingAsk(ask)) {
|
||||
return { decision: "approve" }
|
||||
}
|
||||
|
||||
if (!state || !state.autoApprovalEnabled) {
|
||||
return { decision: "ask" }
|
||||
}
|
||||
|
||||
if (ask === "followup") {
|
||||
if (state.alwaysAllowFollowupQuestions === true) {
|
||||
try {
|
||||
const suggestion = (JSON.parse(text || "{}") as FollowUpData).suggest?.[0]
|
||||
|
||||
if (
|
||||
suggestion &&
|
||||
typeof state.followupAutoApproveTimeoutMs === "number" &&
|
||||
state.followupAutoApproveTimeoutMs > 0
|
||||
) {
|
||||
return {
|
||||
decision: "timeout",
|
||||
timeout: state.followupAutoApproveTimeoutMs,
|
||||
fn: () => ({ askResponse: "messageResponse", text: suggestion.answer }),
|
||||
}
|
||||
} else {
|
||||
return { decision: "ask" }
|
||||
}
|
||||
} catch (error) {
|
||||
return { decision: "ask" }
|
||||
}
|
||||
} else {
|
||||
return { decision: "ask" }
|
||||
}
|
||||
}
|
||||
|
||||
if (ask === "browser_action_launch") {
|
||||
return state.alwaysAllowBrowser === true ? { decision: "approve" } : { decision: "ask" }
|
||||
}
|
||||
|
||||
if (ask === "use_mcp_server") {
|
||||
if (!text) {
|
||||
return { decision: "ask" }
|
||||
}
|
||||
|
||||
try {
|
||||
const mcpServerUse = JSON.parse(text) as McpServerUse
|
||||
|
||||
if (mcpServerUse.type === "use_mcp_tool") {
|
||||
return state.alwaysAllowMcp === true && isMcpToolAlwaysAllowed(mcpServerUse, state.mcpServers)
|
||||
? { decision: "approve" }
|
||||
: { decision: "ask" }
|
||||
} else if (mcpServerUse.type === "access_mcp_resource") {
|
||||
return state.alwaysAllowMcp === true ? { decision: "approve" } : { decision: "ask" }
|
||||
}
|
||||
} catch (error) {
|
||||
return { decision: "ask" }
|
||||
}
|
||||
|
||||
return { decision: "ask" }
|
||||
}
|
||||
|
||||
if (ask === "command") {
|
||||
if (!text) {
|
||||
return { decision: "ask" }
|
||||
}
|
||||
|
||||
if (state.alwaysAllowExecute === true) {
|
||||
const decision = getCommandDecision(text, state.allowedCommands || [], state.deniedCommands || [])
|
||||
|
||||
if (decision === "auto_approve") {
|
||||
return { decision: "approve" }
|
||||
} else if (decision === "auto_deny") {
|
||||
return { decision: "deny" }
|
||||
} else {
|
||||
return { decision: "ask" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (ask === "tool") {
|
||||
let tool: ClineSayTool | undefined
|
||||
|
||||
try {
|
||||
tool = JSON.parse(text || "{}")
|
||||
} catch (error) {
|
||||
console.error("Failed to parse tool:", error)
|
||||
}
|
||||
|
||||
if (!tool) {
|
||||
return { decision: "ask" }
|
||||
}
|
||||
|
||||
if (tool.tool === "updateTodoList") {
|
||||
return state.alwaysAllowUpdateTodoList === true ? { decision: "approve" } : { decision: "ask" }
|
||||
}
|
||||
|
||||
if (tool?.tool === "fetchInstructions") {
|
||||
if (tool.content === "create_mode") {
|
||||
return state.alwaysAllowModeSwitch === true ? { decision: "approve" } : { decision: "ask" }
|
||||
}
|
||||
|
||||
if (tool.content === "create_mcp_server") {
|
||||
return state.alwaysAllowMcp === true ? { decision: "approve" } : { decision: "ask" }
|
||||
}
|
||||
}
|
||||
|
||||
if (tool?.tool === "switchMode") {
|
||||
return state.alwaysAllowModeSwitch === true ? { decision: "approve" } : { decision: "ask" }
|
||||
}
|
||||
|
||||
if (["newTask", "finishTask"].includes(tool?.tool)) {
|
||||
return state.alwaysAllowSubtasks === true ? { decision: "approve" } : { decision: "ask" }
|
||||
}
|
||||
|
||||
const isOutsideWorkspace = !!tool.isOutsideWorkspace
|
||||
|
||||
if (isReadOnlyToolAction(tool)) {
|
||||
return state.alwaysAllowReadOnly === true &&
|
||||
(!isOutsideWorkspace || state.alwaysAllowReadOnlyOutsideWorkspace === true)
|
||||
? { decision: "approve" }
|
||||
: { decision: "ask" }
|
||||
}
|
||||
|
||||
if (isWriteToolAction(tool)) {
|
||||
return state.alwaysAllowWrite === true &&
|
||||
(!isOutsideWorkspace || state.alwaysAllowWriteOutsideWorkspace === true) &&
|
||||
(!isProtected || state.alwaysAllowWriteProtected === true)
|
||||
? { decision: "approve" }
|
||||
: { decision: "ask" }
|
||||
}
|
||||
}
|
||||
|
||||
return { decision: "ask" }
|
||||
}
|
||||
|
||||
export { AutoApprovalHandler } from "./AutoApprovalHandler"
|
||||
13
src/core/auto-approval/mcp.ts
Normal file
13
src/core/auto-approval/mcp.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import type { McpServerUse } from "@roo-code/types"
|
||||
|
||||
import type { McpServer, McpTool } from "../../shared/mcp"
|
||||
|
||||
export function isMcpToolAlwaysAllowed(mcpServerUse: McpServerUse, mcpServers: McpServer[] | undefined): boolean {
|
||||
if (mcpServerUse.type === "use_mcp_tool" && mcpServerUse.toolName) {
|
||||
const server = mcpServers?.find((s: McpServer) => s.name === mcpServerUse.serverName)
|
||||
const tool = server?.tools?.find((t: McpTool) => t.name === mcpServerUse.toolName)
|
||||
return tool?.alwaysAllow || false
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
18
src/core/auto-approval/tools.ts
Normal file
18
src/core/auto-approval/tools.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import type { ClineSayTool } from "../../shared/ExtensionMessage"
|
||||
|
||||
export function isWriteToolAction(tool: ClineSayTool): boolean {
|
||||
return ["editedExistingFile", "appliedDiff", "newFileCreated", "insertContent", "generateImage"].includes(tool.tool)
|
||||
}
|
||||
|
||||
export function isReadOnlyToolAction(tool: ClineSayTool): boolean {
|
||||
return [
|
||||
"readFile",
|
||||
"listFiles",
|
||||
"listFilesTopLevel",
|
||||
"listFilesRecursive",
|
||||
"listCodeDefinitionNames",
|
||||
"searchFiles",
|
||||
"codebaseSearch",
|
||||
"runSlashCommand",
|
||||
].includes(tool.tool)
|
||||
}
|
||||
|
|
@ -40,6 +40,7 @@ interface ExportResult {
|
|||
|
||||
interface ImportResult {
|
||||
success: boolean
|
||||
slug?: string
|
||||
error?: string
|
||||
}
|
||||
|
||||
|
|
@ -411,7 +412,7 @@ export class CustomModesManager {
|
|||
const errorMessage = `Invalid mode configuration: ${errorMessages}`
|
||||
logger.error("Mode validation failed", { slug, errors: validationResult.error.errors })
|
||||
vscode.window.showErrorMessage(t("common:customModes.errors.updateFailed", { error: errorMessage }))
|
||||
return
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
|
||||
const isProjectMode = config.source === "project"
|
||||
|
|
@ -457,6 +458,7 @@ export class CustomModesManager {
|
|||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
logger.error("Failed to update custom mode", { slug, error: errorMessage })
|
||||
vscode.window.showErrorMessage(t("common:customModes.errors.updateFailed", { error: errorMessage }))
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -989,7 +991,8 @@ export class CustomModesManager {
|
|||
// Refresh the modes after import
|
||||
await this.refreshMergedState()
|
||||
|
||||
return { success: true }
|
||||
// Return the imported mode's slug so the UI can activate it
|
||||
return { success: true, slug: importData.customModes[0]?.slug }
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
logger.error("Failed to import mode with rules", { error: errorMessage })
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// npx vitest src/core/sliding-window/__tests__/sliding-window.spec.ts
|
||||
// cd src && npx vitest run core/context-management/__tests__/context-management.spec.ts
|
||||
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
|
||||
|
|
@ -9,12 +9,7 @@ import { BaseProvider } from "../../../api/providers/base-provider"
|
|||
import { ApiMessage } from "../../task-persistence/apiMessages"
|
||||
import * as condenseModule from "../../condense"
|
||||
|
||||
import {
|
||||
TOKEN_BUFFER_PERCENTAGE,
|
||||
estimateTokenCount,
|
||||
truncateConversation,
|
||||
truncateConversationIfNeeded,
|
||||
} from "../index"
|
||||
import { TOKEN_BUFFER_PERCENTAGE, estimateTokenCount, truncateConversation, manageContext } from "../index"
|
||||
|
||||
// Create a mock ApiHandler for testing
|
||||
class MockApiHandler extends BaseProvider {
|
||||
|
|
@ -49,7 +44,7 @@ class MockApiHandler extends BaseProvider {
|
|||
const mockApiHandler = new MockApiHandler()
|
||||
const taskId = "test-task-id"
|
||||
|
||||
describe("Sliding Window", () => {
|
||||
describe("Context Management", () => {
|
||||
beforeEach(() => {
|
||||
if (!TelemetryService.hasInstance()) {
|
||||
TelemetryService.createInstance([])
|
||||
|
|
@ -234,9 +229,9 @@ describe("Sliding Window", () => {
|
|||
})
|
||||
|
||||
/**
|
||||
* Tests for the truncateConversationIfNeeded function
|
||||
* Tests for the manageContext function
|
||||
*/
|
||||
describe("truncateConversationIfNeeded", () => {
|
||||
describe("manageContext", () => {
|
||||
const createModelInfo = (contextWindow: number, maxTokens?: number): ModelInfo => ({
|
||||
contextWindow,
|
||||
supportsPromptCache: true,
|
||||
|
|
@ -261,7 +256,7 @@ describe("Sliding Window", () => {
|
|||
{ ...messages[messages.length - 1], content: "" },
|
||||
]
|
||||
|
||||
const result = await truncateConversationIfNeeded({
|
||||
const result = await manageContext({
|
||||
messages: messagesWithSmallContent,
|
||||
totalTokens,
|
||||
contextWindow: modelInfo.contextWindow,
|
||||
|
|
@ -302,7 +297,7 @@ describe("Sliding Window", () => {
|
|||
messagesWithSmallContent[4],
|
||||
]
|
||||
|
||||
const result = await truncateConversationIfNeeded({
|
||||
const result = await manageContext({
|
||||
messages: messagesWithSmallContent,
|
||||
totalTokens,
|
||||
contextWindow: modelInfo.contextWindow,
|
||||
|
|
@ -337,7 +332,7 @@ describe("Sliding Window", () => {
|
|||
|
||||
// Test below threshold
|
||||
const belowThreshold = 69999
|
||||
const result1 = await truncateConversationIfNeeded({
|
||||
const result1 = await manageContext({
|
||||
messages: messagesWithSmallContent,
|
||||
totalTokens: belowThreshold,
|
||||
contextWindow: modelInfo1.contextWindow,
|
||||
|
|
@ -351,7 +346,7 @@ describe("Sliding Window", () => {
|
|||
currentProfileId: "default",
|
||||
})
|
||||
|
||||
const result2 = await truncateConversationIfNeeded({
|
||||
const result2 = await manageContext({
|
||||
messages: messagesWithSmallContent,
|
||||
totalTokens: belowThreshold,
|
||||
contextWindow: modelInfo2.contextWindow,
|
||||
|
|
@ -372,7 +367,7 @@ describe("Sliding Window", () => {
|
|||
|
||||
// Test above threshold
|
||||
const aboveThreshold = 70001
|
||||
const result3 = await truncateConversationIfNeeded({
|
||||
const result3 = await manageContext({
|
||||
messages: messagesWithSmallContent,
|
||||
totalTokens: aboveThreshold,
|
||||
contextWindow: modelInfo1.contextWindow,
|
||||
|
|
@ -386,7 +381,7 @@ describe("Sliding Window", () => {
|
|||
currentProfileId: "default",
|
||||
})
|
||||
|
||||
const result4 = await truncateConversationIfNeeded({
|
||||
const result4 = await manageContext({
|
||||
messages: messagesWithSmallContent,
|
||||
totalTokens: aboveThreshold,
|
||||
contextWindow: modelInfo2.contextWindow,
|
||||
|
|
@ -422,7 +417,7 @@ describe("Sliding Window", () => {
|
|||
// Set base tokens so total is well below threshold + buffer even with small content added
|
||||
const dynamicBuffer = modelInfo.contextWindow * TOKEN_BUFFER_PERCENTAGE
|
||||
const baseTokensForSmall = availableTokens - smallContentTokens - dynamicBuffer - 10
|
||||
const resultWithSmall = await truncateConversationIfNeeded({
|
||||
const resultWithSmall = await manageContext({
|
||||
messages: messagesWithSmallContent,
|
||||
totalTokens: baseTokensForSmall,
|
||||
contextWindow: modelInfo.contextWindow,
|
||||
|
|
@ -457,7 +452,7 @@ describe("Sliding Window", () => {
|
|||
|
||||
// Set base tokens so we're just below threshold without content, but over with content
|
||||
const baseTokensForLarge = availableTokens - Math.floor(largeContentTokens / 2)
|
||||
const resultWithLarge = await truncateConversationIfNeeded({
|
||||
const resultWithLarge = await manageContext({
|
||||
messages: messagesWithLargeContent,
|
||||
totalTokens: baseTokensForLarge,
|
||||
contextWindow: modelInfo.contextWindow,
|
||||
|
|
@ -485,7 +480,7 @@ describe("Sliding Window", () => {
|
|||
|
||||
// Set base tokens so we're just below threshold without content
|
||||
const baseTokensForVeryLarge = availableTokens - Math.floor(veryLargeContentTokens / 2)
|
||||
const resultWithVeryLarge = await truncateConversationIfNeeded({
|
||||
const resultWithVeryLarge = await manageContext({
|
||||
messages: messagesWithVeryLargeContent,
|
||||
totalTokens: baseTokensForVeryLarge,
|
||||
contextWindow: modelInfo.contextWindow,
|
||||
|
|
@ -523,7 +518,7 @@ describe("Sliding Window", () => {
|
|||
messagesWithSmallContent[4],
|
||||
]
|
||||
|
||||
const result = await truncateConversationIfNeeded({
|
||||
const result = await manageContext({
|
||||
messages: messagesWithSmallContent,
|
||||
totalTokens,
|
||||
contextWindow: modelInfo.contextWindow,
|
||||
|
|
@ -570,7 +565,7 @@ describe("Sliding Window", () => {
|
|||
{ ...messages[messages.length - 1], content: "" },
|
||||
]
|
||||
|
||||
const result = await truncateConversationIfNeeded({
|
||||
const result = await manageContext({
|
||||
messages: messagesWithSmallContent,
|
||||
totalTokens,
|
||||
contextWindow: modelInfo.contextWindow,
|
||||
|
|
@ -637,7 +632,7 @@ describe("Sliding Window", () => {
|
|||
messagesWithSmallContent[4],
|
||||
]
|
||||
|
||||
const result = await truncateConversationIfNeeded({
|
||||
const result = await manageContext({
|
||||
messages: messagesWithSmallContent,
|
||||
totalTokens,
|
||||
contextWindow: modelInfo.contextWindow,
|
||||
|
|
@ -684,7 +679,7 @@ describe("Sliding Window", () => {
|
|||
messagesWithSmallContent[4],
|
||||
]
|
||||
|
||||
const result = await truncateConversationIfNeeded({
|
||||
const result = await manageContext({
|
||||
messages: messagesWithSmallContent,
|
||||
totalTokens,
|
||||
contextWindow: modelInfo.contextWindow,
|
||||
|
|
@ -741,7 +736,7 @@ describe("Sliding Window", () => {
|
|||
{ ...messages[messages.length - 1], content: "" },
|
||||
]
|
||||
|
||||
const result = await truncateConversationIfNeeded({
|
||||
const result = await manageContext({
|
||||
messages: messagesWithSmallContent,
|
||||
totalTokens,
|
||||
contextWindow,
|
||||
|
|
@ -793,7 +788,7 @@ describe("Sliding Window", () => {
|
|||
{ ...messages[messages.length - 1], content: "" },
|
||||
]
|
||||
|
||||
const result = await truncateConversationIfNeeded({
|
||||
const result = await manageContext({
|
||||
messages: messagesWithSmallContent,
|
||||
totalTokens,
|
||||
contextWindow,
|
||||
|
|
@ -880,7 +875,7 @@ describe("Sliding Window", () => {
|
|||
.spyOn(condenseModule, "summarizeConversation")
|
||||
.mockResolvedValue(mockSummarizeResponse)
|
||||
|
||||
const result = await truncateConversationIfNeeded({
|
||||
const result = await manageContext({
|
||||
messages: messagesWithSmallContent,
|
||||
totalTokens,
|
||||
contextWindow,
|
||||
|
|
@ -946,7 +941,7 @@ describe("Sliding Window", () => {
|
|||
.spyOn(condenseModule, "summarizeConversation")
|
||||
.mockResolvedValue(mockSummarizeResponse)
|
||||
|
||||
const result = await truncateConversationIfNeeded({
|
||||
const result = await manageContext({
|
||||
messages: messagesWithSmallContent,
|
||||
totalTokens,
|
||||
contextWindow,
|
||||
|
|
@ -1000,7 +995,7 @@ describe("Sliding Window", () => {
|
|||
vi.clearAllMocks()
|
||||
const summarizeSpy = vi.spyOn(condenseModule, "summarizeConversation")
|
||||
|
||||
const result = await truncateConversationIfNeeded({
|
||||
const result = await manageContext({
|
||||
messages: messagesWithSmallContent,
|
||||
totalTokens,
|
||||
contextWindow,
|
||||
|
|
@ -1030,10 +1025,10 @@ describe("Sliding Window", () => {
|
|||
})
|
||||
|
||||
/**
|
||||
* Tests for the getMaxTokens function (private but tested through truncateConversationIfNeeded)
|
||||
* Tests for the getMaxTokens function (private but tested through manageContext)
|
||||
*/
|
||||
describe("getMaxTokens", () => {
|
||||
// We'll test this indirectly through truncateConversationIfNeeded
|
||||
// We'll test this indirectly through manageContext
|
||||
const createModelInfo = (contextWindow: number, maxTokens?: number): ModelInfo => ({
|
||||
contextWindow,
|
||||
supportsPromptCache: true, // Not relevant for getMaxTokens
|
||||
|
|
@ -1061,7 +1056,7 @@ describe("Sliding Window", () => {
|
|||
|
||||
// Account for the dynamic buffer which is 10% of context window (10,000 tokens)
|
||||
// Below max tokens and buffer - no truncation
|
||||
const result1 = await truncateConversationIfNeeded({
|
||||
const result1 = await manageContext({
|
||||
messages: messagesWithSmallContent,
|
||||
totalTokens: 39999, // Well below threshold + dynamic buffer
|
||||
contextWindow: modelInfo.contextWindow,
|
||||
|
|
@ -1082,7 +1077,7 @@ describe("Sliding Window", () => {
|
|||
})
|
||||
|
||||
// Above max tokens - truncate
|
||||
const result2 = await truncateConversationIfNeeded({
|
||||
const result2 = await manageContext({
|
||||
messages: messagesWithSmallContent,
|
||||
totalTokens: 50001, // Above threshold
|
||||
contextWindow: modelInfo.contextWindow,
|
||||
|
|
@ -1114,7 +1109,7 @@ describe("Sliding Window", () => {
|
|||
|
||||
// Account for the dynamic buffer which is 10% of context window (10,000 tokens)
|
||||
// Below max tokens and buffer - no truncation
|
||||
const result1 = await truncateConversationIfNeeded({
|
||||
const result1 = await manageContext({
|
||||
messages: messagesWithSmallContent,
|
||||
totalTokens: 81807, // Well below threshold + dynamic buffer (91808 - 10000 = 81808)
|
||||
contextWindow: modelInfo.contextWindow,
|
||||
|
|
@ -1135,7 +1130,7 @@ describe("Sliding Window", () => {
|
|||
})
|
||||
|
||||
// Above max tokens - truncate
|
||||
const result2 = await truncateConversationIfNeeded({
|
||||
const result2 = await manageContext({
|
||||
messages: messagesWithSmallContent,
|
||||
totalTokens: 81809, // Above threshold (81808)
|
||||
contextWindow: modelInfo.contextWindow,
|
||||
|
|
@ -1166,7 +1161,7 @@ describe("Sliding Window", () => {
|
|||
]
|
||||
|
||||
// Below max tokens and buffer - no truncation
|
||||
const result1 = await truncateConversationIfNeeded({
|
||||
const result1 = await manageContext({
|
||||
messages: messagesWithSmallContent,
|
||||
totalTokens: 34999, // Well below threshold + buffer
|
||||
contextWindow: modelInfo.contextWindow,
|
||||
|
|
@ -1182,7 +1177,7 @@ describe("Sliding Window", () => {
|
|||
expect(result1.messages).toEqual(messagesWithSmallContent)
|
||||
|
||||
// Above max tokens - truncate
|
||||
const result2 = await truncateConversationIfNeeded({
|
||||
const result2 = await manageContext({
|
||||
messages: messagesWithSmallContent,
|
||||
totalTokens: 40001, // Above threshold
|
||||
contextWindow: modelInfo.contextWindow,
|
||||
|
|
@ -1211,7 +1206,7 @@ describe("Sliding Window", () => {
|
|||
|
||||
// Account for the dynamic buffer which is 10% of context window (20,000 tokens for this test)
|
||||
// Below max tokens and buffer - no truncation
|
||||
const result1 = await truncateConversationIfNeeded({
|
||||
const result1 = await manageContext({
|
||||
messages: messagesWithSmallContent,
|
||||
totalTokens: 149999, // Well below threshold + dynamic buffer
|
||||
contextWindow: modelInfo.contextWindow,
|
||||
|
|
@ -1227,7 +1222,7 @@ describe("Sliding Window", () => {
|
|||
expect(result1.messages).toEqual(messagesWithSmallContent)
|
||||
|
||||
// Above max tokens - truncate
|
||||
const result2 = await truncateConversationIfNeeded({
|
||||
const result2 = await manageContext({
|
||||
messages: messagesWithSmallContent,
|
||||
totalTokens: 170001, // Above threshold
|
||||
contextWindow: modelInfo.contextWindow,
|
||||
|
|
@ -8,7 +8,18 @@ import { ApiMessage } from "../task-persistence/apiMessages"
|
|||
import { ANTHROPIC_DEFAULT_MAX_TOKENS } from "@roo-code/types"
|
||||
|
||||
/**
|
||||
* Default percentage of the context window to use as a buffer when deciding when to truncate
|
||||
* Context Management
|
||||
*
|
||||
* This module provides Context Management for conversations, combining:
|
||||
* - Intelligent condensation of prior messages when approaching configured thresholds
|
||||
* - Sliding window truncation as a fallback when necessary
|
||||
*
|
||||
* Behavior and exports are preserved exactly from the previous sliding-window implementation.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Default percentage of the context window to use as a buffer when deciding when to truncate.
|
||||
* Used by Context Management to determine when to trigger condensation or (fallback) sliding window truncation.
|
||||
*/
|
||||
export const TOKEN_BUFFER_PERCENTAGE = 0.1
|
||||
|
||||
|
|
@ -33,6 +44,8 @@ export async function estimateTokenCount(
|
|||
* The first message is always retained, and a specified fraction (rounded to an even number)
|
||||
* of messages from the beginning (excluding the first) is removed.
|
||||
*
|
||||
* This implements the sliding window truncation behavior.
|
||||
*
|
||||
* @param {ApiMessage[]} messages - The conversation messages.
|
||||
* @param {number} fracToRemove - The fraction (between 0 and 1) of messages (excluding the first) to remove.
|
||||
* @param {string} taskId - The task ID for the conversation, used for telemetry
|
||||
|
|
@ -50,20 +63,16 @@ export function truncateConversation(messages: ApiMessage[], fracToRemove: numbe
|
|||
}
|
||||
|
||||
/**
|
||||
* Conditionally truncates the conversation messages if the total token count
|
||||
* exceeds the model's limit, considering the size of incoming content.
|
||||
* Context Management: Conditionally manages the conversation context when approaching limits.
|
||||
*
|
||||
* @param {ApiMessage[]} messages - The conversation messages.
|
||||
* @param {number} totalTokens - The total number of tokens in the conversation (excluding the last user message).
|
||||
* @param {number} contextWindow - The context window size.
|
||||
* @param {number} maxTokens - The maximum number of tokens allowed.
|
||||
* @param {ApiHandler} apiHandler - The API handler to use for token counting.
|
||||
* @param {boolean} autoCondenseContext - Whether to use LLM summarization or sliding window implementation
|
||||
* @param {string} systemPrompt - The system prompt, used for estimating the new context size after summarizing.
|
||||
* @returns {ApiMessage[]} The original or truncated conversation messages.
|
||||
* Attempts intelligent condensation of prior messages when thresholds are reached.
|
||||
* Falls back to sliding window truncation if condensation is unavailable or fails.
|
||||
*
|
||||
* @param {ContextManagementOptions} options - The options for truncation/condensation
|
||||
* @returns {Promise<ApiMessage[]>} The original, condensed, or truncated conversation messages.
|
||||
*/
|
||||
|
||||
type TruncateOptions = {
|
||||
export type ContextManagementOptions = {
|
||||
messages: ApiMessage[]
|
||||
totalTokens: number
|
||||
contextWindow: number
|
||||
|
|
@ -79,16 +88,15 @@ type TruncateOptions = {
|
|||
currentProfileId: string
|
||||
}
|
||||
|
||||
type TruncateResponse = SummarizeResponse & { prevContextTokens: number }
|
||||
export type ContextManagementResult = SummarizeResponse & { prevContextTokens: number }
|
||||
|
||||
/**
|
||||
* Conditionally truncates the conversation messages if the total token count
|
||||
* exceeds the model's limit, considering the size of incoming content.
|
||||
* Conditionally manages conversation context (condense and fallback truncation).
|
||||
*
|
||||
* @param {TruncateOptions} options - The options for truncation
|
||||
* @returns {Promise<ApiMessage[]>} The original or truncated conversation messages.
|
||||
* @param {ContextManagementOptions} options - The options for truncation/condensation
|
||||
* @returns {Promise<ApiMessage[]>} The original, condensed, or truncated conversation messages.
|
||||
*/
|
||||
export async function truncateConversationIfNeeded({
|
||||
export async function manageContext({
|
||||
messages,
|
||||
totalTokens,
|
||||
contextWindow,
|
||||
|
|
@ -102,7 +110,7 @@ export async function truncateConversationIfNeeded({
|
|||
condensingApiHandler,
|
||||
profileThresholds,
|
||||
currentProfileId,
|
||||
}: TruncateOptions): Promise<TruncateResponse> {
|
||||
}: ContextManagementOptions): Promise<ContextManagementResult> {
|
||||
let error: string | undefined
|
||||
let cost = 0
|
||||
// Calculate the maximum tokens reserved for response
|
||||
71
src/core/diff/stats.ts
Normal file
71
src/core/diff/stats.ts
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import { parsePatch, createTwoFilesPatch } from "diff"
|
||||
|
||||
/**
|
||||
* Diff utilities for backend (extension) use.
|
||||
* Source of truth for diff normalization and stats.
|
||||
*/
|
||||
|
||||
export interface DiffStats {
|
||||
added: number
|
||||
removed: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove non-semantic diff noise like "No newline at end of file"
|
||||
*/
|
||||
export function sanitizeUnifiedDiff(diff: string): string {
|
||||
if (!diff) return diff
|
||||
return diff.replace(/\r\n/g, "\n").replace(/(^|\n)[ \t]*(?:\\ )?No newline at end of file[ \t]*(?=\n|$)/gi, "$1")
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute +/− counts from a unified diff (ignores headers/hunk lines)
|
||||
*/
|
||||
export function computeUnifiedDiffStats(diff?: string): DiffStats | null {
|
||||
if (!diff) return null
|
||||
|
||||
try {
|
||||
const patches = parsePatch(diff)
|
||||
if (!patches || patches.length === 0) return null
|
||||
|
||||
let added = 0
|
||||
let removed = 0
|
||||
|
||||
for (const p of patches) {
|
||||
for (const h of (p as any).hunks ?? []) {
|
||||
for (const l of h.lines ?? []) {
|
||||
const ch = (l as string)[0]
|
||||
if (ch === "+") added++
|
||||
else if (ch === "-") removed++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (added > 0 || removed > 0) return { added, removed }
|
||||
return { added: 0, removed: 0 }
|
||||
} catch {
|
||||
// If parsing fails for any reason, signal no stats
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute diff stats from any supported diff format (unified or search-replace)
|
||||
* Tries unified diff format first, then falls back to search-replace format
|
||||
*/
|
||||
export function computeDiffStats(diff?: string): DiffStats | null {
|
||||
if (!diff) return null
|
||||
return computeUnifiedDiffStats(diff)
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a unified diff for a brand new file (all content lines are additions).
|
||||
* Trailing newline is ignored for line counting and emission.
|
||||
*/
|
||||
export function convertNewFileToUnifiedDiff(content: string, filePath?: string): string {
|
||||
const newFileName = filePath || "file"
|
||||
// Normalize EOLs; rely on library for unified patch formatting
|
||||
const normalized = (content || "").replace(/\r\n/g, "\n")
|
||||
// Old file is empty (/dev/null), new file has content; zero context to show all lines as additions
|
||||
return createTwoFilesPatch("/dev/null", newFileName, "", normalized, undefined, undefined, { context: 0 })
|
||||
}
|
||||
|
|
@ -409,7 +409,7 @@ CAPABILITIES
|
|||
- When the user initially gives you a task, a recursive list of all filepaths in the current workspace directory ('/test/path') will be included in environment_details. This provides an overview of the project's file structure, offering key insights into the project from directory/file names (how developers conceptualize and organize their code) and file extensions (the language used). This can also guide decision-making on which files to explore further. If you need to further explore directories such as outside the current workspace directory, you can use the list_files tool. If you pass 'true' for the recursive parameter, it will list files recursively. Otherwise, it will list files at the top level, which is better suited for generic directories where you don't necessarily need the nested structure, like the Desktop.
|
||||
- You can use search_files to perform regex searches across files in a specified directory, outputting context-rich results that include surrounding lines. This is particularly useful for understanding code patterns, finding specific implementations, or identifying areas that need refactoring.
|
||||
- You can use the list_code_definition_names tool to get an overview of source code definitions for all files at the top level of a specified directory. This can be particularly useful when you need to understand the broader context and relationships between certain parts of the code. You may need to call this tool multiple times to understand various parts of the codebase related to the task.
|
||||
- For example, when asked to make edits or improvements you might analyze the file structure in the initial environment_details to get an overview of the project, then use list_code_definition_names to get further insight using source code definitions for files located in relevant directories, then read_file to examine the contents of relevant files, analyze the code and suggest improvements or make necessary edits, then use the write_to_file tool to apply the changes. If you refactored code that could affect other parts of the codebase, you could use search_files to ensure you update other files as needed.
|
||||
- For example, when asked to make edits or improvements you might analyze the file structure in the initial environment_details to get an overview of the project, then use list_code_definition_names to get further insight using source code definitions for files located in relevant directories, then read_file to examine the contents of relevant files, analyze the code and suggest improvements or make necessary edits, then use the write_to_file or insert_content tool to apply the changes. If you refactored code that could affect other parts of the codebase, you could use search_files to ensure you update other files as needed.
|
||||
- You can use the execute_command tool to run commands on the user's computer whenever you feel it can help accomplish the user's task. When you need to execute a CLI command, you must provide a clear explanation of what the command does. Prefer to execute complex CLI commands over creating executable scripts, since they are more flexible and easier to run. Interactive and long-running commands are allowed, since the commands are run in the user's VSCode terminal. The user may keep commands running in the background and you will be kept updated on their status along the way. Each command you execute is run in a new terminal instance.
|
||||
|
||||
====
|
||||
|
|
@ -429,6 +429,7 @@ RULES
|
|||
- Before using the execute_command tool, you must first think about the SYSTEM INFORMATION context provided to understand the user's environment and tailor your commands to ensure they are compatible with their system. You must also consider if the command you need to run should be executed in a specific directory outside of the current working directory '/test/path', and if so prepend with `cd`'ing into that directory && then executing the command (as one command since you are stuck operating from '/test/path'). For example, if you needed to run `npm install` in a project outside of '/test/path', you would need to prepend with a `cd` i.e. pseudocode for this would be `cd (path to project) && (command, in this case npm install)`.
|
||||
- When using the search_files tool, craft your regex patterns carefully to balance specificity and flexibility. Based on the user's task you may use it to find code patterns, TODO comments, function definitions, or any text-based information across the project. The results include context, so analyze the surrounding code to better understand the matches. Leverage the search_files tool in combination with other tools for more comprehensive analysis. For example, use it to find specific code patterns, then use read_file to examine the full context of interesting matches before using write_to_file to make informed changes.
|
||||
- When creating a new project (such as an app, website, or any software project), organize all new files within a dedicated project directory unless the user specifies otherwise. Use appropriate file paths when writing files, as the write_to_file tool will automatically create any necessary directories. Structure the project logically, adhering to best practices for the specific type of project being created. Unless otherwise specified, new projects should be easily run without additional setup, for example most projects can be built in HTML, CSS, and JavaScript - which you can open in a browser.
|
||||
|
||||
- For editing files, you have access to these tools: write_to_file (for creating new files or complete file rewrites), insert_content (for adding lines to files).
|
||||
- The insert_content tool adds lines of text to files at a specific line number, such as adding a new function to a JavaScript file or inserting a new route in a Python file. Use line number 0 to append at the end of the file, or any positive number to insert before that line.
|
||||
- You should always prefer using other editing tools over write_to_file when making changes to existing files since write_to_file is much slower and cannot handle large files.
|
||||
|
|
|
|||
|
|
@ -342,7 +342,6 @@ CAPABILITIES
|
|||
- When the user initially gives you a task, a recursive list of all filepaths in the current workspace directory ('/test/path') will be included in environment_details. This provides an overview of the project's file structure, offering key insights into the project from directory/file names (how developers conceptualize and organize their code) and file extensions (the language used). This can also guide decision-making on which files to explore further. If you need to further explore directories such as outside the current workspace directory, you can use the list_files tool. If you pass 'true' for the recursive parameter, it will list files recursively. Otherwise, it will list files at the top level, which is better suited for generic directories where you don't necessarily need the nested structure, like the Desktop.
|
||||
- You can use search_files to perform regex searches across files in a specified directory, outputting context-rich results that include surrounding lines. This is particularly useful for understanding code patterns, finding specific implementations, or identifying areas that need refactoring.
|
||||
- You can use the list_code_definition_names tool to get an overview of source code definitions for all files at the top level of a specified directory. This can be particularly useful when you need to understand the broader context and relationships between certain parts of the code. You may need to call this tool multiple times to understand various parts of the codebase related to the task.
|
||||
- For example, when asked to make edits or improvements you might analyze the file structure in the initial environment_details to get an overview of the project, then use list_code_definition_names to get further insight using source code definitions for files located in relevant directories, then read_file to examine the contents of relevant files, analyze the code and suggest improvements or make necessary edits, then use the write_to_file tool to apply the changes. If you refactored code that could affect other parts of the codebase, you could use search_files to ensure you update other files as needed.
|
||||
- You can use the execute_command tool to run commands on the user's computer whenever you feel it can help accomplish the user's task. When you need to execute a CLI command, you must provide a clear explanation of what the command does. Prefer to execute complex CLI commands over creating executable scripts, since they are more flexible and easier to run. Interactive and long-running commands are allowed, since the commands are run in the user's VSCode terminal. The user may keep commands running in the background and you will be kept updated on their status along the way. Each command you execute is run in a new terminal instance.
|
||||
|
||||
====
|
||||
|
|
@ -360,12 +359,9 @@ RULES
|
|||
- You cannot `cd` into a different directory to complete a task. You are stuck operating from '/test/path', so be sure to pass in the correct 'path' parameter when using tools that require a path.
|
||||
- Do not use the ~ character or $HOME to refer to the home directory.
|
||||
- Before using the execute_command tool, you must first think about the SYSTEM INFORMATION context provided to understand the user's environment and tailor your commands to ensure they are compatible with their system. You must also consider if the command you need to run should be executed in a specific directory outside of the current working directory '/test/path', and if so prepend with `cd`'ing into that directory && then executing the command (as one command since you are stuck operating from '/test/path'). For example, if you needed to run `npm install` in a project outside of '/test/path', you would need to prepend with a `cd` i.e. pseudocode for this would be `cd (path to project) && (command, in this case npm install)`.
|
||||
- When using the search_files tool, craft your regex patterns carefully to balance specificity and flexibility. Based on the user's task you may use it to find code patterns, TODO comments, function definitions, or any text-based information across the project. The results include context, so analyze the surrounding code to better understand the matches. Leverage the search_files tool in combination with other tools for more comprehensive analysis. For example, use it to find specific code patterns, then use read_file to examine the full context of interesting matches before using write_to_file to make informed changes.
|
||||
- When creating a new project (such as an app, website, or any software project), organize all new files within a dedicated project directory unless the user specifies otherwise. Use appropriate file paths when writing files, as the write_to_file tool will automatically create any necessary directories. Structure the project logically, adhering to best practices for the specific type of project being created. Unless otherwise specified, new projects should be easily run without additional setup, for example most projects can be built in HTML, CSS, and JavaScript - which you can open in a browser.
|
||||
- For editing files, you have access to these tools: write_to_file (for creating new files or complete file rewrites), insert_content (for adding lines to files).
|
||||
- The insert_content tool adds lines of text to files at a specific line number, such as adding a new function to a JavaScript file or inserting a new route in a Python file. Use line number 0 to append at the end of the file, or any positive number to insert before that line.
|
||||
- You should always prefer using other editing tools over write_to_file when making changes to existing files since write_to_file is much slower and cannot handle large files.
|
||||
- When using the write_to_file tool to modify a file, use the tool directly with the desired content. You do not need to display the content before using the tool. ALWAYS provide the COMPLETE file content in your response. This is NON-NEGOTIABLE. Partial updates or placeholders like '// rest of code unchanged' are STRICTLY FORBIDDEN. You MUST include ALL parts of the file, even if they haven't been modified. Failure to do so will result in incomplete or broken code, severely impacting the user's project.
|
||||
- When using the search_files tool, craft your regex patterns carefully to balance specificity and flexibility. Based on the user's task you may use it to find code patterns, TODO comments, function definitions, or any text-based information across the project. The results include context, so analyze the surrounding code to better understand the matches.
|
||||
|
||||
|
||||
- Some modes have restrictions on which files they can edit. If you attempt to edit a restricted file, the operation will be rejected with a FileRestrictionError that will specify which file patterns are allowed for the current mode.
|
||||
- Be sure to consider the type of project (e.g. Python, JavaScript, web application) when determining the appropriate structure and files to include. Also consider what files may be most relevant to accomplishing the task, for example looking at a project's manifest file would help you understand the project's dependencies, which you could incorporate into any code you write.
|
||||
* For example, in architect mode trying to edit app.js would be rejected because architect mode can only edit files matching "\.md$"
|
||||
|
|
|
|||
|
|
@ -408,7 +408,7 @@ CAPABILITIES
|
|||
- When the user initially gives you a task, a recursive list of all filepaths in the current workspace directory ('/test/path') will be included in environment_details. This provides an overview of the project's file structure, offering key insights into the project from directory/file names (how developers conceptualize and organize their code) and file extensions (the language used). This can also guide decision-making on which files to explore further. If you need to further explore directories such as outside the current workspace directory, you can use the list_files tool. If you pass 'true' for the recursive parameter, it will list files recursively. Otherwise, it will list files at the top level, which is better suited for generic directories where you don't necessarily need the nested structure, like the Desktop.
|
||||
- You can use search_files to perform regex searches across files in a specified directory, outputting context-rich results that include surrounding lines. This is particularly useful for understanding code patterns, finding specific implementations, or identifying areas that need refactoring.
|
||||
- You can use the list_code_definition_names tool to get an overview of source code definitions for all files at the top level of a specified directory. This can be particularly useful when you need to understand the broader context and relationships between certain parts of the code. You may need to call this tool multiple times to understand various parts of the codebase related to the task.
|
||||
- For example, when asked to make edits or improvements you might analyze the file structure in the initial environment_details to get an overview of the project, then use list_code_definition_names to get further insight using source code definitions for files located in relevant directories, then read_file to examine the contents of relevant files, analyze the code and suggest improvements or make necessary edits, then use the write_to_file tool to apply the changes. If you refactored code that could affect other parts of the codebase, you could use search_files to ensure you update other files as needed.
|
||||
- For example, when asked to make edits or improvements you might analyze the file structure in the initial environment_details to get an overview of the project, then use list_code_definition_names to get further insight using source code definitions for files located in relevant directories, then read_file to examine the contents of relevant files, analyze the code and suggest improvements or make necessary edits, then use the write_to_file or insert_content tool to apply the changes. If you refactored code that could affect other parts of the codebase, you could use search_files to ensure you update other files as needed.
|
||||
- You can use the execute_command tool to run commands on the user's computer whenever you feel it can help accomplish the user's task. When you need to execute a CLI command, you must provide a clear explanation of what the command does. Prefer to execute complex CLI commands over creating executable scripts, since they are more flexible and easier to run. Interactive and long-running commands are allowed, since the commands are run in the user's VSCode terminal. The user may keep commands running in the background and you will be kept updated on their status along the way. Each command you execute is run in a new terminal instance.
|
||||
|
||||
====
|
||||
|
|
@ -428,6 +428,7 @@ RULES
|
|||
- Before using the execute_command tool, you must first think about the SYSTEM INFORMATION context provided to understand the user's environment and tailor your commands to ensure they are compatible with their system. You must also consider if the command you need to run should be executed in a specific directory outside of the current working directory '/test/path', and if so prepend with `cd`'ing into that directory && then executing the command (as one command since you are stuck operating from '/test/path'). For example, if you needed to run `npm install` in a project outside of '/test/path', you would need to prepend with a `cd` i.e. pseudocode for this would be `cd (path to project) && (command, in this case npm install)`.
|
||||
- When using the search_files tool, craft your regex patterns carefully to balance specificity and flexibility. Based on the user's task you may use it to find code patterns, TODO comments, function definitions, or any text-based information across the project. The results include context, so analyze the surrounding code to better understand the matches. Leverage the search_files tool in combination with other tools for more comprehensive analysis. For example, use it to find specific code patterns, then use read_file to examine the full context of interesting matches before using write_to_file to make informed changes.
|
||||
- When creating a new project (such as an app, website, or any software project), organize all new files within a dedicated project directory unless the user specifies otherwise. Use appropriate file paths when writing files, as the write_to_file tool will automatically create any necessary directories. Structure the project logically, adhering to best practices for the specific type of project being created. Unless otherwise specified, new projects should be easily run without additional setup, for example most projects can be built in HTML, CSS, and JavaScript - which you can open in a browser.
|
||||
|
||||
- For editing files, you have access to these tools: write_to_file (for creating new files or complete file rewrites), insert_content (for adding lines to files).
|
||||
- The insert_content tool adds lines of text to files at a specific line number, such as adding a new function to a JavaScript file or inserting a new route in a Python file. Use line number 0 to append at the end of the file, or any positive number to insert before that line.
|
||||
- You should always prefer using other editing tools over write_to_file when making changes to existing files since write_to_file is much slower and cannot handle large files.
|
||||
|
|
|
|||
|
|
@ -475,7 +475,7 @@ CAPABILITIES
|
|||
- When the user initially gives you a task, a recursive list of all filepaths in the current workspace directory ('/test/path') will be included in environment_details. This provides an overview of the project's file structure, offering key insights into the project from directory/file names (how developers conceptualize and organize their code) and file extensions (the language used). This can also guide decision-making on which files to explore further. If you need to further explore directories such as outside the current workspace directory, you can use the list_files tool. If you pass 'true' for the recursive parameter, it will list files recursively. Otherwise, it will list files at the top level, which is better suited for generic directories where you don't necessarily need the nested structure, like the Desktop.
|
||||
- You can use search_files to perform regex searches across files in a specified directory, outputting context-rich results that include surrounding lines. This is particularly useful for understanding code patterns, finding specific implementations, or identifying areas that need refactoring.
|
||||
- You can use the list_code_definition_names tool to get an overview of source code definitions for all files at the top level of a specified directory. This can be particularly useful when you need to understand the broader context and relationships between certain parts of the code. You may need to call this tool multiple times to understand various parts of the codebase related to the task.
|
||||
- For example, when asked to make edits or improvements you might analyze the file structure in the initial environment_details to get an overview of the project, then use list_code_definition_names to get further insight using source code definitions for files located in relevant directories, then read_file to examine the contents of relevant files, analyze the code and suggest improvements or make necessary edits, then use the write_to_file tool to apply the changes. If you refactored code that could affect other parts of the codebase, you could use search_files to ensure you update other files as needed.
|
||||
- For example, when asked to make edits or improvements you might analyze the file structure in the initial environment_details to get an overview of the project, then use list_code_definition_names to get further insight using source code definitions for files located in relevant directories, then read_file to examine the contents of relevant files, analyze the code and suggest improvements or make necessary edits, then use the write_to_file or insert_content tool to apply the changes. If you refactored code that could affect other parts of the codebase, you could use search_files to ensure you update other files as needed.
|
||||
- You can use the execute_command tool to run commands on the user's computer whenever you feel it can help accomplish the user's task. When you need to execute a CLI command, you must provide a clear explanation of what the command does. Prefer to execute complex CLI commands over creating executable scripts, since they are more flexible and easier to run. Interactive and long-running commands are allowed, since the commands are run in the user's VSCode terminal. The user may keep commands running in the background and you will be kept updated on their status along the way. Each command you execute is run in a new terminal instance.
|
||||
- You have access to MCP servers that may provide additional tools and resources. Each server may provide different capabilities that you can use to accomplish tasks more effectively.
|
||||
|
||||
|
|
@ -497,6 +497,7 @@ RULES
|
|||
- Before using the execute_command tool, you must first think about the SYSTEM INFORMATION context provided to understand the user's environment and tailor your commands to ensure they are compatible with their system. You must also consider if the command you need to run should be executed in a specific directory outside of the current working directory '/test/path', and if so prepend with `cd`'ing into that directory && then executing the command (as one command since you are stuck operating from '/test/path'). For example, if you needed to run `npm install` in a project outside of '/test/path', you would need to prepend with a `cd` i.e. pseudocode for this would be `cd (path to project) && (command, in this case npm install)`.
|
||||
- When using the search_files tool, craft your regex patterns carefully to balance specificity and flexibility. Based on the user's task you may use it to find code patterns, TODO comments, function definitions, or any text-based information across the project. The results include context, so analyze the surrounding code to better understand the matches. Leverage the search_files tool in combination with other tools for more comprehensive analysis. For example, use it to find specific code patterns, then use read_file to examine the full context of interesting matches before using write_to_file to make informed changes.
|
||||
- When creating a new project (such as an app, website, or any software project), organize all new files within a dedicated project directory unless the user specifies otherwise. Use appropriate file paths when writing files, as the write_to_file tool will automatically create any necessary directories. Structure the project logically, adhering to best practices for the specific type of project being created. Unless otherwise specified, new projects should be easily run without additional setup, for example most projects can be built in HTML, CSS, and JavaScript - which you can open in a browser.
|
||||
|
||||
- For editing files, you have access to these tools: write_to_file (for creating new files or complete file rewrites), insert_content (for adding lines to files).
|
||||
- The insert_content tool adds lines of text to files at a specific line number, such as adding a new function to a JavaScript file or inserting a new route in a Python file. Use line number 0 to append at the end of the file, or any positive number to insert before that line.
|
||||
- You should always prefer using other editing tools over write_to_file when making changes to existing files since write_to_file is much slower and cannot handle large files.
|
||||
|
|
|
|||
|
|
@ -414,7 +414,7 @@ CAPABILITIES
|
|||
- When the user initially gives you a task, a recursive list of all filepaths in the current workspace directory ('/test/path') will be included in environment_details. This provides an overview of the project's file structure, offering key insights into the project from directory/file names (how developers conceptualize and organize their code) and file extensions (the language used). This can also guide decision-making on which files to explore further. If you need to further explore directories such as outside the current workspace directory, you can use the list_files tool. If you pass 'true' for the recursive parameter, it will list files recursively. Otherwise, it will list files at the top level, which is better suited for generic directories where you don't necessarily need the nested structure, like the Desktop.
|
||||
- You can use search_files to perform regex searches across files in a specified directory, outputting context-rich results that include surrounding lines. This is particularly useful for understanding code patterns, finding specific implementations, or identifying areas that need refactoring.
|
||||
- You can use the list_code_definition_names tool to get an overview of source code definitions for all files at the top level of a specified directory. This can be particularly useful when you need to understand the broader context and relationships between certain parts of the code. You may need to call this tool multiple times to understand various parts of the codebase related to the task.
|
||||
- For example, when asked to make edits or improvements you might analyze the file structure in the initial environment_details to get an overview of the project, then use list_code_definition_names to get further insight using source code definitions for files located in relevant directories, then read_file to examine the contents of relevant files, analyze the code and suggest improvements or make necessary edits, then use the write_to_file tool to apply the changes. If you refactored code that could affect other parts of the codebase, you could use search_files to ensure you update other files as needed.
|
||||
- For example, when asked to make edits or improvements you might analyze the file structure in the initial environment_details to get an overview of the project, then use list_code_definition_names to get further insight using source code definitions for files located in relevant directories, then read_file to examine the contents of relevant files, analyze the code and suggest improvements or make necessary edits, then use the write_to_file or insert_content tool to apply the changes. If you refactored code that could affect other parts of the codebase, you could use search_files to ensure you update other files as needed.
|
||||
- You can use the execute_command tool to run commands on the user's computer whenever you feel it can help accomplish the user's task. When you need to execute a CLI command, you must provide a clear explanation of what the command does. Prefer to execute complex CLI commands over creating executable scripts, since they are more flexible and easier to run. Interactive and long-running commands are allowed, since the commands are run in the user's VSCode terminal. The user may keep commands running in the background and you will be kept updated on their status along the way. Each command you execute is run in a new terminal instance.
|
||||
|
||||
====
|
||||
|
|
@ -434,6 +434,7 @@ RULES
|
|||
- Before using the execute_command tool, you must first think about the SYSTEM INFORMATION context provided to understand the user's environment and tailor your commands to ensure they are compatible with their system. You must also consider if the command you need to run should be executed in a specific directory outside of the current working directory '/test/path', and if so prepend with `cd`'ing into that directory && then executing the command (as one command since you are stuck operating from '/test/path'). For example, if you needed to run `npm install` in a project outside of '/test/path', you would need to prepend with a `cd` i.e. pseudocode for this would be `cd (path to project) && (command, in this case npm install)`.
|
||||
- When using the search_files tool, craft your regex patterns carefully to balance specificity and flexibility. Based on the user's task you may use it to find code patterns, TODO comments, function definitions, or any text-based information across the project. The results include context, so analyze the surrounding code to better understand the matches. Leverage the search_files tool in combination with other tools for more comprehensive analysis. For example, use it to find specific code patterns, then use read_file to examine the full context of interesting matches before using write_to_file to make informed changes.
|
||||
- When creating a new project (such as an app, website, or any software project), organize all new files within a dedicated project directory unless the user specifies otherwise. Use appropriate file paths when writing files, as the write_to_file tool will automatically create any necessary directories. Structure the project logically, adhering to best practices for the specific type of project being created. Unless otherwise specified, new projects should be easily run without additional setup, for example most projects can be built in HTML, CSS, and JavaScript - which you can open in a browser.
|
||||
|
||||
- For editing files, you have access to these tools: write_to_file (for creating new files or complete file rewrites), insert_content (for adding lines to files).
|
||||
- The insert_content tool adds lines of text to files at a specific line number, such as adding a new function to a JavaScript file or inserting a new route in a Python file. Use line number 0 to append at the end of the file, or any positive number to insert before that line.
|
||||
- You should always prefer using other editing tools over write_to_file when making changes to existing files since write_to_file is much slower and cannot handle large files.
|
||||
|
|
|
|||
|
|
@ -409,7 +409,7 @@ CAPABILITIES
|
|||
- When the user initially gives you a task, a recursive list of all filepaths in the current workspace directory ('/test/path') will be included in environment_details. This provides an overview of the project's file structure, offering key insights into the project from directory/file names (how developers conceptualize and organize their code) and file extensions (the language used). This can also guide decision-making on which files to explore further. If you need to further explore directories such as outside the current workspace directory, you can use the list_files tool. If you pass 'true' for the recursive parameter, it will list files recursively. Otherwise, it will list files at the top level, which is better suited for generic directories where you don't necessarily need the nested structure, like the Desktop.
|
||||
- You can use search_files to perform regex searches across files in a specified directory, outputting context-rich results that include surrounding lines. This is particularly useful for understanding code patterns, finding specific implementations, or identifying areas that need refactoring.
|
||||
- You can use the list_code_definition_names tool to get an overview of source code definitions for all files at the top level of a specified directory. This can be particularly useful when you need to understand the broader context and relationships between certain parts of the code. You may need to call this tool multiple times to understand various parts of the codebase related to the task.
|
||||
- For example, when asked to make edits or improvements you might analyze the file structure in the initial environment_details to get an overview of the project, then use list_code_definition_names to get further insight using source code definitions for files located in relevant directories, then read_file to examine the contents of relevant files, analyze the code and suggest improvements or make necessary edits, then use the write_to_file tool to apply the changes. If you refactored code that could affect other parts of the codebase, you could use search_files to ensure you update other files as needed.
|
||||
- For example, when asked to make edits or improvements you might analyze the file structure in the initial environment_details to get an overview of the project, then use list_code_definition_names to get further insight using source code definitions for files located in relevant directories, then read_file to examine the contents of relevant files, analyze the code and suggest improvements or make necessary edits, then use the write_to_file or insert_content tool to apply the changes. If you refactored code that could affect other parts of the codebase, you could use search_files to ensure you update other files as needed.
|
||||
- You can use the execute_command tool to run commands on the user's computer whenever you feel it can help accomplish the user's task. When you need to execute a CLI command, you must provide a clear explanation of what the command does. Prefer to execute complex CLI commands over creating executable scripts, since they are more flexible and easier to run. Interactive and long-running commands are allowed, since the commands are run in the user's VSCode terminal. The user may keep commands running in the background and you will be kept updated on their status along the way. Each command you execute is run in a new terminal instance.
|
||||
|
||||
====
|
||||
|
|
@ -429,6 +429,7 @@ RULES
|
|||
- Before using the execute_command tool, you must first think about the SYSTEM INFORMATION context provided to understand the user's environment and tailor your commands to ensure they are compatible with their system. You must also consider if the command you need to run should be executed in a specific directory outside of the current working directory '/test/path', and if so prepend with `cd`'ing into that directory && then executing the command (as one command since you are stuck operating from '/test/path'). For example, if you needed to run `npm install` in a project outside of '/test/path', you would need to prepend with a `cd` i.e. pseudocode for this would be `cd (path to project) && (command, in this case npm install)`.
|
||||
- When using the search_files tool, craft your regex patterns carefully to balance specificity and flexibility. Based on the user's task you may use it to find code patterns, TODO comments, function definitions, or any text-based information across the project. The results include context, so analyze the surrounding code to better understand the matches. Leverage the search_files tool in combination with other tools for more comprehensive analysis. For example, use it to find specific code patterns, then use read_file to examine the full context of interesting matches before using write_to_file to make informed changes.
|
||||
- When creating a new project (such as an app, website, or any software project), organize all new files within a dedicated project directory unless the user specifies otherwise. Use appropriate file paths when writing files, as the write_to_file tool will automatically create any necessary directories. Structure the project logically, adhering to best practices for the specific type of project being created. Unless otherwise specified, new projects should be easily run without additional setup, for example most projects can be built in HTML, CSS, and JavaScript - which you can open in a browser.
|
||||
|
||||
- For editing files, you have access to these tools: write_to_file (for creating new files or complete file rewrites), insert_content (for adding lines to files).
|
||||
- The insert_content tool adds lines of text to files at a specific line number, such as adding a new function to a JavaScript file or inserting a new route in a Python file. Use line number 0 to append at the end of the file, or any positive number to insert before that line.
|
||||
- You should always prefer using other editing tools over write_to_file when making changes to existing files since write_to_file is much slower and cannot handle large files.
|
||||
|
|
|
|||
|
|
@ -462,7 +462,7 @@ CAPABILITIES
|
|||
- When the user initially gives you a task, a recursive list of all filepaths in the current workspace directory ('/test/path') will be included in environment_details. This provides an overview of the project's file structure, offering key insights into the project from directory/file names (how developers conceptualize and organize their code) and file extensions (the language used). This can also guide decision-making on which files to explore further. If you need to further explore directories such as outside the current workspace directory, you can use the list_files tool. If you pass 'true' for the recursive parameter, it will list files recursively. Otherwise, it will list files at the top level, which is better suited for generic directories where you don't necessarily need the nested structure, like the Desktop.
|
||||
- You can use search_files to perform regex searches across files in a specified directory, outputting context-rich results that include surrounding lines. This is particularly useful for understanding code patterns, finding specific implementations, or identifying areas that need refactoring.
|
||||
- You can use the list_code_definition_names tool to get an overview of source code definitions for all files at the top level of a specified directory. This can be particularly useful when you need to understand the broader context and relationships between certain parts of the code. You may need to call this tool multiple times to understand various parts of the codebase related to the task.
|
||||
- For example, when asked to make edits or improvements you might analyze the file structure in the initial environment_details to get an overview of the project, then use list_code_definition_names to get further insight using source code definitions for files located in relevant directories, then read_file to examine the contents of relevant files, analyze the code and suggest improvements or make necessary edits, then use the write_to_file tool to apply the changes. If you refactored code that could affect other parts of the codebase, you could use search_files to ensure you update other files as needed.
|
||||
- For example, when asked to make edits or improvements you might analyze the file structure in the initial environment_details to get an overview of the project, then use list_code_definition_names to get further insight using source code definitions for files located in relevant directories, then read_file to examine the contents of relevant files, analyze the code and suggest improvements or make necessary edits, then use the write_to_file or insert_content tool to apply the changes. If you refactored code that could affect other parts of the codebase, you could use search_files to ensure you update other files as needed.
|
||||
- You can use the execute_command tool to run commands on the user's computer whenever you feel it can help accomplish the user's task. When you need to execute a CLI command, you must provide a clear explanation of what the command does. Prefer to execute complex CLI commands over creating executable scripts, since they are more flexible and easier to run. Interactive and long-running commands are allowed, since the commands are run in the user's VSCode terminal. The user may keep commands running in the background and you will be kept updated on their status along the way. Each command you execute is run in a new terminal instance.
|
||||
- You can use the browser_action tool to interact with websites (including html files and locally running development servers) through a Puppeteer-controlled browser when you feel it is necessary in accomplishing the user's task. This tool is particularly useful for web development tasks as it allows you to launch a browser, navigate to pages, interact with elements through clicks and keyboard input, and capture the results through screenshots and console logs. This tool may be useful at key stages of web development tasks-such as after implementing new features, making substantial changes, when troubleshooting issues, or to verify the result of your work. You can analyze the provided screenshots to ensure correct rendering or identify errors, and review console logs for runtime issues.
|
||||
- For example, if asked to add a component to a react website, you might create the necessary files, use execute_command to run the site locally, then use browser_action to launch the browser, navigate to the local server, and verify the component renders & functions correctly before closing the browser.
|
||||
|
|
@ -484,6 +484,7 @@ RULES
|
|||
- Before using the execute_command tool, you must first think about the SYSTEM INFORMATION context provided to understand the user's environment and tailor your commands to ensure they are compatible with their system. You must also consider if the command you need to run should be executed in a specific directory outside of the current working directory '/test/path', and if so prepend with `cd`'ing into that directory && then executing the command (as one command since you are stuck operating from '/test/path'). For example, if you needed to run `npm install` in a project outside of '/test/path', you would need to prepend with a `cd` i.e. pseudocode for this would be `cd (path to project) && (command, in this case npm install)`.
|
||||
- When using the search_files tool, craft your regex patterns carefully to balance specificity and flexibility. Based on the user's task you may use it to find code patterns, TODO comments, function definitions, or any text-based information across the project. The results include context, so analyze the surrounding code to better understand the matches. Leverage the search_files tool in combination with other tools for more comprehensive analysis. For example, use it to find specific code patterns, then use read_file to examine the full context of interesting matches before using write_to_file to make informed changes.
|
||||
- When creating a new project (such as an app, website, or any software project), organize all new files within a dedicated project directory unless the user specifies otherwise. Use appropriate file paths when writing files, as the write_to_file tool will automatically create any necessary directories. Structure the project logically, adhering to best practices for the specific type of project being created. Unless otherwise specified, new projects should be easily run without additional setup, for example most projects can be built in HTML, CSS, and JavaScript - which you can open in a browser.
|
||||
|
||||
- For editing files, you have access to these tools: write_to_file (for creating new files or complete file rewrites), insert_content (for adding lines to files).
|
||||
- The insert_content tool adds lines of text to files at a specific line number, such as adding a new function to a JavaScript file or inserting a new route in a Python file. Use line number 0 to append at the end of the file, or any positive number to insert before that line.
|
||||
- You should always prefer using other editing tools over write_to_file when making changes to existing files since write_to_file is much slower and cannot handle large files.
|
||||
|
|
|
|||
|
|
@ -409,7 +409,7 @@ CAPABILITIES
|
|||
- When the user initially gives you a task, a recursive list of all filepaths in the current workspace directory ('/test/path') will be included in environment_details. This provides an overview of the project's file structure, offering key insights into the project from directory/file names (how developers conceptualize and organize their code) and file extensions (the language used). This can also guide decision-making on which files to explore further. If you need to further explore directories such as outside the current workspace directory, you can use the list_files tool. If you pass 'true' for the recursive parameter, it will list files recursively. Otherwise, it will list files at the top level, which is better suited for generic directories where you don't necessarily need the nested structure, like the Desktop.
|
||||
- You can use search_files to perform regex searches across files in a specified directory, outputting context-rich results that include surrounding lines. This is particularly useful for understanding code patterns, finding specific implementations, or identifying areas that need refactoring.
|
||||
- You can use the list_code_definition_names tool to get an overview of source code definitions for all files at the top level of a specified directory. This can be particularly useful when you need to understand the broader context and relationships between certain parts of the code. You may need to call this tool multiple times to understand various parts of the codebase related to the task.
|
||||
- For example, when asked to make edits or improvements you might analyze the file structure in the initial environment_details to get an overview of the project, then use list_code_definition_names to get further insight using source code definitions for files located in relevant directories, then read_file to examine the contents of relevant files, analyze the code and suggest improvements or make necessary edits, then use the write_to_file tool to apply the changes. If you refactored code that could affect other parts of the codebase, you could use search_files to ensure you update other files as needed.
|
||||
- For example, when asked to make edits or improvements you might analyze the file structure in the initial environment_details to get an overview of the project, then use list_code_definition_names to get further insight using source code definitions for files located in relevant directories, then read_file to examine the contents of relevant files, analyze the code and suggest improvements or make necessary edits, then use the write_to_file or insert_content tool to apply the changes. If you refactored code that could affect other parts of the codebase, you could use search_files to ensure you update other files as needed.
|
||||
- You can use the execute_command tool to run commands on the user's computer whenever you feel it can help accomplish the user's task. When you need to execute a CLI command, you must provide a clear explanation of what the command does. Prefer to execute complex CLI commands over creating executable scripts, since they are more flexible and easier to run. Interactive and long-running commands are allowed, since the commands are run in the user's VSCode terminal. The user may keep commands running in the background and you will be kept updated on their status along the way. Each command you execute is run in a new terminal instance.
|
||||
|
||||
====
|
||||
|
|
@ -429,6 +429,7 @@ RULES
|
|||
- Before using the execute_command tool, you must first think about the SYSTEM INFORMATION context provided to understand the user's environment and tailor your commands to ensure they are compatible with their system. You must also consider if the command you need to run should be executed in a specific directory outside of the current working directory '/test/path', and if so prepend with `cd`'ing into that directory && then executing the command (as one command since you are stuck operating from '/test/path'). For example, if you needed to run `npm install` in a project outside of '/test/path', you would need to prepend with a `cd` i.e. pseudocode for this would be `cd (path to project) && (command, in this case npm install)`.
|
||||
- When using the search_files tool, craft your regex patterns carefully to balance specificity and flexibility. Based on the user's task you may use it to find code patterns, TODO comments, function definitions, or any text-based information across the project. The results include context, so analyze the surrounding code to better understand the matches. Leverage the search_files tool in combination with other tools for more comprehensive analysis. For example, use it to find specific code patterns, then use read_file to examine the full context of interesting matches before using write_to_file to make informed changes.
|
||||
- When creating a new project (such as an app, website, or any software project), organize all new files within a dedicated project directory unless the user specifies otherwise. Use appropriate file paths when writing files, as the write_to_file tool will automatically create any necessary directories. Structure the project logically, adhering to best practices for the specific type of project being created. Unless otherwise specified, new projects should be easily run without additional setup, for example most projects can be built in HTML, CSS, and JavaScript - which you can open in a browser.
|
||||
|
||||
- For editing files, you have access to these tools: write_to_file (for creating new files or complete file rewrites), insert_content (for adding lines to files).
|
||||
- The insert_content tool adds lines of text to files at a specific line number, such as adding a new function to a JavaScript file or inserting a new route in a Python file. Use line number 0 to append at the end of the file, or any positive number to insert before that line.
|
||||
- You should always prefer using other editing tools over write_to_file when making changes to existing files since write_to_file is much slower and cannot handle large files.
|
||||
|
|
|
|||
|
|
@ -497,7 +497,7 @@ CAPABILITIES
|
|||
- When the user initially gives you a task, a recursive list of all filepaths in the current workspace directory ('/test/path') will be included in environment_details. This provides an overview of the project's file structure, offering key insights into the project from directory/file names (how developers conceptualize and organize their code) and file extensions (the language used). This can also guide decision-making on which files to explore further. If you need to further explore directories such as outside the current workspace directory, you can use the list_files tool. If you pass 'true' for the recursive parameter, it will list files recursively. Otherwise, it will list files at the top level, which is better suited for generic directories where you don't necessarily need the nested structure, like the Desktop.
|
||||
- You can use search_files to perform regex searches across files in a specified directory, outputting context-rich results that include surrounding lines. This is particularly useful for understanding code patterns, finding specific implementations, or identifying areas that need refactoring.
|
||||
- You can use the list_code_definition_names tool to get an overview of source code definitions for all files at the top level of a specified directory. This can be particularly useful when you need to understand the broader context and relationships between certain parts of the code. You may need to call this tool multiple times to understand various parts of the codebase related to the task.
|
||||
- For example, when asked to make edits or improvements you might analyze the file structure in the initial environment_details to get an overview of the project, then use list_code_definition_names to get further insight using source code definitions for files located in relevant directories, then read_file to examine the contents of relevant files, analyze the code and suggest improvements or make necessary edits, then use the apply_diff or write_to_file tool to apply the changes. If you refactored code that could affect other parts of the codebase, you could use search_files to ensure you update other files as needed.
|
||||
- For example, when asked to make edits or improvements you might analyze the file structure in the initial environment_details to get an overview of the project, then use list_code_definition_names to get further insight using source code definitions for files located in relevant directories, then read_file to examine the contents of relevant files, analyze the code and suggest improvements or make necessary edits, then use the apply_diff, write_to_file, or insert_content tool to apply the changes. If you refactored code that could affect other parts of the codebase, you could use search_files to ensure you update other files as needed.
|
||||
- You can use the execute_command tool to run commands on the user's computer whenever you feel it can help accomplish the user's task. When you need to execute a CLI command, you must provide a clear explanation of what the command does. Prefer to execute complex CLI commands over creating executable scripts, since they are more flexible and easier to run. Interactive and long-running commands are allowed, since the commands are run in the user's VSCode terminal. The user may keep commands running in the background and you will be kept updated on their status along the way. Each command you execute is run in a new terminal instance.
|
||||
|
||||
====
|
||||
|
|
@ -517,6 +517,7 @@ RULES
|
|||
- Before using the execute_command tool, you must first think about the SYSTEM INFORMATION context provided to understand the user's environment and tailor your commands to ensure they are compatible with their system. You must also consider if the command you need to run should be executed in a specific directory outside of the current working directory '/test/path', and if so prepend with `cd`'ing into that directory && then executing the command (as one command since you are stuck operating from '/test/path'). For example, if you needed to run `npm install` in a project outside of '/test/path', you would need to prepend with a `cd` i.e. pseudocode for this would be `cd (path to project) && (command, in this case npm install)`.
|
||||
- When using the search_files tool, craft your regex patterns carefully to balance specificity and flexibility. Based on the user's task you may use it to find code patterns, TODO comments, function definitions, or any text-based information across the project. The results include context, so analyze the surrounding code to better understand the matches. Leverage the search_files tool in combination with other tools for more comprehensive analysis. For example, use it to find specific code patterns, then use read_file to examine the full context of interesting matches before using apply_diff or write_to_file to make informed changes.
|
||||
- When creating a new project (such as an app, website, or any software project), organize all new files within a dedicated project directory unless the user specifies otherwise. Use appropriate file paths when writing files, as the write_to_file tool will automatically create any necessary directories. Structure the project logically, adhering to best practices for the specific type of project being created. Unless otherwise specified, new projects should be easily run without additional setup, for example most projects can be built in HTML, CSS, and JavaScript - which you can open in a browser.
|
||||
|
||||
- For editing files, you have access to these tools: apply_diff (for surgical edits - targeted changes to specific lines or functions), write_to_file (for creating new files or complete file rewrites), insert_content (for adding lines to files).
|
||||
- The insert_content tool adds lines of text to files at a specific line number, such as adding a new function to a JavaScript file or inserting a new route in a Python file. Use line number 0 to append at the end of the file, or any positive number to insert before that line.
|
||||
- You should always prefer using other editing tools over write_to_file when making changes to existing files since write_to_file is much slower and cannot handle large files.
|
||||
|
|
|
|||
|
|
@ -409,7 +409,7 @@ CAPABILITIES
|
|||
- When the user initially gives you a task, a recursive list of all filepaths in the current workspace directory ('/test/path') will be included in environment_details. This provides an overview of the project's file structure, offering key insights into the project from directory/file names (how developers conceptualize and organize their code) and file extensions (the language used). This can also guide decision-making on which files to explore further. If you need to further explore directories such as outside the current workspace directory, you can use the list_files tool. If you pass 'true' for the recursive parameter, it will list files recursively. Otherwise, it will list files at the top level, which is better suited for generic directories where you don't necessarily need the nested structure, like the Desktop.
|
||||
- You can use search_files to perform regex searches across files in a specified directory, outputting context-rich results that include surrounding lines. This is particularly useful for understanding code patterns, finding specific implementations, or identifying areas that need refactoring.
|
||||
- You can use the list_code_definition_names tool to get an overview of source code definitions for all files at the top level of a specified directory. This can be particularly useful when you need to understand the broader context and relationships between certain parts of the code. You may need to call this tool multiple times to understand various parts of the codebase related to the task.
|
||||
- For example, when asked to make edits or improvements you might analyze the file structure in the initial environment_details to get an overview of the project, then use list_code_definition_names to get further insight using source code definitions for files located in relevant directories, then read_file to examine the contents of relevant files, analyze the code and suggest improvements or make necessary edits, then use the write_to_file tool to apply the changes. If you refactored code that could affect other parts of the codebase, you could use search_files to ensure you update other files as needed.
|
||||
- For example, when asked to make edits or improvements you might analyze the file structure in the initial environment_details to get an overview of the project, then use list_code_definition_names to get further insight using source code definitions for files located in relevant directories, then read_file to examine the contents of relevant files, analyze the code and suggest improvements or make necessary edits, then use the write_to_file or insert_content tool to apply the changes. If you refactored code that could affect other parts of the codebase, you could use search_files to ensure you update other files as needed.
|
||||
- You can use the execute_command tool to run commands on the user's computer whenever you feel it can help accomplish the user's task. When you need to execute a CLI command, you must provide a clear explanation of what the command does. Prefer to execute complex CLI commands over creating executable scripts, since they are more flexible and easier to run. Interactive and long-running commands are allowed, since the commands are run in the user's VSCode terminal. The user may keep commands running in the background and you will be kept updated on their status along the way. Each command you execute is run in a new terminal instance.
|
||||
|
||||
====
|
||||
|
|
@ -429,6 +429,7 @@ RULES
|
|||
- Before using the execute_command tool, you must first think about the SYSTEM INFORMATION context provided to understand the user's environment and tailor your commands to ensure they are compatible with their system. You must also consider if the command you need to run should be executed in a specific directory outside of the current working directory '/test/path', and if so prepend with `cd`'ing into that directory && then executing the command (as one command since you are stuck operating from '/test/path'). For example, if you needed to run `npm install` in a project outside of '/test/path', you would need to prepend with a `cd` i.e. pseudocode for this would be `cd (path to project) && (command, in this case npm install)`.
|
||||
- When using the search_files tool, craft your regex patterns carefully to balance specificity and flexibility. Based on the user's task you may use it to find code patterns, TODO comments, function definitions, or any text-based information across the project. The results include context, so analyze the surrounding code to better understand the matches. Leverage the search_files tool in combination with other tools for more comprehensive analysis. For example, use it to find specific code patterns, then use read_file to examine the full context of interesting matches before using write_to_file to make informed changes.
|
||||
- When creating a new project (such as an app, website, or any software project), organize all new files within a dedicated project directory unless the user specifies otherwise. Use appropriate file paths when writing files, as the write_to_file tool will automatically create any necessary directories. Structure the project logically, adhering to best practices for the specific type of project being created. Unless otherwise specified, new projects should be easily run without additional setup, for example most projects can be built in HTML, CSS, and JavaScript - which you can open in a browser.
|
||||
|
||||
- For editing files, you have access to these tools: write_to_file (for creating new files or complete file rewrites), insert_content (for adding lines to files).
|
||||
- The insert_content tool adds lines of text to files at a specific line number, such as adding a new function to a JavaScript file or inserting a new route in a Python file. Use line number 0 to append at the end of the file, or any positive number to insert before that line.
|
||||
- You should always prefer using other editing tools over write_to_file when making changes to existing files since write_to_file is much slower and cannot handle large files.
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue