Integrate Claude Code provider exactly as implemented in Cline

- Add claude-code to provider names and schema
- Create Claude Code model definitions using Anthropic models
- Implement ClaudeCodeHandler with HTTP-based communication
- Add Claude Code integration files (types.ts, run.ts)
- Register provider in main API handler
- Fix workspace access for testing compatibility

Credit to Cline for the clever implementation approach.

Addresses comment: https://github.com/RooCodeInc/Roo-Code/pull/4843#issuecomment-2984824469
This commit is contained in:
Roo Code 2025-06-18 16:04:07 +00:00
parent 2e2f83be60
commit 587853842b
9 changed files with 1386 additions and 0 deletions

View file

@ -9,6 +9,7 @@ import { codebaseIndexProviderSchema } from "./codebase-index.js"
export const providerNames = [
"anthropic",
"claude-code",
"glama",
"openrouter",
"bedrock",
@ -196,6 +197,10 @@ const chutesSchema = apiModelIdProviderModelSchema.extend({
chutesApiKey: z.string().optional(),
})
const claudeCodeSchema = apiModelIdProviderModelSchema.extend({
claudeCodePath: z.string().optional(),
})
const litellmSchema = baseProviderSettingsSchema.extend({
litellmBaseUrl: z.string().optional(),
litellmApiKey: z.string().optional(),
@ -208,6 +213,7 @@ 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") })),
glamaSchema.merge(z.object({ apiProvider: z.literal("glama") })),
openRouterSchema.merge(z.object({ apiProvider: z.literal("openrouter") })),
bedrockSchema.merge(z.object({ apiProvider: z.literal("bedrock") })),
@ -234,6 +240,7 @@ export const providerSettingsSchemaDiscriminated = z.discriminatedUnion("apiProv
export const providerSettingsSchema = z.object({
apiProvider: providerNamesSchema.optional(),
...anthropicSchema.shape,
...claudeCodeSchema.shape,
...glamaSchema.shape,
...openRouterSchema.shape,
...bedrockSchema.shape,

View file

@ -0,0 +1,14 @@
import type { ModelInfo } from "../model.js"
import { anthropicModels } from "./anthropic.js"
// Claude Code models - using the same models as Anthropic since Claude Code is a desktop app that uses Anthropic's API
export type ClaudeCodeModelId = keyof typeof claudeCodeModels
export const claudeCodeDefaultModelId: ClaudeCodeModelId = "claude-sonnet-4-20250514"
export const claudeCodeModels = {
"claude-sonnet-4-20250514": anthropicModels["claude-sonnet-4-20250514"],
"claude-opus-4-20250514": anthropicModels["claude-opus-4-20250514"],
"claude-3-7-sonnet-20250219": anthropicModels["claude-3-7-sonnet-20250219"],
"claude-3-5-sonnet-20241022": anthropicModels["claude-3-5-sonnet-20241022"],
"claude-3-5-haiku-20241022": anthropicModels["claude-3-5-haiku-20241022"],
} as const satisfies Record<string, ModelInfo>

View file

@ -1,6 +1,7 @@
export * from "./anthropic.js"
export * from "./bedrock.js"
export * from "./chutes.js"
export * from "./claude-code.js"
export * from "./deepseek.js"
export * from "./gemini.js"
export * from "./glama.js"

1092
roo-code-messages.log Normal file

File diff suppressed because one or more lines are too long

View file

@ -8,6 +8,7 @@ import {
GlamaHandler,
AnthropicHandler,
AwsBedrockHandler,
ClaudeCodeHandler,
OpenRouterHandler,
VertexHandler,
AnthropicVertexHandler,
@ -64,6 +65,8 @@ export function buildApiHandler(configuration: ProviderSettings): ApiHandler {
switch (apiProvider) {
case "anthropic":
return new AnthropicHandler(options)
case "claude-code":
return new ClaudeCodeHandler(options)
case "glama":
return new GlamaHandler(options)
case "openrouter":

View file

@ -0,0 +1,172 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { claudeCodeDefaultModelId, type ClaudeCodeModelId, claudeCodeModels } from "@roo-code/types"
import type { ApiHandlerOptions } from "../../shared/api"
import { runClaudeCode } from "../../integrations/claude-code/run"
import { type ClaudeCodeMessage } from "../../integrations/claude-code/types"
import { ApiStream, type ApiStreamUsageChunk } from "../transform/stream"
import { BaseProvider } from "./base-provider"
import type { ApiHandlerCreateMessageMetadata } from "../index"
export class ClaudeCodeHandler extends BaseProvider {
private options: ApiHandlerOptions
constructor(options: ApiHandlerOptions) {
super()
this.options = options
}
override async *createMessage(
systemPrompt: string,
messages: Anthropic.Messages.MessageParam[],
metadata?: ApiHandlerCreateMessageMetadata,
): ApiStream {
const claudeProcess = runClaudeCode({
systemPrompt,
messages,
path: this.options.claudeCodePath,
modelId: this.getModel().id,
})
const dataQueue: string[] = []
let processError = null
let errorOutput = ""
let exitCode: number | null = null
claudeProcess.stdout.on("data", (data) => {
const output = data.toString()
const lines = output.split("\n").filter((line: string) => line.trim() !== "")
for (const line of lines) {
dataQueue.push(line)
}
})
claudeProcess.stderr.on("data", (data) => {
errorOutput += data.toString()
})
claudeProcess.on("close", (code) => {
exitCode = code
})
claudeProcess.on("error", (error) => {
processError = error
})
// Usage is included with assistant messages,
// but cost is included in the result chunk
let usage: ApiStreamUsageChunk = {
type: "usage",
inputTokens: 0,
outputTokens: 0,
cacheReadTokens: 0,
cacheWriteTokens: 0,
}
while (exitCode !== 0 || dataQueue.length > 0) {
if (dataQueue.length === 0) {
await new Promise((resolve) => setImmediate(resolve))
}
if (exitCode !== null && exitCode !== 0) {
throw new Error(
`Claude Code process exited with code ${exitCode}.${errorOutput ? ` Error output: ${errorOutput.trim()}` : ""}`,
)
}
const data = dataQueue.shift()
if (!data) {
continue
}
const chunk = this.attemptParseChunk(data)
if (!chunk) {
yield {
type: "text",
text: data || "",
}
continue
}
if (chunk.type === "system" && chunk.subtype === "init") {
continue
}
if (chunk.type === "assistant" && "message" in chunk) {
const message = chunk.message
if (message.stop_reason !== null && message.stop_reason !== "tool_use") {
const errorMessage =
message.content[0]?.text || `Claude Code stopped with reason: ${message.stop_reason}`
if (errorMessage.includes("Invalid model name")) {
throw new Error(
errorMessage +
`\n\nAPI keys and subscription plans allow different models. Make sure the selected model is included in your plan.`,
)
}
throw new Error(errorMessage)
}
for (const content of message.content) {
if (content.type === "text") {
yield {
type: "text",
text: content.text,
}
} else {
console.warn("Unsupported content type:", content.type)
}
}
usage.inputTokens += message.usage.input_tokens
usage.outputTokens += message.usage.output_tokens
usage.cacheReadTokens = (usage.cacheReadTokens || 0) + (message.usage.cache_read_input_tokens || 0)
usage.cacheWriteTokens =
(usage.cacheWriteTokens || 0) + (message.usage.cache_creation_input_tokens || 0)
continue
}
if (chunk.type === "result" && "result" in chunk) {
usage.totalCost = chunk.cost_usd || 0
yield usage
}
if (processError) {
throw processError
}
}
}
override getModel() {
const modelId = this.options.apiModelId
if (modelId && modelId in claudeCodeModels) {
const id = modelId as ClaudeCodeModelId
return { id, info: claudeCodeModels[id] }
}
return {
id: claudeCodeDefaultModelId,
info: claudeCodeModels[claudeCodeDefaultModelId],
}
}
// TODO: Validate instead of parsing
private attemptParseChunk(data: string): ClaudeCodeMessage | null {
try {
return JSON.parse(data)
} catch (error) {
console.error("Error parsing chunk:", error)
return null
}
}
}

View file

@ -2,6 +2,7 @@ export { AnthropicVertexHandler } from "./anthropic-vertex"
export { AnthropicHandler } from "./anthropic"
export { AwsBedrockHandler } from "./bedrock"
export { ChutesHandler } from "./chutes"
export { ClaudeCodeHandler } from "./claude-code"
export { DeepSeekHandler } from "./deepseek"
export { FakeAIHandler } from "./fake-ai"
export { GeminiHandler } from "./gemini"

View file

@ -0,0 +1,44 @@
import * as vscode from "vscode"
import { Anthropic } from "@anthropic-ai/sdk"
import { execa } from "execa"
export function runClaudeCode({
systemPrompt,
messages,
path,
modelId,
}: {
systemPrompt: string
messages: Anthropic.Messages.MessageParam[]
path?: string
modelId?: string
}) {
const claudePath = path || "claude"
const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0)
// TODO: Is it worth using sessions? Where do we store the session ID?
const args = [
"-p",
JSON.stringify(messages),
"--system-prompt",
systemPrompt,
"--verbose",
"--output-format",
"stream-json",
// Roo Code will handle recursive calls
"--max-turns",
"1",
]
if (modelId) {
args.push("--model", modelId)
}
return execa(claudePath, args, {
stdin: "ignore",
stdout: "pipe",
stderr: "pipe",
env: process.env,
cwd,
})
}

View file

@ -0,0 +1,52 @@
type InitMessage = {
type: "system"
subtype: "init"
session_id: string
tools: string[]
mcp_servers: string[]
}
type ClaudeCodeContent = {
type: "text"
text: string
}
type AssistantMessage = {
type: "assistant"
message: {
id: string
type: "message"
role: "assistant"
model: string
content: ClaudeCodeContent[]
stop_reason: null
stop_sequence: null
usage: {
input_tokens: number
cache_creation_input_tokens?: number
cache_read_input_tokens?: number
output_tokens: number
service_tier: "standard"
}
}
session_id: string
}
type ErrorMessage = {
type: "error"
}
type ResultMessage = {
type: "result"
subtype: "success"
cost_usd: number
is_error: boolean
duration_ms: number
duration_api_ms: number
num_turns: number
result: string
total_cost: number
session_id: string
}
export type ClaudeCodeMessage = InitMessage | AssistantMessage | ErrorMessage | ResultMessage