Merge main into bring_back_parallel_tool_calls

Resolved conflicts:
- src/core/assistant-message/presentAssistantMessage.ts: Keep new nativeArgs validation and add experiment check
- src/core/task/Task.ts: Keep shouldIncludeTools and use experiment-based parallelToolCallsEnabled
This commit is contained in:
Roo Code 2026-01-23 05:46:20 +00:00
commit 72fdff746a
433 changed files with 10908 additions and 20053 deletions

View file

@ -70,15 +70,20 @@ jobs:
comment.body.includes(commentIdentifier)
);
if (existingComment) {
return;
}
const comment = commentIdentifier + '\n🚀 **Preview deployed!**\n\nYour changes have been deployed to Vercel:\n\n**Preview URL:** ' + deploymentUrl + '\n\nThis preview will be updated automatically when you push new commits to this PR.';
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: comment
});
if (existingComment) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existingComment.id,
body: comment
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: comment
});
}

5
AGENTS.md Normal file
View file

@ -0,0 +1,5 @@
# AGENTS.md
This file provides guidance to agents when working with code in this repository.
- Settings View Pattern: When working on `SettingsView`, inputs must bind to the local `cachedState`, NOT the live `useExtensionState()`. The `cachedState` acts as a buffer for user edits, isolating them from the `ContextProxy` source-of-truth until the user explicitly clicks "Save". Wiring inputs directly to the live state causes race conditions.

View file

