mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
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:
commit
72fdff746a
433 changed files with 10908 additions and 20053 deletions
25
.github/workflows/website-preview.yml
vendored
25
.github/workflows/website-preview.yml
vendored
|
|
@ -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
5
AGENTS.md
Normal 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.
|
||||
24
CHANGELOG.md
24
CHANGELOG.md
|
|
@ -1,5 +1,29 @@
|
|||
# Roo Code Changelog
|
||||
|
||||
## [3.42.0] - 2026-01-22
|
||||
|
||||

|
||||
|
||||
- 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)
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
|
|
|
|||
401
apps/web-roo-code/src/app/slack/page.tsx
Normal file
401
apps/web-roo-code/src/app/slack/page.tsx
Normal 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 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 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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
|
|
|
|||
548
apps/web-roo-code/src/components/slack/slack-thread-demo.tsx
Normal file
548
apps/web-roo-code/src/components/slack/slack-thread-demo.tsx
Normal 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'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'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'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'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've built the Roo Code Slack integration marketing page. Here'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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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",
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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>"
|
||||
`;
|
||||
|
|
@ -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()
|
||||
})
|
||||
})
|
||||
|
|
@ -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")}`
|
||||
}
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
export * from "./custom-tool-registry.js"
|
||||
export * from "./serialize.js"
|
||||
export * from "./format-xml.js"
|
||||
export * from "./format-native.js"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
306
packages/core/src/worktree/__tests__/worktree-include.spec.ts
Normal file
306
packages/core/src/worktree/__tests__/worktree-include.spec.ts
Normal 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")
|
||||
})
|
||||
})
|
||||
})
|
||||
146
packages/core/src/worktree/__tests__/worktree-service.spec.ts
Normal file
146
packages/core/src/worktree/__tests__/worktree-service.spec.ts
Normal 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,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
13
packages/core/src/worktree/index.ts
Normal file
13
packages/core/src/worktree/index.ts
Normal 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"
|
||||
17
packages/core/src/worktree/types.ts
Normal file
17
packages/core/src/worktree/types.ts
Normal 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"
|
||||
428
packages/core/src/worktree/worktree-include.ts
Normal file
428
packages/core/src/worktree/worktree-include.ts
Normal 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()
|
||||
444
packages/core/src/worktree/worktree-service.ts
Normal file
444
packages/core/src/worktree/worktree-service.ts
Normal 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()
|
||||
|
|
@ -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 }),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
})
|
||||
})
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)",
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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.",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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":
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ export const ollamaDefaultModelInfo: ModelInfo = {
|
|||
contextWindow: 200_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
supportsNativeTools: true,
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
cacheWritesPrice: 0,
|
||||
|
|
|
|||
29
packages/types/src/providers/openai-codex-rate-limits.ts
Normal file
29
packages/types/src/providers/openai-codex-rate-limits.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
/**
|
||||
* OpenAI Codex usage/rate limit information (ChatGPT subscription)
|
||||
*/
|
||||
export interface OpenAiCodexRateLimitInfo {
|
||||
primary?: {
|
||||
/** Used percent in 0–100 */
|
||||
usedPercent: number
|
||||
/** Window length in minutes, when provided */
|
||||
windowMinutes?: number
|
||||
/** Reset time (unix ms since epoch), when provided */
|
||||
resetsAt?: number
|
||||
}
|
||||
secondary?: {
|
||||
/** Used percent in 0–100 */
|
||||
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
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -90,7 +90,6 @@ export const vercelAiGatewayDefaultModelInfo: ModelInfo = {
|
|||
contextWindow: 200000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: true,
|
||||
supportsNativeTools: true,
|
||||
inputPrice: 3,
|
||||
outputPrice: 15,
|
||||
cacheWritesPrice: 3.75,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ export const commandIds = [
|
|||
"popoutButtonClicked",
|
||||
"cloudButtonClicked",
|
||||
"settingsButtonClicked",
|
||||
"worktreesButtonClicked",
|
||||
|
||||
"openInNewTab",
|
||||
|
||||
|
|
|
|||
129
packages/types/src/worktree.ts
Normal file
129
packages/types/src/worktree.ts
Normal 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
73
pnpm-lock.yaml
generated
|
|
@ -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
BIN
releases/3.42.0-release.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 MiB |
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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":
|
||||
|
|
|
|||
|
|
@ -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([
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
|
@ -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
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -129,7 +129,6 @@ describe("FireworksHandler", () => {
|
|||
contextWindow: 256000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: true,
|
||||
supportsNativeTools: true,
|
||||
supportsTemperature: true,
|
||||
preserveReasoning: true,
|
||||
defaultTemperature: 1.0,
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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: [],
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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", () => {
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Add table
Reference in a new issue