mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-10-06 02:47:56 +00:00
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:
parent
2e2f83be60
commit
587853842b
9 changed files with 1386 additions and 0 deletions
|
|
@ -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,
|
||||
|
|
|
|||
14
packages/types/src/providers/claude-code.ts
Normal file
14
packages/types/src/providers/claude-code.ts
Normal 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>
|
||||
|
|
@ -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
1092
roo-code-messages.log
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -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":
|
||||
|
|
|
|||
172
src/api/providers/claude-code.ts
Normal file
172
src/api/providers/claude-code.ts
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
44
src/integrations/claude-code/run.ts
Normal file
44
src/integrations/claude-code/run.ts
Normal 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,
|
||||
})
|
||||
}
|
||||
52
src/integrations/claude-code/types.ts
Normal file
52
src/integrations/claude-code/types.ts
Normal 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
|
||||
Loading…
Add table
Reference in a new issue