@ -1,5 +1,29 @@
# Roo Code Changelog
## [3.42.0] - 2026-01-22
![3.42.0 Release - ChatGPT Usage Tracking](/releases/3.42.0-release.png)
- Added UI to track your ChatGPT usage limits in the OpenAI Codex provider (PR #10813 by @hannesrudolph)
- Removed deprecated Claude Code provider (PR #10883 by @daniel-lxs)
- Streamlined codebase by removing legacy XML tool calling functionality (#10848 by @hannesrudolph, PR #10841 by @hannesrudolph)
- Standardize model selectors across all providers: Improved consistency of model selection UI (#10650 by @hannesrudolph, PR #10294 by @hannesrudolph)
- Enable prompt caching for Cerebras zai-glm-4.7 model (#10601 by @jahanson, PR #10670 by @app/roomote)
- Add Kimi K2 thinking model to VertexAI provider (#9268 by @diwakar-s-maurya, PR #9269 by @app/roomote)
- Warn users when too many MCP tools are enabled (PR #10772 by @app/roomote)
- Migrate context condensing prompt to customSupportPrompts (PR #10881 by @hannesrudolph)
- Unify export path logic and default to Downloads folder (PR #10882 by @hannesrudolph)
- Performance improvements for webview state synchronization (PR #10842 by @hannesrudolph)
- Fix: Handle mode selector empty state on workspace switch (#10660 by @hannesrudolph, PR #9674 by @app/roomote)
- Fix: Resolve race condition in context condensing prompt input (PR #10876 by @hannesrudolph)
- Fix: Prevent double emission of text/reasoning in OpenAI native and codex handlers (PR #10888 by @hannesrudolph)
- Fix: Prevent task abortion when resuming via IPC/bridge (PR #10892 by @cte)
- Fix: Enforce file restrictions for all editing tools (PR #10896 by @app/roomote)
- Fix: Remove custom condensing model option (PR #10901 by @hannesrudolph)
- Unify user content tags to <user_message> for consistent prompt formatting (#10658 by @hannesrudolph, PR #10723 by @app/roomote)
- Clarify linked SKILL.md file handling in prompts (PR #10907 by @hannesrudolph)
- Fix: Padding on Roo Code Cloud teaser (PR #10889 by @app/roomote)
## [3.41.3] - 2026-01-18
- Fix: Thinking block word-breaking to prevent horizontal scroll in the chat UI (PR #10806 by @roomote)

View file

@ -30,6 +30,8 @@
"@trpc/client": "^11.8.1",
"@vscode/ripgrep": "^1.15.9",
"commander": "^12.1.0",
"cross-spawn": "^7.0.6",
"execa": "^9.5.2",
"fuzzysort": "^3.1.0",
"ink": "^6.6.0",
"p-wait-for": "^5.0.2",

View file

@ -56,7 +56,6 @@ import { useRooCodeCloudModels } from "@/hooks/use-roo-code-cloud-models"
import {
Button,
Checkbox,
FormControl,
FormField,
FormItem,
@ -111,7 +110,6 @@ export function NewRun() {
const [provider, setModelSource] = useState<"roo" | "openrouter" | "other">("other")
const [executionMethod, setExecutionMethod] = useState<ExecutionMethod>("vscode")
const [useNativeToolProtocol, setUseNativeToolProtocol] = useState(true)
const [commandExecutionTimeout, setCommandExecutionTimeout] = useState(20)
const [terminalShellIntegrationTimeout, setTerminalShellIntegrationTimeout] = useState(30) // seconds
@ -464,7 +462,6 @@ export function NewRun() {
...(runValues.settings || {}),
apiProvider: "openrouter",
openRouterModelId: selection.model,
toolProtocol: useNativeToolProtocol ? "native" : "xml",
commandExecutionTimeout,
terminalShellIntegrationTimeout: terminalShellIntegrationTimeout * 1000,
}
@ -474,7 +471,6 @@ export function NewRun() {
...(runValues.settings || {}),
apiProvider: "roo",
apiModelId: selection.model,
toolProtocol: useNativeToolProtocol ? "native" : "xml",
commandExecutionTimeout,
terminalShellIntegrationTimeout: terminalShellIntegrationTimeout * 1000,
}
@ -485,7 +481,6 @@ export function NewRun() {
...EVALS_SETTINGS,
...providerSettings,
...importedSettings.globalSettings,
toolProtocol: useNativeToolProtocol ? "native" : "xml",
commandExecutionTimeout,
terminalShellIntegrationTimeout: terminalShellIntegrationTimeout * 1000,
}
@ -512,7 +507,6 @@ export function NewRun() {
configSelections,
importedSettings,
router,
useNativeToolProtocol,
commandExecutionTimeout,
terminalShellIntegrationTimeout,
],
@ -688,26 +682,6 @@ export function NewRun() {
</div>
)}
<div className="mt-4 p-4 rounded-md bg-muted/30 border border-border space-y-3">
<Label className="text-sm font-medium text-muted-foreground">
Tool Protocol Options
</Label>
<div className="flex flex-col gap-2.5 pl-1">
<label
htmlFor="native-other"
className="flex items-center gap-2 cursor-pointer">
<Checkbox
id="native-other"
checked={useNativeToolProtocol}
onCheckedChange={(checked: boolean) =>
setUseNativeToolProtocol(checked)
}
/>
<span className="text-sm">Use Native Tool Calls</span>
</label>
</div>
</div>
{settings && (
<SettingsDiff defaultSettings={EVALS_SETTINGS} customSettings={settings} />
)}
@ -792,26 +766,6 @@ export function NewRun() {
</div>
))}
</div>
<div className="mt-4 p-4 rounded-md bg-muted/30 border border-border space-y-3">
<Label className="text-sm font-medium text-muted-foreground">
Tool Protocol Options
</Label>
<div className="flex flex-col gap-2.5 pl-1">
<label
htmlFor="native"
className="flex items-center gap-2 cursor-pointer">
<Checkbox
id="native"
checked={useNativeToolProtocol}
onCheckedChange={(checked: boolean) =>
setUseNativeToolProtocol(checked)
}
/>
<span className="text-sm">Use Native Tool Calls</span>
</label>
</div>
</div>
</>
)}

View file

@ -100,6 +100,19 @@ export default function CookiePolicy() {
<td className="border border-border px-4 py-3">1 year</td>
<td className="border border-border px-4 py-3 font-mono text-sm">ph_*</td>
</tr>
<tr>
<td className="border border-border px-4 py-3 font-medium">HubSpot</td>
<td className="border border-border px-4 py-3">
Marketing automation and visitor tracking
</td>
<td className="border border-border px-4 py-3">
Analytics (only with your consent)
</td>
<td className="border border-border px-4 py-3">13 months</td>
<td className="border border-border px-4 py-3 font-mono text-sm">
hubspotutk, __hstc, __hssrc, __hssc
</td>
</tr>
</tbody>
</table>
</div>
@ -122,6 +135,15 @@ export default function CookiePolicy() {
PostHog Privacy Policy
</a>
</p>
<p>
<a
href="https://legal.hubspot.com/privacy-policy"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline">
HubSpot Privacy Policy
</a>
</p>
<h2 className="mt-12 text-2xl font-bold">Essential cookies</h2>
<p>
@ -133,10 +155,10 @@ export default function CookiePolicy() {
<h2 className="mt-12 text-2xl font-bold">Analytics cookies</h2>
<p>
We use PostHog analytics cookies to understand how visitors interact with our website. This
helps us improve our services and user experience. Analytics cookies are placed only if you give
consent through our cookie banner. The lawful basis for processing these cookies is your
consent, which you can withdraw at any time.
We use PostHog and HubSpot analytics cookies to understand how visitors interact with our
website. This helps us improve our services, user experience, and marketing efforts. Analytics
cookies are placed only if you give consent through our cookie banner. The lawful basis for
processing these cookies is your consent, which you can withdraw at any time.
</p>
<h2 className="mt-12 text-2xl font-bold">Third-party services</h2>

View file

@ -291,11 +291,7 @@ export default function PricingPage() {
<li>To pay for Cloud Agents running time (${PRICE_CREDITS}/hour)</li>
<li>
To pay for AI model inference costs (
<a
href="https://app.roocode.com/provider/pricing"
target="_blank"
rel="noopener noreferrer"
className="underline">
<a href="/provider" target="_blank" rel="noopener noreferrer" className="underline">
varies by model
</a>
)

View file

@ -0,0 +1,401 @@
import {
ArrowRight,
Brain,
CreditCard,
GitBranch,
GraduationCap,
Link2,
MessageSquare,
Settings,
Shield,
Slack,
Users,
Zap,
} from "lucide-react"
import type { LucideIcon } from "lucide-react"
import type { Metadata } from "next"
import { AnimatedBackground } from "@/components/homepage"
import { SlackThreadDemo } from "@/components/slack/slack-thread-demo"
import { Button } from "@/components/ui"
import { EXTERNAL_LINKS } from "@/lib/constants"
import { SEO } from "@/lib/seo"
import { ogImageUrl } from "@/lib/og"
const TITLE = "Roo Code for Slack"
const DESCRIPTION =
"Mention @Roomote in any channel to explain code, plan features, or ship a PR, all without leaving the conversation."
const OG_DESCRIPTION = "Your AI Team in Slack"
const PATH = "/slack"
export const metadata: Metadata = {
title: TITLE,
description: DESCRIPTION,
alternates: {
canonical: `${SEO.url}${PATH}`,
},
openGraph: {
title: TITLE,
description: DESCRIPTION,
url: `${SEO.url}${PATH}`,
siteName: SEO.name,
images: [
{
url: ogImageUrl(TITLE, OG_DESCRIPTION),
width: 1200,
height: 630,
alt: TITLE,
},
],
locale: SEO.locale,
type: "website",
},
twitter: {
card: SEO.twitterCard,
title: TITLE,
description: DESCRIPTION,
images: [ogImageUrl(TITLE, OG_DESCRIPTION)],
},
keywords: [
...SEO.keywords,
"slack integration",
"slack bot",
"AI in slack",
"code assistant slack",
"@Roomote",
"team collaboration",
],
}
// Invalidate cache when a request comes in, at most once every hour.
export const revalidate = 3600
type ValueProp = {
icon: LucideIcon
title: string
description: string
}
const VALUE_PROPS: ValueProp[] = [
{
icon: GitBranch,
title: "Discussion to PR.",
description:
"Your team discusses a feature in Slack. @Roomote turns the discussion into a plan. Then builds it. All without leaving the conversation.",
},
{
icon: Brain,
title: "Thread-aware.",
description:
'@Roomote reads the full thread before responding. Ask "Can we add caching here?" and it knows exactly what code you mean.',
},
{
icon: Link2,
title: "Chain agents.",
description:
"Start with a Planner to spec it out. Then call the Coder to build it. Multi-step workflows, one Slack thread.",
},
{
icon: Users,
title: "Open to all.",
description:
"Anyone on your team can ask @Roomote to fix bugs, build features, or investigate issues. Engineering gets looped in only when needed.",
},
{
icon: GraduationCap,
title: "Built-in learning.",
description: "Public channel mentions show everyone how to leverage agents. Learn by watching.",
},
{
icon: Shield,
title: "Safe by design.",
description: "Agents never touch main/master directly. They produce branches and PRs. You approve.",
},
]
type WorkflowStep = {
step: number
title: string
description: string
}
const WORKFLOW_STEPS: WorkflowStep[] = [
{
step: 1,
title: "Turn the discussion into a plan",
description: "Your team discusses a feature. When it gets complex, summon the Planner agent.",
},
{
step: 2,
title: "Refine the plan in the thread",
description:
"The team reviews the spec in the thread, suggests changes, asks questions. Mention @Roomote again to refine.",
},
{
step: 3,
title: "Build the plan",
description: "Once the plan looks good, hand it off to the Coder agent to implement.",
},
{
step: 4,
title: "Review and ship",
description: "The Coder creates a branch and opens a PR. The team reviews, and the feature ships.",
},
]
type OnboardingStep = {
icon: LucideIcon
title: string
description: string
link?: {
href: string
text: string
}
}
const ONBOARDING_STEPS: OnboardingStep[] = [
{
icon: CreditCard,
title: "1. Team Plan",
description: "Slack requires a Team plan.",
link: {
href: EXTERNAL_LINKS.CLOUD_APP_TEAM_TRIAL,
text: "Start a free trial",
},
},
{
icon: Settings,
title: "2. Connect",
description: 'Sign in to Roo Code Cloud and go to Settings. Click "Connect" next to Slack.',
},
{
icon: Slack,
title: "3. Authorize",
description: "Authorize the Roo Code app to access your Slack workspace.",
},
{
icon: MessageSquare,
title: "4. Add to channels",
description: "Add @Roomote to the channels where you want it available.",
},
]
export default function SlackPage(): JSX.Element {
return (
<>
{/* Hero Section */}
<section className="relative flex pt-32 pb-20 items-center overflow-hidden">
<AnimatedBackground />
<div className="container relative flex flex-col items-center h-full z-10 mx-auto px-4 sm:px-6 lg:px-8">
<div className="grid w-full max-w-6xl grid-cols-1 items-center gap-10 lg:grid-cols-2 lg:gap-12">
<div className="text-center lg:text-left">
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-violet-100 dark:bg-violet-900/30 text-violet-700 dark:text-violet-300 text-sm font-medium mb-6">
<Slack className="size-4" />
Powered by Roo Code Cloud
</div>
<h1 className="text-4xl font-bold tracking-tight mb-6 md:text-5xl lg:text-6xl">
<span className="text-violet-500">@Roomote:</span> Your AI Team in&nbsp;Slack
</h1>
<p className="text-xl text-muted-foreground mb-8 max-w-2xl mx-auto lg:mx-0">
Mention @Roomote in any channel to explain code, plan features, or ship a PR, all
without leaving the conversation.
</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center lg:justify-start">
<Button
size="xl"
className="bg-violet-600 hover:bg-violet-700 text-white transition-all duration-300 shadow-lg hover:shadow-violet-500/25"
asChild>
<a
href={EXTERNAL_LINKS.CLOUD_APP_SIGNUP}
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center">
Get Started
<ArrowRight className="ml-2 size-5" />
</a>
</Button>
<Button variant="outline" size="xl" className="backdrop-blur-sm" asChild>
<a
href={EXTERNAL_LINKS.SLACK_DOCS}
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center">
Read the Docs
</a>
</Button>
</div>
</div>
<div className="flex justify-center lg:justify-end">
<SlackThreadDemo />
</div>
</div>
</div>
</section>
{/* Value Props Section */}
<section className="py-24 bg-muted/30">
<div className="container mx-auto px-4 sm:px-6 lg:px-8 relative">
<div className="absolute inset-y-0 left-1/2 h-full w-full max-w-[1200px] -translate-x-1/2 z-1">
<div className="absolute left-1/2 top-1/2 h-[800px] w-full -translate-x-1/2 -translate-y-1/2 rounded-full bg-violet-500/10 dark:bg-violet-700/20 blur-[140px]" />
</div>
<div className="text-center mb-16">
<h2 className="text-3xl font-bold tracking-tight sm:text-4xl mb-4">
Why your team will love using Roo Code in&nbsp;Slack
</h2>
<p className="text-xl text-muted-foreground max-w-2xl mx-auto">
AI agents that understand context, chain together for complex work, and keep your team in
control.
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 max-w-6xl mx-auto relative">
{VALUE_PROPS.map((prop, index) => {
const Icon = prop.icon
return (
<div
key={index}
className="bg-background p-8 rounded-2xl border border-border hover:shadow-lg transition-all duration-300">
<div className="bg-violet-100 dark:bg-violet-900/20 w-12 h-12 rounded-lg flex items-center justify-center mb-6">
<Icon className="size-6 text-violet-600 dark:text-violet-400" />
</div>
<h3 className="text-xl font-semibold mb-3">{prop.title}</h3>
<p className="text-muted-foreground leading-relaxed">{prop.description}</p>
</div>
)
})}
</div>
</div>
</section>
{/* Featured Workflow Section */}
<section className="relative overflow-hidden border-t border-border py-24 lg:py-32">
<div className="container relative z-10 mx-auto px-4 sm:px-6 lg:px-8">
<div className="absolute inset-y-0 left-1/2 h-full w-full max-w-[1200px] -translate-x-1/2 z-1">
<div className="absolute left-1/2 top-1/2 h-[400px] w-full -translate-x-1/2 -translate-y-1/2 rounded-full bg-blue-500/10 dark:bg-blue-700/20 blur-[140px]" />
</div>
<div className="mx-auto mb-12 max-w-5xl text-center">
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 text-sm font-medium mb-6">
<Zap className="size-4" />
Featured Workflow
</div>
<h2 className="text-3xl font-bold tracking-tight sm:text-5xl mb-4">
Thread to Shipped Feature
</h2>
<p className="text-xl text-muted-foreground max-w-2xl mx-auto">
Turn Slack discussions into working code. No context lost, no meetings needed.
</p>
</div>
<div className="relative mx-auto max-w-6xl">
<div className="grid grid-cols-1 lg:grid-cols-5 gap-8 lg:gap-10 items-center">
{/* YouTube Video Embed */}
<div className="lg:col-span-3 overflow-hidden rounded-2xl border border-border bg-background shadow-lg">
<iframe
className="aspect-video w-full"
src="https://www.youtube-nocookie.com/embed/dJM_8HHGe1E?rel=0"
title="Roo Code Slack Integration Demo"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
referrerPolicy="strict-origin-when-cross-origin"
allowFullScreen
/>
</div>
{/* Workflow Steps */}
<div className="lg:col-span-2 space-y-3">
{WORKFLOW_STEPS.map((step) => (
<div
key={step.step}
className="relative border border-border rounded-xl bg-background p-4 transition-all duration-300 hover:shadow-md hover:border-blue-500/30">
<div className="flex items-start gap-3">
<div className="bg-blue-100 dark:bg-blue-900/30 w-7 h-7 rounded-full flex items-center justify-center text-blue-700 dark:text-blue-300 font-bold text-xs shrink-0 mt-0.5">
{step.step}
</div>
<div className="min-w-0">
<h3 className="text-base font-semibold text-foreground mb-0.5">
{step.title}
</h3>
<p className="text-sm leading-snug text-muted-foreground">
{step.description}
</p>
</div>
</div>
</div>
))}
</div>
</div>
</div>
</div>
</section>
{/* Onboarding Section */}
<section className="py-24 bg-muted/30">
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-16">
<h2 className="text-3xl font-bold tracking-tight sm:text-4xl mb-4">Get started in minutes</h2>
<p className="text-xl text-muted-foreground max-w-2xl mx-auto">
Connect your Slack workspace and start working with AI agents.
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8 max-w-5xl mx-auto">
{ONBOARDING_STEPS.map((step, index) => {
const Icon = step.icon
return (
<div key={index} className="text-center">
<div className="bg-violet-100 dark:bg-violet-900/20 w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-6">
<Icon className="size-8 text-violet-600 dark:text-violet-400" />
</div>
<h3 className="text-lg font-semibold mb-2">{step.title}</h3>
<p className="text-muted-foreground">
{step.description}
{step.link && (
<>
{" "}
<a
href={step.link.href}
target="_blank"
rel="noopener noreferrer"
className="text-violet-600 dark:text-violet-400 hover:underline">
{step.link.text}
</a>
</>
)}
</p>
</div>
)
})}
</div>
</div>
</section>
{/* CTA Section */}
<section className="py-24">
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
<div className="mx-auto max-w-4xl rounded-3xl border border-border/50 bg-gradient-to-br from-violet-500/10 via-purple-500/5 to-blue-500/5 p-8 text-center shadow-2xl backdrop-blur-xl dark:border-white/10 sm:p-16">
<h2 className="mb-6 text-3xl font-bold tracking-tight sm:text-4xl">
Start using Roo Code in Slack
</h2>
<p className="mx-auto mb-10 max-w-2xl text-lg text-muted-foreground">
Start a free 14 day Team trial.
</p>
<div className="flex flex-col justify-center space-y-4 sm:flex-row sm:space-x-4 sm:space-y-0">
<Button
size="lg"
className="bg-foreground text-background hover:bg-foreground/90 transition-all duration-300"
asChild>
<a
href={EXTERNAL_LINKS.CLOUD_APP_TEAM_TRIAL}
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center">
Start free trial
<ArrowRight className="ml-2 h-4 w-4" />
</a>
</Button>
</div>
</div>
</div>
</section>
</>
)
}

View file

@ -13,7 +13,7 @@ import { EXTERNAL_LINKS } from "@/lib/constants"
import { useLogoSrc } from "@/lib/hooks/use-logo-src"
import { ScrollButton } from "@/components/ui"
import ThemeToggle from "@/components/chromes/theme-toggle"
import { Brain, ChevronDown, Cloud, Puzzle, X } from "lucide-react"
import { Brain, ChevronDown, Cloud, Puzzle, Slack, X } from "lucide-react"
interface NavBarProps {
stars: string | null
@ -54,6 +54,12 @@ export function NavBar({ stars, downloads }: NavBarProps) {
<Cloud className="size-3 inline mr-2 -mt-0.5" />
Roo Code Cloud
</Link>
<Link
href="/slack"
className="block px-4 py-2 text-sm transition-colors hover:bg-accent hover:text-foreground">
<Slack className="size-3 inline mr-2 -mt-0.5" />
Roo Code for Slack
</Link>
<Link
href="/provider"
className="block px-4 py-2 text-sm transition-colors hover:bg-accent hover:text-foreground">
@ -190,6 +196,12 @@ export function NavBar({ stars, downloads }: NavBarProps) {
onClick={() => setIsMenuOpen(false)}>
Roo Code Cloud
</Link>
<Link
href="/slack"
className="block w-full p-5 py-3 text-left text-foreground active:opacity-50"
onClick={() => setIsMenuOpen(false)}>
Roo Code for Slack
</Link>
<Link
href="/provider"
className="block w-full p-5 py-3 text-left text-foreground active:opacity-50"

View file

@ -0,0 +1,50 @@
"use client"
import { useEffect, useState } from "react"
import Script from "next/script"
import { hasConsent, onConsentChange } from "@/lib/analytics/consent-manager"
// HubSpot Account ID
const HUBSPOT_ID = "243714031"
/**
* HubSpot Tracking Provider
* Loads HubSpot tracking script only after user consent is given, following GDPR requirements
*/
export function HubSpotProvider({ 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 && (
<>
{/* HubSpot Embed Code */}
<Script
id="hs-script-loader"
src={`//js-na2.hs-scripts.com/${HUBSPOT_ID}.js`}
strategy="afterInteractive"
async
defer
/>
</>
)}
{children}
</>
)
}

View file

@ -4,6 +4,7 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
import { ThemeProvider } from "next-themes"
import { GoogleTagManagerProvider } from "./google-tag-manager-provider"
import { HubSpotProvider } from "./hubspot-provider"
import { PostHogProvider } from "./posthog-provider"
const queryClient = new QueryClient()
@ -12,11 +13,13 @@ export const Providers = ({ children }: { children: React.ReactNode }) => {
return (
<QueryClientProvider client={queryClient}>
<GoogleTagManagerProvider>
<PostHogProvider>
<ThemeProvider attribute="class" defaultTheme="dark" enableSystem={false}>
{children}
</ThemeProvider>
</PostHogProvider>
<HubSpotProvider>
<PostHogProvider>
<ThemeProvider attribute="class" defaultTheme="dark" enableSystem={false}>
{children}
</ThemeProvider>
</PostHogProvider>
</HubSpotProvider>
</GoogleTagManagerProvider>
</QueryClientProvider>
)

View file

@ -0,0 +1,548 @@
"use client"
import type { ReactNode } from "react"
import { useEffect, useMemo, useRef, useState } from "react"
import { CheckCircle2, Paperclip } from "lucide-react"
import { cn } from "@/lib/utils"
type SlackMessage = {
id: string
author: string
timeLabel: string
body: ReactNode
avatarText: string
avatarClassName: string
kind: "human" | "bot"
}
function usePrefersReducedMotion(): boolean {
const [reduced, setReduced] = useState(false)
useEffect(() => {
const media = window.matchMedia("(prefers-reduced-motion: reduce)")
const onChange = () => setReduced(media.matches)
onChange()
if (typeof media.addEventListener === "function") {
media.addEventListener("change", onChange)
return () => media.removeEventListener("change", onChange)
}
media.addListener?.(onChange)
return () => media.removeListener?.(onChange)
}, [])
return reduced
}
type TypingDotsProps = {
className?: string
}
function TypingDots({ className }: TypingDotsProps): JSX.Element {
return (
<span className={cn("inline-flex items-center gap-1", className)} aria-hidden="true">
<span className="h-1.5 w-1.5 rounded-full bg-[#8B8D91] animate-pulse [animation-delay:0ms]" />
<span className="h-1.5 w-1.5 rounded-full bg-[#8B8D91] animate-pulse [animation-delay:180ms]" />
<span className="h-1.5 w-1.5 rounded-full bg-[#8B8D91] animate-pulse [animation-delay:360ms]" />
</span>
)
}
type FakeLinkProps = {
children: ReactNode
className?: string
}
function FakeLink({ children, className }: FakeLinkProps): JSX.Element {
return (
<span className={cn("text-violet-300 underline underline-offset-2", "cursor-default", className)}>
{children}
</span>
)
}
type SlackMessageRowProps = {
message: SlackMessage
isNew: boolean
reduceMotion: boolean
}
function SlackMessageRow({ message, isNew, reduceMotion }: SlackMessageRowProps): JSX.Element {
let animation = ""
if (!reduceMotion && isNew) {
animation = "animate-in fade-in slide-in-from-bottom-2 duration-500"
}
return (
<div className={cn("flex gap-3", animation)}>
<div
className={cn(
"mt-0.5 flex h-9 w-9 shrink-0 items-center justify-center rounded-md text-xs font-semibold",
message.avatarClassName,
)}>
{message.avatarText}
</div>
<div className="min-w-0">
<div className="flex flex-wrap items-baseline gap-x-2 gap-y-1">
<span className="text-[13px] font-semibold text-[#F8F8F9]">{message.author}</span>
<span className="text-[11px] text-[#8B8D91]">{message.timeLabel}</span>
{message.kind === "bot" && (
<span className="inline-flex items-center gap-1 rounded-full bg-violet-500/20 px-2 py-0.5 text-[10px] font-medium text-violet-200">
<CheckCircle2 className="h-3 w-3" />
App
</span>
)}
</div>
<div className="mt-1 text-[13px] leading-relaxed text-[#D1D2D3]">{message.body}</div>
</div>
</div>
)
}
export type SlackThreadDemoProps = {
className?: string
}
export function SlackThreadDemo({ className }: SlackThreadDemoProps): JSX.Element {
const reduceMotion = usePrefersReducedMotion()
const [stepIndex, setStepIndex] = useState(0)
const scrollViewportRef = useRef<HTMLDivElement>(null)
const messages: SlackMessage[] = useMemo(
() => [
{
id: "m1",
author: "Avery Lee",
timeLabel: "Monday at 2:56 PM",
avatarText: "AL",
avatarClassName: "bg-[#2B2D31] text-[#F8F8F9] ring-1 ring-white/10",
kind: "human",
body: (
<span>We need to add a page to our Marketing site that highlights using Roo Code from Slack.</span>
),
},
{
id: "m2",
author: "Avery Lee",
timeLabel: "Monday at 2:58 PM",
avatarText: "AL",
avatarClassName: "bg-[#2B2D31] text-[#F8F8F9] ring-1 ring-white/10",
kind: "human",
body: (
<div className="space-y-2">
<div>
The documentation for using Roo Code from Slack is here:{" "}
<FakeLink className="hover:text-violet-200">
https://docs.roocode.com/roo-code-cloud/slack-integration
</FakeLink>
</div>
<div className="text-[#B8BBC0]">Here are some pages from our site we can use for guidance:</div>
<ol className="list-decimal pl-5 text-[#D1D2D3]">
<li>
<FakeLink className="hover:text-violet-200">https://roocode.com</FakeLink>
</li>
<li>
<FakeLink className="hover:text-violet-200">https://roocode.com/extension</FakeLink>
</li>
<li>
<FakeLink className="hover:text-violet-200">https://roocode.com/cloud</FakeLink>
</li>
</ol>
</div>
),
},
{
id: "m3",
author: "Avery Lee",
timeLabel: "Monday at 3:08 PM",
avatarText: "AL",
avatarClassName: "bg-[#2B2D31] text-[#F8F8F9] ring-1 ring-white/10",
kind: "human",
body: (
<div className="space-y-3">
<div>This is the start of a wireframe I have in mind for this page</div>
<div className="w-full max-w-[420px] rounded-lg border border-white/10 bg-black/20 p-3">
<div className="flex items-center gap-2 text-[12px] text-[#B8BBC0]">
<Paperclip className="h-4 w-4" />
IMG_9721.heic
</div>
<div className="mt-3 h-24 w-full rounded-md bg-gradient-to-br from-white/10 via-white/5 to-white/0" />
</div>
</div>
),
},
{
id: "m4",
author: "Avery Lee",
timeLabel: "Monday at 3:09 PM",
avatarText: "AL",
avatarClassName: "bg-[#2B2D31] text-[#F8F8F9] ring-1 ring-white/10",
kind: "human",
body: (
<span>
<FakeLink className="no-underline hover:text-violet-200">@Roomote</FakeLink> let&apos;s create
the plan to deliver this
</span>
),
},
{
id: "m5",
author: "Roomote",
timeLabel: "Monday at 3:09 PM",
avatarText: "R",
avatarClassName: "bg-violet-500/20 text-violet-200 ring-1 ring-violet-500/30",
kind: "bot",
body: (
<div className="space-y-3">
<div className="rounded-lg border border-white/10 bg-black/20 px-3 py-2 text-[#D1D2D3]">
Calling <span className="font-semibold text-[#F8F8F9]">Planneroo</span> to get started on
your task on{" "}
<code className="rounded bg-white/10 px-1.5 py-0.5 text-[12px] text-[#F8F8F9]">
RooCodeInc/Roo-Code
</code>
</div>
<div>
<button
type="button"
className="inline-flex items-center rounded-md border border-white/10 bg-transparent px-2 py-1 text-[12px] font-medium text-[#D1D2D3] hover:bg-white/5">
Cancel
</button>
</div>
</div>
),
},
{
id: "m6",
author: "Roomote",
timeLabel: "Monday at 3:10 PM",
avatarText: "R",
avatarClassName: "bg-violet-500/20 text-violet-200 ring-1 ring-violet-500/30",
kind: "bot",
body: (
<div className="space-x-2">
<span>Cool, I&apos;ll knock this out real quick.</span>
<FakeLink className="hover:text-violet-200">Follow along</FakeLink>
</div>
),
},
{
id: "m7",
author: "Roomote",
timeLabel: "Monday at 3:12 PM",
avatarText: "R",
avatarClassName: "bg-violet-500/20 text-violet-200 ring-1 ring-violet-500/30",
kind: "bot",
body: (
<div className="space-y-2">
<div className="font-semibold text-[#F8F8F9]">Todo List:</div>
<div className="rounded-lg border border-white/10 bg-black/20 px-3 py-2">
<ul className="space-y-1">
{[
"Analyze existing page structures and component patterns",
"Review marketing content requirements and wireframe details",
"Create detailed component architecture plan",
"Design page structure and section breakdown",
"Plan navigation updates and integration points",
"Test the page and verify all sections work",
].map((item) => (
<li key={item} className="text-[#D1D2D3]">
<span className="mr-2"></span>
<span className="line-through opacity-80">{item}</span>
</li>
))}
</ul>
</div>
<div className="text-[12px] text-[#8B8D91]">(edited)</div>
</div>
),
},
{
id: "m8",
author: "Roomote",
timeLabel: "Monday at 3:16 PM",
avatarText: "R",
avatarClassName: "bg-violet-500/20 text-violet-200 ring-1 ring-violet-500/30",
kind: "bot",
body: (
<div className="space-y-3">
<p>
I&apos;ve created a comprehensive implementation plan for the Roo Code Slack integration
marketing page at{" "}
<code className="rounded bg-white/10 px-1.5 py-0.5 text-[12px] text-[#F8F8F9]">
plans/slack-marketing-page-plan.md
</code>
.
</p>
<div className="rounded-lg border border-white/10 bg-black/20 px-3 py-2">
<div className="text-[12px] font-semibold text-[#F8F8F9]">Plan Overview</div>
<ul className="mt-2 space-y-1 text-[#D1D2D3]">
<li>
<span className="mr-2"></span>Hero + dual CTAs
</li>
<li>
<span className="mr-2"></span>Value props grid
</li>
<li>
<span className="mr-2"></span>Thread to Shipped Feature workflow
</li>
<li>
<span className="mr-2"></span>Onboarding steps + CTA
</li>
</ul>
</div>
<div className="space-x-2">
<span className="text-[#B8BBC0]">Full document:</span>
<FakeLink className="hover:text-violet-200">View artifact</FakeLink>
</div>
<div className="text-[12px] italic text-[#8B8D91]">
Want to follow up? Just @-mention me in your response.
</div>
</div>
),
},
{
id: "m9",
author: "Avery Lee",
timeLabel: "Monday at 3:17 PM",
avatarText: "AL",
avatarClassName: "bg-[#2B2D31] text-[#F8F8F9] ring-1 ring-white/10",
kind: "human",
body: (
<div className="space-x-2">
<FakeLink className="no-underline hover:text-violet-200">@Roomote</FakeLink>
<span>this looks great, let&apos;s use Coderoo to build this</span>
</div>
),
},
{
id: "m10",
author: "Roomote",
timeLabel: "Monday at 3:23 PM",
avatarText: "R",
avatarClassName: "bg-violet-500/20 text-violet-200 ring-1 ring-violet-500/30",
kind: "bot",
body: (
<div className="space-y-3">
<p>
I&apos;ve built the Roo Code Slack integration marketing page. Here&apos;s what was
implemented:
</p>
<div className="rounded-lg border border-white/10 bg-black/20 px-3 py-2">
<div className="text-[12px] font-semibold text-[#F8F8F9]">Files</div>
<ul className="mt-2 space-y-1 text-[#D1D2D3]">
<li>
<span className="mr-2"></span>
<code className="rounded bg-white/10 px-1.5 py-0.5 text-[12px] text-[#F8F8F9]">
apps/web-roo-code/src/app/slack/page.tsx
</code>{" "}
Slack marketing page
</li>
<li>
<span className="mr-2"></span>
<code className="rounded bg-white/10 px-1.5 py-0.5 text-[12px] text-[#F8F8F9]">
apps/web-roo-code/src/lib/constants.ts
</code>{" "}
added{" "}
<code className="rounded bg-white/10 px-1.5 py-0.5 text-[12px] text-[#F8F8F9]">
SLACK_DOCS
</code>
</li>
<li>
<span className="mr-2"></span>
<code className="rounded bg-white/10 px-1.5 py-0.5 text-[12px] text-[#F8F8F9]">
apps/web-roo-code/src/components/chromes/nav-bar.tsx
</code>{" "}
added Slack to Product dropdown
</li>
</ul>
</div>
<div className="rounded-lg border border-white/10 bg-black/20 px-3 py-2">
<div className="text-[12px] font-semibold text-[#F8F8F9]">Pull Request</div>
<div className="mt-2">
<span className="font-semibold text-[#F8F8F9]">PR #10853</span>:{" "}
<FakeLink className="hover:text-violet-200">
https://github.com/RooCodeInc/Roo-Code/pull/10853
</FakeLink>
</div>
</div>
<p className="text-[#B8BBC0]">
The page is accessible at{" "}
<code className="rounded bg-white/10 px-1.5 py-0.5 text-[12px] text-[#F8F8F9]">/slack</code>{" "}
and includes navigation links in desktop and mobile.
</p>
</div>
),
},
],
[],
)
type DemoPhase =
| { kind: "show"; messageIndex: number }
| { kind: "typing"; messageIndex: number }
| { kind: "reset" }
const phases: DemoPhase[] = useMemo(() => {
const next: DemoPhase[] = []
if (messages.length === 0) return [{ kind: "reset" }]
next.push({ kind: "typing", messageIndex: 0 })
next.push({ kind: "show", messageIndex: 0 })
for (let messageIndex = 1; messageIndex < messages.length; messageIndex += 1) {
next.push({ kind: "typing", messageIndex })
next.push({ kind: "show", messageIndex })
}
next.push({ kind: "reset" })
return next
}, [messages])
const lastShowPhaseIndex = useMemo(() => {
let lastIndex = -1
for (let idx = 0; idx < phases.length; idx += 1) {
if (phases[idx]?.kind === "show") lastIndex = idx
}
return lastIndex
}, [phases])
useEffect(() => {
if (reduceMotion) {
setStepIndex(lastShowPhaseIndex >= 0 ? lastShowPhaseIndex : 0)
return
}
const active = phases[stepIndex] ?? phases.at(0)
const isLastMessageShow = active?.kind === "show" && stepIndex === lastShowPhaseIndex
const durationMs = (() => {
const base = 2200
if (active?.kind === "reset") return 500
if (active?.kind === "typing") return 900
return isLastMessageShow ? base * 2 : base
})()
const timer = window.setTimeout(() => {
setStepIndex((prev) => (prev + 1) % phases.length)
}, durationMs)
return () => window.clearTimeout(timer)
}, [lastShowPhaseIndex, phases, reduceMotion, stepIndex])
const activePhase = phases[stepIndex] ?? phases.at(0) ?? { kind: "reset" }
function getVisibleCount(phase: DemoPhase): number {
if (phase.kind === "reset") return 0
if (phase.kind === "typing") return phase.messageIndex
return phase.messageIndex + 1
}
const visibleCount = getVisibleCount(activePhase)
const visibleMessages = messages.slice(0, visibleCount)
const typingTarget = activePhase.kind === "typing" ? messages[activePhase.messageIndex] : undefined
useEffect(() => {
const viewport = scrollViewportRef.current
if (!viewport) return
if (activePhase.kind === "reset" || visibleCount <= 1) {
viewport.scrollTo({ top: 0, behavior: "auto" })
return
}
viewport.scrollTo({
top: viewport.scrollHeight,
behavior: reduceMotion ? "auto" : "smooth",
})
}, [activePhase.kind, reduceMotion, visibleCount])
return (
<div
className={cn("w-full max-w-[620px] h-[520px] sm:h-[560px]", className)}
role="img"
aria-label="Animated Slack thread showing Roo Code responding as @Roomote">
<div
aria-hidden="true"
className="relative flex h-full flex-col overflow-hidden rounded-2xl border border-white/10 bg-[#1A1D21] shadow-2xl shadow-black/30">
<div className="flex items-center justify-between border-b border-white/10 px-4 py-3">
<div className="flex items-center gap-2">
<div className="h-2.5 w-2.5 rounded-full bg-[#F24A4A]" />
<div className="h-2.5 w-2.5 rounded-full bg-[#F2C94C]" />
<div className="h-2.5 w-2.5 rounded-full bg-[#27AE60]" />
<div className="ml-3 text-sm font-semibold text-[#F8F8F9]">Thread</div>
</div>
<div className="flex items-center gap-2 text-[11px] text-[#8B8D91]">
<span className="h-2 w-2 rounded-full bg-[#27AE60]" />
Live demo
</div>
</div>
<div
ref={scrollViewportRef}
className="flex-1 overflow-y-auto px-4 py-5 [scrollbar-width:thin] [scrollbar-color:rgba(255,255,255,0.18)_transparent]">
<div
className={cn(
"space-y-5 transition-opacity duration-300 will-change-opacity",
activePhase.kind === "reset" ? "opacity-0" : "opacity-100",
)}>
{visibleMessages.map((message) => (
<SlackMessageRow
key={message.id}
message={message}
reduceMotion={reduceMotion}
isNew={
activePhase.kind === "show" && messages[activePhase.messageIndex]?.id === message.id
}
/>
))}
{typingTarget && (
<div className={cn(reduceMotion ? "" : "animate-in fade-in duration-300", "flex gap-3")}>
<div
className={cn(
"mt-0.5 flex h-9 w-9 shrink-0 items-center justify-center rounded-md text-xs font-semibold",
typingTarget.avatarClassName,
)}>
{typingTarget.avatarText}
</div>
<div className="min-w-0">
<div className="flex items-baseline gap-x-2">
<span className="text-[13px] font-semibold text-[#F8F8F9]">
{typingTarget.author}
</span>
{typingTarget.kind === "bot" && (
<span className="inline-flex items-center gap-1 rounded-full bg-violet-500/20 px-2 py-0.5 text-[10px] font-medium text-violet-200">
<CheckCircle2 className="h-3 w-3" />
App
</span>
)}
<span className="text-[11px] text-[#8B8D91]">typing</span>
</div>
<div className="mt-2">
<TypingDots />
</div>
</div>
</div>
)}
</div>
</div>
<div className="flex items-center justify-between border-t border-white/10 px-4 py-3">
<div className="flex items-center gap-1.5">
{messages.map((message, idx) => (
<span
key={message.id}
className={cn(
"h-1.5 w-5 rounded-full transition-colors duration-300",
Math.max(0, visibleCount - 1) === idx ? "bg-violet-300" : "bg-white/10",
)}
/>
))}
</div>
</div>
</div>
</div>
)
}

View file

@ -9,6 +9,7 @@ export const EXTERNAL_LINKS = {
BLUESKY: "https://bsky.app/profile/roocode.bsky.social",
YOUTUBE: "https://www.youtube.com/@RooCodeYT",
DOCUMENTATION: "https://docs.roocode.com",
SLACK_DOCS: "https://docs.roocode.com/roo-code-cloud/slack-integration",
CAREERS: "https://careers.roocode.com",
ISSUES: "https://github.com/RooCodeInc/Roo-Code/issues",
FEATURE_REQUESTS: "https://github.com/RooCodeInc/Roo-Code/discussions/categories/feature-requests",
@ -28,6 +29,7 @@ export const EXTERNAL_LINKS = {
CLOUD_APP_SIGNUP: "https://app.roocode.com/sign-up",
CLOUD_APP_SIGNUP_HOME: "https://app.roocode.com/sign-up?redirect_url=/cloud-agents/setup",
CLOUD_APP_SIGNUP_PRO: "https://app.roocode.com/sign-up?redirect_url=/cloud-agents/setup",
CLOUD_APP_TEAM_TRIAL: "https://app.roocode.com/checkout/team",
SUPPORT: "mailto:support@roocode.com",
}

View file

@ -18,6 +18,7 @@
"@roo-code/types": "workspace:^",
"esbuild": "^0.25.0",
"execa": "^9.5.2",
"ignore": "^7.0.3",
"openai": "^5.12.2",
"zod": "^3.25.61"
},

View file

@ -1,129 +0,0 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`XML Protocol snapshots > should generate correct XML description for all fixtures combined 1`] = `
"# Custom Tools
The following custom tools are available for this mode. Use them in the same way as built-in tools.
## simple
Description: Simple tool
Parameters:
- value: (required) The input value (type: string)
Usage:
<simple>
<value>value value here</value>
</simple>
## cached
Description: Cached tool
Parameters:
Usage:
<cached>
</cached>
## legacy
Description: Legacy tool using args
Parameters:
- input: (required) The input string (type: string)
Usage:
<legacy>
<input>input value here</input>
</legacy>
## multi_toolA
Description: Tool A
Parameters:
Usage:
<multi_toolA>
</multi_toolA>
## multi_toolB
Description: Tool B
Parameters:
Usage:
<multi_toolB>
</multi_toolB>
## mixed_validTool
Description: Valid
Parameters:
Usage:
<mixed_validTool>
</mixed_validTool>"
`;
exports[`XML Protocol snapshots > should generate correct XML description for cached tool 1`] = `
"# Custom Tools
The following custom tools are available for this mode. Use them in the same way as built-in tools.
## cached
Description: Cached tool
Parameters:
Usage:
<cached>
</cached>"
`;
exports[`XML Protocol snapshots > should generate correct XML description for legacy tool (using args) 1`] = `
"# Custom Tools
The following custom tools are available for this mode. Use them in the same way as built-in tools.
## legacy
Description: Legacy tool using args
Parameters:
- input: (required) The input string (type: string)
Usage:
<legacy>
<input>input value here</input>
</legacy>"
`;
exports[`XML Protocol snapshots > should generate correct XML description for mixed export tool 1`] = `
"# Custom Tools
The following custom tools are available for this mode. Use them in the same way as built-in tools.
## mixed_validTool
Description: Valid
Parameters:
Usage:
<mixed_validTool>
</mixed_validTool>"
`;
exports[`XML Protocol snapshots > should generate correct XML description for multi export tools 1`] = `
"# Custom Tools
The following custom tools are available for this mode. Use them in the same way as built-in tools.
## multi_toolA
Description: Tool A
Parameters:
Usage:
<multi_toolA>
</multi_toolA>
## multi_toolB
Description: Tool B
Parameters:
Usage:
<multi_toolB>
</multi_toolB>"
`;
exports[`XML Protocol snapshots > should generate correct XML description for simple tool 1`] = `
"# Custom Tools
The following custom tools are available for this mode. Use them in the same way as built-in tools.
## simple
Description: Simple tool
Parameters:
- value: (required) The input value (type: string)
Usage:
<simple>
<value>value value here</value>
</simple>"
`;

View file

@ -1,192 +0,0 @@
// pnpm --filter @roo-code/core test src/custom-tools/__tests__/format-xml.spec.ts
import { type SerializedCustomToolDefinition, parametersSchema as z, defineCustomTool } from "@roo-code/types"
import { serializeCustomTool, serializeCustomTools } from "../serialize.js"
import { formatXml } from "../format-xml.js"
import simpleTool from "./fixtures/simple.js"
import cachedTool from "./fixtures/cached.js"
import legacyTool from "./fixtures/legacy.js"
import { toolA, toolB } from "./fixtures/multi.js"
import { validTool as mixedValidTool } from "./fixtures/mixed.js"
const fixtureTools = {
simple: simpleTool,
cached: cachedTool,
legacy: legacyTool,
multi_toolA: toolA,
multi_toolB: toolB,
mixed_validTool: mixedValidTool,
}
describe("formatXml", () => {
it("should return empty string for empty tools array", () => {
expect(formatXml([])).toBe("")
})
it("should throw for undefined tools", () => {
expect(() => formatXml(undefined as unknown as SerializedCustomToolDefinition[])).toThrow()
})
it("should generate description for a single tool without args", () => {
const tool = defineCustomTool({
name: "my_tool",
description: "A simple tool that does something",
async execute() {
return "done"
},
})
const serialized = serializeCustomTool(tool)
const result = formatXml([serialized])
expect(result).toContain("# Custom Tools")
expect(result).toContain("## my_tool")
expect(result).toContain("Description: A simple tool that does something")
expect(result).toContain("Parameters: None")
expect(result).toContain("<my_tool>")
expect(result).toContain("</my_tool>")
})
it("should generate description for a tool with required args", () => {
const tool = defineCustomTool({
name: "greeter",
description: "Greets a person by name",
parameters: z.object({
name: z.string().describe("The name of the person to greet"),
}),
async execute({ name }) {
return `Hello, ${name}!`
},
})
const serialized = serializeCustomTool(tool)
const result = formatXml([serialized])
expect(result).toContain("## greeter")
expect(result).toContain("Description: Greets a person by name")
expect(result).toContain("Parameters:")
expect(result).toContain("- name: (required) The name of the person to greet (type: string)")
expect(result).toContain("<greeter>")
expect(result).toContain("<name>name value here</name>")
expect(result).toContain("</greeter>")
})
it("should generate description for a tool with optional args", () => {
const tool = defineCustomTool({
name: "configurable_tool",
description: "A tool with optional configuration",
parameters: z.object({
input: z.string().describe("The input to process"),
format: z.string().optional().describe("Output format"),
}),
async execute({ input, format }) {
return format ? `${input} (${format})` : input
},
})
const serialized = serializeCustomTool(tool)
const result = formatXml([serialized])
expect(result).toContain("- input: (required) The input to process (type: string)")
expect(result).toContain("- format: (optional) Output format (type: string)")
expect(result).toContain("<input>input value here</input>")
expect(result).toContain("<format>optional format value</format>")
})
it("should generate descriptions for multiple tools", () => {
const tools = [
defineCustomTool({
name: "tool_a",
description: "First tool",
async execute() {
return "a"
},
}),
defineCustomTool({
name: "tool_b",
description: "Second tool",
parameters: z.object({
value: z.number().describe("A numeric value"),
}),
async execute() {
return "b"
},
}),
]
const serialized = serializeCustomTools(tools)
const result = formatXml(serialized)
expect(result).toContain("## tool_a")
expect(result).toContain("Description: First tool")
expect(result).toContain("## tool_b")
expect(result).toContain("Description: Second tool")
expect(result).toContain("- value: (required) A numeric value (type: number)")
})
it("should treat args in required array as required", () => {
// Using a raw SerializedToolDefinition to test the required behavior.
const tools: SerializedCustomToolDefinition[] = [
{
name: "test_tool",
description: "Test tool",
parameters: {
type: "object",
properties: {
data: {
type: "object",
description: "Some data",
},
},
required: ["data"],
},
},
]
const result = formatXml(tools)
expect(result).toContain("- data: (required) Some data (type: object)")
expect(result).toContain("<data>data value here</data>")
})
})
describe("XML Protocol snapshots", () => {
it("should generate correct XML description for simple tool", () => {
const serialized = serializeCustomTool(fixtureTools.simple)
const result = formatXml([serialized])
expect(result).toMatchSnapshot()
})
it("should generate correct XML description for cached tool", () => {
const serialized = serializeCustomTool(fixtureTools.cached)
const result = formatXml([serialized])
expect(result).toMatchSnapshot()
})
it("should generate correct XML description for legacy tool (using args)", () => {
const serialized = serializeCustomTool(fixtureTools.legacy)
const result = formatXml([serialized])
expect(result).toMatchSnapshot()
})
it("should generate correct XML description for multi export tools", () => {
const serializedA = serializeCustomTool(fixtureTools.multi_toolA)
const serializedB = serializeCustomTool(fixtureTools.multi_toolB)
const result = formatXml([serializedA, serializedB])
expect(result).toMatchSnapshot()
})
it("should generate correct XML description for mixed export tool", () => {
const serialized = serializeCustomTool(fixtureTools.mixed_validTool)
const result = formatXml([serialized])
expect(result).toMatchSnapshot()
})
it("should generate correct XML description for all fixtures combined", () => {
const allSerialized = Object.values(fixtureTools).map(serializeCustomTool)
const result = formatXml(allSerialized)
expect(result).toMatchSnapshot()
})
})

View file

@ -1,89 +0,0 @@
import type { SerializedCustomToolDefinition, SerializedCustomToolParameters } from "@roo-code/types"
/**
* Extract the type string from a parameter schema.
* Handles both direct `type` property and `anyOf` schemas (used for nullable types).
*/
function getParameterType(parameter: SerializedCustomToolParameters): string {
// Direct type property
if (parameter.type) {
return String(parameter.type)
}
// Handle anyOf schema (used for nullable types like `string | null`)
if (parameter.anyOf && Array.isArray(parameter.anyOf)) {
const types = parameter.anyOf
.map((schema) => (typeof schema === "object" && schema.type ? String(schema.type) : null))
.filter((t): t is string => t !== null && t !== "null")
if (types.length > 0) {
return types.join(" | ")
}
}
return "unknown"
}
function getParameterDescription(name: string, parameter: SerializedCustomToolParameters, required: string[]): string {
const requiredText = required.includes(name) ? "(required)" : "(optional)"
const typeText = getParameterType(parameter)
return `- ${name}: ${requiredText} ${parameter.description ?? ""} (type: ${typeText})`
}
function getUsage(tool: SerializedCustomToolDefinition): string {
const lines: string[] = [`<${tool.name}>`]
if (tool.parameters) {
const required = tool.parameters.required ?? []
for (const [argName, _argType] of Object.entries(tool.parameters.properties ?? {})) {
const placeholder = required.includes(argName) ? `${argName} value here` : `optional ${argName} value`
lines.push(`<${argName}>${placeholder}</${argName}>`)
}
}
lines.push(`</${tool.name}>`)
return lines.join("\n")
}
function getDescription(tool: SerializedCustomToolDefinition): string {
const parts: string[] = []
parts.push(`## ${tool.name}`)
parts.push(`Description: ${tool.description}`)
if (tool.parameters?.properties) {
const required = tool.parameters?.required ?? []
parts.push("Parameters:")
for (const [name, parameter] of Object.entries(tool.parameters.properties)) {
// What should we do with `boolean` values for `parameter`?
if (typeof parameter !== "object") {
continue
}
parts.push(getParameterDescription(name, parameter, required))
}
} else {
parts.push("Parameters: None")
}
parts.push("Usage:")
parts.push(getUsage(tool))
return parts.join("\n")
}
export function formatXml(tools: SerializedCustomToolDefinition[]): string {
if (tools.length === 0) {
return ""
}
const descriptions = tools.map((tool) => getDescription(tool))
return `# Custom Tools
The following custom tools are available for this mode. Use them in the same way as built-in tools.
${descriptions.join("\n\n")}`
}

View file

@ -1,4 +1,3 @@
export * from "./custom-tool-registry.js"
export * from "./serialize.js"
export * from "./format-xml.js"
export * from "./format-native.js"

View file

@ -1,3 +1,4 @@
export * from "./custom-tools/index.js"
export * from "./debug-log/index.js"
export * from "./message-utils/index.js"
export * from "./worktree/index.js"

View file

@ -0,0 +1,306 @@
import * as fs from "fs/promises"
import * as path from "path"
import * as os from "os"
import { execFile } from "child_process"
import { promisify } from "util"
import { WorktreeIncludeService } from "../worktree-include.js"
const execFileAsync = promisify(execFile)
async function execGit(cwd: string, args: string[]): Promise<string> {
const { stdout } = await execFileAsync("git", args, { cwd, encoding: "utf8" })
return stdout
}
describe("WorktreeIncludeService", () => {
let service: WorktreeIncludeService
let tempDir: string
beforeEach(async () => {
service = new WorktreeIncludeService()
// Create a temp directory for each test
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "worktree-test-"))
})
afterEach(async () => {
// Clean up temp directory
try {
await fs.rm(tempDir, { recursive: true })
} catch {
// Ignore cleanup errors
}
})
describe("hasWorktreeInclude", () => {
it("should return true when .worktreeinclude exists", async () => {
await fs.writeFile(path.join(tempDir, ".worktreeinclude"), "node_modules")
const result = await service.hasWorktreeInclude(tempDir)
expect(result).toBe(true)
})
it("should return false when .worktreeinclude does not exist", async () => {
const result = await service.hasWorktreeInclude(tempDir)
expect(result).toBe(false)
})
it("should return false for non-existent directory", async () => {
const result = await service.hasWorktreeInclude("/non/existent/path")
expect(result).toBe(false)
})
})
describe("branchHasWorktreeInclude", () => {
it("should detect .worktreeinclude on the specified branch", async () => {
const repoDir = path.join(tempDir, "repo")
await fs.mkdir(repoDir, { recursive: true })
await execGit(repoDir, ["init"])
await execGit(repoDir, ["config", "user.name", "Test User"])
await execGit(repoDir, ["config", "user.email", "test@example.com"])
await fs.writeFile(path.join(repoDir, "README.md"), "test")
await execGit(repoDir, ["add", "README.md"])
await execGit(repoDir, ["commit", "-m", "init"])
const baseBranch = (await execGit(repoDir, ["rev-parse", "--abbrev-ref", "HEAD"])).trim()
expect(await service.branchHasWorktreeInclude(repoDir, baseBranch)).toBe(false)
await execGit(repoDir, ["checkout", "-b", "with-include"])
await fs.writeFile(path.join(repoDir, ".worktreeinclude"), "node_modules")
await execGit(repoDir, ["add", ".worktreeinclude"])
await execGit(repoDir, ["commit", "-m", "add include"])
expect(await service.branchHasWorktreeInclude(repoDir, "with-include")).toBe(true)
}, 30_000)
})
describe("getStatus", () => {
it("should return correct status when both files exist", async () => {
const gitignoreContent = "node_modules\n.env\ndist"
await fs.writeFile(path.join(tempDir, ".worktreeinclude"), "node_modules")
await fs.writeFile(path.join(tempDir, ".gitignore"), gitignoreContent)
const result = await service.getStatus(tempDir)
expect(result.exists).toBe(true)
expect(result.hasGitignore).toBe(true)
expect(result.gitignoreContent).toBe(gitignoreContent)
})
it("should return correct status when only .gitignore exists", async () => {
const gitignoreContent = "node_modules\n.env"
await fs.writeFile(path.join(tempDir, ".gitignore"), gitignoreContent)
const result = await service.getStatus(tempDir)
expect(result.exists).toBe(false)
expect(result.hasGitignore).toBe(true)
expect(result.gitignoreContent).toBe(gitignoreContent)
})
it("should return correct status when only .worktreeinclude exists", async () => {
await fs.writeFile(path.join(tempDir, ".worktreeinclude"), "node_modules")
const result = await service.getStatus(tempDir)
expect(result.exists).toBe(true)
expect(result.hasGitignore).toBe(false)
expect(result.gitignoreContent).toBeUndefined()
})
it("should return correct status when neither file exists", async () => {
const result = await service.getStatus(tempDir)
expect(result.exists).toBe(false)
expect(result.hasGitignore).toBe(false)
expect(result.gitignoreContent).toBeUndefined()
})
})
describe("createWorktreeInclude", () => {
it("should create .worktreeinclude file with specified content", async () => {
const content = "node_modules\n.env\ndist"
await service.createWorktreeInclude(tempDir, content)
const fileContent = await fs.readFile(path.join(tempDir, ".worktreeinclude"), "utf-8")
expect(fileContent).toBe(content)
})
it("should overwrite existing .worktreeinclude file", async () => {
await fs.writeFile(path.join(tempDir, ".worktreeinclude"), "old content")
const newContent = "new content"
await service.createWorktreeInclude(tempDir, newContent)
const fileContent = await fs.readFile(path.join(tempDir, ".worktreeinclude"), "utf-8")
expect(fileContent).toBe(newContent)
})
})
describe("copyWorktreeIncludeFiles", () => {
let sourceDir: string
let targetDir: string
beforeEach(async () => {
sourceDir = path.join(tempDir, "source")
targetDir = path.join(tempDir, "target")
await fs.mkdir(sourceDir, { recursive: true })
await fs.mkdir(targetDir, { recursive: true })
})
it("should return empty array when no .worktreeinclude exists", async () => {
await fs.writeFile(path.join(sourceDir, ".gitignore"), "node_modules")
const result = await service.copyWorktreeIncludeFiles(sourceDir, targetDir)
expect(result).toEqual([])
})
it("should return empty array when no .gitignore exists", async () => {
await fs.writeFile(path.join(sourceDir, ".worktreeinclude"), "node_modules")
const result = await service.copyWorktreeIncludeFiles(sourceDir, targetDir)
expect(result).toEqual([])
})
it("should return empty array when patterns do not match", async () => {
// .worktreeinclude wants node_modules, .gitignore only ignores .env
await fs.writeFile(path.join(sourceDir, ".worktreeinclude"), "node_modules")
await fs.writeFile(path.join(sourceDir, ".gitignore"), ".env")
await fs.mkdir(path.join(sourceDir, "node_modules"), { recursive: true })
const result = await service.copyWorktreeIncludeFiles(sourceDir, targetDir)
expect(result).toEqual([])
})
it("should copy files that match both patterns", async () => {
// Both files include node_modules
await fs.writeFile(path.join(sourceDir, ".worktreeinclude"), "node_modules")
await fs.writeFile(path.join(sourceDir, ".gitignore"), "node_modules")
// Create a file in node_modules
await fs.mkdir(path.join(sourceDir, "node_modules"), { recursive: true })
await fs.writeFile(path.join(sourceDir, "node_modules", "package.json"), '{"name": "test"}')
const result = await service.copyWorktreeIncludeFiles(sourceDir, targetDir)
expect(result).toContain("node_modules")
// Verify the file was copied
const copiedContent = await fs.readFile(path.join(targetDir, "node_modules", "package.json"), "utf-8")
expect(copiedContent).toBe('{"name": "test"}')
})
it("should only copy intersection of patterns", async () => {
// .worktreeinclude: node_modules, dist
// .gitignore: node_modules, .env
// Only node_modules should be copied (intersection)
await fs.writeFile(path.join(sourceDir, ".worktreeinclude"), "node_modules\ndist")
await fs.writeFile(path.join(sourceDir, ".gitignore"), "node_modules\n.env")
await fs.mkdir(path.join(sourceDir, "node_modules"), { recursive: true })
await fs.mkdir(path.join(sourceDir, "dist"), { recursive: true })
await fs.writeFile(path.join(sourceDir, ".env"), "SECRET=123")
await fs.writeFile(path.join(sourceDir, "node_modules", "test.txt"), "test")
await fs.writeFile(path.join(sourceDir, "dist", "main.js"), "console.log('dist')")
const result = await service.copyWorktreeIncludeFiles(sourceDir, targetDir)
// Only node_modules should be in the result (matches both)
expect(result).toContain("node_modules")
expect(result).not.toContain("dist") // only in .worktreeinclude
expect(result).not.toContain(".env") // only in .gitignore
// Verify node_modules was copied
const nodeModulesExists = await fs
.access(path.join(targetDir, "node_modules"))
.then(() => true)
.catch(() => false)
expect(nodeModulesExists).toBe(true)
// Verify dist was NOT copied
const distExists = await fs
.access(path.join(targetDir, "dist"))
.then(() => true)
.catch(() => false)
expect(distExists).toBe(false)
})
it("should skip .git directory", async () => {
await fs.writeFile(path.join(sourceDir, ".worktreeinclude"), ".git")
await fs.writeFile(path.join(sourceDir, ".gitignore"), ".git")
await fs.mkdir(path.join(sourceDir, ".git"), { recursive: true })
await fs.writeFile(path.join(sourceDir, ".git", "config"), "[core]")
const result = await service.copyWorktreeIncludeFiles(sourceDir, targetDir)
expect(result).not.toContain(".git")
})
it("should copy single files", async () => {
await fs.writeFile(path.join(sourceDir, ".worktreeinclude"), ".env.local")
await fs.writeFile(path.join(sourceDir, ".gitignore"), ".env.local")
await fs.writeFile(path.join(sourceDir, ".env.local"), "LOCAL_VAR=value")
const result = await service.copyWorktreeIncludeFiles(sourceDir, targetDir)
expect(result).toContain(".env.local")
const copiedContent = await fs.readFile(path.join(targetDir, ".env.local"), "utf-8")
expect(copiedContent).toBe("LOCAL_VAR=value")
})
it("should ignore comment lines in pattern files", async () => {
await fs.writeFile(path.join(sourceDir, ".worktreeinclude"), "# comment\nnode_modules\n# another comment")
await fs.writeFile(path.join(sourceDir, ".gitignore"), "node_modules")
await fs.mkdir(path.join(sourceDir, "node_modules"), { recursive: true })
const result = await service.copyWorktreeIncludeFiles(sourceDir, targetDir)
expect(result).toContain("node_modules")
})
it("should call progress callback with bytesCopied progress", async () => {
// Set up files to copy
await fs.writeFile(path.join(sourceDir, ".worktreeinclude"), "node_modules\n.env.local")
await fs.writeFile(path.join(sourceDir, ".gitignore"), "node_modules\n.env.local")
await fs.mkdir(path.join(sourceDir, "node_modules"), { recursive: true })
await fs.writeFile(path.join(sourceDir, "node_modules", "test.txt"), "test")
await fs.writeFile(path.join(sourceDir, ".env.local"), "LOCAL_VAR=value")
const progressCalls: Array<{ bytesCopied: number; itemName: string }> = []
const onProgress = vi.fn((progress: { bytesCopied: number; itemName: string }) => {
progressCalls.push({ ...progress })
})
await service.copyWorktreeIncludeFiles(sourceDir, targetDir, onProgress)
// Should be called multiple times (initial + after each copy)
expect(onProgress).toHaveBeenCalled()
// bytesCopied should increase over time
expect(progressCalls.length).toBeGreaterThan(0)
const finalCall = progressCalls[progressCalls.length - 1]
expect(finalCall?.bytesCopied).toBeGreaterThan(0)
// Each call should have an item name
expect(progressCalls.every((p) => typeof p.itemName === "string")).toBe(true)
})
it("should not fail when progress callback is not provided", async () => {
await fs.writeFile(path.join(sourceDir, ".worktreeinclude"), "node_modules")
await fs.writeFile(path.join(sourceDir, ".gitignore"), "node_modules")
await fs.mkdir(path.join(sourceDir, "node_modules"), { recursive: true })
// Should not throw when no callback is provided
const result = await service.copyWorktreeIncludeFiles(sourceDir, targetDir)
expect(result).toContain("node_modules")
})
})
})

View file

@ -0,0 +1,146 @@
import * as path from "path"
import { WorktreeService } from "../worktree-service.js"
describe("WorktreeService", () => {
describe("normalizePath", () => {
let service: WorktreeService
beforeEach(() => {
service = new WorktreeService()
})
// Access private method for testing
const callNormalizePath = (service: WorktreeService, p: string): string => {
// @ts-expect-error - accessing private method for testing
return service.normalizePath(p)
}
it("should normalize paths with trailing slashes", () => {
const result = callNormalizePath(service, "/home/user/project/")
expect(result).toBe(path.normalize("/home/user/project"))
})
it("should normalize paths with multiple trailing slashes", () => {
const result = callNormalizePath(service, "/home/user/project///")
// path.normalize already handles multiple slashes
expect(result).toBe(path.normalize("/home/user/project"))
})
it("should preserve root path /", () => {
// This is a critical test - the old regex would turn "/" into ""
// On Windows, path.normalize("/") returns "\", on Unix it returns "/"
const result = callNormalizePath(service, "/")
expect(result).toBe(path.sep)
})
it("should handle paths without trailing slashes", () => {
const result = callNormalizePath(service, "/home/user/project")
expect(result).toBe(path.normalize("/home/user/project"))
})
it("should handle relative paths", () => {
const result = callNormalizePath(service, "./some/path/")
expect(result).toBe(path.normalize("./some/path"))
})
it("should handle empty string", () => {
const result = callNormalizePath(service, "")
expect(result).toBe(".")
})
it("should handle Windows-style paths on non-Windows", () => {
// path.normalize will convert separators appropriately
const result = callNormalizePath(service, "C:\\Users\\test\\project")
// On Unix, this stays as-is; on Windows it would normalize
expect(result).toBeTruthy()
})
})
describe("parseWorktreeOutput", () => {
let service: WorktreeService
beforeEach(() => {
service = new WorktreeService()
})
// Access private method for testing
const callParseWorktreeOutput = (
service: WorktreeService,
output: string,
currentCwd: string,
): ReturnType<WorktreeService["parseWorktreeOutput"]> => {
// @ts-expect-error - accessing private method for testing
return service.parseWorktreeOutput(output, currentCwd)
}
it("should parse porcelain output correctly", () => {
const output = `worktree /home/user/repo
HEAD abc123def456
branch refs/heads/main
worktree /home/user/repo-feature
HEAD def456abc123
branch refs/heads/feature/test
`
const result = callParseWorktreeOutput(service, output, "/home/user/repo")
expect(result).toHaveLength(2)
expect(result[0]).toMatchObject({
path: "/home/user/repo",
branch: "main",
commitHash: "abc123def456",
isCurrent: true,
})
expect(result[1]).toMatchObject({
path: "/home/user/repo-feature",
branch: "feature/test",
commitHash: "def456abc123",
isCurrent: false,
})
})
it("should handle detached HEAD worktrees", () => {
const output = `worktree /home/user/repo-detached
HEAD abc123def456
detached
`
const result = callParseWorktreeOutput(service, output, "/home/user/other")
expect(result).toHaveLength(1)
expect(result[0]).toMatchObject({
path: "/home/user/repo-detached",
isDetached: true,
branch: "",
})
})
it("should handle locked worktrees", () => {
const output = `worktree /home/user/repo-locked
HEAD abc123def456
branch refs/heads/locked-branch
locked some reason here
`
const result = callParseWorktreeOutput(service, output, "/home/user/other")
expect(result).toHaveLength(1)
expect(result[0]).toMatchObject({
isLocked: true,
lockReason: "some reason here",
})
})
it("should handle bare worktrees", () => {
const output = `worktree /home/user/repo.git
bare
`
const result = callParseWorktreeOutput(service, output, "/home/user/other")
expect(result).toHaveLength(1)
expect(result[0]).toMatchObject({
path: "/home/user/repo.git",
isBare: true,
})
})
})
})

View file

@ -0,0 +1,13 @@
/**
* Worktree Module
*
* Platform-agnostic git worktree management functionality.
* These exports are decoupled from VSCode and can be used by any consumer.
*/
// Types
export * from "./types.js"
// Services
export { WorktreeService, worktreeService } from "./worktree-service.js"
export { WorktreeIncludeService, worktreeIncludeService, type CopyProgressCallback } from "./worktree-include.js"

View file

@ -0,0 +1,17 @@
/**
* Worktree Types
*
* Re-exports platform-agnostic type definitions from @roo-code/types.
*/
export type {
Worktree,
WorktreeResult,
BranchInfo,
CreateWorktreeOptions,
MergeWorktreeOptions,
MergeWorktreeResult,
WorktreeIncludeStatus,
WorktreeListResponse,
WorktreeDefaultsResponse,
} from "@roo-code/types"

View file

@ -0,0 +1,428 @@
/**
* WorktreeIncludeService
*
* Platform-agnostic service for handling .worktreeinclude files.
* Used to copy untracked files (like node_modules) when creating worktrees.
*/
import { execFile, spawn } from "child_process"
import * as fs from "fs/promises"
import * as path from "path"
import { promisify } from "util"
import ignore, { type Ignore } from "ignore"
import type { WorktreeIncludeStatus } from "./types.js"
/**
* Progress info for copy tracking.
* Shows activity without trying to predict total size (which is inaccurate).
*/
export interface CopyProgress {
/** Current bytes copied */
bytesCopied: number
/** Name of current item being copied */
itemName: string
}
/**
* Callback for reporting copy progress during worktree file copying.
*/
export type CopyProgressCallback = (progress: CopyProgress) => void
const execFileAsync = promisify(execFile)
/**
* Service for managing .worktreeinclude files and copying files to new worktrees.
* All methods are platform-agnostic and don't depend on VSCode APIs.
*/
export class WorktreeIncludeService {
/**
* Check if .worktreeinclude exists in a directory
*/
async hasWorktreeInclude(dir: string): Promise<boolean> {
try {
await fs.access(path.join(dir, ".worktreeinclude"))
return true
} catch {
return false
}
}
/**
* Check if a specific branch has .worktreeinclude file (in git, not local filesystem)
* @param cwd - Current working directory (git repo)
* @param branch - Branch name to check
*/
async branchHasWorktreeInclude(cwd: string, branch: string): Promise<boolean> {
try {
const ref = `${branch}:.worktreeinclude`
// Use git cat-file -e to check if the file exists on the branch (without printing contents)
await execFileAsync("git", ["cat-file", "-e", "--", ref], { cwd })
return true
} catch {
// File doesn't exist on this branch
return false
}
}
/**
* Get the status of .worktreeinclude and .gitignore
*/
async getStatus(dir: string): Promise<WorktreeIncludeStatus> {
const worktreeIncludePath = path.join(dir, ".worktreeinclude")
const gitignorePath = path.join(dir, ".gitignore")
let exists = false
let hasGitignore = false
let gitignoreContent: string | undefined
try {
await fs.access(worktreeIncludePath)
exists = true
} catch {
exists = false
}
try {
gitignoreContent = await fs.readFile(gitignorePath, "utf-8")
hasGitignore = true
} catch {
hasGitignore = false
}
return {
exists,
hasGitignore,
gitignoreContent,
}
}
/**
* Create a .worktreeinclude file with the specified content
*/
async createWorktreeInclude(dir: string, content: string): Promise<void> {
await fs.writeFile(path.join(dir, ".worktreeinclude"), content, "utf-8")
}
/**
* Copy files matching .worktreeinclude patterns from source to target.
* Only copies files that are ALSO in .gitignore (to avoid copying tracked files).
*
* @param sourceDir - The source directory containing the files to copy
* @param targetDir - The target directory where files will be copied
* @param onProgress - Optional callback to report copy progress (size-based)
* @returns Array of copied file/directory paths
*/
async copyWorktreeIncludeFiles(
sourceDir: string,
targetDir: string,
onProgress?: CopyProgressCallback,
): Promise<string[]> {
const worktreeIncludePath = path.join(sourceDir, ".worktreeinclude")
const gitignorePath = path.join(sourceDir, ".gitignore")
// Check if both files exist
let hasWorktreeInclude = false
let hasGitignore = false
try {
await fs.access(worktreeIncludePath)
hasWorktreeInclude = true
} catch {
hasWorktreeInclude = false
}
try {
await fs.access(gitignorePath)
hasGitignore = true
} catch {
hasGitignore = false
}
if (!hasWorktreeInclude || !hasGitignore) {
return []
}
// Parse both files
const worktreeIncludePatterns = await this.parseIgnoreFile(worktreeIncludePath)
const gitignorePatterns = await this.parseIgnoreFile(gitignorePath)
if (worktreeIncludePatterns.length === 0 || gitignorePatterns.length === 0) {
return []
}
// Create ignore matchers
const worktreeIncludeMatcher = ignore().add(worktreeIncludePatterns)
const gitignoreMatcher = ignore().add(gitignorePatterns)
// Find items that match BOTH patterns (intersection)
const itemsToCopy = await this.findMatchingItems(sourceDir, worktreeIncludeMatcher, gitignoreMatcher)
if (itemsToCopy.length === 0) {
return []
}
let bytesCopied = 0
// Report initial progress
if (onProgress && itemsToCopy.length > 0) {
onProgress({ bytesCopied: 0, itemName: itemsToCopy[0]! })
}
// Copy the items with progress tracking (no total size calculation)
const copiedItems: string[] = []
for (const item of itemsToCopy) {
const sourcePath = path.join(sourceDir, item)
const targetPath = path.join(targetDir, item)
try {
const stats = await fs.stat(sourcePath)
if (stats.isDirectory()) {
// Copy directory with progress tracking
bytesCopied = await this.copyDirectoryWithProgress(
sourcePath,
targetPath,
item,
bytesCopied,
onProgress,
)
} else {
// Report progress before copying
onProgress?.({ bytesCopied, itemName: item })
// Ensure parent directory exists
await fs.mkdir(path.dirname(targetPath), { recursive: true })
await fs.copyFile(sourcePath, targetPath)
// Update bytes copied
bytesCopied += this.getSizeOnDisk(stats)
}
copiedItems.push(item)
// Report progress after copying
onProgress?.({ bytesCopied, itemName: item })
} catch (error) {
// Log but don't fail on individual copy errors
console.error(`Failed to copy ${item}:`, error)
}
}
return copiedItems
}
/**
* Get the size on disk of a file (accounts for filesystem block allocation).
* Uses blksize to calculate actual disk usage including block overhead.
*/
private getSizeOnDisk(stats: { size: number; blksize?: number }): number {
// Calculate size on disk using filesystem block size
if (stats.blksize !== undefined && stats.blksize > 0) {
return stats.blksize * Math.ceil(stats.size / stats.blksize)
}
// Fallback to logical size when blksize not available
return stats.size
}
/**
* Get the total size on disk of a file or directory (recursively).
* Uses native Node.js fs operations for cross-platform compatibility.
*/
private async getPathSize(targetPath: string): Promise<number> {
try {
const stats = await fs.stat(targetPath)
if (stats.isFile()) {
return this.getSizeOnDisk(stats)
}
if (stats.isDirectory()) {
return await this.getDirectorySizeRecursive(targetPath)
}
return 0
} catch {
return 0
}
}
/**
* Recursively calculate directory size on disk using Node.js fs.
* Uses parallel processing for better performance on large directories.
*/
private async getDirectorySizeRecursive(dirPath: string): Promise<number> {
try {
const entries = await fs.readdir(dirPath, { withFileTypes: true })
const sizes = await Promise.all(
entries.map(async (entry) => {
const entryPath = path.join(dirPath, entry.name)
try {
if (entry.isFile()) {
const stats = await fs.stat(entryPath)
return this.getSizeOnDisk(stats)
} else if (entry.isDirectory()) {
return await this.getDirectorySizeRecursive(entryPath)
}
return 0
} catch {
return 0 // Skip inaccessible files
}
}),
)
return sizes.reduce((sum, size) => sum + size, 0)
} catch {
return 0
}
}
/**
* Get the current size of a directory (for progress tracking).
*/
private async getCurrentDirectorySize(dirPath: string): Promise<number> {
try {
await fs.access(dirPath)
return await this.getDirectorySizeRecursive(dirPath)
} catch {
return 0
}
}
/**
* Copy directory with progress polling using native cp command.
* Starts native copy and polls target directory size to report progress.
* Returns the updated bytesCopied count.
*/
private async copyDirectoryWithProgress(
source: string,
target: string,
itemName: string,
bytesCopiedBefore: number,
onProgress?: CopyProgressCallback,
): Promise<number> {
// Ensure parent directory exists
await fs.mkdir(path.dirname(target), { recursive: true })
const isWindows = process.platform === "win32"
// Start the copy process
const copyPromise = new Promise<void>((resolve, reject) => {
let proc: ReturnType<typeof spawn>
if (isWindows) {
proc = spawn("robocopy", [source, target, "/E", "/NFL", "/NDL", "/NJH", "/NJS", "/NC", "/NS", "/NP"], {
windowsHide: true,
})
} else {
proc = spawn("cp", ["-r", "--", source, target])
}
proc.on("close", (code) => {
if (isWindows) {
// robocopy returns non-zero for success (values < 8)
if (code !== null && code < 8) {
resolve()
} else {
reject(new Error(`robocopy failed with code ${code}`))
}
} else {
if (code === 0) {
resolve()
} else {
reject(new Error(`cp failed with code ${code}`))
}
}
})
proc.on("error", reject)
})
// Poll progress while copying
const pollInterval = 500 // Poll every 500ms
let polling = true
const pollProgress = async () => {
while (polling) {
const currentSize = await this.getCurrentDirectorySize(target)
const totalCopied = bytesCopiedBefore + currentSize
onProgress?.({
bytesCopied: totalCopied,
itemName,
})
await new Promise((resolve) => setTimeout(resolve, pollInterval))
}
}
// Start polling and wait for copy to complete
const pollPromise = pollProgress()
try {
await copyPromise
} finally {
polling = false
// Wait for final poll iteration to complete
await pollPromise.catch(() => {})
}
// Get the final size of the copied directory
const finalSize = await this.getPathSize(target)
return bytesCopiedBefore + finalSize
}
/**
* Parse a .gitignore-style file and return the patterns
*/
private async parseIgnoreFile(filePath: string): Promise<string[]> {
try {
const content = await fs.readFile(filePath, "utf-8")
return content
.split("\n")
.map((line) => line.trim())
.filter((line) => line && !line.startsWith("#"))
} catch {
return []
}
}
/**
* Find items in sourceDir that match both matchers
*/
private async findMatchingItems(
sourceDir: string,
includeMatcher: Ignore,
gitignoreMatcher: Ignore,
): Promise<string[]> {
const matchingItems: string[] = []
try {
const entries = await fs.readdir(sourceDir, { withFileTypes: true })
for (const entry of entries) {
const relativePath = entry.name
// Skip .git directory
if (relativePath === ".git") continue
// Check if this path matches both patterns
// For .worktreeinclude, we want items that are "ignored" (matched)
// For .gitignore, we want items that are "ignored" (matched)
const matchesWorktreeInclude = includeMatcher.ignores(relativePath)
const matchesGitignore = gitignoreMatcher.ignores(relativePath)
if (matchesWorktreeInclude && matchesGitignore) {
matchingItems.push(relativePath)
}
}
} catch {
return []
}
return matchingItems
}
}
// Export singleton instance for convenience
export const worktreeIncludeService = new WorktreeIncludeService()

View file

@ -0,0 +1,444 @@
/**
* WorktreeService
*
* Platform-agnostic service for git worktree operations.
* Uses simple-git and native CLI commands - no VSCode dependencies.
*/
import { exec, execFile } from "child_process"
import * as path from "path"
import { promisify } from "util"
import type {
BranchInfo,
CreateWorktreeOptions,
MergeWorktreeOptions,
MergeWorktreeResult,
Worktree,
WorktreeResult,
} from "./types.js"
const execAsync = promisify(exec)
const execFileAsync = promisify(execFile)
/**
* Service for managing git worktrees.
* All methods are platform-agnostic and don't depend on VSCode APIs.
*/
export class WorktreeService {
/**
* Check if git is installed on the system
*/
async checkGitInstalled(): Promise<boolean> {
try {
await execAsync("git --version")
return true
} catch {
return false
}
}
/**
* Check if a directory is a git repository.
*/
async checkGitRepo(cwd: string): Promise<boolean> {
try {
await execAsync("git rev-parse --git-dir", { cwd })
return true
} catch {
return false
}
}
/**
* Get the git repository root path.
*/
async getGitRootPath(cwd: string): Promise<string | null> {
try {
const { stdout } = await execAsync("git rev-parse --show-toplevel", { cwd })
return stdout.trim()
} catch {
return null
}
}
/**
* Get the current worktree path.
*/
async getCurrentWorktreePath(cwd: string): Promise<string | null> {
try {
const { stdout } = await execAsync("git rev-parse --show-toplevel", { cwd })
return stdout.trim()
} catch {
return null
}
}
/**
* Get the current branch name.
*/
async getCurrentBranch(cwd: string): Promise<string | null> {
try {
const { stdout } = await execAsync("git rev-parse --abbrev-ref HEAD", { cwd })
const branch = stdout.trim()
return branch === "HEAD" ? null : branch
} catch {
return null
}
}
/**
* List all worktrees in the repository
*/
async listWorktrees(cwd: string): Promise<Worktree[]> {
try {
const { stdout } = await execAsync("git worktree list --porcelain", { cwd })
return this.parseWorktreeOutput(stdout, cwd)
} catch {
return []
}
}
/**
* Create a new worktree
*/
async createWorktree(cwd: string, options: CreateWorktreeOptions): Promise<WorktreeResult> {
try {
const { path: worktreePath, branch, baseBranch, createNewBranch } = options
// Build the git worktree add command arguments
const args: string[] = ["worktree", "add"]
if (createNewBranch && branch) {
// Create new branch: git worktree add -b <branch> <path> [<base>]
args.push("-b", branch, worktreePath)
if (baseBranch) {
args.push(baseBranch)
}
} else if (branch) {
// Checkout existing branch: git worktree add <path> <branch>
args.push(worktreePath, branch)
} else {
// Detached HEAD at current commit
args.push("--detach", worktreePath)
}
await execFileAsync("git", args, { cwd })
// Get the created worktree info
const worktrees = await this.listWorktrees(cwd)
const createdWorktree = worktrees.find(
(wt) => this.normalizePath(wt.path) === this.normalizePath(worktreePath),
)
return {
success: true,
message: `Worktree created at ${worktreePath}`,
worktree: createdWorktree,
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
return {
success: false,
message: `Failed to create worktree: ${errorMessage}`,
}
}
}
/**
* Delete a worktree
*/
async deleteWorktree(cwd: string, worktreePath: string, force = false): Promise<WorktreeResult> {
try {
// Get worktree info BEFORE deletion to capture the branch name
const worktrees = await this.listWorktrees(cwd)
const worktreeToDelete = worktrees.find(
(wt) => this.normalizePath(wt.path) === this.normalizePath(worktreePath),
)
const args = ["worktree", "remove"]
if (force) {
args.push("--force")
}
args.push(worktreePath)
await execFileAsync("git", args, { cwd })
// Also try to delete the branch if it exists
if (worktreeToDelete?.branch) {
try {
await execFileAsync("git", ["branch", "-d", worktreeToDelete.branch], { cwd })
} catch {
// Branch deletion is best-effort
}
}
return {
success: true,
message: `Worktree removed from ${worktreePath}`,
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
return {
success: false,
message: `Failed to delete worktree: ${errorMessage}`,
}
}
}
/**
* Get available branches
* @param cwd - Current working directory
* @param includeWorktreeBranches - If true, include branches already checked out in worktrees (useful for base branch selection)
*/
async getAvailableBranches(cwd: string, includeWorktreeBranches = false): Promise<BranchInfo> {
try {
// Run all git commands in parallel for better performance
const [worktrees, localResult, remoteResult, currentBranch] = await Promise.all([
this.listWorktrees(cwd),
execAsync('git branch --format="%(refname:short)"', { cwd }),
execAsync('git branch -r --format="%(refname:short)"', { cwd }),
this.getCurrentBranch(cwd),
])
const branchesInWorktrees = new Set(worktrees.map((wt) => wt.branch).filter(Boolean))
// Filter local branches
const localBranches = localResult.stdout
.trim()
.split("\n")
.filter((b) => b && (includeWorktreeBranches || !branchesInWorktrees.has(b)))
// Filter remote branches
const remoteBranches = remoteResult.stdout
.trim()
.split("\n")
.filter(
(b) =>
b &&
!b.includes("HEAD") &&
(includeWorktreeBranches || !branchesInWorktrees.has(b.replace(/^origin\//, ""))),
)
return {
localBranches,
remoteBranches,
currentBranch: currentBranch || "",
}
} catch {
return {
localBranches: [],
remoteBranches: [],
currentBranch: "",
}
}
}
/**
* Merge a worktree branch into target branch
*/
async mergeWorktree(cwd: string, options: MergeWorktreeOptions): Promise<MergeWorktreeResult> {
const { worktreePath, targetBranch, deleteAfterMerge } = options
try {
// Get the worktree info to find its branch
const worktrees = await this.listWorktrees(cwd)
const worktree = worktrees.find((wt) => this.normalizePath(wt.path) === this.normalizePath(worktreePath))
if (!worktree) {
return {
success: false,
message: "Worktree not found",
hasConflicts: false,
conflictingFiles: [],
}
}
const sourceBranch = worktree.branch
if (!sourceBranch) {
return {
success: false,
message: "Worktree has detached HEAD - cannot merge",
hasConflicts: false,
conflictingFiles: [],
}
}
// Find the worktree that has the target branch checked out
const targetWorktree = worktrees.find((wt) => wt.branch === targetBranch)
const mergeCwd = targetWorktree ? targetWorktree.path : cwd
// Check for uncommitted changes in source worktree
try {
const { stdout: statusOutput } = await execAsync("git status --porcelain", { cwd: worktreePath })
if (statusOutput.trim()) {
return {
success: false,
message: "Source worktree has uncommitted changes. Please commit or stash them first.",
hasConflicts: false,
conflictingFiles: [],
sourceBranch,
targetBranch,
}
}
} catch {
// Continue if status check fails
}
// Ensure we're on the target branch
await execFileAsync("git", ["checkout", targetBranch], { cwd: mergeCwd })
// Attempt the merge
try {
await execFileAsync("git", ["merge", sourceBranch, "--no-edit"], { cwd: mergeCwd })
// Merge succeeded
if (deleteAfterMerge) {
await this.deleteWorktree(cwd, worktreePath, false)
}
return {
success: true,
message: `Successfully merged ${sourceBranch} into ${targetBranch}`,
hasConflicts: false,
conflictingFiles: [],
sourceBranch,
targetBranch,
}
} catch (mergeError) {
// Check for merge conflicts
try {
const { stdout: conflictOutput } = await execAsync("git diff --name-only --diff-filter=U", {
cwd: mergeCwd,
})
const conflictingFiles = conflictOutput.trim().split("\n").filter(Boolean)
// Abort the merge to leave repo in clean state
await execAsync("git merge --abort", { cwd: mergeCwd })
return {
success: false,
message: `Merge conflicts detected in ${conflictingFiles.length} file(s)`,
hasConflicts: true,
conflictingFiles,
sourceBranch,
targetBranch,
}
} catch {
// If we can't get conflicts, just report the error
const errorMessage = mergeError instanceof Error ? mergeError.message : String(mergeError)
// Try to abort any in-progress merge
try {
await execAsync("git merge --abort", { cwd: mergeCwd })
} catch {
// Ignore abort errors
}
return {
success: false,
message: `Merge failed: ${errorMessage}`,
hasConflicts: false,
conflictingFiles: [],
sourceBranch,
targetBranch,
}
}
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
return {
success: false,
message: `Merge failed: ${errorMessage}`,
hasConflicts: false,
conflictingFiles: [],
}
}
}
/**
* Checkout a branch in the current worktree
*/
async checkoutBranch(cwd: string, branch: string): Promise<WorktreeResult> {
try {
await execFileAsync("git", ["checkout", branch], { cwd })
return {
success: true,
message: `Checked out branch ${branch}`,
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
return {
success: false,
message: `Failed to checkout branch: ${errorMessage}`,
}
}
}
/**
* Parse git worktree list --porcelain output
*/
private parseWorktreeOutput(output: string, currentCwd: string): Worktree[] {
const worktrees: Worktree[] = []
const entries = output.trim().split("\n\n")
for (const entry of entries) {
if (!entry.trim()) continue
const lines = entry.trim().split("\n")
const worktree: Partial<Worktree> = {
path: "",
branch: "",
commitHash: "",
isCurrent: false,
isBare: false,
isDetached: false,
isLocked: false,
}
for (const line of lines) {
if (line.startsWith("worktree ")) {
worktree.path = line.substring(9).trim()
} else if (line.startsWith("HEAD ")) {
worktree.commitHash = line.substring(5).trim()
} else if (line.startsWith("branch ")) {
// branch refs/heads/main -> main
const branchRef = line.substring(7).trim()
worktree.branch = branchRef.replace(/^refs\/heads\//, "")
} else if (line === "bare") {
worktree.isBare = true
} else if (line === "detached") {
worktree.isDetached = true
} else if (line === "locked") {
worktree.isLocked = true
} else if (line.startsWith("locked ")) {
worktree.isLocked = true
worktree.lockReason = line.substring(7).trim()
}
}
if (worktree.path) {
worktree.isCurrent = this.normalizePath(worktree.path) === this.normalizePath(currentCwd)
worktrees.push(worktree as Worktree)
}
}
return worktrees
}
/**
* Normalize a path for comparison (handle trailing slashes, etc.)
*/
private normalizePath(p: string): string {
// normalize resolves ./.. segments, removes duplicate slashes, and standardizes path separators
let normalized = path.normalize(p)
// however it doesn't remove trailing slashes
// remove trailing slash, except for root paths (handles both / and \)
if (normalized.length > 1 && (normalized.endsWith("/") || normalized.endsWith("\\"))) {
normalized = normalized.slice(0, -1)
}
return normalized
}
}
// Export singleton instance for convenience
export const worktreeService = new WorktreeService()

View file

@ -111,8 +111,8 @@ export class TelemetryService {
this.captureEvent(TelemetryEventName.MODE_SWITCH, { taskId, newMode })
}
public captureToolUsage(taskId: string, tool: string, toolProtocol: string): void {
this.captureEvent(TelemetryEventName.TOOL_USED, { taskId, tool, toolProtocol })
public captureToolUsage(taskId: string, tool: string): void {
this.captureEvent(TelemetryEventName.TOOL_USED, { taskId, tool })
}
public captureCheckpointCreated(taskId: string): void {
@ -127,17 +127,11 @@ export class TelemetryService {
this.captureEvent(TelemetryEventName.CHECKPOINT_RESTORED, { taskId })
}
public captureContextCondensed(
taskId: string,
isAutomaticTrigger: boolean,
usedCustomPrompt?: boolean,
usedCustomApiHandler?: boolean,
): void {
public captureContextCondensed(taskId: string, isAutomaticTrigger: boolean, usedCustomPrompt?: boolean): void {
this.captureEvent(TelemetryEventName.CONTEXT_CONDENSED, {
taskId,
isAutomaticTrigger,
...(usedCustomPrompt !== undefined && { usedCustomPrompt }),
...(usedCustomApiHandler !== undefined && { usedCustomApiHandler }),
})
}

View file

@ -7,11 +7,6 @@ describe("getApiProtocol", () => {
expect(getApiProtocol("anthropic", "gpt-4")).toBe("anthropic")
})
it("should return 'anthropic' for claude-code provider", () => {
expect(getApiProtocol("claude-code")).toBe("anthropic")
expect(getApiProtocol("claude-code", "some-model")).toBe("anthropic")
})
it("should return 'anthropic' for bedrock provider", () => {
expect(getApiProtocol("bedrock")).toBe("anthropic")
expect(getApiProtocol("bedrock", "gpt-4")).toBe("anthropic")

View file

@ -63,7 +63,6 @@ export const globalSettingsSchema = z.object({
openRouterImageApiKey: z.string().optional(),
openRouterImageGenerationSelectedModel: z.string().optional(),
condensingApiConfigId: z.string().optional(),
customCondensingPrompt: z.string().optional(),
autoApprovalEnabled: z.boolean().optional(),
@ -197,6 +196,15 @@ export const globalSettingsSchema = z.object({
hasOpenedModeSelector: z.boolean().optional(),
lastModeExportPath: z.string().optional(),
lastModeImportPath: z.string().optional(),
lastSettingsExportPath: z.string().optional(),
lastTaskExportPath: z.string().optional(),
lastImageSavePath: z.string().optional(),
/**
* Path to worktree to auto-open after switching workspaces.
* Used by the worktree feature to open the Roo Code sidebar in a new window.
*/
worktreeAutoOpenPath: z.string().optional(),
})
export type GlobalSettings = z.infer<typeof globalSettingsSchema>

View file

@ -19,16 +19,6 @@ export const historyItemSchema = z.object({
size: z.number().optional(),
workspace: z.string().optional(),
mode: z.string().optional(),
/**
* The tool protocol used by this task. Once a task uses tools with a specific
* protocol (XML or Native), it is permanently locked to that protocol.
*
* - "xml": Tool calls are parsed from XML text (no tool IDs)
* - "native": Tool calls come as tool_call chunks with IDs
*
* This ensures task resumption works correctly even when NTC settings change.
*/
toolProtocol: z.enum(["xml", "native"]).optional(),
apiConfigName: z.string().optional(), // Provider profile name for sticky profile feature
status: z.enum(["active", "completed", "delegated"]).optional(),
delegatedToId: z.string().optional(), // Last child this parent delegated to

View file

@ -28,5 +28,6 @@ export * from "./tool-params.js"
export * from "./type-fu.js"
export * from "./vscode-extension-host.js"
export * from "./vscode.js"
export * from "./worktree.js"
export * from "./providers/index.js"

View file

@ -110,10 +110,6 @@ export const modelInfoSchema = z.object({
isStealthModel: 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(),
// Default tool protocol preferred by this model (if not specified, falls back to capability/provider defaults)
defaultToolProtocol: z.enum(["xml", "native"]).optional(),
// Exclude specific native tools from being available (only applies to native protocol)
// These tools will be removed from the set of tools available to the model
excludedTools: z.array(z.string()).optional(),

View file

@ -7,7 +7,6 @@ import {
basetenModels,
bedrockModels,
cerebrasModels,
claudeCodeModels,
deepSeekModels,
doubaoModels,
featherlessModels,
@ -123,7 +122,6 @@ export const providerNames = [
"bedrock",
"baseten",
"cerebras",
"claude-code",
"doubao",
"deepseek",
"featherless",
@ -185,9 +183,6 @@ const baseProviderSettingsSchema = z.object({
// Model verbosity.
verbosity: verbosityLevelsSchema.optional(),
// Tool protocol override for this profile.
toolProtocol: z.enum(["xml", "native"]).optional(),
})
// Several of the providers share common model config properties.
@ -202,8 +197,6 @@ const anthropicSchema = apiModelIdProviderModelSchema.extend({
anthropicBeta1MContext: z.boolean().optional(), // Enable 'context-1m-2025-08-07' beta for 1M context window.
})
const claudeCodeSchema = apiModelIdProviderModelSchema.extend({})
const openRouterSchema = baseProviderSettingsSchema.extend({
openRouterApiKey: z.string().optional(),
openRouterModelId: z.string().optional(),
@ -432,7 +425,6 @@ const defaultSchema = z.object({
export const providerSettingsSchemaDiscriminated = z.discriminatedUnion("apiProvider", [
anthropicSchema.merge(z.object({ apiProvider: z.literal("anthropic") })),
claudeCodeSchema.merge(z.object({ apiProvider: z.literal("claude-code") })),
openRouterSchema.merge(z.object({ apiProvider: z.literal("openrouter") })),
bedrockSchema.merge(z.object({ apiProvider: z.literal("bedrock") })),
vertexSchema.merge(z.object({ apiProvider: z.literal("vertex") })),
@ -474,7 +466,6 @@ export const providerSettingsSchemaDiscriminated = z.discriminatedUnion("apiProv
export const providerSettingsSchema = z.object({
apiProvider: providerNamesSchema.optional(),
...anthropicSchema.shape,
...claudeCodeSchema.shape,
...openRouterSchema.shape,
...bedrockSchema.shape,
...vertexSchema.shape,
@ -563,7 +554,6 @@ export const isTypicalProvider = (key: unknown): key is TypicalProvider =>
export const modelIdKeysByProvider: Record<TypicalProvider, ModelIdKey> = {
anthropic: "apiModelId",
"claude-code": "apiModelId",
openrouter: "openRouterModelId",
bedrock: "apiModelId",
vertex: "apiModelId",
@ -603,7 +593,7 @@ export const modelIdKeysByProvider: Record<TypicalProvider, ModelIdKey> = {
*/
// Providers that use Anthropic-style API protocol.
export const ANTHROPIC_STYLE_PROVIDERS: ProviderName[] = ["anthropic", "claude-code", "bedrock", "minimax"]
export const ANTHROPIC_STYLE_PROVIDERS: ProviderName[] = ["anthropic", "bedrock", "minimax"]
export const getApiProtocol = (provider: ProviderName | undefined, modelId?: string): "anthropic" | "openai" => {
if (provider && ANTHROPIC_STYLE_PROVIDERS.includes(provider)) {
@ -650,7 +640,6 @@ export const MODELS_BY_PROVIDER: Record<
label: "Cerebras",
models: Object.keys(cerebrasModels),
},
"claude-code": { id: "claude-code", label: "Claude Code", models: Object.keys(claudeCodeModels) },
deepseek: {
id: "deepseek",
label: "DeepSeek",

View file

@ -1,46 +0,0 @@
import { normalizeClaudeCodeModelId } from "../claude-code.js"
describe("normalizeClaudeCodeModelId", () => {
test("should return valid model IDs unchanged", () => {
expect(normalizeClaudeCodeModelId("claude-sonnet-4-5")).toBe("claude-sonnet-4-5")
expect(normalizeClaudeCodeModelId("claude-opus-4-5")).toBe("claude-opus-4-5")
expect(normalizeClaudeCodeModelId("claude-haiku-4-5")).toBe("claude-haiku-4-5")
})
test("should normalize sonnet models with date suffix to claude-sonnet-4-5", () => {
// Sonnet 4.5 with date
expect(normalizeClaudeCodeModelId("claude-sonnet-4-5-20250929")).toBe("claude-sonnet-4-5")
// Sonnet 4 (legacy)
expect(normalizeClaudeCodeModelId("claude-sonnet-4-20250514")).toBe("claude-sonnet-4-5")
// Claude 3.7 Sonnet
expect(normalizeClaudeCodeModelId("claude-3-7-sonnet-20250219")).toBe("claude-sonnet-4-5")
// Claude 3.5 Sonnet
expect(normalizeClaudeCodeModelId("claude-3-5-sonnet-20241022")).toBe("claude-sonnet-4-5")
})
test("should normalize opus models with date suffix to claude-opus-4-5", () => {
// Opus 4.5 with date
expect(normalizeClaudeCodeModelId("claude-opus-4-5-20251101")).toBe("claude-opus-4-5")
// Opus 4.1 (legacy)
expect(normalizeClaudeCodeModelId("claude-opus-4-1-20250805")).toBe("claude-opus-4-5")
// Opus 4 (legacy)
expect(normalizeClaudeCodeModelId("claude-opus-4-20250514")).toBe("claude-opus-4-5")
})
test("should normalize haiku models with date suffix to claude-haiku-4-5", () => {
// Haiku 4.5 with date
expect(normalizeClaudeCodeModelId("claude-haiku-4-5-20251001")).toBe("claude-haiku-4-5")
// Claude 3.5 Haiku
expect(normalizeClaudeCodeModelId("claude-3-5-haiku-20241022")).toBe("claude-haiku-4-5")
})
test("should handle case-insensitive model family matching", () => {
expect(normalizeClaudeCodeModelId("Claude-Sonnet-4-5-20250929")).toBe("claude-sonnet-4-5")
expect(normalizeClaudeCodeModelId("CLAUDE-OPUS-4-5-20251101")).toBe("claude-opus-4-5")
})
test("should fallback to default for unrecognized models", () => {
expect(normalizeClaudeCodeModelId("unknown-model")).toBe("claude-sonnet-4-5")
expect(normalizeClaudeCodeModelId("gpt-4")).toBe("claude-sonnet-4-5")
})
})

View file

@ -11,8 +11,6 @@ export const anthropicModels = {
contextWindow: 200_000, // Default 200K, extendable to 1M with beta flag 'context-1m-2025-08-07'
supportsImages: true,
supportsPromptCache: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 3.0, // $3 per million input tokens (≤200K context)
outputPrice: 15.0, // $15 per million output tokens (≤200K context)
cacheWritesPrice: 3.75, // $3.75 per million tokens
@ -34,8 +32,6 @@ export const anthropicModels = {
contextWindow: 200_000, // Default 200K, extendable to 1M with beta flag 'context-1m-2025-08-07'
supportsImages: true,
supportsPromptCache: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 3.0, // $3 per million input tokens (≤200K context)
outputPrice: 15.0, // $15 per million output tokens (≤200K context)
cacheWritesPrice: 3.75, // $3.75 per million tokens
@ -57,8 +53,6 @@ export const anthropicModels = {
contextWindow: 200_000,
supportsImages: true,
supportsPromptCache: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 5.0, // $5 per million input tokens
outputPrice: 25.0, // $25 per million output tokens
cacheWritesPrice: 6.25, // $6.25 per million tokens
@ -70,8 +64,6 @@ export const anthropicModels = {
contextWindow: 200_000,
supportsImages: true,
supportsPromptCache: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 15.0, // $15 per million input tokens
outputPrice: 75.0, // $75 per million output tokens
cacheWritesPrice: 18.75, // $18.75 per million tokens
@ -83,8 +75,6 @@ export const anthropicModels = {
contextWindow: 200_000,
supportsImages: true,
supportsPromptCache: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 15.0, // $15 per million input tokens
outputPrice: 75.0, // $75 per million output tokens
cacheWritesPrice: 18.75, // $18.75 per million tokens
@ -96,8 +86,6 @@ export const anthropicModels = {
contextWindow: 200_000,
supportsImages: true,
supportsPromptCache: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 3.0, // $3 per million input tokens
outputPrice: 15.0, // $15 per million output tokens
cacheWritesPrice: 3.75, // $3.75 per million tokens
@ -110,8 +98,6 @@ export const anthropicModels = {
contextWindow: 200_000,
supportsImages: true,
supportsPromptCache: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 3.0, // $3 per million input tokens
outputPrice: 15.0, // $15 per million output tokens
cacheWritesPrice: 3.75, // $3.75 per million tokens
@ -122,8 +108,6 @@ export const anthropicModels = {
contextWindow: 200_000,
supportsImages: true,
supportsPromptCache: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 3.0, // $3 per million input tokens
outputPrice: 15.0, // $15 per million output tokens
cacheWritesPrice: 3.75, // $3.75 per million tokens
@ -134,8 +118,6 @@ export const anthropicModels = {
contextWindow: 200_000,
supportsImages: false,
supportsPromptCache: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 1.0,
outputPrice: 5.0,
cacheWritesPrice: 1.25,
@ -146,8 +128,6 @@ export const anthropicModels = {
contextWindow: 200_000,
supportsImages: true,
supportsPromptCache: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 15.0,
outputPrice: 75.0,
cacheWritesPrice: 18.75,
@ -158,8 +138,6 @@ export const anthropicModels = {
contextWindow: 200_000,
supportsImages: true,
supportsPromptCache: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0.25,
outputPrice: 1.25,
cacheWritesPrice: 0.3,
@ -170,8 +148,6 @@ export const anthropicModels = {
contextWindow: 200_000,
supportsImages: true,
supportsPromptCache: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 1.0,
outputPrice: 5.0,
cacheWritesPrice: 1.25,

View file

@ -9,7 +9,6 @@ export const basetenModels = {
contextWindow: 262_000,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
inputPrice: 0.6,
outputPrice: 2.5,
cacheWritesPrice: 0,
@ -21,7 +20,6 @@ export const basetenModels = {
contextWindow: 200_000,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
inputPrice: 0.6,
outputPrice: 2.2,
cacheWritesPrice: 0,
@ -33,7 +31,6 @@ export const basetenModels = {
contextWindow: 163_840,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
inputPrice: 2.55,
outputPrice: 5.95,
cacheWritesPrice: 0,
@ -45,7 +42,6 @@ export const basetenModels = {
contextWindow: 163_840,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
inputPrice: 2.55,
outputPrice: 5.95,
cacheWritesPrice: 0,
@ -57,7 +53,6 @@ export const basetenModels = {
contextWindow: 163_840,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
inputPrice: 0.77,
outputPrice: 0.77,
cacheWritesPrice: 0,
@ -69,7 +64,6 @@ export const basetenModels = {
contextWindow: 163_840,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
inputPrice: 0.5,
outputPrice: 1.5,
cacheWritesPrice: 0,
@ -82,7 +76,6 @@ export const basetenModels = {
contextWindow: 163_840,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
inputPrice: 0.3,
outputPrice: 0.45,
cacheWritesPrice: 0,
@ -95,7 +88,6 @@ export const basetenModels = {
contextWindow: 128_072,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
inputPrice: 0.1,
outputPrice: 0.5,
cacheWritesPrice: 0,
@ -107,7 +99,6 @@ export const basetenModels = {
contextWindow: 262_144,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
inputPrice: 0.22,
outputPrice: 0.8,
cacheWritesPrice: 0,
@ -119,7 +110,6 @@ export const basetenModels = {
contextWindow: 262_144,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
inputPrice: 0.38,
outputPrice: 1.53,
cacheWritesPrice: 0,
@ -131,7 +121,6 @@ export const basetenModels = {
contextWindow: 262_000,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
inputPrice: 0.6,
outputPrice: 2.5,
cacheWritesPrice: 0,

View file

@ -19,8 +19,6 @@ export const bedrockModels = {
supportsImages: true,
supportsPromptCache: true,
supportsReasoningBudget: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 3.0,
outputPrice: 15.0,
cacheWritesPrice: 3.75,
@ -34,7 +32,6 @@ export const bedrockModels = {
contextWindow: 300_000,
supportsImages: true,
supportsPromptCache: true,
supportsNativeTools: true,
inputPrice: 0.8,
outputPrice: 3.2,
cacheWritesPrice: 0.8, // per million tokens
@ -48,7 +45,6 @@ export const bedrockModels = {
contextWindow: 300_000,
supportsImages: true,
supportsPromptCache: false,
supportsNativeTools: true,
inputPrice: 1.0,
outputPrice: 4.0,
cacheWritesPrice: 1.0, // per million tokens
@ -60,7 +56,6 @@ export const bedrockModels = {
contextWindow: 300_000,
supportsImages: true,
supportsPromptCache: true,
supportsNativeTools: true,
inputPrice: 0.06,
outputPrice: 0.24,
cacheWritesPrice: 0.06, // per million tokens
@ -74,7 +69,6 @@ export const bedrockModels = {
contextWindow: 1_000_000,
supportsImages: true,
supportsPromptCache: true,
supportsNativeTools: true,
inputPrice: 0.33,
outputPrice: 2.75,
cacheWritesPrice: 0,
@ -89,7 +83,6 @@ export const bedrockModels = {
contextWindow: 128_000,
supportsImages: false,
supportsPromptCache: true,
supportsNativeTools: true,
inputPrice: 0.035,
outputPrice: 0.14,
cacheWritesPrice: 0.035, // per million tokens
@ -104,8 +97,6 @@ export const bedrockModels = {
supportsImages: true,
supportsPromptCache: true,
supportsReasoningBudget: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 3.0,
outputPrice: 15.0,
cacheWritesPrice: 3.75,
@ -120,8 +111,6 @@ export const bedrockModels = {
supportsImages: true,
supportsPromptCache: true,
supportsReasoningBudget: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 15.0,
outputPrice: 75.0,
cacheWritesPrice: 18.75,
@ -136,8 +125,6 @@ export const bedrockModels = {
supportsImages: true,
supportsPromptCache: true,
supportsReasoningBudget: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 5.0,
outputPrice: 25.0,
cacheWritesPrice: 6.25,
@ -152,8 +139,6 @@ export const bedrockModels = {
supportsImages: true,
supportsPromptCache: true,
supportsReasoningBudget: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 15.0,
outputPrice: 75.0,
cacheWritesPrice: 18.75,
@ -168,8 +153,6 @@ export const bedrockModels = {
supportsImages: true,
supportsPromptCache: true,
supportsReasoningBudget: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 3.0,
outputPrice: 15.0,
cacheWritesPrice: 3.75,
@ -183,8 +166,6 @@ export const bedrockModels = {
contextWindow: 200_000,
supportsImages: true,
supportsPromptCache: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 3.0,
outputPrice: 15.0,
cacheWritesPrice: 3.75,
@ -198,8 +179,6 @@ export const bedrockModels = {
contextWindow: 200_000,
supportsImages: false,
supportsPromptCache: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0.8,
outputPrice: 4.0,
cacheWritesPrice: 1.0,
@ -214,8 +193,6 @@ export const bedrockModels = {
supportsImages: true,
supportsPromptCache: true,
supportsReasoningBudget: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 1.0,
outputPrice: 5.0,
cacheWritesPrice: 1.25, // 5m cache writes
@ -229,8 +206,6 @@ export const bedrockModels = {
contextWindow: 200_000,
supportsImages: true,
supportsPromptCache: false,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 3.0,
outputPrice: 15.0,
},
@ -239,8 +214,6 @@ export const bedrockModels = {
contextWindow: 200_000,
supportsImages: true,
supportsPromptCache: false,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 15.0,
outputPrice: 75.0,
},
@ -249,8 +222,6 @@ export const bedrockModels = {
contextWindow: 200_000,
supportsImages: true,
supportsPromptCache: false,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 3.0,
outputPrice: 15.0,
},
@ -259,8 +230,6 @@ export const bedrockModels = {
contextWindow: 200_000,
supportsImages: true,
supportsPromptCache: false,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0.25,
outputPrice: 1.25,
},
@ -269,7 +238,6 @@ export const bedrockModels = {
contextWindow: 128_000,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
inputPrice: 1.35,
outputPrice: 5.4,
},
@ -278,7 +246,6 @@ export const bedrockModels = {
contextWindow: 128_000,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
inputPrice: 0.5,
outputPrice: 1.5,
description: "GPT-OSS 20B - Optimized for low latency and local/specialized use cases",
@ -288,7 +255,6 @@ export const bedrockModels = {
contextWindow: 128_000,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
inputPrice: 2.0,
outputPrice: 6.0,
description: "GPT-OSS 120B - Production-ready, general-purpose, high-reasoning model",
@ -298,7 +264,6 @@ export const bedrockModels = {
contextWindow: 128_000,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
inputPrice: 0.72,
outputPrice: 0.72,
description: "Llama 3.3 Instruct (70B)",
@ -308,7 +273,6 @@ export const bedrockModels = {
contextWindow: 128_000,
supportsImages: true,
supportsPromptCache: false,
supportsNativeTools: true,
inputPrice: 0.72,
outputPrice: 0.72,
description: "Llama 3.2 Instruct (90B)",
@ -318,7 +282,6 @@ export const bedrockModels = {
contextWindow: 128_000,
supportsImages: true,
supportsPromptCache: false,
supportsNativeTools: true,
inputPrice: 0.16,
outputPrice: 0.16,
description: "Llama 3.2 Instruct (11B)",
@ -328,7 +291,6 @@ export const bedrockModels = {
contextWindow: 128_000,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
inputPrice: 0.15,
outputPrice: 0.15,
description: "Llama 3.2 Instruct (3B)",
@ -338,7 +300,6 @@ export const bedrockModels = {
contextWindow: 128_000,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
inputPrice: 0.1,
outputPrice: 0.1,
description: "Llama 3.2 Instruct (1B)",
@ -348,7 +309,6 @@ export const bedrockModels = {
contextWindow: 128_000,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
inputPrice: 2.4,
outputPrice: 2.4,
description: "Llama 3.1 Instruct (405B)",
@ -358,7 +318,6 @@ export const bedrockModels = {
contextWindow: 128_000,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
inputPrice: 0.72,
outputPrice: 0.72,
description: "Llama 3.1 Instruct (70B)",
@ -368,7 +327,6 @@ export const bedrockModels = {
contextWindow: 128_000,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
inputPrice: 0.9,
outputPrice: 0.9,
description: "Llama 3.1 Instruct (70B) (w/ latency optimized inference)",
@ -378,7 +336,6 @@ export const bedrockModels = {
contextWindow: 8_000,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
inputPrice: 0.22,
outputPrice: 0.22,
description: "Llama 3.1 Instruct (8B)",
@ -388,7 +345,6 @@ export const bedrockModels = {
contextWindow: 8_000,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
inputPrice: 2.65,
outputPrice: 3.5,
},
@ -397,7 +353,6 @@ export const bedrockModels = {
contextWindow: 4_000,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
inputPrice: 0.3,
outputPrice: 0.6,
},
@ -406,7 +361,6 @@ export const bedrockModels = {
contextWindow: 8_000,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
inputPrice: 0.15,
outputPrice: 0.2,
description: "Amazon Titan Text Lite",
@ -416,7 +370,6 @@ export const bedrockModels = {
contextWindow: 8_000,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
inputPrice: 0.2,
outputPrice: 0.6,
description: "Amazon Titan Text Express",
@ -426,8 +379,6 @@ export const bedrockModels = {
contextWindow: 262_144,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
defaultToolProtocol: "native",
preserveReasoning: true,
inputPrice: 0.6,
outputPrice: 2.5,
@ -438,8 +389,6 @@ export const bedrockModels = {
contextWindow: 196_608,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
defaultToolProtocol: "native",
preserveReasoning: true,
inputPrice: 0.3,
outputPrice: 1.2,
@ -450,8 +399,6 @@ export const bedrockModels = {
contextWindow: 262_144,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0.15,
outputPrice: 1.2,
description: "Qwen3 Next 80B (MoE model with 3B active parameters)",
@ -461,8 +408,6 @@ export const bedrockModels = {
contextWindow: 262_144,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0.45,
outputPrice: 1.8,
description: "Qwen3 Coder 480B (MoE model with 35B active parameters)",

View file

@ -10,9 +10,7 @@ export const cerebrasModels = {
maxTokens: 16384, // Conservative default to avoid premature rate limiting (Cerebras reserves quota upfront)
contextWindow: 131072,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
defaultToolProtocol: "native",
supportsPromptCache: true,
inputPrice: 0,
outputPrice: 0,
description:
@ -23,8 +21,6 @@ export const cerebrasModels = {
contextWindow: 64000,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0,
outputPrice: 0,
description: "Intelligent model with ~1400 tokens/s",
@ -34,8 +30,6 @@ export const cerebrasModels = {
contextWindow: 64000,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0,
outputPrice: 0,
description: "Powerful model with ~2600 tokens/s",
@ -45,8 +39,6 @@ export const cerebrasModels = {
contextWindow: 64000,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0,
outputPrice: 0,
description: "SOTA coding performance with ~2500 tokens/s",
@ -56,8 +48,6 @@ export const cerebrasModels = {
contextWindow: 64000,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0,
outputPrice: 0,
description:

View file

@ -51,8 +51,6 @@ export const chutesModels = {
contextWindow: 163840,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0,
outputPrice: 0,
description: "DeepSeek R1 0528 model.",
@ -62,8 +60,6 @@ export const chutesModels = {
contextWindow: 163840,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0,
outputPrice: 0,
description: "DeepSeek R1 model.",
@ -73,8 +69,6 @@ export const chutesModels = {
contextWindow: 163840,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0,
outputPrice: 0,
description: "DeepSeek V3 model.",
@ -84,8 +78,6 @@ export const chutesModels = {
contextWindow: 163840,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0,
outputPrice: 0,
description: "DeepSeek V3.1 model.",
@ -95,8 +87,6 @@ export const chutesModels = {
contextWindow: 163840,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0.23,
outputPrice: 0.9,
description:
@ -107,8 +97,6 @@ export const chutesModels = {
contextWindow: 163840,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 1.0,
outputPrice: 3.0,
description:
@ -119,8 +107,6 @@ export const chutesModels = {
contextWindow: 163840,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0.25,
outputPrice: 0.35,
description:
@ -131,8 +117,6 @@ export const chutesModels = {
contextWindow: 131072, // From Groq
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0,
outputPrice: 0,
description: "Unsloth Llama 3.3 70B Instruct model.",
@ -142,8 +126,6 @@ export const chutesModels = {
contextWindow: 512000,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0,
outputPrice: 0,
description: "ChutesAI Llama 4 Scout 17B Instruct model, 512K context.",
@ -153,8 +135,6 @@ export const chutesModels = {
contextWindow: 128000,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0,
outputPrice: 0,
description: "Unsloth Mistral Nemo Instruct model.",
@ -164,8 +144,6 @@ export const chutesModels = {
contextWindow: 131072,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0,
outputPrice: 0,
description: "Unsloth Gemma 3 12B IT model.",
@ -175,8 +153,6 @@ export const chutesModels = {
contextWindow: 131072,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0,
outputPrice: 0,
description: "Nous DeepHermes 3 Llama 3 8B Preview model.",
@ -186,8 +162,6 @@ export const chutesModels = {
contextWindow: 131072,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0,
outputPrice: 0,
description: "Unsloth Gemma 3 4B IT model.",
@ -197,8 +171,6 @@ export const chutesModels = {
contextWindow: 131072,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0,
outputPrice: 0,
description: "Nvidia Llama 3.3 Nemotron Super 49B model.",
@ -208,8 +180,6 @@ export const chutesModels = {
contextWindow: 131072,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0,
outputPrice: 0,
description: "Nvidia Llama 3.1 Nemotron Ultra 253B model.",
@ -219,8 +189,6 @@ export const chutesModels = {
contextWindow: 256000,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0,
outputPrice: 0,
description: "ChutesAI Llama 4 Maverick 17B Instruct FP8 model.",
@ -230,8 +198,6 @@ export const chutesModels = {
contextWindow: 163840,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0,
outputPrice: 0,
description: "DeepSeek V3 Base model.",
@ -241,8 +207,6 @@ export const chutesModels = {
contextWindow: 163840,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0,
outputPrice: 0,
description: "DeepSeek R1 Zero model.",
@ -252,8 +216,6 @@ export const chutesModels = {
contextWindow: 163840,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0,
outputPrice: 0,
description: "DeepSeek V3 (0324) model.",
@ -263,8 +225,6 @@ export const chutesModels = {
contextWindow: 262144,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0,
outputPrice: 0,
description: "Qwen3 235B A22B Instruct 2507 model with 262K context window.",
@ -274,8 +234,6 @@ export const chutesModels = {
contextWindow: 40960,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0,
outputPrice: 0,
description: "Qwen3 235B A22B model.",
@ -285,8 +243,6 @@ export const chutesModels = {
contextWindow: 40960,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0,
outputPrice: 0,
description: "Qwen3 32B model.",
@ -296,8 +252,6 @@ export const chutesModels = {
contextWindow: 40960,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0,
outputPrice: 0,
description: "Qwen3 30B A3B model.",
@ -307,8 +261,6 @@ export const chutesModels = {
contextWindow: 40960,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0,
outputPrice: 0,
description: "Qwen3 14B model.",
@ -318,8 +270,6 @@ export const chutesModels = {
contextWindow: 40960,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0,
outputPrice: 0,
description: "Qwen3 8B model.",
@ -329,8 +279,6 @@ export const chutesModels = {
contextWindow: 163840,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0,
outputPrice: 0,
description: "Microsoft MAI-DS-R1 FP8 model.",
@ -340,8 +288,6 @@ export const chutesModels = {
contextWindow: 163840,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0,
outputPrice: 0,
description: "TNGTech DeepSeek R1T Chimera model.",
@ -351,8 +297,6 @@ export const chutesModels = {
contextWindow: 151329,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0,
outputPrice: 0,
description:
@ -363,8 +307,6 @@ export const chutesModels = {
contextWindow: 131072,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0,
outputPrice: 0,
description:
@ -375,8 +317,6 @@ export const chutesModels = {
contextWindow: 131072,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 1,
outputPrice: 3,
description: "GLM-4.5-turbo model with 128K token context window, optimized for fast inference.",
@ -386,8 +326,6 @@ export const chutesModels = {
contextWindow: 202752,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0,
outputPrice: 0,
description:
@ -398,8 +336,6 @@ export const chutesModels = {
contextWindow: 202752,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 1.15,
outputPrice: 3.25,
description: "GLM-4.6-turbo model with 200K-token context window, optimized for fast inference.",
@ -409,8 +345,6 @@ export const chutesModels = {
contextWindow: 128000,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0,
outputPrice: 0,
description:
@ -421,8 +355,6 @@ export const chutesModels = {
contextWindow: 262144,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0,
outputPrice: 0,
description: "Qwen3 Coder 480B A35B Instruct FP8 model, optimized for coding tasks.",
@ -432,8 +364,6 @@ export const chutesModels = {
contextWindow: 75000,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0.1481,
outputPrice: 0.5926,
description: "Moonshot AI Kimi K2 Instruct model with 75k context window.",
@ -443,8 +373,6 @@ export const chutesModels = {
contextWindow: 262144,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0.1999,
outputPrice: 0.8001,
description: "Moonshot AI Kimi K2 Instruct 0905 model with 256k context window.",
@ -454,8 +382,6 @@ export const chutesModels = {
contextWindow: 262144,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0.077968332,
outputPrice: 0.31202496,
description: "Qwen3 235B A22B Thinking 2507 model with 262K context window.",
@ -465,8 +391,6 @@ export const chutesModels = {
contextWindow: 131072,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0,
outputPrice: 0,
description:
@ -477,8 +401,6 @@ export const chutesModels = {
contextWindow: 131072,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0,
outputPrice: 0,
description:
@ -489,8 +411,6 @@ export const chutesModels = {
contextWindow: 262144,
supportsImages: true,
supportsPromptCache: false,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0.16,
outputPrice: 0.65,
description:

View file

@ -1,160 +0,0 @@
import type { ModelInfo } from "../model.js"
/**
* Rate limit information from Claude Code API
*/
export interface ClaudeCodeRateLimitInfo {
// 5-hour limit info
fiveHour: {
status: string
utilization: number
resetTime: number // Unix timestamp
}
// 7-day (weekly) limit info (Sonnet-specific)
weekly?: {
status: string
utilization: number
resetTime: number // Unix timestamp
}
// 7-day unified limit info
weeklyUnified?: {
status: string
utilization: number
resetTime: number // Unix timestamp
}
// Representative claim type
representativeClaim?: string
// Overage status
overage?: {
status: string
disabledReason?: string
}
// Fallback percentage
fallbackPercentage?: number
// Organization ID
organizationId?: string
// Timestamp when this was fetched
fetchedAt: number
}
// Regex pattern to strip date suffix from model names
const DATE_SUFFIX_PATTERN = /-\d{8}$/
// Models that work with Claude Code OAuth tokens
// See: https://docs.anthropic.com/en/docs/claude-code
// NOTE: Claude Code is subscription-based with no per-token cost - pricing fields are 0
export const claudeCodeModels = {
"claude-haiku-4-5": {
maxTokens: 32768,
contextWindow: 200_000,
supportsImages: true,
supportsPromptCache: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
supportsReasoningEffort: ["disable", "low", "medium", "high"],
reasoningEffort: "medium",
description: "Claude Haiku 4.5 - Fast and efficient with thinking",
},
"claude-sonnet-4-5": {
maxTokens: 32768,
contextWindow: 200_000,
supportsImages: true,
supportsPromptCache: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
supportsReasoningEffort: ["disable", "low", "medium", "high"],
reasoningEffort: "medium",
description: "Claude Sonnet 4.5 - Balanced performance with thinking",
},
"claude-opus-4-5": {
maxTokens: 32768,
contextWindow: 200_000,
supportsImages: true,
supportsPromptCache: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
supportsReasoningEffort: ["disable", "low", "medium", "high"],
reasoningEffort: "medium",
description: "Claude Opus 4.5 - Most capable with thinking",
},
} as const satisfies Record<string, ModelInfo>
// Claude Code - Only models that work with Claude Code OAuth tokens
export type ClaudeCodeModelId = keyof typeof claudeCodeModels
export const claudeCodeDefaultModelId: ClaudeCodeModelId = "claude-sonnet-4-5"
/**
* Model family patterns for normalization.
* Maps regex patterns to their canonical Claude Code model IDs.
*
* Order matters - more specific patterns should come first.
*/
const MODEL_FAMILY_PATTERNS: Array<{ pattern: RegExp; target: ClaudeCodeModelId }> = [
// Opus models (any version) → claude-opus-4-5
{ pattern: /opus/i, target: "claude-opus-4-5" },
// Haiku models (any version) → claude-haiku-4-5
{ pattern: /haiku/i, target: "claude-haiku-4-5" },
// Sonnet models (any version) → claude-sonnet-4-5
{ pattern: /sonnet/i, target: "claude-sonnet-4-5" },
]
/**
* Normalizes a Claude model ID to a valid Claude Code model ID.
*
* This function handles backward compatibility for legacy model names
* that may include version numbers or date suffixes. It maps:
* - claude-sonnet-4-5-20250929, claude-sonnet-4-20250514, claude-3-7-sonnet-20250219, claude-3-5-sonnet-20241022 claude-sonnet-4-5
* - claude-opus-4-5-20251101, claude-opus-4-1-20250805, claude-opus-4-20250514 claude-opus-4-5
* - claude-haiku-4-5-20251001, claude-3-5-haiku-20241022 claude-haiku-4-5
*
* @param modelId - The model ID to normalize (may be a legacy format)
* @returns A valid ClaudeCodeModelId, or the original ID if already valid
*
* @example
* normalizeClaudeCodeModelId("claude-sonnet-4-5") // returns "claude-sonnet-4-5"
* normalizeClaudeCodeModelId("claude-3-5-sonnet-20241022") // returns "claude-sonnet-4-5"
* normalizeClaudeCodeModelId("claude-opus-4-1-20250805") // returns "claude-opus-4-5"
*/
export function normalizeClaudeCodeModelId(modelId: string): ClaudeCodeModelId {
// If already a valid model ID, return as-is
// Use Object.hasOwn() instead of 'in' operator to avoid matching inherited properties like 'toString'
if (Object.hasOwn(claudeCodeModels, modelId)) {
return modelId as ClaudeCodeModelId
}
// Strip date suffix if present (e.g., -20250514)
const withoutDate = modelId.replace(DATE_SUFFIX_PATTERN, "")
// Check if stripping the date makes it valid
if (Object.hasOwn(claudeCodeModels, withoutDate)) {
return withoutDate as ClaudeCodeModelId
}
// Match by model family
for (const { pattern, target } of MODEL_FAMILY_PATTERNS) {
if (pattern.test(modelId)) {
return target
}
}
// Fallback to default if no match (shouldn't happen with valid Claude models)
return claudeCodeDefaultModelId
}
/**
* Reasoning effort configuration for Claude Code thinking mode.
* Maps reasoning effort level to budget_tokens for the thinking process.
*
* Note: With interleaved thinking (enabled via beta header), budget_tokens
* can exceed max_tokens as the token limit becomes the entire context window.
* The max_tokens is drawn from the model's maxTokens definition.
*
* @see https://docs.anthropic.com/en/docs/build-with-claude/extended-thinking#interleaved-thinking
*/
export const claudeCodeReasoningConfig = {
low: { budgetTokens: 16_000 },
medium: { budgetTokens: 32_000 },
high: { budgetTokens: 64_000 },
} as const
export type ClaudeCodeReasoningLevel = keyof typeof claudeCodeReasoningConfig

View file

@ -8,7 +8,6 @@ export const deepInfraDefaultModelInfo: ModelInfo = {
contextWindow: 262144,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
inputPrice: 0.3,
outputPrice: 1.2,
description: "Qwen 3 Coder 480B A35B Instruct Turbo model, 256K context.",

View file

@ -14,8 +14,6 @@ export const deepSeekModels = {
contextWindow: 128_000,
supportsImages: false,
supportsPromptCache: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0.28, // $0.28 per million tokens (cache miss) - Updated Dec 9, 2025
outputPrice: 0.42, // $0.42 per million tokens - Updated Dec 9, 2025
cacheWritesPrice: 0.28, // $0.28 per million tokens (cache miss) - Updated Dec 9, 2025
@ -27,8 +25,6 @@ export const deepSeekModels = {
contextWindow: 128_000,
supportsImages: false,
supportsPromptCache: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
preserveReasoning: true,
inputPrice: 0.28, // $0.28 per million tokens (cache miss) - Updated Dec 9, 2025
outputPrice: 0.42, // $0.42 per million tokens - Updated Dec 9, 2025

View file

@ -8,8 +8,6 @@ export const doubaoModels = {
contextWindow: 128_000,
supportsImages: true,
supportsPromptCache: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0.0001, // $0.0001 per million tokens (cache miss)
outputPrice: 0.0004, // $0.0004 per million tokens
cacheWritesPrice: 0.0001, // $0.0001 per million tokens (cache miss)
@ -21,8 +19,6 @@ export const doubaoModels = {
contextWindow: 128_000,
supportsImages: true,
supportsPromptCache: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0.0002, // $0.0002 per million tokens
outputPrice: 0.0008, // $0.0008 per million tokens
cacheWritesPrice: 0.0002, // $0.0002 per million
@ -34,8 +30,6 @@ export const doubaoModels = {
contextWindow: 128_000,
supportsImages: true,
supportsPromptCache: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0.00015, // $0.00015 per million tokens
outputPrice: 0.0006, // $0.0006 per million tokens
cacheWritesPrice: 0.00015, // $0.00015 per million

View file

@ -13,7 +13,6 @@ export const featherlessModels = {
contextWindow: 32678,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
inputPrice: 0,
outputPrice: 0,
description: "DeepSeek V3 0324 model.",
@ -23,7 +22,6 @@ export const featherlessModels = {
contextWindow: 32678,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
inputPrice: 0,
outputPrice: 0,
description: "DeepSeek R1 0528 model.",
@ -33,7 +31,6 @@ export const featherlessModels = {
contextWindow: 32678,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
inputPrice: 0,
outputPrice: 0,
description: "Kimi K2 Instruct model.",
@ -43,7 +40,6 @@ export const featherlessModels = {
contextWindow: 32678,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
inputPrice: 0,
outputPrice: 0,
description: "GPT-OSS 120B model.",
@ -53,7 +49,6 @@ export const featherlessModels = {
contextWindow: 32678,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
inputPrice: 0,
outputPrice: 0,
description: "Qwen3 Coder 480B A35B Instruct model.",

View file

@ -24,8 +24,6 @@ export const fireworksModels = {
contextWindow: 262144,
supportsImages: false,
supportsPromptCache: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0.6,
outputPrice: 2.5,
cacheReadsPrice: 0.15,
@ -37,8 +35,6 @@ export const fireworksModels = {
contextWindow: 128000,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0.6,
outputPrice: 2.5,
description:
@ -49,7 +45,6 @@ export const fireworksModels = {
contextWindow: 256000,
supportsImages: false,
supportsPromptCache: true,
supportsNativeTools: true,
supportsTemperature: true,
preserveReasoning: true,
defaultTemperature: 1.0,
@ -64,8 +59,6 @@ export const fireworksModels = {
contextWindow: 204800,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0.3,
outputPrice: 1.2,
description:
@ -76,8 +69,6 @@ export const fireworksModels = {
contextWindow: 256000,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0.22,
outputPrice: 0.88,
description: "Latest Qwen3 thinking model, competitive against the best closed source models in Jul 2025.",
@ -87,8 +78,6 @@ export const fireworksModels = {
contextWindow: 256000,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0.45,
outputPrice: 1.8,
description: "Qwen3's most agentic code model to date.",
@ -98,8 +87,6 @@ export const fireworksModels = {
contextWindow: 160000,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 3,
outputPrice: 8,
description:
@ -110,8 +97,6 @@ export const fireworksModels = {
contextWindow: 128000,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0.9,
outputPrice: 0.9,
description:
@ -122,8 +107,6 @@ export const fireworksModels = {
contextWindow: 163840,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0.56,
outputPrice: 1.68,
description:
@ -134,8 +117,6 @@ export const fireworksModels = {
contextWindow: 128000,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0.55,
outputPrice: 2.19,
description:
@ -146,8 +127,6 @@ export const fireworksModels = {
contextWindow: 128000,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0.55,
outputPrice: 2.19,
description:
@ -158,8 +137,6 @@ export const fireworksModels = {
contextWindow: 198000,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0.55,
outputPrice: 2.19,
description:
@ -170,8 +147,6 @@ export const fireworksModels = {
contextWindow: 128000,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0.07,
outputPrice: 0.3,
description:
@ -182,8 +157,6 @@ export const fireworksModels = {
contextWindow: 128000,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0.15,
outputPrice: 0.6,
description:

View file

@ -10,8 +10,6 @@ export const geminiModels = {
maxTokens: 65_536,
contextWindow: 1_048_576,
supportsImages: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
supportsPromptCache: true,
supportsReasoningEffort: ["low", "high"],
reasoningEffort: "low",
@ -37,8 +35,6 @@ export const geminiModels = {
maxTokens: 65_536,
contextWindow: 1_048_576,
supportsImages: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
supportsPromptCache: true,
supportsReasoningEffort: ["minimal", "low", "medium", "high"],
reasoningEffort: "medium",
@ -55,8 +51,6 @@ export const geminiModels = {
maxTokens: 64_000,
contextWindow: 1_048_576,
supportsImages: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
supportsPromptCache: true,
inputPrice: 2.5, // This is the pricing for prompts above 200k tokens.
@ -85,8 +79,6 @@ export const geminiModels = {
maxTokens: 65_535,
contextWindow: 1_048_576,
supportsImages: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
supportsPromptCache: true,
inputPrice: 2.5, // This is the pricing for prompts above 200k tokens.
@ -114,8 +106,6 @@ export const geminiModels = {
maxTokens: 65_535,
contextWindow: 1_048_576,
supportsImages: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
supportsPromptCache: true,
inputPrice: 2.5, // This is the pricing for prompts above 200k tokens.
@ -141,8 +131,6 @@ export const geminiModels = {
maxTokens: 65_535,
contextWindow: 1_048_576,
supportsImages: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
supportsPromptCache: true,
inputPrice: 2.5, // This is the pricing for prompts above 200k tokens.
@ -172,8 +160,6 @@ export const geminiModels = {
maxTokens: 65_536,
contextWindow: 1_048_576,
supportsImages: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
supportsPromptCache: true,
inputPrice: 0.3,
@ -187,8 +173,6 @@ export const geminiModels = {
maxTokens: 65_536,
contextWindow: 1_048_576,
supportsImages: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
supportsPromptCache: true,
inputPrice: 0.3,
@ -202,8 +186,6 @@ export const geminiModels = {
maxTokens: 64_000,
contextWindow: 1_048_576,
supportsImages: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
supportsPromptCache: true,
inputPrice: 0.3,
@ -219,8 +201,6 @@ export const geminiModels = {
maxTokens: 65_536,
contextWindow: 1_048_576,
supportsImages: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
supportsPromptCache: true,
inputPrice: 0.1,
@ -234,8 +214,6 @@ export const geminiModels = {
maxTokens: 65_536,
contextWindow: 1_048_576,
supportsImages: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
supportsPromptCache: true,
inputPrice: 0.1,

View file

@ -19,8 +19,6 @@ export const groqModels = {
contextWindow: 131072,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0.05,
outputPrice: 0.08,
description: "Meta Llama 3.1 8B Instant model, 128K context.",
@ -30,8 +28,6 @@ export const groqModels = {
contextWindow: 131072,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0.59,
outputPrice: 0.79,
description: "Meta Llama 3.3 70B Versatile model, 128K context.",
@ -41,8 +37,6 @@ export const groqModels = {
contextWindow: 131072,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0.11,
outputPrice: 0.34,
description: "Meta Llama 4 Scout 17B Instruct model, 128K context.",
@ -52,8 +46,6 @@ export const groqModels = {
contextWindow: 131072,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0.29,
outputPrice: 0.59,
description: "Alibaba Qwen 3 32B model, 128K context.",
@ -63,8 +55,6 @@ export const groqModels = {
contextWindow: 262144,
supportsImages: false,
supportsPromptCache: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0.6,
outputPrice: 2.5,
cacheReadsPrice: 0.15,
@ -76,8 +66,6 @@ export const groqModels = {
contextWindow: 131072,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0.15,
outputPrice: 0.75,
description:
@ -88,8 +76,6 @@ export const groqModels = {
contextWindow: 131072,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0.1,
outputPrice: 0.5,
description:

View file

@ -3,7 +3,6 @@ export * from "./baseten.js"
export * from "./bedrock.js"
export * from "./cerebras.js"
export * from "./chutes.js"
export * from "./claude-code.js"
export * from "./deepseek.js"
export * from "./doubao.js"
export * from "./featherless.js"
@ -19,6 +18,7 @@ export * from "./moonshot.js"
export * from "./ollama.js"
export * from "./openai.js"
export * from "./openai-codex.js"
export * from "./openai-codex-rate-limits.js"
export * from "./openrouter.js"
export * from "./qwen-code.js"
export * from "./requesty.js"
@ -38,7 +38,6 @@ import { basetenDefaultModelId } from "./baseten.js"
import { bedrockDefaultModelId } from "./bedrock.js"
import { cerebrasDefaultModelId } from "./cerebras.js"
import { chutesDefaultModelId } from "./chutes.js"
import { claudeCodeDefaultModelId } from "./claude-code.js"
import { deepSeekDefaultModelId } from "./deepseek.js"
import { doubaoDefaultModelId } from "./doubao.js"
import { featherlessDefaultModelId } from "./featherless.js"
@ -127,8 +126,6 @@ export function getProviderDefaultModelId(
return deepInfraDefaultModelId
case "vscode-lm":
return vscodeLlmDefaultModelId
case "claude-code":
return claudeCodeDefaultModelId
case "cerebras":
return cerebrasDefaultModelId
case "sambanova":

View file

@ -18,7 +18,6 @@ export const ioIntelligenceModels = {
contextWindow: 128000,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
description: "DeepSeek R1 reasoning model",
},
"meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8": {
@ -26,7 +25,6 @@ export const ioIntelligenceModels = {
contextWindow: 430000,
supportsImages: true,
supportsPromptCache: false,
supportsNativeTools: true,
description: "Llama 4 Maverick 17B model",
},
"Intel/Qwen3-Coder-480B-A35B-Instruct-int4-mixed-ar": {
@ -34,7 +32,6 @@ export const ioIntelligenceModels = {
contextWindow: 106000,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
description: "Qwen3 Coder 480B specialized for coding",
},
"openai/gpt-oss-120b": {
@ -42,7 +39,6 @@ export const ioIntelligenceModels = {
contextWindow: 131072,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
description: "OpenAI GPT-OSS 120B model",
},
} as const satisfies Record<string, ModelInfo>

View file

@ -8,8 +8,6 @@ export const litellmDefaultModelInfo: ModelInfo = {
contextWindow: 200_000,
supportsImages: true,
supportsPromptCache: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 3.0,
outputPrice: 15.0,
cacheWritesPrice: 3.75,

View file

@ -10,8 +10,6 @@ export const lMStudioDefaultModelInfo: ModelInfo = {
contextWindow: 200_000,
supportsImages: true,
supportsPromptCache: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0,
outputPrice: 0,
cacheWritesPrice: 0,

View file

@ -13,8 +13,6 @@ export const minimaxModels = {
contextWindow: 192_000,
supportsImages: false,
supportsPromptCache: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
includedTools: ["search_and_replace"],
excludedTools: ["apply_diff"],
preserveReasoning: true,
@ -30,8 +28,6 @@ export const minimaxModels = {
contextWindow: 192_000,
supportsImages: false,
supportsPromptCache: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
includedTools: ["search_and_replace"],
excludedTools: ["apply_diff"],
preserveReasoning: true,
@ -47,8 +43,6 @@ export const minimaxModels = {
contextWindow: 192_000,
supportsImages: false,
supportsPromptCache: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
includedTools: ["search_and_replace"],
excludedTools: ["apply_diff"],
preserveReasoning: true,

View file

@ -11,8 +11,6 @@ export const mistralModels = {
contextWindow: 128_000,
supportsImages: true,
supportsPromptCache: false,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 2.0,
outputPrice: 5.0,
},
@ -21,8 +19,6 @@ export const mistralModels = {
contextWindow: 131_000,
supportsImages: true,
supportsPromptCache: false,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0.4,
outputPrice: 2.0,
},
@ -31,8 +27,6 @@ export const mistralModels = {
contextWindow: 131_000,
supportsImages: true,
supportsPromptCache: false,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0.4,
outputPrice: 2.0,
},
@ -41,8 +35,6 @@ export const mistralModels = {
contextWindow: 256_000,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0.3,
outputPrice: 0.9,
},
@ -51,8 +43,6 @@ export const mistralModels = {
contextWindow: 131_000,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 2.0,
outputPrice: 6.0,
},
@ -61,8 +51,6 @@ export const mistralModels = {
contextWindow: 131_000,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0.1,
outputPrice: 0.1,
},
@ -71,8 +59,6 @@ export const mistralModels = {
contextWindow: 131_000,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0.04,
outputPrice: 0.04,
},
@ -81,8 +67,6 @@ export const mistralModels = {
contextWindow: 32_000,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0.2,
outputPrice: 0.6,
},
@ -91,8 +75,6 @@ export const mistralModels = {
contextWindow: 131_000,
supportsImages: true,
supportsPromptCache: false,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 2.0,
outputPrice: 6.0,
},

View file

@ -11,8 +11,6 @@ export const moonshotModels = {
contextWindow: 131_072,
supportsImages: false,
supportsPromptCache: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0.6, // $0.60 per million tokens (cache miss)
outputPrice: 2.5, // $2.50 per million tokens
cacheWritesPrice: 0, // $0 per million tokens (cache miss)
@ -24,8 +22,6 @@ export const moonshotModels = {
contextWindow: 262144,
supportsImages: false,
supportsPromptCache: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0.6,
outputPrice: 2.5,
cacheReadsPrice: 0.15,
@ -37,8 +33,6 @@ export const moonshotModels = {
contextWindow: 262_144,
supportsImages: false,
supportsPromptCache: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 2.4, // $2.40 per million tokens (cache miss)
outputPrice: 10, // $10.00 per million tokens
cacheWritesPrice: 0, // $0 per million tokens (cache miss)
@ -50,8 +44,6 @@ export const moonshotModels = {
contextWindow: 262_144, // 262,144 tokens
supportsImages: false, // Text-only (no image/vision support)
supportsPromptCache: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0.6, // $0.60 per million tokens (cache miss)
outputPrice: 2.5, // $2.50 per million tokens
cacheWritesPrice: 0, // $0 per million tokens (cache miss)

View file

@ -8,7 +8,6 @@ export const ollamaDefaultModelInfo: ModelInfo = {
contextWindow: 200_000,
supportsImages: true,
supportsPromptCache: true,
supportsNativeTools: true,
inputPrice: 0,
outputPrice: 0,
cacheWritesPrice: 0,

View file

@ -0,0 +1,29 @@
/**
* OpenAI Codex usage/rate limit information (ChatGPT subscription)
*/
export interface OpenAiCodexRateLimitInfo {
primary?: {
/** Used percent in 0100 */
usedPercent: number
/** Window length in minutes, when provided */
windowMinutes?: number
/** Reset time (unix ms since epoch), when provided */
resetsAt?: number
}
secondary?: {
/** Used percent in 0100 */
usedPercent: number
/** Window length in minutes, when provided */
windowMinutes?: number
/** Reset time (unix ms since epoch), when provided */
resetsAt?: number
}
credits?: {
hasCredits: boolean
unlimited: boolean
balance?: string
}
planType?: string
/** Timestamp when this was fetched (unix ms since epoch) */
fetchedAt: number
}

View file

@ -27,8 +27,6 @@ export const openAiCodexModels = {
"gpt-5.1-codex-max": {
maxTokens: 128000,
contextWindow: 400000,
supportsNativeTools: true,
defaultToolProtocol: "native",
includedTools: ["apply_patch"],
excludedTools: ["apply_diff", "write_to_file"],
supportsImages: true,
@ -44,8 +42,6 @@ export const openAiCodexModels = {
"gpt-5.1-codex": {
maxTokens: 128000,
contextWindow: 400000,
supportsNativeTools: true,
defaultToolProtocol: "native",
includedTools: ["apply_patch"],
excludedTools: ["apply_diff", "write_to_file"],
supportsImages: true,
@ -61,8 +57,6 @@ export const openAiCodexModels = {
"gpt-5.2-codex": {
maxTokens: 128000,
contextWindow: 400000,
supportsNativeTools: true,
defaultToolProtocol: "native",
includedTools: ["apply_patch"],
excludedTools: ["apply_diff", "write_to_file"],
supportsImages: true,
@ -77,8 +71,6 @@ export const openAiCodexModels = {
"gpt-5.1": {
maxTokens: 128000,
contextWindow: 400000,
supportsNativeTools: true,
defaultToolProtocol: "native",
includedTools: ["apply_patch"],
excludedTools: ["apply_diff", "write_to_file"],
supportsImages: true,
@ -95,8 +87,6 @@ export const openAiCodexModels = {
"gpt-5": {
maxTokens: 128000,
contextWindow: 400000,
supportsNativeTools: true,
defaultToolProtocol: "native",
includedTools: ["apply_patch"],
excludedTools: ["apply_diff", "write_to_file"],
supportsImages: true,
@ -113,8 +103,6 @@ export const openAiCodexModels = {
"gpt-5-codex": {
maxTokens: 128000,
contextWindow: 400000,
supportsNativeTools: true,
defaultToolProtocol: "native",
includedTools: ["apply_patch"],
excludedTools: ["apply_diff", "write_to_file"],
supportsImages: true,
@ -130,8 +118,6 @@ export const openAiCodexModels = {
"gpt-5-codex-mini": {
maxTokens: 128000,
contextWindow: 400000,
supportsNativeTools: true,
defaultToolProtocol: "native",
includedTools: ["apply_patch"],
excludedTools: ["apply_diff", "write_to_file"],
supportsImages: true,
@ -147,8 +133,6 @@ export const openAiCodexModels = {
"gpt-5.1-codex-mini": {
maxTokens: 128000,
contextWindow: 400000,
supportsNativeTools: true,
defaultToolProtocol: "native",
includedTools: ["apply_patch"],
excludedTools: ["apply_diff", "write_to_file"],
supportsImages: true,
@ -163,8 +147,6 @@ export const openAiCodexModels = {
"gpt-5.2": {
maxTokens: 128000,
contextWindow: 400000,
supportsNativeTools: true,
defaultToolProtocol: "native",
includedTools: ["apply_patch"],
excludedTools: ["apply_diff", "write_to_file"],
supportsImages: true,

View file

@ -9,8 +9,6 @@ export const openAiNativeModels = {
"gpt-5.1-codex-max": {
maxTokens: 128000,
contextWindow: 400000,
supportsNativeTools: true,
defaultToolProtocol: "native",
includedTools: ["apply_patch"],
excludedTools: ["apply_diff", "write_to_file"],
supportsImages: true,
@ -29,8 +27,6 @@ export const openAiNativeModels = {
"gpt-5.2": {
maxTokens: 128000,
contextWindow: 400000,
supportsNativeTools: true,
defaultToolProtocol: "native",
includedTools: ["apply_patch"],
excludedTools: ["apply_diff", "write_to_file"],
supportsImages: true,
@ -52,8 +48,6 @@ export const openAiNativeModels = {
"gpt-5.2-codex": {
maxTokens: 128000,
contextWindow: 400000,
supportsNativeTools: true,
defaultToolProtocol: "native",
includedTools: ["apply_patch"],
excludedTools: ["apply_diff", "write_to_file"],
supportsImages: true,
@ -72,8 +66,6 @@ export const openAiNativeModels = {
"gpt-5.2-chat-latest": {
maxTokens: 16_384,
contextWindow: 128_000,
supportsNativeTools: true,
defaultToolProtocol: "native",
includedTools: ["apply_patch"],
excludedTools: ["apply_diff", "write_to_file"],
supportsImages: true,
@ -86,8 +78,6 @@ export const openAiNativeModels = {
"gpt-5.1": {
maxTokens: 128000,
contextWindow: 400000,
supportsNativeTools: true,
defaultToolProtocol: "native",
includedTools: ["apply_patch"],
excludedTools: ["apply_diff", "write_to_file"],
supportsImages: true,
@ -109,8 +99,6 @@ export const openAiNativeModels = {
"gpt-5.1-codex": {
maxTokens: 128000,
contextWindow: 400000,
supportsNativeTools: true,
defaultToolProtocol: "native",
includedTools: ["apply_patch"],
excludedTools: ["apply_diff", "write_to_file"],
supportsImages: true,
@ -128,8 +116,6 @@ export const openAiNativeModels = {
"gpt-5.1-codex-mini": {
maxTokens: 128000,
contextWindow: 400000,
supportsNativeTools: true,
defaultToolProtocol: "native",
includedTools: ["apply_patch"],
excludedTools: ["apply_diff", "write_to_file"],
supportsImages: true,
@ -146,8 +132,6 @@ export const openAiNativeModels = {
"gpt-5": {
maxTokens: 128000,
contextWindow: 400000,
supportsNativeTools: true,
defaultToolProtocol: "native",
includedTools: ["apply_patch"],
excludedTools: ["apply_diff", "write_to_file"],
supportsImages: true,
@ -168,8 +152,6 @@ export const openAiNativeModels = {
"gpt-5-mini": {
maxTokens: 128000,
contextWindow: 400000,
supportsNativeTools: true,
defaultToolProtocol: "native",
includedTools: ["apply_patch"],
excludedTools: ["apply_diff", "write_to_file"],
supportsImages: true,
@ -190,8 +172,6 @@ export const openAiNativeModels = {
"gpt-5-codex": {
maxTokens: 128000,
contextWindow: 400000,
supportsNativeTools: true,
defaultToolProtocol: "native",
includedTools: ["apply_patch"],
excludedTools: ["apply_diff", "write_to_file"],
supportsImages: true,
@ -208,8 +188,6 @@ export const openAiNativeModels = {
"gpt-5-nano": {
maxTokens: 128000,
contextWindow: 400000,
supportsNativeTools: true,
defaultToolProtocol: "native",
includedTools: ["apply_patch"],
excludedTools: ["apply_diff", "write_to_file"],
supportsImages: true,
@ -227,8 +205,6 @@ export const openAiNativeModels = {
"gpt-5-chat-latest": {
maxTokens: 128000,
contextWindow: 400000,
supportsNativeTools: true,
defaultToolProtocol: "native",
includedTools: ["apply_patch"],
excludedTools: ["apply_diff", "write_to_file"],
supportsImages: true,
@ -241,8 +217,6 @@ export const openAiNativeModels = {
"gpt-4.1": {
maxTokens: 32_768,
contextWindow: 1_047_576,
supportsNativeTools: true,
defaultToolProtocol: "native",
includedTools: ["apply_patch"],
excludedTools: ["apply_diff", "write_to_file"],
supportsImages: true,
@ -258,8 +232,6 @@ export const openAiNativeModels = {
"gpt-4.1-mini": {
maxTokens: 32_768,
contextWindow: 1_047_576,
supportsNativeTools: true,
defaultToolProtocol: "native",
includedTools: ["apply_patch"],
excludedTools: ["apply_diff", "write_to_file"],
supportsImages: true,
@ -275,8 +247,6 @@ export const openAiNativeModels = {
"gpt-4.1-nano": {
maxTokens: 32_768,
contextWindow: 1_047_576,
supportsNativeTools: true,
defaultToolProtocol: "native",
includedTools: ["apply_patch"],
excludedTools: ["apply_diff", "write_to_file"],
supportsImages: true,
@ -292,8 +262,6 @@ export const openAiNativeModels = {
o3: {
maxTokens: 100_000,
contextWindow: 200_000,
supportsNativeTools: true,
defaultToolProtocol: "native",
supportsImages: true,
supportsPromptCache: true,
inputPrice: 2.0,
@ -310,8 +278,6 @@ export const openAiNativeModels = {
"o3-high": {
maxTokens: 100_000,
contextWindow: 200_000,
supportsNativeTools: true,
defaultToolProtocol: "native",
supportsImages: true,
supportsPromptCache: true,
inputPrice: 2.0,
@ -323,8 +289,6 @@ export const openAiNativeModels = {
"o3-low": {
maxTokens: 100_000,
contextWindow: 200_000,
supportsNativeTools: true,
defaultToolProtocol: "native",
supportsImages: true,
supportsPromptCache: true,
inputPrice: 2.0,
@ -336,8 +300,6 @@ export const openAiNativeModels = {
"o4-mini": {
maxTokens: 100_000,
contextWindow: 200_000,
supportsNativeTools: true,
defaultToolProtocol: "native",
supportsImages: true,
supportsPromptCache: true,
inputPrice: 1.1,
@ -354,8 +316,6 @@ export const openAiNativeModels = {
"o4-mini-high": {
maxTokens: 100_000,
contextWindow: 200_000,
supportsNativeTools: true,
defaultToolProtocol: "native",
supportsImages: true,
supportsPromptCache: true,
inputPrice: 1.1,
@ -367,8 +327,6 @@ export const openAiNativeModels = {
"o4-mini-low": {
maxTokens: 100_000,
contextWindow: 200_000,
supportsNativeTools: true,
defaultToolProtocol: "native",
supportsImages: true,
supportsPromptCache: true,
inputPrice: 1.1,
@ -380,8 +338,6 @@ export const openAiNativeModels = {
"o3-mini": {
maxTokens: 100_000,
contextWindow: 200_000,
supportsNativeTools: true,
defaultToolProtocol: "native",
supportsImages: false,
supportsPromptCache: true,
inputPrice: 1.1,
@ -394,8 +350,6 @@ export const openAiNativeModels = {
"o3-mini-high": {
maxTokens: 100_000,
contextWindow: 200_000,
supportsNativeTools: true,
defaultToolProtocol: "native",
supportsImages: false,
supportsPromptCache: true,
inputPrice: 1.1,
@ -407,8 +361,6 @@ export const openAiNativeModels = {
"o3-mini-low": {
maxTokens: 100_000,
contextWindow: 200_000,
supportsNativeTools: true,
defaultToolProtocol: "native",
supportsImages: false,
supportsPromptCache: true,
inputPrice: 1.1,
@ -420,8 +372,6 @@ export const openAiNativeModels = {
o1: {
maxTokens: 100_000,
contextWindow: 200_000,
supportsNativeTools: true,
defaultToolProtocol: "native",
supportsImages: true,
supportsPromptCache: true,
inputPrice: 15,
@ -432,8 +382,6 @@ export const openAiNativeModels = {
"o1-preview": {
maxTokens: 32_768,
contextWindow: 128_000,
supportsNativeTools: true,
defaultToolProtocol: "native",
supportsImages: true,
supportsPromptCache: true,
inputPrice: 15,
@ -444,8 +392,6 @@ export const openAiNativeModels = {
"o1-mini": {
maxTokens: 65_536,
contextWindow: 128_000,
supportsNativeTools: true,
defaultToolProtocol: "native",
supportsImages: true,
supportsPromptCache: true,
inputPrice: 1.1,
@ -456,8 +402,6 @@ export const openAiNativeModels = {
"gpt-4o": {
maxTokens: 16_384,
contextWindow: 128_000,
supportsNativeTools: true,
defaultToolProtocol: "native",
supportsImages: true,
supportsPromptCache: true,
inputPrice: 2.5,
@ -471,8 +415,6 @@ export const openAiNativeModels = {
"gpt-4o-mini": {
maxTokens: 16_384,
contextWindow: 128_000,
supportsNativeTools: true,
defaultToolProtocol: "native",
supportsImages: true,
supportsPromptCache: true,
inputPrice: 0.15,
@ -486,8 +428,6 @@ export const openAiNativeModels = {
"codex-mini-latest": {
maxTokens: 16_384,
contextWindow: 200_000,
supportsNativeTools: true,
defaultToolProtocol: "native",
supportsImages: false,
supportsPromptCache: false,
inputPrice: 1.5,
@ -501,8 +441,6 @@ export const openAiNativeModels = {
"gpt-5-2025-08-07": {
maxTokens: 128000,
contextWindow: 400000,
supportsNativeTools: true,
defaultToolProtocol: "native",
includedTools: ["apply_patch"],
excludedTools: ["apply_diff", "write_to_file"],
supportsImages: true,
@ -523,8 +461,6 @@ export const openAiNativeModels = {
"gpt-5-mini-2025-08-07": {
maxTokens: 128000,
contextWindow: 400000,
supportsNativeTools: true,
defaultToolProtocol: "native",
includedTools: ["apply_patch"],
excludedTools: ["apply_diff", "write_to_file"],
supportsImages: true,
@ -545,8 +481,6 @@ export const openAiNativeModels = {
"gpt-5-nano-2025-08-07": {
maxTokens: 128000,
contextWindow: 400000,
supportsNativeTools: true,
defaultToolProtocol: "native",
includedTools: ["apply_patch"],
excludedTools: ["apply_diff", "write_to_file"],
supportsImages: true,
@ -570,8 +504,6 @@ export const openAiModelInfoSaneDefaults: ModelInfo = {
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
supportsNativeTools: true,
defaultToolProtocol: "native",
}
// https://learn.microsoft.com/en-us/azure/ai-services/openai/api-version-deprecation

View file

@ -8,7 +8,6 @@ export const openRouterDefaultModelInfo: ModelInfo = {
contextWindow: 200_000,
supportsImages: true,
supportsPromptCache: true,
supportsNativeTools: true,
inputPrice: 3.0,
outputPrice: 15.0,
cacheWritesPrice: 3.75,

View file

@ -10,8 +10,6 @@ export const qwenCodeModels = {
contextWindow: 1_000_000,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0,
outputPrice: 0,
cacheWritesPrice: 0,
@ -23,8 +21,6 @@ export const qwenCodeModels = {
contextWindow: 1_000_000,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0,
outputPrice: 0,
cacheWritesPrice: 0,

View file

@ -9,8 +9,6 @@ export const requestyDefaultModelInfo: ModelInfo = {
contextWindow: 200_000,
supportsImages: true,
supportsPromptCache: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 3.0,
outputPrice: 15.0,
cacheWritesPrice: 3.75,

View file

@ -19,8 +19,6 @@ export const sambaNovaModels = {
contextWindow: 16384,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0.1,
outputPrice: 0.2,
description: "Meta Llama 3.1 8B Instruct model with 16K context window.",
@ -30,8 +28,6 @@ export const sambaNovaModels = {
contextWindow: 131072,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0.6,
outputPrice: 1.2,
description: "Meta Llama 3.3 70B Instruct model with 128K context window.",
@ -42,8 +38,6 @@ export const sambaNovaModels = {
supportsImages: false,
supportsPromptCache: false,
supportsReasoningBudget: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 5.0,
outputPrice: 7.0,
description: "DeepSeek R1 reasoning model with 32K context window.",
@ -53,8 +47,6 @@ export const sambaNovaModels = {
contextWindow: 32768,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 3.0,
outputPrice: 4.5,
description: "DeepSeek V3 model with 32K context window.",
@ -64,8 +56,6 @@ export const sambaNovaModels = {
contextWindow: 32768,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 3.0,
outputPrice: 4.5,
description: "DeepSeek V3.1 model with 32K context window.",
@ -75,8 +65,6 @@ export const sambaNovaModels = {
contextWindow: 131072,
supportsImages: true,
supportsPromptCache: false,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0.63,
outputPrice: 1.8,
description: "Meta Llama 4 Maverick 17B 128E Instruct model with 128K context window.",
@ -86,8 +74,6 @@ export const sambaNovaModels = {
contextWindow: 8192,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0.4,
outputPrice: 0.8,
description: "Alibaba Qwen 3 32B model with 8K context window.",
@ -97,8 +83,6 @@ export const sambaNovaModels = {
contextWindow: 131072,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0.22,
outputPrice: 0.59,
description: "OpenAI gpt oss 120b model with 128k context window.",

View file

@ -7,7 +7,6 @@ export const unboundDefaultModelInfo: ModelInfo = {
contextWindow: 200_000,
supportsImages: true,
supportsPromptCache: true,
supportsNativeTools: true,
inputPrice: 3.0,
outputPrice: 15.0,
cacheWritesPrice: 3.75,

View file

@ -90,7 +90,6 @@ export const vercelAiGatewayDefaultModelInfo: ModelInfo = {
contextWindow: 200000,
supportsImages: true,
supportsPromptCache: true,
supportsNativeTools: true,
inputPrice: 3,
outputPrice: 15,
cacheWritesPrice: 3.75,

View file

@ -10,8 +10,6 @@ export const vertexModels = {
maxTokens: 65_536,
contextWindow: 1_048_576,
supportsImages: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
supportsPromptCache: true,
supportsReasoningEffort: ["low", "high"],
reasoningEffort: "low",
@ -37,8 +35,6 @@ export const vertexModels = {
maxTokens: 65_536,
contextWindow: 1_048_576,
supportsImages: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
supportsPromptCache: true,
supportsReasoningEffort: ["minimal", "low", "medium", "high"],
reasoningEffort: "medium",
@ -54,8 +50,6 @@ export const vertexModels = {
maxTokens: 65_535,
contextWindow: 1_048_576,
supportsImages: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
supportsPromptCache: true,
inputPrice: 0.15,
@ -68,8 +62,6 @@ export const vertexModels = {
maxTokens: 65_535,
contextWindow: 1_048_576,
supportsImages: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
supportsPromptCache: true,
inputPrice: 0.15,
@ -79,8 +71,6 @@ export const vertexModels = {
maxTokens: 64_000,
contextWindow: 1_048_576,
supportsImages: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
supportsPromptCache: true,
inputPrice: 0.3,
@ -94,8 +84,6 @@ export const vertexModels = {
maxTokens: 65_535,
contextWindow: 1_048_576,
supportsImages: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
supportsPromptCache: false,
inputPrice: 0.15,
@ -108,8 +96,6 @@ export const vertexModels = {
maxTokens: 65_535,
contextWindow: 1_048_576,
supportsImages: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
supportsPromptCache: false,
inputPrice: 0.15,
@ -119,8 +105,6 @@ export const vertexModels = {
maxTokens: 65_535,
contextWindow: 1_048_576,
supportsImages: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
supportsPromptCache: true,
inputPrice: 2.5,
@ -130,8 +114,6 @@ export const vertexModels = {
maxTokens: 65_535,
contextWindow: 1_048_576,
supportsImages: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
supportsPromptCache: true,
inputPrice: 2.5,
@ -141,8 +123,6 @@ export const vertexModels = {
maxTokens: 65_535,
contextWindow: 1_048_576,
supportsImages: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
supportsPromptCache: true,
inputPrice: 2.5,
@ -154,8 +134,6 @@ export const vertexModels = {
maxTokens: 64_000,
contextWindow: 1_048_576,
supportsImages: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
supportsPromptCache: true,
inputPrice: 2.5,
@ -182,8 +160,6 @@ export const vertexModels = {
maxTokens: 65_535,
contextWindow: 1_048_576,
supportsImages: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
supportsPromptCache: false,
inputPrice: 0,
@ -193,8 +169,6 @@ export const vertexModels = {
maxTokens: 8192,
contextWindow: 2_097_152,
supportsImages: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
supportsPromptCache: false,
inputPrice: 0,
@ -204,8 +178,6 @@ export const vertexModels = {
maxTokens: 8192,
contextWindow: 1_048_576,
supportsImages: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
supportsPromptCache: true,
inputPrice: 0.15,
@ -215,8 +187,6 @@ export const vertexModels = {
maxTokens: 8192,
contextWindow: 1_048_576,
supportsImages: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
supportsPromptCache: false,
inputPrice: 0.075,
@ -226,8 +196,6 @@ export const vertexModels = {
maxTokens: 8192,
contextWindow: 32_768,
supportsImages: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
supportsPromptCache: false,
inputPrice: 0,
@ -237,8 +205,6 @@ export const vertexModels = {
maxTokens: 8192,
contextWindow: 1_048_576,
supportsImages: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
supportsPromptCache: true,
inputPrice: 0.075,
@ -248,8 +214,6 @@ export const vertexModels = {
maxTokens: 8192,
contextWindow: 2_097_152,
supportsImages: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
supportsPromptCache: false,
inputPrice: 1.25,
@ -260,8 +224,6 @@ export const vertexModels = {
contextWindow: 200_000, // Default 200K, extendable to 1M with beta flag 'context-1m-2025-08-07'
supportsImages: true,
supportsPromptCache: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 3.0, // $3 per million input tokens (≤200K context)
outputPrice: 15.0, // $15 per million output tokens (≤200K context)
cacheWritesPrice: 3.75, // $3.75 per million tokens
@ -283,8 +245,6 @@ export const vertexModels = {
contextWindow: 200_000, // Default 200K, extendable to 1M with beta flag 'context-1m-2025-08-07'
supportsImages: true,
supportsPromptCache: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 3.0, // $3 per million input tokens (≤200K context)
outputPrice: 15.0, // $15 per million output tokens (≤200K context)
cacheWritesPrice: 3.75, // $3.75 per million tokens
@ -306,8 +266,6 @@ export const vertexModels = {
contextWindow: 200_000,
supportsImages: true,
supportsPromptCache: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 1.0,
outputPrice: 5.0,
cacheWritesPrice: 1.25,
@ -319,8 +277,6 @@ export const vertexModels = {
contextWindow: 200_000,
supportsImages: true,
supportsPromptCache: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 5.0,
outputPrice: 25.0,
cacheWritesPrice: 6.25,
@ -332,8 +288,6 @@ export const vertexModels = {
contextWindow: 200_000,
supportsImages: true,
supportsPromptCache: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 15.0,
outputPrice: 75.0,
cacheWritesPrice: 18.75,
@ -345,8 +299,6 @@ export const vertexModels = {
contextWindow: 200_000,
supportsImages: true,
supportsPromptCache: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 15.0,
outputPrice: 75.0,
cacheWritesPrice: 18.75,
@ -357,8 +309,6 @@ export const vertexModels = {
contextWindow: 200_000,
supportsImages: true,
supportsPromptCache: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 3.0,
outputPrice: 15.0,
cacheWritesPrice: 3.75,
@ -371,8 +321,6 @@ export const vertexModels = {
contextWindow: 200_000,
supportsImages: true,
supportsPromptCache: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 3.0,
outputPrice: 15.0,
cacheWritesPrice: 3.75,
@ -383,8 +331,6 @@ export const vertexModels = {
contextWindow: 200_000,
supportsImages: true,
supportsPromptCache: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 3.0,
outputPrice: 15.0,
cacheWritesPrice: 3.75,
@ -395,8 +341,6 @@ export const vertexModels = {
contextWindow: 200_000,
supportsImages: true,
supportsPromptCache: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 3.0,
outputPrice: 15.0,
cacheWritesPrice: 3.75,
@ -407,8 +351,6 @@ export const vertexModels = {
contextWindow: 200_000,
supportsImages: false,
supportsPromptCache: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 1.0,
outputPrice: 5.0,
cacheWritesPrice: 1.25,
@ -419,8 +361,6 @@ export const vertexModels = {
contextWindow: 200_000,
supportsImages: true,
supportsPromptCache: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 15.0,
outputPrice: 75.0,
cacheWritesPrice: 18.75,
@ -431,8 +371,6 @@ export const vertexModels = {
contextWindow: 200_000,
supportsImages: true,
supportsPromptCache: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0.25,
outputPrice: 1.25,
cacheWritesPrice: 0.3,
@ -442,8 +380,6 @@ export const vertexModels = {
maxTokens: 64_000,
contextWindow: 1_048_576,
supportsImages: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
supportsPromptCache: true,
inputPrice: 0.1,
@ -458,7 +394,6 @@ export const vertexModels = {
contextWindow: 131072,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
inputPrice: 0.35,
outputPrice: 1.15,
description: "Meta Llama 4 Maverick 17B Instruct model, 128K context.",
@ -468,7 +403,6 @@ export const vertexModels = {
contextWindow: 163_840,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
inputPrice: 1.35,
outputPrice: 5.4,
description: "DeepSeek R1 (0528). Available in us-central1",
@ -478,7 +412,6 @@ export const vertexModels = {
contextWindow: 163_840,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
inputPrice: 0.6,
outputPrice: 1.7,
description: "DeepSeek V3.1. Available in us-west2",
@ -488,7 +421,6 @@ export const vertexModels = {
contextWindow: 131_072,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
inputPrice: 0.15,
outputPrice: 0.6,
description: "OpenAI gpt-oss 120B. Available in us-central1",
@ -498,7 +430,6 @@ export const vertexModels = {
contextWindow: 131_072,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
inputPrice: 0.075,
outputPrice: 0.3,
description: "OpenAI gpt-oss 20B. Available in us-central1",
@ -508,7 +439,6 @@ export const vertexModels = {
contextWindow: 262_144,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
inputPrice: 1.0,
outputPrice: 4.0,
description: "Qwen3 Coder 480B A35B Instruct. Available in us-south1",
@ -518,11 +448,19 @@ export const vertexModels = {
contextWindow: 262_144,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
inputPrice: 0.25,
outputPrice: 1.0,
description: "Qwen3 235B A22B Instruct. Available in us-south1",
},
"moonshotai/kimi-k2-thinking-maas": {
maxTokens: 16_384,
contextWindow: 262_144,
supportsPromptCache: false,
supportsImages: false,
inputPrice: 0.6,
outputPrice: 2.5,
description: "Kimi K2 Thinking Model with 256K context window.",
},
} as const satisfies Record<string, ModelInfo>
// Vertex AI models that support 1M context window beta

View file

@ -11,8 +11,6 @@ export const xaiModels = {
contextWindow: 256_000,
supportsImages: true,
supportsPromptCache: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0.2,
outputPrice: 1.5,
cacheWritesPrice: 0.02,
@ -26,8 +24,6 @@ export const xaiModels = {
contextWindow: 2_000_000,
supportsImages: true,
supportsPromptCache: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0.2,
outputPrice: 0.5,
cacheWritesPrice: 0.05,
@ -42,8 +38,6 @@ export const xaiModels = {
contextWindow: 2_000_000,
supportsImages: true,
supportsPromptCache: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0.2,
outputPrice: 0.5,
cacheWritesPrice: 0.05,
@ -58,8 +52,6 @@ export const xaiModels = {
contextWindow: 2_000_000,
supportsImages: true,
supportsPromptCache: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0.2,
outputPrice: 0.5,
cacheWritesPrice: 0.05,
@ -74,8 +66,6 @@ export const xaiModels = {
contextWindow: 2_000_000,
supportsImages: true,
supportsPromptCache: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0.2,
outputPrice: 0.5,
cacheWritesPrice: 0.05,
@ -90,8 +80,6 @@ export const xaiModels = {
contextWindow: 256_000,
supportsImages: true,
supportsPromptCache: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 3.0,
outputPrice: 15.0,
cacheWritesPrice: 0.75,
@ -105,8 +93,6 @@ export const xaiModels = {
contextWindow: 131072,
supportsImages: true,
supportsPromptCache: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0.3,
outputPrice: 0.5,
cacheWritesPrice: 0.07,
@ -122,8 +108,6 @@ export const xaiModels = {
contextWindow: 131072,
supportsImages: true,
supportsPromptCache: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 3.0,
outputPrice: 15.0,
cacheWritesPrice: 0.75,

View file

@ -16,8 +16,6 @@ export const internationalZAiModels = {
contextWindow: 131_072,
supportsImages: false,
supportsPromptCache: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0.6,
outputPrice: 2.2,
cacheWritesPrice: 0,
@ -30,8 +28,6 @@ export const internationalZAiModels = {
contextWindow: 131_072,
supportsImages: false,
supportsPromptCache: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0.2,
outputPrice: 1.1,
cacheWritesPrice: 0,
@ -44,8 +40,6 @@ export const internationalZAiModels = {
contextWindow: 131_072,
supportsImages: false,
supportsPromptCache: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 2.2,
outputPrice: 8.9,
cacheWritesPrice: 0,
@ -58,8 +52,6 @@ export const internationalZAiModels = {
contextWindow: 131_072,
supportsImages: false,
supportsPromptCache: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 1.1,
outputPrice: 4.5,
cacheWritesPrice: 0,
@ -71,8 +63,6 @@ export const internationalZAiModels = {
contextWindow: 131_072,
supportsImages: false,
supportsPromptCache: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0,
outputPrice: 0,
cacheWritesPrice: 0,
@ -84,8 +74,6 @@ export const internationalZAiModels = {
contextWindow: 131_072,
supportsImages: true,
supportsPromptCache: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0.6,
outputPrice: 1.8,
cacheWritesPrice: 0,
@ -98,8 +86,6 @@ export const internationalZAiModels = {
contextWindow: 200_000,
supportsImages: false,
supportsPromptCache: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0.6,
outputPrice: 2.2,
cacheWritesPrice: 0,
@ -112,8 +98,6 @@ export const internationalZAiModels = {
contextWindow: 200_000,
supportsImages: false,
supportsPromptCache: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
supportsReasoningEffort: ["disable", "medium"],
reasoningEffort: "medium",
preserveReasoning: true,
@ -129,8 +113,6 @@ export const internationalZAiModels = {
contextWindow: 131_072,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0.1,
outputPrice: 0.1,
cacheWritesPrice: 0,
@ -147,8 +129,6 @@ export const mainlandZAiModels = {
contextWindow: 131_072,
supportsImages: false,
supportsPromptCache: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0.29,
outputPrice: 1.14,
cacheWritesPrice: 0,
@ -161,8 +141,6 @@ export const mainlandZAiModels = {
contextWindow: 131_072,
supportsImages: false,
supportsPromptCache: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0.1,
outputPrice: 0.6,
cacheWritesPrice: 0,
@ -175,8 +153,6 @@ export const mainlandZAiModels = {
contextWindow: 131_072,
supportsImages: false,
supportsPromptCache: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0.29,
outputPrice: 1.14,
cacheWritesPrice: 0,
@ -189,8 +165,6 @@ export const mainlandZAiModels = {
contextWindow: 131_072,
supportsImages: false,
supportsPromptCache: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0.1,
outputPrice: 0.6,
cacheWritesPrice: 0,
@ -202,8 +176,6 @@ export const mainlandZAiModels = {
contextWindow: 131_072,
supportsImages: false,
supportsPromptCache: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0,
outputPrice: 0,
cacheWritesPrice: 0,
@ -215,8 +187,6 @@ export const mainlandZAiModels = {
contextWindow: 131_072,
supportsImages: true,
supportsPromptCache: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0.29,
outputPrice: 0.93,
cacheWritesPrice: 0,
@ -229,8 +199,6 @@ export const mainlandZAiModels = {
contextWindow: 204_800,
supportsImages: false,
supportsPromptCache: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
inputPrice: 0.29,
outputPrice: 1.14,
cacheWritesPrice: 0,
@ -243,8 +211,6 @@ export const mainlandZAiModels = {
contextWindow: 204_800,
supportsImages: false,
supportsPromptCache: true,
supportsNativeTools: true,
defaultToolProtocol: "native",
supportsReasoningEffort: ["disable", "medium"],
reasoningEffort: "medium",
preserveReasoning: true,

View file

@ -57,48 +57,3 @@ 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]
/**
* Default model info properties for native tool support.
* Used to merge with cached model info that may lack these fields.
* Router providers (Requesty, Unbound, LiteLLM) assume all models support native tools.
*/
export const NATIVE_TOOL_DEFAULTS = {
supportsNativeTools: true,
defaultToolProtocol: TOOL_PROTOCOL.NATIVE,
} as const
/**
* 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
}

View file

@ -19,6 +19,8 @@ import type { SerializedCustomToolDefinition } from "./custom-tool.js"
import type { GitCommit } from "./git.js"
import type { McpServer } from "./mcp.js"
import type { ModelRecord, RouterModels } from "./model.js"
import type { OpenAiCodexRateLimitInfo } from "./providers/openai-codex-rate-limits.js"
import type { WorktreeIncludeStatus } from "./worktree.js"
/**
* ExtensionMessage
@ -28,6 +30,8 @@ export interface ExtensionMessage {
type:
| "action"
| "state"
| "taskHistoryUpdated"
| "taskHistoryItemUpdated"
| "selectedImages"
| "theme"
| "workspaceUpdated"
@ -91,10 +95,19 @@ export interface ExtensionMessage {
| "interactionRequired"
| "browserSessionUpdate"
| "browserSessionNavigate"
| "claudeCodeRateLimits"
| "customToolsResult"
| "modes"
| "taskWithAggregatedCosts"
| "openAiCodexRateLimits"
// Worktree response types
| "worktreeList"
| "worktreeResult"
| "worktreeCopyProgress"
| "branchList"
| "worktreeDefaults"
| "worktreeIncludeStatus"
| "branchWorktreeIncludeResult"
| "mergeWorktreeResult"
text?: string
payload?: any // eslint-disable-line @typescript-eslint/no-explicit-any
checkpointWarning?: {
@ -107,12 +120,17 @@ export interface ExtensionMessage {
| "historyButtonClicked"
| "marketplaceButtonClicked"
| "cloudButtonClicked"
| "worktreesButtonClicked"
| "didBecomeVisible"
| "focusInput"
| "switchTab"
| "toggleAutoApprove"
invoke?: "newChat" | "sendMessage" | "primaryButtonClick" | "secondaryButtonClick" | "setChatBoxMessage"
state?: ExtensionState
/**
* Partial state updates are allowed to reduce message size (e.g. omit large fields like taskHistory).
* The webview is responsible for merging.
*/
state?: Partial<ExtensionState>
images?: string[]
filePaths?: string[]
openedTabs?: Array<{
@ -150,7 +168,9 @@ export interface ExtensionMessage {
customMode?: ModeConfig
slug?: string
success?: boolean
values?: Record<string, any> // eslint-disable-line @typescript-eslint/no-explicit-any
/** Generic payload for extension messages that use `values` */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
values?: Record<string, any>
requestId?: string
promptText?: string
results?:
@ -190,6 +210,64 @@ export interface ExtensionMessage {
childrenCost: number
}
historyItem?: HistoryItem
taskHistory?: HistoryItem[] // For taskHistoryUpdated: full sorted task history
/** For taskHistoryItemUpdated: single updated/added history item */
taskHistoryItem?: HistoryItem
// Worktree response properties
worktrees?: Array<{
path: string
branch: string
commitHash: string
isCurrent: boolean
isBare: boolean
isDetached: boolean
isLocked: boolean
lockReason?: string
}>
isGitRepo?: boolean
isMultiRoot?: boolean
isSubfolder?: boolean
gitRootPath?: string
worktreeResult?: {
success: boolean
message: string
worktree?: {
path: string
branch: string
commitHash: string
isCurrent: boolean
isBare: boolean
isDetached: boolean
isLocked: boolean
lockReason?: string
}
}
localBranches?: string[]
remoteBranches?: string[]
currentBranch?: string
suggestedBranch?: string
suggestedPath?: string
worktreeIncludeExists?: boolean
worktreeIncludeStatus?: WorktreeIncludeStatus
hasGitignore?: boolean
gitignoreContent?: string
hasConflicts?: boolean
conflictingFiles?: string[]
sourceBranch?: string
targetBranch?: string
// branchWorktreeIncludeResult
branch?: string
hasWorktreeInclude?: boolean
// worktreeCopyProgress (size-based)
copyProgressBytesCopied?: number
copyProgressTotalBytes?: number
copyProgressItemName?: string
}
export interface OpenAiCodexRateLimitsMessage {
type: "openAiCodexRateLimits"
values?: OpenAiCodexRateLimitInfo
error?: string
}
export type ExtensionState = Pick<
@ -246,7 +324,6 @@ export type ExtensionState = Pick<
| "customModePrompts"
| "customSupportPrompts"
| "enhancementApiConfigId"
| "condensingApiConfigId"
| "customCondensingPrompt"
| "codebaseIndexConfig"
| "codebaseIndexModels"
@ -332,7 +409,6 @@ export type ExtensionState = Pick<
remoteControlEnabled: boolean
taskSyncEnabled: boolean
featureRoomoteControlEnabled: boolean
claudeCodeIsAuthenticated?: boolean
openAiCodexIsAuthenticated?: boolean
debug?: boolean
}
@ -461,8 +537,6 @@ export interface WebviewMessage {
| "cloudLandingPageSignIn"
| "rooCloudSignOut"
| "rooCloudManualUrl"
| "claudeCodeSignIn"
| "claudeCodeSignOut"
| "openAiCodexSignIn"
| "openAiCodexSignOut"
| "switchOrganization"
@ -517,11 +591,23 @@ export interface WebviewMessage {
| "openDebugApiHistory"
| "openDebugUiHistory"
| "downloadErrorDiagnostics"
| "requestClaudeCodeRateLimits"
| "requestOpenAiCodexRateLimits"
| "refreshCustomTools"
| "requestModes"
| "switchMode"
| "debugSetting"
// Worktree messages
| "listWorktrees"
| "createWorktree"
| "deleteWorktree"
| "switchWorktree"
| "getAvailableBranches"
| "getWorktreeDefaults"
| "getWorktreeIncludeStatus"
| "checkBranchWorktreeInclude"
| "createWorktreeInclude"
| "checkoutBranch"
| "mergeWorktree"
text?: string
editedMessageContent?: string
tab?: "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "cloud"
@ -546,6 +632,7 @@ export interface WebviewMessage {
promptMode?: string | "enhance"
customPrompt?: PromptComponent
dataUrls?: string[]
/** Generic payload for webview messages that use `values` */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
values?: Record<string, any>
query?: string
@ -610,6 +697,20 @@ export interface WebviewMessage {
codebaseIndexOpenRouterApiKey?: string
}
updatedSettings?: RooCodeSettings
// Worktree properties
worktreePath?: string
worktreeBranch?: string
worktreeBaseBranch?: string
worktreeCreateNewBranch?: boolean
worktreeForce?: boolean
worktreeNewWindow?: boolean
worktreeTargetBranch?: string
worktreeDeleteAfterMerge?: boolean
worktreeIncludeContent?: string
}
export interface RequestOpenAiCodexRateLimitsMessage {
type: "requestOpenAiCodexRateLimits"
}
export const checkoutDiffPayloadSchema = z.object({

View file

@ -35,6 +35,7 @@ export const commandIds = [
"popoutButtonClicked",
"cloudButtonClicked",
"settingsButtonClicked",
"worktreesButtonClicked",
"openInNewTab",

View file

@ -0,0 +1,129 @@
/**
* Worktree Types
*
* Platform-agnostic type definitions for git worktree operations.
* These types are decoupled from VSCode and can be used by any consumer.
*/
/**
* Represents a git worktree
*/
export interface Worktree {
/** Absolute path to the worktree directory */
path: string
/** Branch name - empty string if detached HEAD */
branch: string
/** Current commit hash */
commitHash: string
/** Whether this is the current worktree (matches cwd) */
isCurrent: boolean
/** Whether this is the bare/main repository */
isBare: boolean
/** Whether HEAD is detached (not on a branch) */
isDetached: boolean
/** Whether the worktree is locked */
isLocked: boolean
/** Reason for lock if locked */
lockReason?: string
}
/**
* Result of a worktree operation (create, delete, etc.)
*/
export interface WorktreeResult {
/** Whether the operation succeeded */
success: boolean
/** Human-readable message describing the result */
message: string
/** The worktree that was affected (if applicable) */
worktree?: Worktree
}
/**
* Branch information for worktree creation
*/
export interface BranchInfo {
/** Local branches available */
localBranches: string[]
/** Remote branches available */
remoteBranches: string[]
/** Currently checked out branch */
currentBranch: string
}
/**
* Options for creating a worktree
*/
export interface CreateWorktreeOptions {
/** Path where the worktree will be created */
path: string
/** Branch name to checkout or create */
branch?: string
/** Base branch to create new branch from */
baseBranch?: string
/** If true, create a new branch; if false, checkout existing branch */
createNewBranch?: boolean
}
/**
* Options for merging a worktree branch
*/
export interface MergeWorktreeOptions {
/** Path to the worktree being merged */
worktreePath: string
/** Target branch to merge into */
targetBranch: string
/** If true, delete the worktree after successful merge */
deleteAfterMerge?: boolean
}
/**
* Result of a merge operation
*/
export interface MergeWorktreeResult {
/** Whether the merge succeeded */
success: boolean
/** Human-readable message describing the result */
message: string
/** Whether there are merge conflicts */
hasConflicts: boolean
/** List of files with conflicts */
conflictingFiles: string[]
/** Source branch that was merged */
sourceBranch?: string
/** Target branch that was merged into */
targetBranch?: string
}
/**
* Status of .worktreeinclude file
*/
export interface WorktreeIncludeStatus {
/** Whether .worktreeinclude exists in the directory */
exists: boolean
/** Whether .gitignore exists in the directory */
hasGitignore: boolean
/** Content of .gitignore (for creating .worktreeinclude) */
gitignoreContent?: string
}
/**
* Response for listWorktrees handler
*/
export interface WorktreeListResponse {
worktrees: Worktree[]
isGitRepo: boolean
error?: string
isMultiRoot: boolean
isSubfolder: boolean
gitRootPath: string
}
/**
* Response for worktree defaults
*/
export interface WorktreeDefaultsResponse {
suggestedBranch: string
suggestedPath: string
error?: string
}

73
pnpm-lock.yaml generated
View file

@ -103,6 +103,12 @@ importers:
commander:
specifier: ^12.1.0
version: 12.1.0
cross-spawn:
specifier: ^7.0.6
version: 7.0.6
execa:
specifier: ^9.5.2
version: 9.6.0
fuzzysort:
specifier: ^3.1.0
version: 3.1.0
@ -551,6 +557,9 @@ importers:
execa:
specifier: ^9.5.2
version: 9.6.0
ignore:
specifier: ^7.0.3
version: 7.0.5
openai:
specifier: ^5.12.2
version: 5.12.2(ws@8.18.3)(zod@3.25.76)
@ -1115,6 +1124,9 @@ importers:
'@radix-ui/react-progress':
specifier: ^1.1.2
version: 1.1.6(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-radio-group':
specifier: ^1.3.8
version: 1.3.8(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-select':
specifier: ^2.1.6
version: 2.2.4(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@ -3074,6 +3086,19 @@ packages:
'@types/react-dom':
optional: true
'@radix-ui/react-radio-group@1.3.8':
resolution: {integrity: sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==}
peerDependencies:
'@types/react': ^18.3.23
'@types/react-dom': ^18.3.5
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-roving-focus@1.1.10':
resolution: {integrity: sha512-dT9aOXUen9JSsxnMPv/0VqySQf5eDQ6LCk5Sw28kamz8wSOW2bJdlX2Bg5VUIIcV+6XlHpWTIuTPCf/UNIyq8Q==}
peerDependencies:
@ -3087,6 +3112,19 @@ packages:
'@types/react-dom':
optional: true
'@radix-ui/react-roving-focus@1.1.11':
resolution: {integrity: sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==}
peerDependencies:
'@types/react': ^18.3.23
'@types/react-dom': ^18.3.5
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-roving-focus@1.1.9':
resolution: {integrity: sha512-ZzrIFnMYHHCNqSNCsuN6l7wlewBEq0O0BCSBkabJMFXVO51LRUTq71gLP1UxFvmrXElqmPjA5VX7IqC9VpazAQ==}
peerDependencies:
@ -12862,6 +12900,24 @@ snapshots:
'@types/react': 18.3.23
'@types/react-dom': 18.3.7(@types/react@18.3.23)
'@radix-ui/react-radio-group@1.3.8(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/primitive': 1.1.3
'@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1)
'@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1)
'@radix-ui/react-direction': 1.1.1(@types/react@18.3.23)(react@18.3.1)
'@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.23)(react@18.3.1)
'@radix-ui/react-use-previous': 1.1.1(@types/react@18.3.23)(react@18.3.1)
'@radix-ui/react-use-size': 1.1.1(@types/react@18.3.23)(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
optionalDependencies:
'@types/react': 18.3.23
'@types/react-dom': 18.3.7(@types/react@18.3.23)
'@radix-ui/react-roving-focus@1.1.10(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/primitive': 1.1.2
@ -12879,6 +12935,23 @@ snapshots:
'@types/react': 18.3.23
'@types/react-dom': 18.3.7(@types/react@18.3.23)
'@radix-ui/react-roving-focus@1.1.11(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/primitive': 1.1.3
'@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1)
'@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1)
'@radix-ui/react-direction': 1.1.1(@types/react@18.3.23)(react@18.3.1)
'@radix-ui/react-id': 1.1.1(@types/react@18.3.23)(react@18.3.1)
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.23)(react@18.3.1)
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.23)(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
optionalDependencies:
'@types/react': 18.3.23
'@types/react-dom': 18.3.7(@types/react@18.3.23)
'@radix-ui/react-roving-focus@1.1.9(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/primitive': 1.1.2

BIN
releases/3.42.0-release.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

View file

@ -288,6 +288,56 @@ describe("History resume delegation - parent metadata transitions", () => {
expect((injectedMsg.content[0] as any).tool_use_id).toBe("toolu_abc123")
})
it("reopenParentFromDelegation injects plain text when no new_task tool_use exists in API history", async () => {
const provider = {
contextProxy: { globalStorageUri: { fsPath: "/storage" } },
getTaskWithId: vi.fn().mockResolvedValue({
historyItem: {
id: "p-no-tool",
status: "delegated",
awaitingChildId: "c-no-tool",
childIds: [],
ts: 100,
task: "Parent without tool_use",
tokensIn: 0,
tokensOut: 0,
totalCost: 0,
},
}),
emit: vi.fn(),
getCurrentTask: vi.fn(() => ({ taskId: "c-no-tool" })),
removeClineFromStack: vi.fn().mockResolvedValue(undefined),
createTaskWithHistoryItem: vi.fn().mockResolvedValue({
taskId: "p-no-tool",
resumeAfterDelegation: vi.fn().mockResolvedValue(undefined),
overwriteClineMessages: vi.fn().mockResolvedValue(undefined),
overwriteApiConversationHistory: vi.fn().mockResolvedValue(undefined),
}),
updateTaskHistory: vi.fn().mockResolvedValue([]),
} as unknown as ClineProvider
// No assistant tool_use in history
const existingUiMessages = [{ type: "ask", ask: "tool", text: "subtask request", ts: 50 }]
const existingApiMessages = [{ role: "user", content: [{ type: "text", text: "Create a subtask" }], ts: 40 }]
vi.mocked(readTaskMessages).mockResolvedValue(existingUiMessages as any)
vi.mocked(readApiMessages).mockResolvedValue(existingApiMessages as any)
await (ClineProvider.prototype as any).reopenParentFromDelegation.call(provider, {
parentTaskId: "p-no-tool",
childTaskId: "c-no-tool",
completionResultSummary: "Subtask completed without tool_use",
})
const apiCall = vi.mocked(saveApiMessages).mock.calls[0][0]
// Should append a user text note
expect(apiCall.messages).toHaveLength(2)
const injected = apiCall.messages[1]
expect(injected.role).toBe("user")
expect((injected.content[0] as any).type).toBe("text")
expect((injected.content[0] as any).text).toContain("Subtask c-no-tool completed")
})
it("reopenParentFromDelegation sets skipPrevResponseIdOnce via resumeAfterDelegation", async () => {
const parentInstance: any = {
skipPrevResponseIdOnce: false,

View file

@ -187,18 +187,21 @@ describe("Nested delegation resume (A → B → C)", () => {
type: "tool_use",
name: "attempt_completion",
params: { result: "C finished" },
nativeArgs: { result: "C finished" },
partial: false,
} as any
const askFinishSubTaskApproval = vi.fn(async () => true)
const handleError = vi.fn(async (_action: string, err: Error) => {
// Fail fast in this test if the tool hits an error path.
throw err
})
await attemptCompletionTool.handle(clineC, blockC, {
askApproval: vi.fn(),
handleError: vi.fn(),
handleError,
pushToolResult: vi.fn(),
removeClosingTag: vi.fn((_, v?: string) => v ?? ""),
askFinishSubTaskApproval,
toolProtocol: "xml",
toolDescription: () => "desc",
} as any)
@ -231,20 +234,21 @@ describe("Nested delegation resume (A → B → C)", () => {
type: "tool_use",
name: "attempt_completion",
params: { result: "B finished" },
nativeArgs: { result: "B finished" },
partial: false,
} as any
await attemptCompletionTool.handle(clineB, blockB, {
askApproval: vi.fn(),
handleError: vi.fn(),
handleError,
pushToolResult: vi.fn(),
removeClosingTag: vi.fn((_, v?: string) => v ?? ""),
askFinishSubTaskApproval,
toolProtocol: "xml",
toolDescription: () => "desc",
} as any)
// After B completes, A must be current
// After B completes, A should become current
// Note: delegation resume may fall back to a non-tool_result user message when the parent history
// does not contain a new_task tool_use. This should not prevent reopening the parent.
expect(currentActiveId).toBe("A")
// Ensure no resume_task asks were scheduled: verified indirectly by startTask:false on both hops

View file

@ -134,6 +134,12 @@ const getCommandsMap = ({ context, outputChannel, provider }: RegisterCommandOpt
if (!visibleProvider) return
visibleProvider.postMessageToWebview({ type: "action", action: "marketplaceButtonClicked" })
},
worktreesButtonClicked: () => {
const visibleProvider = getVisibleProviderOrLog(outputChannel)
if (!visibleProvider) return
TelemetryService.instance.captureTitleButtonClicked("worktrees")
visibleProvider.postMessageToWebview({ type: "action", action: "worktreesButtonClicked" })
},
newTask: handleNewTask,
setCustomStoragePath: async () => {
const { promptForCustomStoragePath } = await import("../utils/storage")

View file

@ -1,7 +1,7 @@
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
import type { ProviderSettings, ModelInfo, ToolProtocol } from "@roo-code/types"
import type { ProviderSettings, ModelInfo } from "@roo-code/types"
import { ApiStream } from "./transform/stream"
@ -29,7 +29,6 @@ import {
HuggingFaceHandler,
ChutesHandler,
LiteLLMHandler,
ClaudeCodeHandler,
QwenCodeHandler,
SambaNovaHandler,
IOIntelligenceHandler,
@ -83,16 +82,10 @@ export interface ApiHandlerCreateMessageMetadata {
* 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
/**
* Controls whether the model can return multiple tool calls in a single response.
* When true, parallel tool calls are enabled (OpenAI's parallel_tool_calls=true).
* When false (default), only one tool call is returned per response.
* Only applies when toolProtocol is "native".
*/
parallelToolCalls?: boolean
/**
@ -132,8 +125,6 @@ export function buildApiHandler(configuration: ProviderSettings): ApiHandler {
switch (apiProvider) {
case "anthropic":
return new AnthropicHandler(options)
case "claude-code":
return new ClaudeCodeHandler(options)
case "openrouter":
return new OpenRouterHandler(options)
case "bedrock":

View file

@ -162,7 +162,7 @@ describe("VertexHandler", () => {
})
expect(mockCreate).toHaveBeenCalledWith(
{
expect.objectContaining({
model: "claude-3-5-sonnet-v2@20241022",
max_tokens: 8192,
temperature: 0,
@ -191,7 +191,10 @@ describe("VertexHandler", () => {
},
],
stream: true,
},
// Tools are now always present (minimum 6 from ALWAYS_AVAILABLE_TOOLS)
tools: expect.any(Array),
tool_choice: expect.any(Object),
}),
undefined,
)
})
@ -1200,13 +1203,11 @@ describe("VertexHandler", () => {
)
})
it("should include tools even when toolProtocol is set to xml (user preference now ignored)", async () => {
// XML protocol deprecation: user preference is now ignored when model supports native tools
it("should include tools when tools are provided", async () => {
handler = new AnthropicVertexHandler({
apiModelId: "claude-3-5-sonnet-v2@20241022",
vertexProjectId: "test-project",
vertexRegion: "us-central1",
toolProtocol: "xml",
})
const mockStream = [
@ -1242,7 +1243,7 @@ describe("VertexHandler", () => {
// Just consume
}
// Native is forced when supportsNativeTools===true, so tools should still be included
// Tool calling is request-driven: if tools are provided, we should include them.
expect(mockCreate).toHaveBeenCalledWith(
expect.objectContaining({
tools: expect.arrayContaining([

View file

@ -420,8 +420,7 @@ describe("AnthropicHandler", () => {
},
]
it("should include tools in request by default (native is default)", async () => {
// Handler uses native protocol by default via model's defaultToolProtocol
it("should include tools in request when tools are provided", async () => {
const stream = handler.createMessage(systemPrompt, messages, {
taskId: "test-task",
tools: mockTools,
@ -451,11 +450,9 @@ describe("AnthropicHandler", () => {
)
})
it("should include tools even when toolProtocol is set to xml (user preference now ignored)", async () => {
// XML protocol deprecation: user preference is now ignored when model supports native tools
it("should include tools when tools are provided", async () => {
const xmlHandler = new AnthropicHandler({
...mockOptions,
toolProtocol: "xml",
})
const stream = xmlHandler.createMessage(systemPrompt, messages, {
@ -468,7 +465,7 @@ describe("AnthropicHandler", () => {
// Just consume
}
// Native is forced when supportsNativeTools===true, so tools should still be included
// Tool calling is request-driven: if tools are provided, we should include them.
expect(mockCreate).toHaveBeenCalledWith(
expect.objectContaining({
tools: expect.arrayContaining([
@ -481,7 +478,7 @@ describe("AnthropicHandler", () => {
)
})
it("should not include tools when no tools are provided", async () => {
it("should always include tools in request (tools are always present after PR #10841)", async () => {
// Handler uses native protocol by default
const stream = handler.createMessage(systemPrompt, messages, {
taskId: "test-task",
@ -492,9 +489,11 @@ describe("AnthropicHandler", () => {
// Just consume
}
// Tools are now always present (minimum 6 from ALWAYS_AVAILABLE_TOOLS)
expect(mockCreate).toHaveBeenCalledWith(
expect.not.objectContaining({
tools: expect.anything(),
expect.objectContaining({
tools: expect.any(Array),
tool_choice: expect.any(Object),
}),
expect.anything(),
)
@ -542,7 +541,7 @@ describe("AnthropicHandler", () => {
)
})
it("should omit both tools and tool_choice when tool_choice is 'none'", async () => {
it("should set tool_choice to undefined when tool_choice is 'none' (tools are still passed)", async () => {
// Handler uses native protocol by default
const stream = handler.createMessage(systemPrompt, messages, {
taskId: "test-task",
@ -555,16 +554,13 @@ describe("AnthropicHandler", () => {
// Just consume
}
// Verify that neither tools nor tool_choice are included in the request
// Tools are now always present (minimum 6 from ALWAYS_AVAILABLE_TOOLS)
// When tool_choice is 'none', the converter returns undefined for tool_choice
// but tools are still passed since they're always present
expect(mockCreate).toHaveBeenCalledWith(
expect.not.objectContaining({
tools: expect.anything(),
}),
expect.anything(),
)
expect(mockCreate).toHaveBeenCalledWith(
expect.not.objectContaining({
tool_choice: expect.anything(),
expect.objectContaining({
tools: expect.any(Array),
tool_choice: undefined,
}),
expect.anything(),
)

View file

@ -57,7 +57,7 @@ describe("BaseOpenAiCompatibleProvider", () => {
vi.restoreAllMocks()
})
describe("XmlMatcher reasoning tags", () => {
describe("TagMatcher reasoning tags", () => {
it("should handle reasoning tags (<think>) from stream", async () => {
mockCreate.mockImplementationOnce(() => {
return {
@ -87,7 +87,7 @@ describe("BaseOpenAiCompatibleProvider", () => {
chunks.push(chunk)
}
// XmlMatcher yields chunks as they're processed
// TagMatcher yields chunks as they're processed
expect(chunks).toEqual([
{ type: "reasoning", text: "Let me think" },
{ type: "reasoning", text: " about this" },
@ -124,7 +124,7 @@ describe("BaseOpenAiCompatibleProvider", () => {
chunks.push(chunk)
}
// When a complete tag arrives in one chunk, XmlMatcher may not parse it
// When a complete tag arrives in one chunk, TagMatcher may not parse it
// This test documents the actual behavior
expect(chunks.length).toBeGreaterThan(0)
expect(chunks[0]).toEqual({ type: "text", text: "Regular text before " })
@ -151,7 +151,7 @@ describe("BaseOpenAiCompatibleProvider", () => {
chunks.push(chunk)
}
// XmlMatcher should handle incomplete tags and flush remaining content
// TagMatcher should handle incomplete tags and flush remaining content
expect(chunks.length).toBeGreaterThan(0)
expect(
chunks.some(

View file

@ -242,11 +242,7 @@ describe("AwsBedrockHandler Native Tool Calling", () => {
})
describe("createMessage with native tools", () => {
it("should include toolConfig when tools are provided with native protocol", async () => {
// Override model info to support native tools
const modelInfo = handler.getModel().info
;(modelInfo as any).supportsNativeTools = true
it("should include toolConfig when tools are provided", async () => {
const handlerWithNativeTools = new AwsBedrockHandler({
apiModelId: "anthropic.claude-3-5-sonnet-20241022-v2:0",
awsAccessKey: "test-access-key",
@ -254,18 +250,9 @@ describe("AwsBedrockHandler Native Tool Calling", () => {
awsRegion: "us-east-1",
})
// Manually set supportsNativeTools
const getModelOriginal = handlerWithNativeTools.getModel.bind(handlerWithNativeTools)
handlerWithNativeTools.getModel = () => {
const model = getModelOriginal()
model.info.supportsNativeTools = true
return model
}
const metadata: ApiHandlerCreateMessageMetadata = {
taskId: "test-task",
tools: testTools,
toolProtocol: "native",
}
const generator = handlerWithNativeTools.createMessage(
@ -285,7 +272,7 @@ describe("AwsBedrockHandler Native Tool Calling", () => {
expect(commandArg.toolConfig.toolChoice).toEqual({ auto: {} })
})
it("should not include toolConfig when toolProtocol is xml", async () => {
it("should always include toolConfig (tools are always present after PR #10841)", async () => {
const handlerWithNativeTools = new AwsBedrockHandler({
apiModelId: "anthropic.claude-3-5-sonnet-20241022-v2:0",
awsAccessKey: "test-access-key",
@ -293,18 +280,9 @@ describe("AwsBedrockHandler Native Tool Calling", () => {
awsRegion: "us-east-1",
})
// Manually set supportsNativeTools
const getModelOriginal = handlerWithNativeTools.getModel.bind(handlerWithNativeTools)
handlerWithNativeTools.getModel = () => {
const model = getModelOriginal()
model.info.supportsNativeTools = true
return model
}
const metadata: ApiHandlerCreateMessageMetadata = {
taskId: "test-task",
tools: testTools,
toolProtocol: "xml", // XML protocol should not use native tools
// Even without explicit tools, tools are always present (minimum 6 from ALWAYS_AVAILABLE_TOOLS)
}
const generator = handlerWithNativeTools.createMessage(
@ -318,10 +296,13 @@ describe("AwsBedrockHandler Native Tool Calling", () => {
expect(mockConverseStreamCommand).toHaveBeenCalled()
const commandArg = mockConverseStreamCommand.mock.calls[0][0] as any
expect(commandArg.toolConfig).toBeUndefined()
// Tools are now always present
expect(commandArg.toolConfig).toBeDefined()
expect(commandArg.toolConfig.tools).toBeDefined()
expect(commandArg.toolConfig.toolChoice).toEqual({ auto: {} })
})
it("should not include toolConfig when tool_choice is none", async () => {
it("should include toolConfig with undefined toolChoice when tool_choice is none", async () => {
const handlerWithNativeTools = new AwsBedrockHandler({
apiModelId: "anthropic.claude-3-5-sonnet-20241022-v2:0",
awsAccessKey: "test-access-key",
@ -329,18 +310,9 @@ describe("AwsBedrockHandler Native Tool Calling", () => {
awsRegion: "us-east-1",
})
// Manually set supportsNativeTools
const getModelOriginal = handlerWithNativeTools.getModel.bind(handlerWithNativeTools)
handlerWithNativeTools.getModel = () => {
const model = getModelOriginal()
model.info.supportsNativeTools = true
return model
}
const metadata: ApiHandlerCreateMessageMetadata = {
taskId: "test-task",
tools: testTools,
toolProtocol: "native",
tool_choice: "none", // Explicitly disable tool use
}
@ -355,7 +327,9 @@ describe("AwsBedrockHandler Native Tool Calling", () => {
expect(mockConverseStreamCommand).toHaveBeenCalled()
const commandArg = mockConverseStreamCommand.mock.calls[0][0] as any
expect(commandArg.toolConfig).toBeUndefined()
// toolConfig is still provided but toolChoice is undefined for "none"
expect(commandArg.toolConfig).toBeDefined()
expect(commandArg.toolConfig.toolChoice).toBeUndefined()
})
it("should include fine-grained tool streaming beta for Claude models with native tools", async () => {
@ -366,18 +340,9 @@ describe("AwsBedrockHandler Native Tool Calling", () => {
awsRegion: "us-east-1",
})
// Manually set supportsNativeTools
const getModelOriginal = handlerWithNativeTools.getModel.bind(handlerWithNativeTools)
handlerWithNativeTools.getModel = () => {
const model = getModelOriginal()
model.info.supportsNativeTools = true
return model
}
const metadata: ApiHandlerCreateMessageMetadata = {
taskId: "test-task",
tools: testTools,
toolProtocol: "native",
}
const generator = handlerWithNativeTools.createMessage(
@ -398,7 +363,7 @@ describe("AwsBedrockHandler Native Tool Calling", () => {
)
})
it("should not include fine-grained tool streaming beta when not using native tools", async () => {
it("should always include fine-grained tool streaming beta for Claude models", async () => {
const handlerWithNativeTools = new AwsBedrockHandler({
apiModelId: "anthropic.claude-3-5-sonnet-20241022-v2:0",
awsAccessKey: "test-access-key",
@ -422,12 +387,11 @@ describe("AwsBedrockHandler Native Tool Calling", () => {
expect(mockConverseStreamCommand).toHaveBeenCalled()
const commandArg = mockConverseStreamCommand.mock.calls[0][0] as any
// Should not include anthropic_beta when not using native tools
if (commandArg.additionalModelRequestFields?.anthropic_beta) {
expect(commandArg.additionalModelRequestFields.anthropic_beta).not.toContain(
"fine-grained-tool-streaming-2025-05-14",
)
}
// Should always include anthropic_beta with fine-grained-tool-streaming for Claude models
expect(commandArg.additionalModelRequestFields).toBeDefined()
expect(commandArg.additionalModelRequestFields.anthropic_beta).toContain(
"fine-grained-tool-streaming-2025-05-14",
)
})
})

View file

@ -221,8 +221,11 @@ describe("AwsBedrockHandler - Extended Thinking", () => {
expect(capturedPayload).toBeDefined()
expect(capturedPayload.inferenceConfig).not.toHaveProperty("topP")
// Verify that additionalModelRequestFields is not present or empty
expect(capturedPayload.additionalModelRequestFields).toBeUndefined()
// Verify that additionalModelRequestFields contains fine-grained-tool-streaming for Claude models
expect(capturedPayload.additionalModelRequestFields).toBeDefined()
expect(capturedPayload.additionalModelRequestFields.anthropic_beta).toContain(
"fine-grained-tool-streaming-2025-05-14",
)
})
it("should enable reasoning when enableReasoningEffort is true in settings", async () => {

View file

@ -754,14 +754,17 @@ describe("AwsBedrockHandler", () => {
expect(mockConverseStreamCommand).toHaveBeenCalled()
const commandArg = mockConverseStreamCommand.mock.calls[0][0] as any
// Should include anthropic_beta in additionalModelRequestFields
// Should include anthropic_beta in additionalModelRequestFields with both 1M context and fine-grained-tool-streaming
expect(commandArg.additionalModelRequestFields).toBeDefined()
expect(commandArg.additionalModelRequestFields.anthropic_beta).toEqual(["context-1m-2025-08-07"])
expect(commandArg.additionalModelRequestFields.anthropic_beta).toContain("context-1m-2025-08-07")
expect(commandArg.additionalModelRequestFields.anthropic_beta).toContain(
"fine-grained-tool-streaming-2025-05-14",
)
// Should not include anthropic_version since thinking is not enabled
expect(commandArg.additionalModelRequestFields.anthropic_version).toBeUndefined()
})
it("should not include anthropic_beta parameter when 1M context is disabled", async () => {
it("should not include 1M context beta when 1M context is disabled but still include fine-grained-tool-streaming", async () => {
const handler = new AwsBedrockHandler({
apiModelId: BEDROCK_1M_CONTEXT_MODEL_IDS[0],
awsAccessKey: "test",
@ -784,11 +787,16 @@ describe("AwsBedrockHandler", () => {
expect(mockConverseStreamCommand).toHaveBeenCalled()
const commandArg = mockConverseStreamCommand.mock.calls[0][0] as any
// Should not include anthropic_beta in additionalModelRequestFields
expect(commandArg.additionalModelRequestFields).toBeUndefined()
// Should include anthropic_beta with fine-grained-tool-streaming for Claude models
expect(commandArg.additionalModelRequestFields).toBeDefined()
expect(commandArg.additionalModelRequestFields.anthropic_beta).toContain(
"fine-grained-tool-streaming-2025-05-14",
)
// Should NOT include 1M context beta
expect(commandArg.additionalModelRequestFields.anthropic_beta).not.toContain("context-1m-2025-08-07")
})
it("should not include anthropic_beta parameter for non-Claude Sonnet 4 models", async () => {
it("should not include 1M context beta for non-Claude Sonnet 4 models but still include fine-grained-tool-streaming", async () => {
const handler = new AwsBedrockHandler({
apiModelId: "anthropic.claude-3-5-sonnet-20241022-v2:0",
awsAccessKey: "test",
@ -811,8 +819,13 @@ describe("AwsBedrockHandler", () => {
expect(mockConverseStreamCommand).toHaveBeenCalled()
const commandArg = mockConverseStreamCommand.mock.calls[0][0] as any
// Should not include anthropic_beta for non-Sonnet 4 models
expect(commandArg.additionalModelRequestFields).toBeUndefined()
// Should include anthropic_beta with fine-grained-tool-streaming for Claude models (even non-Sonnet 4)
expect(commandArg.additionalModelRequestFields).toBeDefined()
expect(commandArg.additionalModelRequestFields.anthropic_beta).toContain(
"fine-grained-tool-streaming-2025-05-14",
)
// Should NOT include 1M context beta for non-Sonnet 4 models
expect(commandArg.additionalModelRequestFields.anthropic_beta).not.toContain("context-1m-2025-08-07")
})
it("should enable 1M context window with cross-region inference for Claude Sonnet 4", () => {
@ -859,9 +872,12 @@ describe("AwsBedrockHandler", () => {
mockConverseStreamCommand.mock.calls.length - 1
][0] as any
// Should include anthropic_beta in additionalModelRequestFields
// Should include anthropic_beta in additionalModelRequestFields with both 1M context and fine-grained-tool-streaming
expect(commandArg.additionalModelRequestFields).toBeDefined()
expect(commandArg.additionalModelRequestFields.anthropic_beta).toEqual(["context-1m-2025-08-07"])
expect(commandArg.additionalModelRequestFields.anthropic_beta).toContain("context-1m-2025-08-07")
expect(commandArg.additionalModelRequestFields.anthropic_beta).toContain(
"fine-grained-tool-streaming-2025-05-14",
)
// Should not include anthropic_version since thinking is not enabled
expect(commandArg.additionalModelRequestFields.anthropic_version).toBeUndefined()
// Model ID should have cross-region prefix

View file

@ -1,169 +0,0 @@
import { ClaudeCodeHandler } from "../claude-code"
import type { ApiHandlerOptions } from "../../../shared/api"
import type { StreamChunk } from "../../../integrations/claude-code/streaming-client"
import type { ApiStreamUsageChunk } from "../../transform/stream"
// Mock the OAuth manager
vi.mock("../../../integrations/claude-code/oauth", () => ({
claudeCodeOAuthManager: {
getAccessToken: vi.fn(),
getEmail: vi.fn(),
loadCredentials: vi.fn(),
saveCredentials: vi.fn(),
clearCredentials: vi.fn(),
isAuthenticated: vi.fn(),
},
generateUserId: vi.fn(() => "user_abc123_account_def456_session_ghi789"),
}))
// Mock the streaming client
vi.mock("../../../integrations/claude-code/streaming-client", () => ({
createStreamingMessage: vi.fn(),
}))
const { claudeCodeOAuthManager } = await import("../../../integrations/claude-code/oauth")
const { createStreamingMessage } = await import("../../../integrations/claude-code/streaming-client")
const mockGetAccessToken = vi.mocked(claudeCodeOAuthManager.getAccessToken)
const mockCreateStreamingMessage = vi.mocked(createStreamingMessage)
describe("ClaudeCodeHandler - Caching Support", () => {
let handler: ClaudeCodeHandler
const mockOptions: ApiHandlerOptions = {
apiModelId: "claude-sonnet-4-5",
}
beforeEach(() => {
handler = new ClaudeCodeHandler(mockOptions)
vi.clearAllMocks()
mockGetAccessToken.mockResolvedValue("test-access-token")
})
it("should collect cache read tokens from API response", async () => {
const mockStream = async function* (): AsyncGenerator<StreamChunk> {
yield { type: "text", text: "Hello!" }
yield {
type: "usage",
inputTokens: 100,
outputTokens: 50,
cacheReadTokens: 80,
cacheWriteTokens: 20,
}
}
mockCreateStreamingMessage.mockReturnValue(mockStream())
const stream = handler.createMessage("System prompt", [{ role: "user", content: "Hello" }])
const chunks = []
for await (const chunk of stream) {
chunks.push(chunk)
}
// Find the usage chunk
const usageChunk = chunks.find((c) => c.type === "usage" && "totalCost" in c) as ApiStreamUsageChunk | undefined
expect(usageChunk).toBeDefined()
expect(usageChunk!.inputTokens).toBe(100)
expect(usageChunk!.outputTokens).toBe(50)
expect(usageChunk!.cacheReadTokens).toBe(80)
expect(usageChunk!.cacheWriteTokens).toBe(20)
})
it("should accumulate cache tokens across multiple messages", async () => {
// Note: The streaming client handles accumulation internally.
// Each usage chunk represents the accumulated totals for that point in the stream.
// This test verifies that we correctly pass through the accumulated values.
const mockStream = async function* (): AsyncGenerator<StreamChunk> {
yield { type: "text", text: "Part 1" }
yield {
type: "usage",
inputTokens: 50,
outputTokens: 25,
cacheReadTokens: 40,
cacheWriteTokens: 10,
}
yield { type: "text", text: "Part 2" }
yield {
type: "usage",
inputTokens: 100, // Accumulated: 50 + 50
outputTokens: 50, // Accumulated: 25 + 25
cacheReadTokens: 70, // Accumulated: 40 + 30
cacheWriteTokens: 30, // Accumulated: 10 + 20
}
}
mockCreateStreamingMessage.mockReturnValue(mockStream())
const stream = handler.createMessage("System prompt", [{ role: "user", content: "Hello" }])
const chunks = []
for await (const chunk of stream) {
chunks.push(chunk)
}
// Get the last usage chunk which should have accumulated totals
const usageChunks = chunks.filter((c) => c.type === "usage" && "totalCost" in c) as ApiStreamUsageChunk[]
expect(usageChunks.length).toBe(2)
const lastUsageChunk = usageChunks[usageChunks.length - 1]
expect(lastUsageChunk.inputTokens).toBe(100) // 50 + 50
expect(lastUsageChunk.outputTokens).toBe(50) // 25 + 25
expect(lastUsageChunk.cacheReadTokens).toBe(70) // 40 + 30
expect(lastUsageChunk.cacheWriteTokens).toBe(30) // 10 + 20
})
it("should handle missing cache token fields gracefully", async () => {
const mockStream = async function* (): AsyncGenerator<StreamChunk> {
yield { type: "text", text: "Hello!" }
yield {
type: "usage",
inputTokens: 100,
outputTokens: 50,
// No cache tokens provided
}
}
mockCreateStreamingMessage.mockReturnValue(mockStream())
const stream = handler.createMessage("System prompt", [{ role: "user", content: "Hello" }])
const chunks = []
for await (const chunk of stream) {
chunks.push(chunk)
}
const usageChunk = chunks.find((c) => c.type === "usage" && "totalCost" in c) as ApiStreamUsageChunk | undefined
expect(usageChunk).toBeDefined()
expect(usageChunk!.inputTokens).toBe(100)
expect(usageChunk!.outputTokens).toBe(50)
expect(usageChunk!.cacheReadTokens).toBeUndefined()
expect(usageChunk!.cacheWriteTokens).toBeUndefined()
})
it("should report zero cost for subscription usage", async () => {
// Claude Code is always subscription-based, cost should always be 0
const mockStream = async function* (): AsyncGenerator<StreamChunk> {
yield { type: "text", text: "Hello!" }
yield {
type: "usage",
inputTokens: 100,
outputTokens: 50,
cacheReadTokens: 80,
cacheWriteTokens: 20,
}
}
mockCreateStreamingMessage.mockReturnValue(mockStream())
const stream = handler.createMessage("System prompt", [{ role: "user", content: "Hello" }])
const chunks = []
for await (const chunk of stream) {
chunks.push(chunk)
}
const usageChunk = chunks.find((c) => c.type === "usage" && "totalCost" in c) as ApiStreamUsageChunk | undefined
expect(usageChunk).toBeDefined()
expect(usageChunk!.totalCost).toBe(0) // Should always be 0 for Claude Code (subscription-based)
})
})

View file

@ -1,597 +0,0 @@
import { ClaudeCodeHandler } from "../claude-code"
import { ApiHandlerOptions } from "../../../shared/api"
import type { StreamChunk } from "../../../integrations/claude-code/streaming-client"
// Mock the OAuth manager
vi.mock("../../../integrations/claude-code/oauth", () => ({
claudeCodeOAuthManager: {
getAccessToken: vi.fn(),
getEmail: vi.fn(),
loadCredentials: vi.fn(),
saveCredentials: vi.fn(),
clearCredentials: vi.fn(),
isAuthenticated: vi.fn(),
},
generateUserId: vi.fn(() => "user_abc123_account_def456_session_ghi789"),
}))
// Mock the streaming client
vi.mock("../../../integrations/claude-code/streaming-client", () => ({
createStreamingMessage: vi.fn(),
}))
const { claudeCodeOAuthManager } = await import("../../../integrations/claude-code/oauth")
const { createStreamingMessage } = await import("../../../integrations/claude-code/streaming-client")
const mockGetAccessToken = vi.mocked(claudeCodeOAuthManager.getAccessToken)
const mockGetEmail = vi.mocked(claudeCodeOAuthManager.getEmail)
const mockCreateStreamingMessage = vi.mocked(createStreamingMessage)
describe("ClaudeCodeHandler", () => {
let handler: ClaudeCodeHandler
beforeEach(() => {
vi.clearAllMocks()
const options: ApiHandlerOptions = {
apiModelId: "claude-sonnet-4-5",
}
handler = new ClaudeCodeHandler(options)
})
test("should create handler with correct model configuration", () => {
const model = handler.getModel()
expect(model.id).toBe("claude-sonnet-4-5")
expect(model.info.supportsImages).toBe(true)
expect(model.info.supportsPromptCache).toBe(true)
})
test("should use default model when invalid model provided", () => {
const options: ApiHandlerOptions = {
apiModelId: "invalid-model",
}
const handlerWithInvalidModel = new ClaudeCodeHandler(options)
const model = handlerWithInvalidModel.getModel()
expect(model.id).toBe("claude-sonnet-4-5") // default model
})
test("should return model maxTokens from model definition", () => {
const options: ApiHandlerOptions = {
apiModelId: "claude-opus-4-5",
}
const handlerWithModel = new ClaudeCodeHandler(options)
const model = handlerWithModel.getModel()
expect(model.id).toBe("claude-opus-4-5")
// Model maxTokens is 32768 as defined in claudeCodeModels for opus
expect(model.info.maxTokens).toBe(32768)
})
test("should support reasoning effort configuration", () => {
const options: ApiHandlerOptions = {
apiModelId: "claude-sonnet-4-5",
}
const handler = new ClaudeCodeHandler(options)
const model = handler.getModel()
// Default model has supportsReasoningEffort
expect(model.info.supportsReasoningEffort).toEqual(["disable", "low", "medium", "high"])
expect(model.info.reasoningEffort).toBe("medium")
})
test("should throw error when not authenticated", async () => {
const systemPrompt = "You are a helpful assistant"
const messages = [{ role: "user" as const, content: "Hello" }]
mockGetAccessToken.mockResolvedValue(null)
const stream = handler.createMessage(systemPrompt, messages)
const iterator = stream[Symbol.asyncIterator]()
await expect(iterator.next()).rejects.toThrow(/not authenticated/i)
})
test("should call createStreamingMessage with thinking enabled by default", async () => {
const systemPrompt = "You are a helpful assistant"
const messages = [{ role: "user" as const, content: "Hello" }]
mockGetAccessToken.mockResolvedValue("test-access-token")
// Mock empty async generator
const mockGenerator = async function* (): AsyncGenerator<StreamChunk> {
// Empty generator for basic test
}
mockCreateStreamingMessage.mockReturnValue(mockGenerator())
const stream = handler.createMessage(systemPrompt, messages)
// Need to start iterating to trigger the call
const iterator = stream[Symbol.asyncIterator]()
await iterator.next()
// Verify createStreamingMessage was called with correct parameters
// Default model has reasoning effort of "medium" so thinking should be enabled
// With interleaved thinking, maxTokens comes from model definition (32768 for claude-sonnet-4-5)
expect(mockCreateStreamingMessage).toHaveBeenCalledWith({
accessToken: "test-access-token",
model: "claude-sonnet-4-5",
systemPrompt,
messages,
maxTokens: 32768, // model's maxTokens from claudeCodeModels definition
thinking: {
type: "enabled",
budget_tokens: 32000, // medium reasoning budget_tokens
},
tools: undefined,
toolChoice: undefined,
metadata: {
user_id: "user_abc123_account_def456_session_ghi789",
},
})
})
test("should disable thinking when reasoningEffort is set to disable", async () => {
const options: ApiHandlerOptions = {
apiModelId: "claude-sonnet-4-5",
reasoningEffort: "disable",
}
const handlerNoThinking = new ClaudeCodeHandler(options)
const systemPrompt = "You are a helpful assistant"
const messages = [{ role: "user" as const, content: "Hello" }]
mockGetAccessToken.mockResolvedValue("test-access-token")
// Mock empty async generator
const mockGenerator = async function* (): AsyncGenerator<StreamChunk> {
// Empty generator for basic test
}
mockCreateStreamingMessage.mockReturnValue(mockGenerator())
const stream = handlerNoThinking.createMessage(systemPrompt, messages)
// Need to start iterating to trigger the call
const iterator = stream[Symbol.asyncIterator]()
await iterator.next()
// Verify createStreamingMessage was called with thinking disabled
expect(mockCreateStreamingMessage).toHaveBeenCalledWith({
accessToken: "test-access-token",
model: "claude-sonnet-4-5",
systemPrompt,
messages,
maxTokens: 32768, // model maxTokens from claudeCodeModels definition
thinking: { type: "disabled" },
tools: undefined,
toolChoice: undefined,
metadata: {
user_id: "user_abc123_account_def456_session_ghi789",
},
})
})
test("should use high reasoning config when reasoningEffort is high", async () => {
const options: ApiHandlerOptions = {
apiModelId: "claude-sonnet-4-5",
reasoningEffort: "high",
}
const handlerHighThinking = new ClaudeCodeHandler(options)
const systemPrompt = "You are a helpful assistant"
const messages = [{ role: "user" as const, content: "Hello" }]
mockGetAccessToken.mockResolvedValue("test-access-token")
// Mock empty async generator
const mockGenerator = async function* (): AsyncGenerator<StreamChunk> {
// Empty generator for basic test
}
mockCreateStreamingMessage.mockReturnValue(mockGenerator())
const stream = handlerHighThinking.createMessage(systemPrompt, messages)
// Need to start iterating to trigger the call
const iterator = stream[Symbol.asyncIterator]()
await iterator.next()
// Verify createStreamingMessage was called with high thinking config
// With interleaved thinking, maxTokens comes from model definition (32768 for claude-sonnet-4-5)
expect(mockCreateStreamingMessage).toHaveBeenCalledWith({
accessToken: "test-access-token",
model: "claude-sonnet-4-5",
systemPrompt,
messages,
maxTokens: 32768, // model's maxTokens from claudeCodeModels definition
thinking: {
type: "enabled",
budget_tokens: 64000, // high reasoning budget_tokens
},
tools: undefined,
toolChoice: undefined,
metadata: {
user_id: "user_abc123_account_def456_session_ghi789",
},
})
})
test("should handle text content from streaming", async () => {
const systemPrompt = "You are a helpful assistant"
const messages = [{ role: "user" as const, content: "Hello" }]
mockGetAccessToken.mockResolvedValue("test-access-token")
// Mock async generator that yields text chunks
const mockGenerator = async function* (): AsyncGenerator<StreamChunk> {
yield { type: "text", text: "Hello " }
yield { type: "text", text: "there!" }
}
mockCreateStreamingMessage.mockReturnValue(mockGenerator())
const stream = handler.createMessage(systemPrompt, messages)
const results = []
for await (const chunk of stream) {
results.push(chunk)
}
expect(results).toHaveLength(2)
expect(results[0]).toEqual({
type: "text",
text: "Hello ",
})
expect(results[1]).toEqual({
type: "text",
text: "there!",
})
})
test("should handle reasoning content from streaming", async () => {
const systemPrompt = "You are a helpful assistant"
const messages = [{ role: "user" as const, content: "Hello" }]
mockGetAccessToken.mockResolvedValue("test-access-token")
// Mock async generator that yields reasoning chunks
const mockGenerator = async function* (): AsyncGenerator<StreamChunk> {
yield { type: "reasoning", text: "I need to think about this carefully..." }
}
mockCreateStreamingMessage.mockReturnValue(mockGenerator())
const stream = handler.createMessage(systemPrompt, messages)
const results = []
for await (const chunk of stream) {
results.push(chunk)
}
expect(results).toHaveLength(1)
expect(results[0]).toEqual({
type: "reasoning",
text: "I need to think about this carefully...",
})
})
test("should handle mixed content types from streaming", async () => {
const systemPrompt = "You are a helpful assistant"
const messages = [{ role: "user" as const, content: "Hello" }]
mockGetAccessToken.mockResolvedValue("test-access-token")
// Mock async generator that yields mixed content
const mockGenerator = async function* (): AsyncGenerator<StreamChunk> {
yield { type: "reasoning", text: "Let me think about this..." }
yield { type: "text", text: "Here's my response!" }
}
mockCreateStreamingMessage.mockReturnValue(mockGenerator())
const stream = handler.createMessage(systemPrompt, messages)
const results = []
for await (const chunk of stream) {
results.push(chunk)
}
expect(results).toHaveLength(2)
expect(results[0]).toEqual({
type: "reasoning",
text: "Let me think about this...",
})
expect(results[1]).toEqual({
type: "text",
text: "Here's my response!",
})
})
test("should handle tool call partial chunks from streaming", async () => {
const systemPrompt = "You are a helpful assistant"
const messages = [{ role: "user" as const, content: "Hello" }]
mockGetAccessToken.mockResolvedValue("test-access-token")
// Mock async generator that yields tool call partial chunks
const mockGenerator = async function* (): AsyncGenerator<StreamChunk> {
yield { type: "tool_call_partial", index: 0, id: "tool_123", name: "read_file", arguments: undefined }
yield { type: "tool_call_partial", index: 0, id: undefined, name: undefined, arguments: '{"path":' }
yield { type: "tool_call_partial", index: 0, id: undefined, name: undefined, arguments: '"test.txt"}' }
}
mockCreateStreamingMessage.mockReturnValue(mockGenerator())
const stream = handler.createMessage(systemPrompt, messages)
const results = []
for await (const chunk of stream) {
results.push(chunk)
}
expect(results).toHaveLength(3)
expect(results[0]).toEqual({
type: "tool_call_partial",
index: 0,
id: "tool_123",
name: "read_file",
arguments: undefined,
})
expect(results[1]).toEqual({
type: "tool_call_partial",
index: 0,
id: undefined,
name: undefined,
arguments: '{"path":',
})
expect(results[2]).toEqual({
type: "tool_call_partial",
index: 0,
id: undefined,
name: undefined,
arguments: '"test.txt"}',
})
})
test("should handle usage and cost tracking from streaming", async () => {
const systemPrompt = "You are a helpful assistant"
const messages = [{ role: "user" as const, content: "Hello" }]
mockGetAccessToken.mockResolvedValue("test-access-token")
// Mock async generator with text and usage
const mockGenerator = async function* (): AsyncGenerator<StreamChunk> {
yield { type: "text", text: "Hello there!" }
yield {
type: "usage",
inputTokens: 10,
outputTokens: 20,
cacheReadTokens: 5,
cacheWriteTokens: 3,
}
}
mockCreateStreamingMessage.mockReturnValue(mockGenerator())
const stream = handler.createMessage(systemPrompt, messages)
const results = []
for await (const chunk of stream) {
results.push(chunk)
}
// Should have text chunk and usage chunk
expect(results).toHaveLength(2)
expect(results[0]).toEqual({
type: "text",
text: "Hello there!",
})
// Claude Code is subscription-based, no per-token cost
expect(results[1]).toEqual({
type: "usage",
inputTokens: 10,
outputTokens: 20,
cacheReadTokens: 5,
cacheWriteTokens: 3,
totalCost: 0,
})
})
test("should handle usage without cache tokens", async () => {
const systemPrompt = "You are a helpful assistant"
const messages = [{ role: "user" as const, content: "Hello" }]
mockGetAccessToken.mockResolvedValue("test-access-token")
// Mock async generator with usage without cache tokens
const mockGenerator = async function* (): AsyncGenerator<StreamChunk> {
yield { type: "text", text: "Hello there!" }
yield {
type: "usage",
inputTokens: 10,
outputTokens: 20,
}
}
mockCreateStreamingMessage.mockReturnValue(mockGenerator())
const stream = handler.createMessage(systemPrompt, messages)
const results = []
for await (const chunk of stream) {
results.push(chunk)
}
// Claude Code is subscription-based, no per-token cost
expect(results).toHaveLength(2)
expect(results[1]).toEqual({
type: "usage",
inputTokens: 10,
outputTokens: 20,
cacheReadTokens: undefined,
cacheWriteTokens: undefined,
totalCost: 0,
})
})
test("should handle API errors from streaming", async () => {
const systemPrompt = "You are a helpful assistant"
const messages = [{ role: "user" as const, content: "Hello" }]
mockGetAccessToken.mockResolvedValue("test-access-token")
// Mock async generator that yields an error
const mockGenerator = async function* (): AsyncGenerator<StreamChunk> {
yield { type: "error", error: "Invalid model name" }
}
mockCreateStreamingMessage.mockReturnValue(mockGenerator())
const stream = handler.createMessage(systemPrompt, messages)
const iterator = stream[Symbol.asyncIterator]()
// Should throw an error
await expect(iterator.next()).rejects.toThrow("Invalid model name")
})
test("should handle authentication refresh and continue streaming", async () => {
const systemPrompt = "You are a helpful assistant"
const messages = [{ role: "user" as const, content: "Hello" }]
// First call returns a valid token
mockGetAccessToken.mockResolvedValue("refreshed-token")
const mockGenerator = async function* (): AsyncGenerator<StreamChunk> {
yield { type: "text", text: "Response after refresh" }
}
mockCreateStreamingMessage.mockReturnValue(mockGenerator())
const stream = handler.createMessage(systemPrompt, messages)
const results = []
for await (const chunk of stream) {
results.push(chunk)
}
expect(results).toHaveLength(1)
expect(results[0]).toEqual({
type: "text",
text: "Response after refresh",
})
expect(mockCreateStreamingMessage).toHaveBeenCalledWith(
expect.objectContaining({
accessToken: "refreshed-token",
}),
)
})
describe("completePrompt", () => {
test("should throw error when not authenticated", async () => {
mockGetAccessToken.mockResolvedValue(null)
await expect(handler.completePrompt("Test prompt")).rejects.toThrow(/not authenticated/i)
})
test("should complete prompt and return text response", async () => {
mockGetAccessToken.mockResolvedValue("test-access-token")
mockGetEmail.mockResolvedValue("test@example.com")
// Mock async generator that yields text chunks
const mockGenerator = async function* (): AsyncGenerator<StreamChunk> {
yield { type: "text", text: "Hello " }
yield { type: "text", text: "world!" }
yield { type: "usage", inputTokens: 10, outputTokens: 5 }
}
mockCreateStreamingMessage.mockReturnValue(mockGenerator())
const result = await handler.completePrompt("Say hello")
expect(result).toBe("Hello world!")
})
test("should call createStreamingMessage with empty system prompt and thinking disabled", async () => {
mockGetAccessToken.mockResolvedValue("test-access-token")
mockGetEmail.mockResolvedValue("test@example.com")
// Mock empty async generator
const mockGenerator = async function* (): AsyncGenerator<StreamChunk> {
yield { type: "text", text: "Response" }
}
mockCreateStreamingMessage.mockReturnValue(mockGenerator())
await handler.completePrompt("Test prompt")
// Verify createStreamingMessage was called with correct parameters
// System prompt is empty because the prompt text contains all context
// createStreamingMessage will still prepend the Claude Code branding
expect(mockCreateStreamingMessage).toHaveBeenCalledWith({
accessToken: "test-access-token",
model: "claude-sonnet-4-5",
systemPrompt: "", // Empty - branding is added by createStreamingMessage
messages: [{ role: "user", content: "Test prompt" }],
maxTokens: 32768,
thinking: { type: "disabled" }, // No thinking for simple completions
metadata: {
user_id: "user_abc123_account_def456_session_ghi789",
},
})
})
test("should handle API errors from streaming", async () => {
mockGetAccessToken.mockResolvedValue("test-access-token")
mockGetEmail.mockResolvedValue("test@example.com")
// Mock async generator that yields an error
const mockGenerator = async function* (): AsyncGenerator<StreamChunk> {
yield { type: "error", error: "API rate limit exceeded" }
}
mockCreateStreamingMessage.mockReturnValue(mockGenerator())
await expect(handler.completePrompt("Test prompt")).rejects.toThrow("API rate limit exceeded")
})
test("should return empty string when no text chunks received", async () => {
mockGetAccessToken.mockResolvedValue("test-access-token")
mockGetEmail.mockResolvedValue("test@example.com")
// Mock async generator that only yields usage
const mockGenerator = async function* (): AsyncGenerator<StreamChunk> {
yield { type: "usage", inputTokens: 10, outputTokens: 0 }
}
mockCreateStreamingMessage.mockReturnValue(mockGenerator())
const result = await handler.completePrompt("Test prompt")
expect(result).toBe("")
})
test("should use opus model maxTokens when configured", async () => {
const options: ApiHandlerOptions = {
apiModelId: "claude-opus-4-5",
}
const handlerOpus = new ClaudeCodeHandler(options)
mockGetAccessToken.mockResolvedValue("test-access-token")
mockGetEmail.mockResolvedValue("test@example.com")
const mockGenerator = async function* (): AsyncGenerator<StreamChunk> {
yield { type: "text", text: "Response" }
}
mockCreateStreamingMessage.mockReturnValue(mockGenerator())
await handlerOpus.completePrompt("Test prompt")
expect(mockCreateStreamingMessage).toHaveBeenCalledWith(
expect.objectContaining({
model: "claude-opus-4-5",
maxTokens: 32768, // opus model maxTokens
}),
)
})
})
})

View file

@ -199,7 +199,6 @@ describe("DeepInfraHandler", () => {
const messageGenerator = handler.createMessage("test prompt", [], {
taskId: "test-task-id",
tools: testTools,
toolProtocol: "native",
})
await messageGenerator.next()
@ -213,9 +212,11 @@ describe("DeepInfraHandler", () => {
}),
}),
]),
parallel_tool_calls: false,
}),
)
// parallel_tool_calls should be false when not explicitly set
const callArgs = mockCreate.mock.calls[0][0]
expect(callArgs).toHaveProperty("parallel_tool_calls", false)
})
it("should include tool_choice when provided", async () => {
@ -232,7 +233,6 @@ describe("DeepInfraHandler", () => {
const messageGenerator = handler.createMessage("test prompt", [], {
taskId: "test-task-id",
tools: testTools,
toolProtocol: "native",
tool_choice: "auto",
})
await messageGenerator.next()
@ -244,7 +244,7 @@ describe("DeepInfraHandler", () => {
)
})
it("should not include tools when toolProtocol is xml", async () => {
it("should always include tools and tool_choice in request (tools are always present after PR #10841)", async () => {
mockWithResponse.mockResolvedValueOnce({
data: {
[Symbol.asyncIterator]: () => ({
@ -257,14 +257,15 @@ describe("DeepInfraHandler", () => {
const messageGenerator = handler.createMessage("test prompt", [], {
taskId: "test-task-id",
tools: testTools,
toolProtocol: "xml",
})
await messageGenerator.next()
const callArgs = mockCreate.mock.calls[mockCreate.mock.calls.length - 1][0]
expect(callArgs).not.toHaveProperty("tools")
expect(callArgs).not.toHaveProperty("tool_choice")
// Tools are now always present (minimum 6 from ALWAYS_AVAILABLE_TOOLS)
expect(callArgs).toHaveProperty("tools")
expect(callArgs).toHaveProperty("tool_choice")
// parallel_tool_calls should be false when not explicitly set
expect(callArgs).toHaveProperty("parallel_tool_calls", false)
})
it("should yield tool_call_partial chunks during streaming", async () => {
@ -321,7 +322,6 @@ describe("DeepInfraHandler", () => {
const stream = handler.createMessage("test prompt", [], {
taskId: "test-task-id",
tools: testTools,
toolProtocol: "native",
})
const chunks = []
@ -360,7 +360,6 @@ describe("DeepInfraHandler", () => {
const messageGenerator = handler.createMessage("test prompt", [], {
taskId: "test-task-id",
tools: testTools,
toolProtocol: "native",
parallelToolCalls: true,
})
await messageGenerator.next()

View file

@ -129,7 +129,6 @@ describe("FireworksHandler", () => {
contextWindow: 256000,
supportsImages: false,
supportsPromptCache: true,
supportsNativeTools: true,
supportsTemperature: true,
preserveReasoning: true,
defaultTemperature: 1.0,

View file

@ -5,7 +5,10 @@ import { GeminiHandler } from "../gemini"
import type { ApiHandlerOptions } from "../../../shared/api"
describe("GeminiHandler backend support", () => {
it("passes tools for URL context and grounding in config", async () => {
it("createMessage uses function declarations (URL context and grounding are only for completePrompt)", async () => {
// URL context and grounding are mutually exclusive with function declarations
// in Gemini API, so createMessage only uses function declarations.
// URL context/grounding are only added in completePrompt.
const options = {
apiProvider: "gemini",
enableUrlContext: true,
@ -17,7 +20,9 @@ describe("GeminiHandler backend support", () => {
handler["client"].models.generateContentStream = stub
await handler.createMessage("instr", [] as any).next()
const config = stub.mock.calls[0][0].config
expect(config.tools).toEqual([{ urlContext: {} }, { googleSearch: {} }])
// createMessage always uses function declarations only
// (tools are always present from ALWAYS_AVAILABLE_TOOLS)
expect(config.tools).toEqual([{ functionDeclarations: expect.any(Array) }])
})
it("completePrompt passes config overrides without tools when URL context and grounding disabled", async () => {

View file

@ -255,7 +255,6 @@ describe("IOIntelligenceHandler", () => {
description: "Llama 4 Maverick 17B model",
supportsImages: true,
supportsPromptCache: false,
supportsNativeTools: true,
})
})
@ -272,7 +271,6 @@ describe("IOIntelligenceHandler", () => {
description: "Llama 4 Maverick 17B model",
supportsImages: true,
supportsPromptCache: false,
supportsNativeTools: true,
})
})

View file

@ -3,7 +3,7 @@ import { Anthropic } from "@anthropic-ai/sdk"
import { LiteLLMHandler } from "../lite-llm"
import { ApiHandlerOptions } from "../../../shared/api"
import { litellmDefaultModelId, litellmDefaultModelInfo, TOOL_PROTOCOL } from "@roo-code/types"
import { litellmDefaultModelId, litellmDefaultModelInfo } from "@roo-code/types"
// Mock vscode first to avoid import errors
vi.mock("vscode", () => ({}))
@ -41,11 +41,11 @@ vi.mock("../fetchers/modelCache", () => ({
"llama-3": { ...litellmDefaultModelInfo, maxTokens: 8192 },
"gpt-4-turbo": { ...litellmDefaultModelInfo, maxTokens: 8192 },
// Gemini models for thought signature injection tests
"gemini-3-pro": { ...litellmDefaultModelInfo, maxTokens: 8192, supportsNativeTools: true },
"gemini-3-flash": { ...litellmDefaultModelInfo, maxTokens: 8192, supportsNativeTools: true },
"gemini-2.5-pro": { ...litellmDefaultModelInfo, maxTokens: 8192, supportsNativeTools: true },
"google/gemini-3-pro": { ...litellmDefaultModelInfo, maxTokens: 8192, supportsNativeTools: true },
"vertex_ai/gemini-3-pro": { ...litellmDefaultModelInfo, maxTokens: 8192, supportsNativeTools: true },
"gemini-3-pro": { ...litellmDefaultModelInfo, maxTokens: 8192 },
"gemini-3-flash": { ...litellmDefaultModelInfo, maxTokens: 8192 },
"gemini-2.5-pro": { ...litellmDefaultModelInfo, maxTokens: 8192 },
"google/gemini-3-pro": { ...litellmDefaultModelInfo, maxTokens: 8192 },
"vertex_ai/gemini-3-pro": { ...litellmDefaultModelInfo, maxTokens: 8192 },
})
}),
getModelsFromCache: vi.fn().mockReturnValue(undefined),
@ -583,10 +583,10 @@ describe("LiteLLMHandler", () => {
}
handler = new LiteLLMHandler(optionsWithGemini)
// Mock fetchModel to return a Gemini model with native tool support
// Mock fetchModel to return a Gemini model
vi.spyOn(handler as any, "fetchModel").mockResolvedValue({
id: "gemini-3-pro",
info: { ...litellmDefaultModelInfo, maxTokens: 8192, supportsNativeTools: true },
info: { ...litellmDefaultModelInfo, maxTokens: 8192 },
})
const systemPrompt = "You are a helpful assistant"
@ -632,7 +632,6 @@ describe("LiteLLMHandler", () => {
function: { name: "read_file", description: "Read a file", parameters: {} },
},
],
toolProtocol: TOOL_PROTOCOL.NATIVE,
}
const generator = handler.createMessage(systemPrompt, messages, metadata as any)
@ -661,7 +660,7 @@ describe("LiteLLMHandler", () => {
vi.spyOn(handler as any, "fetchModel").mockResolvedValue({
id: "gpt-4",
info: { ...litellmDefaultModelInfo, maxTokens: 8192, supportsNativeTools: true },
info: { ...litellmDefaultModelInfo, maxTokens: 8192 },
})
const systemPrompt = "You are a helpful assistant"
@ -700,7 +699,6 @@ describe("LiteLLMHandler", () => {
function: { name: "read_file", description: "Read a file", parameters: {} },
},
],
toolProtocol: TOOL_PROTOCOL.NATIVE,
}
const generator = handler.createMessage(systemPrompt, messages, metadata as any)

View file

@ -80,9 +80,11 @@ describe("LmStudioHandler Native Tools", () => {
}),
}),
]),
parallel_tool_calls: false,
}),
)
// parallel_tool_calls should be false when not explicitly set
const callArgs = mockCreate.mock.calls[0][0]
expect(callArgs).toHaveProperty("parallel_tool_calls", false)
})
it("should include tool_choice when provided", async () => {
@ -108,7 +110,7 @@ describe("LmStudioHandler Native Tools", () => {
)
})
it("should not include tools when toolProtocol is xml", async () => {
it("should always include tools and tool_choice in request (tools are always present after PR #10841)", async () => {
mockCreate.mockImplementationOnce(() => ({
[Symbol.asyncIterator]: async function* () {
yield {
@ -119,14 +121,15 @@ describe("LmStudioHandler Native Tools", () => {
const stream = handler.createMessage("test prompt", [], {
taskId: "test-task-id",
tools: testTools,
toolProtocol: "xml",
})
await stream.next()
const callArgs = mockCreate.mock.calls[mockCreate.mock.calls.length - 1][0]
expect(callArgs).not.toHaveProperty("tools")
expect(callArgs).not.toHaveProperty("tool_choice")
// Tools are now always present (minimum 6 from ALWAYS_AVAILABLE_TOOLS)
expect(callArgs).toHaveProperty("tools")
expect(callArgs).toHaveProperty("tool_choice")
// parallel_tool_calls should be false when not explicitly set
expect(callArgs).toHaveProperty("parallel_tool_calls", false)
})
it("should yield tool_call_partial chunks during streaming", async () => {
@ -280,7 +283,7 @@ describe("LmStudioHandler Native Tools", () => {
expect(endChunks[0].id).toBe("call_lmstudio_test")
})
it("should work with parallel tool calls disabled", async () => {
it("should work with parallel tool calls disabled (sends false)", async () => {
mockCreate.mockImplementationOnce(() => ({
[Symbol.asyncIterator]: async function* () {
yield {
@ -296,11 +299,9 @@ describe("LmStudioHandler Native Tools", () => {
})
await stream.next()
expect(mockCreate).toHaveBeenCalledWith(
expect.objectContaining({
parallel_tool_calls: false,
}),
)
// When parallelToolCalls is false, the parameter should be sent as false
const callArgs = mockCreate.mock.calls[0][0]
expect(callArgs).toHaveProperty("parallel_tool_calls", false)
})
it("should handle reasoning content alongside tool calls", async () => {

View file

@ -119,12 +119,17 @@ describe("MistralHandler", () => {
const iterator = handler.createMessage(systemPrompt, messages)
const result = await iterator.next()
expect(mockCreate).toHaveBeenCalledWith({
model: mockOptions.apiModelId,
messages: expect.any(Array),
maxTokens: expect.any(Number),
temperature: 0,
})
expect(mockCreate).toHaveBeenCalledWith(
expect.objectContaining({
model: mockOptions.apiModelId,
messages: expect.any(Array),
maxTokens: expect.any(Number),
temperature: 0,
// Tools are now always present (minimum 6 from ALWAYS_AVAILABLE_TOOLS)
tools: expect.any(Array),
toolChoice: "any",
}),
)
expect(result.value).toBeDefined()
expect(result.done).toBe(false)
@ -288,19 +293,19 @@ describe("MistralHandler", () => {
)
})
it("should not include tools when toolProtocol is xml", async () => {
it("should always include tools in request (tools are always present after PR #10841)", async () => {
const metadata: ApiHandlerCreateMessageMetadata = {
taskId: "test-task",
tools: mockTools,
toolProtocol: "xml",
}
const iterator = handler.createMessage(systemPrompt, messages, metadata)
await iterator.next()
// Tools are now always present (minimum 6 from ALWAYS_AVAILABLE_TOOLS)
expect(mockCreate).toHaveBeenCalledWith(
expect.not.objectContaining({
tools: expect.anything(),
expect.objectContaining({
tools: expect.any(Array),
toolChoice: "any",
}),
)
})

View file

@ -265,15 +265,14 @@ describe("NativeOllamaHandler", () => {
})
describe("tool calling", () => {
it("should include tools when model supports native tools", async () => {
// Mock model with native tool support
it("should include tools when tools are provided", async () => {
// Model metadata should not gate tool inclusion; metadata.tools controls it.
mockGetOllamaModels.mockResolvedValue({
"llama3.2": {
contextWindow: 128000,
maxTokens: 4096,
supportsImages: true,
supportsPromptCache: false,
supportsNativeTools: true,
},
})
@ -341,15 +340,14 @@ describe("NativeOllamaHandler", () => {
)
})
it("should not include tools when model does not support native tools", async () => {
// Mock model without native tool support
it("should include tools even when model metadata doesn't advertise tool support", async () => {
// Model metadata should not gate tool inclusion; metadata.tools controls it.
mockGetOllamaModels.mockResolvedValue({
llama2: {
contextWindow: 4096,
maxTokens: 4096,
supportsImages: false,
supportsPromptCache: false,
supportsNativeTools: false,
},
})
@ -379,23 +377,22 @@ describe("NativeOllamaHandler", () => {
// consume stream
}
// Verify tools were NOT passed
// Verify tools were passed
expect(mockChat).toHaveBeenCalledWith(
expect.not.objectContaining({
tools: expect.anything(),
expect.objectContaining({
tools: expect.any(Array),
}),
)
})
it("should not include tools when toolProtocol is xml", async () => {
// Mock model with native tool support
it("should not include tools when no tools are provided", async () => {
// Model metadata should not gate tool inclusion; metadata.tools controls it.
mockGetOllamaModels.mockResolvedValue({
"llama3.2": {
contextWindow: 128000,
maxTokens: 4096,
supportsImages: true,
supportsPromptCache: false,
supportsNativeTools: true,
},
})
@ -412,21 +409,8 @@ describe("NativeOllamaHandler", () => {
yield { message: { content: "Response" } }
})
const tools = [
{
type: "function" as const,
function: {
name: "get_weather",
description: "Get the weather",
parameters: { type: "object", properties: {} },
},
},
]
const stream = handler.createMessage("System", [{ role: "user" as const, content: "Test" }], {
taskId: "test",
tools,
toolProtocol: "xml",
})
// Consume the stream
@ -434,7 +418,7 @@ describe("NativeOllamaHandler", () => {
// consume stream
}
// Verify tools were NOT passed (XML protocol forces XML format)
// Verify tools were NOT passed
expect(mockChat).toHaveBeenCalledWith(
expect.not.objectContaining({
tools: expect.anything(),
@ -443,14 +427,13 @@ describe("NativeOllamaHandler", () => {
})
it("should yield tool_call_partial when model returns tool calls", async () => {
// Mock model with native tool support
// Model metadata should not gate tool inclusion; metadata.tools controls it.
mockGetOllamaModels.mockResolvedValue({
"llama3.2": {
contextWindow: 128000,
maxTokens: 4096,
supportsImages: true,
supportsPromptCache: false,
supportsNativeTools: true,
},
})
@ -520,14 +503,13 @@ describe("NativeOllamaHandler", () => {
})
it("should yield tool_call_end events after tool_call_partial chunks", async () => {
// Mock model with native tool support
// Model metadata should not gate tool inclusion; metadata.tools controls it.
mockGetOllamaModels.mockResolvedValue({
"llama3.2": {
contextWindow: 128000,
maxTokens: 4096,
supportsImages: true,
supportsPromptCache: false,
supportsNativeTools: true,
},
})

View file

@ -72,7 +72,6 @@ describe("OpenAiCodexHandler native tool calls", () => {
const stream = handler.createMessage("system", [{ role: "user", content: "hello" } as any], {
taskId: "t",
toolProtocol: "native",
tools: [],
})

View file

@ -5,7 +5,7 @@ import { OpenAiNativeHandler } from "../openai-native"
import type { ApiHandlerOptions } from "../../../shared/api"
describe("OpenAiHandler native tools", () => {
it("includes tools in request when custom model info lacks supportsNativeTools (regression test)", async () => {
it("includes tools in request when tools are provided via metadata (regression test)", async () => {
const mockCreate = vi.fn().mockImplementationOnce(() => ({
[Symbol.asyncIterator]: async function* () {
yield {
@ -14,10 +14,8 @@ describe("OpenAiHandler native tools", () => {
},
}))
// Set openAiCustomModelInfo WITHOUT supportsNativeTools to simulate
// a user-provided custom model info that doesn't specify native tool support.
// The getModel() fix should merge NATIVE_TOOL_DEFAULTS to ensure
// supportsNativeTools defaults to true.
// Set openAiCustomModelInfo without any tool capability flags; tools should
// still be passed whenever metadata.tools is present.
const handler = new OpenAiHandler({
openAiApiKey: "test-key",
openAiBaseUrl: "https://example.com/v1",
@ -49,17 +47,9 @@ describe("OpenAiHandler native tools", () => {
},
]
// Mimic the behavior in Task.attemptApiRequest() where tools are only
// included when modelInfo.supportsNativeTools is true. This is the
// actual regression path being tested - without the getModel() fix,
// supportsNativeTools would be undefined and tools wouldn't be passed.
const modelInfo = handler.getModel().info
const supportsNativeTools = modelInfo.supportsNativeTools ?? false
const stream = handler.createMessage("system", [], {
taskId: "test-task-id",
...(supportsNativeTools && { tools }),
...(supportsNativeTools && { toolProtocol: "native" as const }),
tools,
})
await stream.next()
@ -71,13 +61,10 @@ describe("OpenAiHandler native tools", () => {
function: expect.objectContaining({ name: "test_tool" }),
}),
]),
parallel_tool_calls: false,
}),
expect.anything(),
)
// Verify parallel_tool_calls is NOT included when parallelToolCalls is not explicitly true
// This is required for LiteLLM/Bedrock compatibility (see COM-406)
const callArgs = mockCreate.mock.calls[0][0]
expect(callArgs).not.toHaveProperty("parallel_tool_calls")
})
})
@ -131,7 +118,6 @@ describe("OpenAiNativeHandler MCP tool schema handling", () => {
const stream = handler.createMessage("system prompt", [], {
taskId: "test-task-id",
tools: mcpTools,
toolProtocol: "native" as const,
})
// Consume the stream
@ -199,7 +185,6 @@ describe("OpenAiNativeHandler MCP tool schema handling", () => {
const stream = handler.createMessage("system prompt", [], {
taskId: "test-task-id",
tools: regularTools,
toolProtocol: "native" as const,
})
// Consume the stream
@ -281,7 +266,6 @@ describe("OpenAiNativeHandler MCP tool schema handling", () => {
const stream = handler.createMessage("system prompt", [], {
taskId: "test-task-id",
tools: mcpToolsWithNestedObjects,
toolProtocol: "native" as const,
})
// Consume the stream

View file

@ -221,45 +221,6 @@ describe("OpenAiNativeHandler", () => {
expect(modelInfo.id).toBe("gpt-5.1-codex-max") // Default model
expect(modelInfo.info).toBeDefined()
})
it("should have defaultToolProtocol: native for all OpenAI Native models", () => {
// Test that all models have defaultToolProtocol: native
const testModels = [
"gpt-5.1-codex-max",
"gpt-5.2",
"gpt-5.1",
"gpt-5",
"gpt-5-mini",
"gpt-5-nano",
"gpt-4.1",
"gpt-4.1-mini",
"gpt-4.1-nano",
"o3",
"o3-high",
"o3-low",
"o4-mini",
"o4-mini-high",
"o4-mini-low",
"o3-mini",
"o3-mini-high",
"o3-mini-low",
"o1",
"o1-preview",
"o1-mini",
"gpt-4o",
"gpt-4o-mini",
"codex-mini-latest",
]
for (const modelId of testModels) {
const testHandler = new OpenAiNativeHandler({
openAiNativeApiKey: "test-api-key",
apiModelId: modelId,
})
const modelInfo = testHandler.getModel()
expect(modelInfo.info.defaultToolProtocol).toBe("native")
}
})
})
describe("GPT-5 models", () => {

View file

@ -633,11 +633,14 @@ describe("OpenAiHandler", () => {
stream: true,
stream_options: { include_usage: true },
temperature: 0,
tools: undefined,
tool_choice: undefined,
parallel_tool_calls: false,
},
{ path: "/models/chat/completions" },
)
// Verify max_tokens is NOT included when includeMaxTokens is not set
// Verify max_tokens is NOT included when not explicitly set
const callArgs = mockCreate.mock.calls[0][0]
expect(callArgs).not.toHaveProperty("max_completion_tokens")
})
@ -679,11 +682,14 @@ describe("OpenAiHandler", () => {
{ role: "system", content: systemPrompt },
{ role: "user", content: "Hello!" },
],
tools: undefined,
tool_choice: undefined,
parallel_tool_calls: false,
},
{ path: "/models/chat/completions" },
)
// Verify max_tokens is NOT included when includeMaxTokens is not set
// Verify max_tokens is NOT included when not explicitly set
const callArgs = mockCreate.mock.calls[0][0]
expect(callArgs).not.toHaveProperty("max_completion_tokens")
})

View file

@ -42,7 +42,6 @@ vitest.mock("../fetchers/modelCache", () => ({
contextWindow: 200000,
supportsImages: true,
supportsPromptCache: true,
supportsNativeTools: true,
inputPrice: 3,
outputPrice: 15,
cacheWritesPrice: 3.75,
@ -66,7 +65,6 @@ vitest.mock("../fetchers/modelCache", () => ({
contextWindow: 128000,
supportsImages: true,
supportsPromptCache: false,
supportsNativeTools: true,
inputPrice: 2.5,
outputPrice: 10,
description: "GPT-4o",
@ -76,7 +74,6 @@ vitest.mock("../fetchers/modelCache", () => ({
contextWindow: 200000,
supportsImages: true,
supportsPromptCache: false,
supportsNativeTools: true,
inputPrice: 15,
outputPrice: 60,
description: "OpenAI o1",
@ -129,7 +126,6 @@ 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 () => {

View file

@ -127,7 +127,7 @@ describe("QwenCodeHandler Native Tools", () => {
)
})
it("should not include tools when toolProtocol is xml", async () => {
it("should always include tools and tool_choice (tools are guaranteed to be present after ALWAYS_AVAILABLE_TOOLS)", async () => {
mockCreate.mockImplementationOnce(() => ({
[Symbol.asyncIterator]: async function* () {
yield {
@ -138,14 +138,14 @@ describe("QwenCodeHandler Native Tools", () => {
const stream = handler.createMessage("test prompt", [], {
taskId: "test-task-id",
tools: testTools,
toolProtocol: "xml",
})
await stream.next()
// Tools are now always present (minimum 6 from ALWAYS_AVAILABLE_TOOLS)
const callArgs = mockCreate.mock.calls[mockCreate.mock.calls.length - 1][0]
expect(callArgs).not.toHaveProperty("tools")
expect(callArgs).not.toHaveProperty("tool_choice")
expect(callArgs).toHaveProperty("tools")
expect(callArgs).toHaveProperty("tool_choice")
expect(callArgs).toHaveProperty("parallel_tool_calls", false)
})
it("should yield tool_call_partial chunks during streaming", async () => {

View file

@ -3,15 +3,12 @@
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
import { TOOL_PROTOCOL } from "@roo-code/types"
import { RequestyHandler } from "../requesty"
import { ApiHandlerOptions } from "../../../shared/api"
import { Package } from "../../../shared/package"
import { ApiHandlerCreateMessageMetadata } from "../../index"
const mockCreate = vitest.fn()
const mockResolveToolProtocol = vitest.fn()
vitest.mock("openai", () => {
return {
@ -27,10 +24,6 @@ vitest.mock("openai", () => {
vitest.mock("delay", () => ({ default: vitest.fn(() => Promise.resolve()) }))
vitest.mock("../../../utils/resolveToolProtocol", () => ({
resolveToolProtocol: (...args: any[]) => mockResolveToolProtocol(...args),
}))
vitest.mock("../fetchers/modelCache", () => ({
getModels: vitest.fn().mockImplementation(() => {
return Promise.resolve({
@ -244,9 +237,7 @@ describe("RequestyHandler", () => {
mockCreate.mockResolvedValue(mockStream)
})
it("should include tools in request when toolProtocol is native", async () => {
mockResolveToolProtocol.mockReturnValue(TOOL_PROTOCOL.NATIVE)
it("should include tools in request when tools are provided", async () => {
const metadata: ApiHandlerCreateMessageMetadata = {
taskId: "test-task",
tools: mockTools,
@ -273,30 +264,7 @@ describe("RequestyHandler", () => {
)
})
it("should not include tools when toolProtocol is not native", async () => {
mockResolveToolProtocol.mockReturnValue(TOOL_PROTOCOL.XML)
const metadata: ApiHandlerCreateMessageMetadata = {
taskId: "test-task",
tools: mockTools,
tool_choice: "auto",
}
const handler = new RequestyHandler(mockOptions)
const iterator = handler.createMessage(systemPrompt, messages, metadata)
await iterator.next()
expect(mockCreate).toHaveBeenCalledWith(
expect.not.objectContaining({
tools: expect.anything(),
tool_choice: expect.anything(),
}),
)
})
it("should handle tool_call_partial chunks in streaming response", async () => {
mockResolveToolProtocol.mockReturnValue(TOOL_PROTOCOL.NATIVE)
const mockStreamWithToolCalls = {
async *[Symbol.asyncIterator]() {
yield {

View file

@ -101,27 +101,22 @@ vitest.mock("../../providers/fetchers/modelCache", () => ({
supportsPromptCache: true,
inputPrice: 0,
outputPrice: 0,
defaultToolProtocol: "native",
},
"minimax/minimax-m2:free": {
maxTokens: 32_768,
contextWindow: 1_000_000,
supportsImages: false,
supportsPromptCache: true,
supportsNativeTools: true,
inputPrice: 0.15,
outputPrice: 0.6,
defaultToolProtocol: "native",
},
"anthropic/claude-haiku-4.5": {
maxTokens: 8_192,
contextWindow: 200_000,
supportsImages: true,
supportsPromptCache: true,
supportsNativeTools: true,
inputPrice: 0.8,
outputPrice: 4,
defaultToolProtocol: "native",
},
}
}
@ -428,24 +423,12 @@ describe("RooHandler", () => {
}
})
it("should have defaultToolProtocol: native for all roo provider models", () => {
// Test that all models have defaultToolProtocol: native
const testModels = ["minimax/minimax-m2:free", "anthropic/claude-haiku-4.5", "xai/grok-code-fast-1"]
for (const modelId of testModels) {
const handlerWithModel = new RooHandler({ apiModelId: modelId })
const modelInfo = handlerWithModel.getModel()
expect(modelInfo.id).toBe(modelId)
expect((modelInfo.info as any).defaultToolProtocol).toBe("native")
}
})
it("should return cached model info with settings applied from API", () => {
const handlerWithMinimax = new RooHandler({
apiModelId: "minimax/minimax-m2:free",
})
const modelInfo = handlerWithMinimax.getModel()
// The settings from API should already be applied in the cached model info
expect(modelInfo.info.supportsNativeTools).toBe(true)
expect(modelInfo.info.inputPrice).toBe(0.15)
expect(modelInfo.info.outputPrice).toBe(0.6)
})

Some files were not shown because too many files have changed in this diff Show more