Skip to content

Commit 6aa8c54

Browse files
committed
integrate claude code
1 parent 347a292 commit 6aa8c54

File tree

13 files changed

+349
-0
lines changed

13 files changed

+349
-0
lines changed

packages/types/src/provider-settings.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { codebaseIndexProviderSchema } from "./codebase-index.js"
99

1010
export const providerNames = [
1111
"anthropic",
12+
"claude-code",
1213
"glama",
1314
"openrouter",
1415
"bedrock",
@@ -76,6 +77,10 @@ const anthropicSchema = apiModelIdProviderModelSchema.extend({
7677
anthropicUseAuthToken: z.boolean().optional(),
7778
})
7879

80+
const claudeCodeSchema = apiModelIdProviderModelSchema.extend({
81+
claudeCodePath: z.string().optional(),
82+
})
83+
7984
const glamaSchema = baseProviderSettingsSchema.extend({
8085
glamaModelId: z.string().optional(),
8186
glamaApiKey: z.string().optional(),
@@ -208,6 +213,7 @@ const defaultSchema = z.object({
208213

209214
export const providerSettingsSchemaDiscriminated = z.discriminatedUnion("apiProvider", [
210215
anthropicSchema.merge(z.object({ apiProvider: z.literal("anthropic") })),
216+
claudeCodeSchema.merge(z.object({ apiProvider: z.literal("claude-code") })),
211217
glamaSchema.merge(z.object({ apiProvider: z.literal("glama") })),
212218
openRouterSchema.merge(z.object({ apiProvider: z.literal("openrouter") })),
213219
bedrockSchema.merge(z.object({ apiProvider: z.literal("bedrock") })),
@@ -234,6 +240,7 @@ export const providerSettingsSchemaDiscriminated = z.discriminatedUnion("apiProv
234240
export const providerSettingsSchema = z.object({
235241
apiProvider: providerNamesSchema.optional(),
236242
...anthropicSchema.shape,
243+
...claudeCodeSchema.shape,
237244
...glamaSchema.shape,
238245
...openRouterSchema.shape,
239246
...bedrockSchema.shape,
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import type { ModelInfo } from "../model.js"
2+
import { anthropicModels } from "./anthropic.js"
3+
4+
// Claude Code models - subset of Anthropic models available through Claude Code CLI
5+
6+
export type ClaudeCodeModelId = keyof typeof claudeCodeModels
7+
export const claudeCodeDefaultModelId: ClaudeCodeModelId = "claude-sonnet-4-20250514"
8+
9+
export const claudeCodeModels = {
10+
"claude-sonnet-4-20250514": anthropicModels["claude-sonnet-4-20250514"],
11+
"claude-opus-4-20250514": anthropicModels["claude-opus-4-20250514"],
12+
"claude-3-7-sonnet-20250219": anthropicModels["claude-3-7-sonnet-20250219"],
13+
"claude-3-5-sonnet-20241022": anthropicModels["claude-3-5-sonnet-20241022"],
14+
"claude-3-5-haiku-20241022": anthropicModels["claude-3-5-haiku-20241022"],
15+
} as const satisfies Record<string, ModelInfo>

packages/types/src/providers/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export * from "./anthropic.js"
22
export * from "./bedrock.js"
33
export * from "./chutes.js"
4+
export * from "./claude-code.js"
45
export * from "./deepseek.js"
56
export * from "./gemini.js"
67
export * from "./glama.js"

src/api/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
GroqHandler,
2828
ChutesHandler,
2929
LiteLLMHandler,
30+
ClaudeCodeHandler,
3031
} from "./providers"
3132

3233
export interface SingleCompletionHandler {
@@ -64,6 +65,8 @@ export function buildApiHandler(configuration: ProviderSettings): ApiHandler {
6465
switch (apiProvider) {
6566
case "anthropic":
6667
return new AnthropicHandler(options)
68+
case "claude-code":
69+
return new ClaudeCodeHandler(options)
6770
case "glama":
6871
return new GlamaHandler(options)
6972
case "openrouter":

src/api/providers/claude-code.ts

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
import type { Anthropic } from "@anthropic-ai/sdk"
2+
import { claudeCodeDefaultModelId, type ClaudeCodeModelId, claudeCodeModels } from "@roo-code/types"
3+
import type { ApiHandlerOptions } from "../../shared/api"
4+
import { type ApiHandler, type ApiHandlerCreateMessageMetadata } from ".."
5+
import { ApiStreamUsageChunk, type ApiStream } from "../transform/stream"
6+
import { runClaudeCode } from "../../integrations/claude-code/run"
7+
import { ClaudeCodeMessage } from "../../integrations/claude-code/types"
8+
import { BaseProvider } from "./base-provider"
9+
10+
export class ClaudeCodeHandler extends BaseProvider implements ApiHandler {
11+
private options: ApiHandlerOptions
12+
13+
constructor(options: ApiHandlerOptions) {
14+
super()
15+
this.options = options
16+
}
17+
18+
async *createMessage(
19+
systemPrompt: string,
20+
messages: Anthropic.Messages.MessageParam[],
21+
metadata?: ApiHandlerCreateMessageMetadata,
22+
): ApiStream {
23+
const claudeProcess = runClaudeCode({
24+
systemPrompt,
25+
messages,
26+
path: this.options.claudeCodePath,
27+
modelId: this.getModel().id,
28+
})
29+
30+
const dataQueue: string[] = []
31+
let processError: Error | null = null
32+
let errorOutput = ""
33+
let exitCode: number | null = null
34+
35+
claudeProcess.stdout.on("data", (data) => {
36+
const output = data.toString()
37+
const lines = output.split("\n").filter((line: string) => line.trim() !== "")
38+
39+
for (const line of lines) {
40+
dataQueue.push(line)
41+
}
42+
})
43+
44+
claudeProcess.stderr.on("data", (data) => {
45+
errorOutput += data.toString()
46+
})
47+
48+
claudeProcess.on("close", (code) => {
49+
exitCode = code
50+
})
51+
52+
claudeProcess.on("error", (error) => {
53+
processError = error
54+
})
55+
56+
// Usage is included with assistant messages,
57+
// but cost is included in the result chunk
58+
let usage: ApiStreamUsageChunk = {
59+
type: "usage",
60+
inputTokens: 0,
61+
outputTokens: 0,
62+
cacheReadTokens: 0,
63+
cacheWriteTokens: 0,
64+
}
65+
66+
while (exitCode !== 0 || dataQueue.length > 0) {
67+
if (dataQueue.length === 0) {
68+
await new Promise((resolve) => setImmediate(resolve))
69+
}
70+
71+
if (exitCode !== null && exitCode !== 0) {
72+
throw new Error(
73+
`Claude Code process exited with code ${exitCode}.${errorOutput ? ` Error output: ${errorOutput.trim()}` : ""}`,
74+
)
75+
}
76+
77+
const data = dataQueue.shift()
78+
if (!data) {
79+
continue
80+
}
81+
82+
const chunk = this.attemptParseChunk(data)
83+
84+
if (!chunk) {
85+
yield {
86+
type: "text",
87+
text: data || "",
88+
}
89+
90+
continue
91+
}
92+
93+
if (chunk.type === "system" && chunk.subtype === "init") {
94+
continue
95+
}
96+
97+
if (chunk.type === "assistant" && "message" in chunk) {
98+
const message = chunk.message
99+
100+
if (message.stop_reason !== null && message.stop_reason !== "tool_use") {
101+
const errorMessage =
102+
message.content[0]?.text || `Claude Code stopped with reason: ${message.stop_reason}`
103+
104+
if (errorMessage.includes("Invalid model name")) {
105+
throw new Error(
106+
errorMessage +
107+
`\n\nAPI keys and subscription plans allow different models. Make sure the selected model is included in your plan.`,
108+
)
109+
}
110+
111+
throw new Error(errorMessage)
112+
}
113+
114+
for (const content of message.content) {
115+
if (content.type === "text") {
116+
yield {
117+
type: "text",
118+
text: content.text,
119+
}
120+
} else {
121+
console.warn("Unsupported content type:", content.type)
122+
}
123+
}
124+
125+
usage.inputTokens += message.usage.input_tokens
126+
usage.outputTokens += message.usage.output_tokens
127+
usage.cacheReadTokens = (usage.cacheReadTokens || 0) + (message.usage.cache_read_input_tokens || 0)
128+
usage.cacheWriteTokens =
129+
(usage.cacheWriteTokens || 0) + (message.usage.cache_creation_input_tokens || 0)
130+
131+
continue
132+
}
133+
134+
if (chunk.type === "result" && "result" in chunk) {
135+
usage.totalCost = chunk.cost_usd || 0
136+
137+
yield usage
138+
}
139+
140+
if (processError) {
141+
throw processError
142+
}
143+
}
144+
}
145+
146+
getModel() {
147+
const modelId = this.options.apiModelId
148+
if (modelId && modelId in claudeCodeModels) {
149+
const id = modelId as ClaudeCodeModelId
150+
return { id, info: claudeCodeModels[id] }
151+
}
152+
153+
return {
154+
id: claudeCodeDefaultModelId,
155+
info: claudeCodeModels[claudeCodeDefaultModelId],
156+
}
157+
}
158+
159+
// TODO: Validate instead of parsing
160+
private attemptParseChunk(data: string): ClaudeCodeMessage | null {
161+
try {
162+
return JSON.parse(data)
163+
} catch (error) {
164+
console.error("Error parsing chunk:", error)
165+
return null
166+
}
167+
}
168+
}

src/api/providers/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export { AnthropicVertexHandler } from "./anthropic-vertex"
22
export { AnthropicHandler } from "./anthropic"
33
export { AwsBedrockHandler } from "./bedrock"
44
export { ChutesHandler } from "./chutes"
5+
export { ClaudeCodeHandler } from "./claude-code"
56
export { DeepSeekHandler } from "./deepseek"
67
export { FakeAIHandler } from "./fake-ai"
78
export { GeminiHandler } from "./gemini"
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import * as vscode from "vscode"
2+
import Anthropic from "@anthropic-ai/sdk"
3+
import { execa } from "execa"
4+
5+
const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0)
6+
7+
export function runClaudeCode({
8+
systemPrompt,
9+
messages,
10+
path,
11+
modelId,
12+
}: {
13+
systemPrompt: string
14+
messages: Anthropic.Messages.MessageParam[]
15+
path?: string
16+
modelId?: string
17+
}) {
18+
const claudePath = path || "claude"
19+
20+
// TODO: Is it worth using sessions? Where do we store the session ID?
21+
const args = [
22+
"-p",
23+
JSON.stringify(messages),
24+
"--system-prompt",
25+
systemPrompt,
26+
"--verbose",
27+
"--output-format",
28+
"stream-json",
29+
// Cline will handle recursive calls
30+
"--max-turns",
31+
"1",
32+
]
33+
34+
if (modelId) {
35+
args.push("--model", modelId)
36+
}
37+
38+
return execa(claudePath, args, {
39+
stdin: "ignore",
40+
stdout: "pipe",
41+
stderr: "pipe",
42+
env: process.env,
43+
cwd,
44+
})
45+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
type InitMessage = {
2+
type: "system"
3+
subtype: "init"
4+
session_id: string
5+
tools: string[]
6+
mcp_servers: string[]
7+
}
8+
9+
type ClaudeCodeContent = {
10+
type: "text"
11+
text: string
12+
}
13+
14+
type AssistantMessage = {
15+
type: "assistant"
16+
message: {
17+
id: string
18+
type: "message"
19+
role: "assistant"
20+
model: string
21+
content: ClaudeCodeContent[]
22+
stop_reason: null
23+
stop_sequence: null
24+
usage: {
25+
input_tokens: number
26+
cache_creation_input_tokens?: number
27+
cache_read_input_tokens?: number
28+
output_tokens: number
29+
service_tier: "standard"
30+
}
31+
}
32+
session_id: string
33+
}
34+
35+
type ErrorMessage = {
36+
type: "error"
37+
}
38+
39+
type ResultMessage = {
40+
type: "result"
41+
subtype: "success"
42+
cost_usd: number
43+
is_error: boolean
44+
duration_ms: number
45+
duration_api_ms: number
46+
num_turns: number
47+
result: string
48+
total_cost: number
49+
session_id: string
50+
}
51+
52+
export type ClaudeCodeMessage = InitMessage | AssistantMessage | ErrorMessage | ResultMessage

webview-ui/src/components/settings/ApiOptions.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
litellmDefaultModelId,
1414
openAiNativeDefaultModelId,
1515
anthropicDefaultModelId,
16+
claudeCodeDefaultModelId,
1617
geminiDefaultModelId,
1718
deepSeekDefaultModelId,
1819
mistralDefaultModelId,
@@ -36,6 +37,7 @@ import {
3637
Anthropic,
3738
Bedrock,
3839
Chutes,
40+
ClaudeCode,
3941
DeepSeek,
4042
Gemini,
4143
Glama,
@@ -254,6 +256,7 @@ const ApiOptions = ({
254256
requesty: { field: "requestyModelId", default: requestyDefaultModelId },
255257
litellm: { field: "litellmModelId", default: litellmDefaultModelId },
256258
anthropic: { field: "apiModelId", default: anthropicDefaultModelId },
259+
"claude-code": { field: "apiModelId", default: claudeCodeDefaultModelId },
257260
"openai-native": { field: "apiModelId", default: openAiNativeDefaultModelId },
258261
gemini: { field: "apiModelId", default: geminiDefaultModelId },
259262
deepseek: { field: "apiModelId", default: deepSeekDefaultModelId },
@@ -383,6 +386,10 @@ const ApiOptions = ({
383386
<Anthropic apiConfiguration={apiConfiguration} setApiConfigurationField={setApiConfigurationField} />
384387
)}
385388

389+
{selectedProvider === "claude-code" && (
390+
<ClaudeCode apiConfiguration={apiConfiguration} setApiConfigurationField={setApiConfigurationField} />
391+
)}
392+
386393
{selectedProvider === "openai-native" && (
387394
<OpenAI apiConfiguration={apiConfiguration} setApiConfigurationField={setApiConfigurationField} />
388395
)}

0 commit comments

Comments
 (0)