This repository was archived by the owner on May 15, 2026. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 3.4k
Expand file tree
/
Copy pathclaude-code.ts
More file actions
365 lines (315 loc) · 11.4 KB
/
Copy pathclaude-code.ts
File metadata and controls
365 lines (315 loc) · 11.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
import type { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
import {
claudeCodeDefaultModelId,
type ClaudeCodeModelId,
claudeCodeModels,
claudeCodeReasoningConfig,
type ClaudeCodeReasoningLevel,
type ModelInfo,
} from "@roo-code/types"
import { type ApiHandler, ApiHandlerCreateMessageMetadata, type SingleCompletionHandler } from ".."
import { applyModelFamilyDefaults } from "./utils/model-family-defaults"
import { ApiStreamUsageChunk, type ApiStream } from "../transform/stream"
import { claudeCodeOAuthManager, generateUserId } from "../../integrations/claude-code/oauth"
import {
createStreamingMessage,
type StreamChunk,
type ThinkingConfig,
} from "../../integrations/claude-code/streaming-client"
import { t } from "../../i18n"
import { ApiHandlerOptions } from "../../shared/api"
import { countTokens } from "../../utils/countTokens"
import { convertOpenAIToolsToAnthropic } from "../../core/prompts/tools/native-tools/converters"
/**
* Converts OpenAI tool_choice to Anthropic ToolChoice format
* @param toolChoice - OpenAI tool_choice parameter
* @param parallelToolCalls - When true, allows parallel tool calls. When false (default), disables parallel tool calls.
*/
function convertOpenAIToolChoice(
toolChoice: OpenAI.Chat.ChatCompletionCreateParams["tool_choice"],
parallelToolCalls?: boolean,
): Anthropic.Messages.MessageCreateParams["tool_choice"] | undefined {
// Anthropic allows parallel tool calls by default. When parallelToolCalls is false or undefined,
// we disable parallel tool use to ensure one tool call at a time.
const disableParallelToolUse = !parallelToolCalls
if (!toolChoice) {
// Default to auto with parallel tool use control
return { type: "auto", disable_parallel_tool_use: disableParallelToolUse }
}
if (typeof toolChoice === "string") {
switch (toolChoice) {
case "none":
return undefined // Anthropic doesn't have "none", just omit tools
case "auto":
return { type: "auto", disable_parallel_tool_use: disableParallelToolUse }
case "required":
return { type: "any", disable_parallel_tool_use: disableParallelToolUse }
default:
return { type: "auto", disable_parallel_tool_use: disableParallelToolUse }
}
}
// Handle object form { type: "function", function: { name: string } }
if (typeof toolChoice === "object" && "function" in toolChoice) {
return {
type: "tool",
name: toolChoice.function.name,
disable_parallel_tool_use: disableParallelToolUse,
}
}
return { type: "auto", disable_parallel_tool_use: disableParallelToolUse }
}
export class ClaudeCodeHandler implements ApiHandler, SingleCompletionHandler {
private options: ApiHandlerOptions
/**
* Store the last thinking block signature for interleaved thinking with tool use.
* This is captured from thinking_complete events during streaming and
* must be passed back to the API when providing tool results.
* Similar to Gemini's thoughtSignature pattern.
*/
private lastThinkingSignature?: string
constructor(options: ApiHandlerOptions) {
this.options = options
}
/**
* Get the thinking signature from the last response.
* Used by Task.addToApiConversationHistory to persist the signature
* so it can be passed back to the API for tool use continuations.
* This follows the same pattern as Gemini's getThoughtSignature().
*/
public getThoughtSignature(): string | undefined {
return this.lastThinkingSignature
}
/**
* Gets the reasoning effort level for the current request.
* Returns the effective reasoning level (low/medium/high) or null if disabled.
*/
private getReasoningEffort(modelInfo: ModelInfo): ClaudeCodeReasoningLevel | null {
// Check if reasoning is explicitly disabled
if (this.options.enableReasoningEffort === false) {
return null
}
// Get the selected effort from settings or model default
const selectedEffort = this.options.reasoningEffort ?? modelInfo.reasoningEffort
// "disable" or no selection means no reasoning
if (!selectedEffort || selectedEffort === "disable") {
return null
}
// Only allow valid levels for Claude Code
if (selectedEffort === "low" || selectedEffort === "medium" || selectedEffort === "high") {
return selectedEffort
}
return null
}
async *createMessage(
systemPrompt: string,
messages: Anthropic.Messages.MessageParam[],
metadata?: ApiHandlerCreateMessageMetadata,
): ApiStream {
// Reset per-request state that we persist into apiConversationHistory
this.lastThinkingSignature = undefined
// Get access token from OAuth manager
const accessToken = await claudeCodeOAuthManager.getAccessToken()
if (!accessToken) {
throw new Error(
t("common:errors.claudeCode.notAuthenticated", {
defaultValue:
"Not authenticated with Claude Code. Please sign in using the Claude Code OAuth flow.",
}),
)
}
// Get user email for generating user_id metadata
const email = await claudeCodeOAuthManager.getEmail()
const model = this.getModel()
// Validate that the model ID is a valid ClaudeCodeModelId
const modelId = Object.hasOwn(claudeCodeModels, model.id)
? (model.id as ClaudeCodeModelId)
: claudeCodeDefaultModelId
// Generate user_id metadata in the format required by Claude Code API
const userId = generateUserId(email || undefined)
// Convert OpenAI tools to Anthropic format if provided and protocol is native
// Exclude tools when tool_choice is "none" since that means "don't use tools"
const shouldIncludeNativeTools =
metadata?.tools &&
metadata.tools.length > 0 &&
metadata?.toolProtocol !== "xml" &&
metadata?.tool_choice !== "none"
const anthropicTools = shouldIncludeNativeTools ? convertOpenAIToolsToAnthropic(metadata.tools!) : undefined
const anthropicToolChoice = shouldIncludeNativeTools
? convertOpenAIToolChoice(metadata.tool_choice, metadata.parallelToolCalls)
: undefined
// Determine reasoning effort and thinking configuration
const reasoningLevel = this.getReasoningEffort(model.info)
let thinking: ThinkingConfig
// With interleaved thinking (enabled via beta header), budget_tokens can exceed max_tokens
// as the token limit becomes the entire context window. We use the model's maxTokens.
// See: https://docs.anthropic.com/en/docs/build-with-claude/extended-thinking#interleaved-thinking
const maxTokens = model.info.maxTokens ?? 16384
if (reasoningLevel) {
// Use thinking mode with budget_tokens from config
const config = claudeCodeReasoningConfig[reasoningLevel]
thinking = {
type: "enabled",
budget_tokens: config.budgetTokens,
}
} else {
// Explicitly disable thinking
thinking = { type: "disabled" }
}
// Create streaming request using OAuth
const stream = createStreamingMessage({
accessToken,
model: modelId,
systemPrompt,
messages,
maxTokens,
thinking,
tools: anthropicTools,
toolChoice: anthropicToolChoice,
metadata: {
user_id: userId,
},
})
// Track usage for cost calculation
let inputTokens = 0
let outputTokens = 0
let cacheReadTokens = 0
let cacheWriteTokens = 0
for await (const chunk of stream) {
switch (chunk.type) {
case "text":
yield {
type: "text",
text: chunk.text,
}
break
case "reasoning":
yield {
type: "reasoning",
text: chunk.text,
}
break
case "thinking_complete":
// Capture the signature for persistence in api_conversation_history
// This enables tool use continuations where thinking blocks must be passed back
if (chunk.signature) {
this.lastThinkingSignature = chunk.signature
}
// Emit a complete thinking block with signature
// This is critical for interleaved thinking with tool use
// The signature must be included when passing thinking blocks back to the API
yield {
type: "reasoning",
text: chunk.thinking,
signature: chunk.signature,
}
break
case "tool_call_partial":
yield {
type: "tool_call_partial",
index: chunk.index,
id: chunk.id,
name: chunk.name,
arguments: chunk.arguments,
}
break
case "usage": {
inputTokens = chunk.inputTokens
outputTokens = chunk.outputTokens
cacheReadTokens = chunk.cacheReadTokens || 0
cacheWriteTokens = chunk.cacheWriteTokens || 0
// Claude Code is subscription-based, no per-token cost
const usageChunk: ApiStreamUsageChunk = {
type: "usage",
inputTokens,
outputTokens,
cacheReadTokens: cacheReadTokens > 0 ? cacheReadTokens : undefined,
cacheWriteTokens: cacheWriteTokens > 0 ? cacheWriteTokens : undefined,
totalCost: 0,
}
yield usageChunk
break
}
case "error":
throw new Error(chunk.error)
}
}
}
getModel(): { id: string; info: ModelInfo } {
const modelId = this.options.apiModelId
if (modelId && Object.hasOwn(claudeCodeModels, modelId)) {
const id = modelId as ClaudeCodeModelId
// Apply model family defaults for consistent behavior across providers
const info = applyModelFamilyDefaults(id, { ...claudeCodeModels[id] })
return { id, info }
}
// Apply model family defaults for consistent behavior across providers
const info = applyModelFamilyDefaults(claudeCodeDefaultModelId, {
...claudeCodeModels[claudeCodeDefaultModelId],
})
return {
id: claudeCodeDefaultModelId,
info,
}
}
async countTokens(content: Anthropic.Messages.ContentBlockParam[]): Promise<number> {
if (content.length === 0) {
return 0
}
return countTokens(content, { useWorker: true })
}
/**
* Completes a prompt using the Claude Code API.
* This is used for context condensing and prompt enhancement.
* The Claude Code branding is automatically prepended by createStreamingMessage.
*/
async completePrompt(prompt: string): Promise<string> {
// Get access token from OAuth manager
const accessToken = await claudeCodeOAuthManager.getAccessToken()
if (!accessToken) {
throw new Error(
t("common:errors.claudeCode.notAuthenticated", {
defaultValue:
"Not authenticated with Claude Code. Please sign in using the Claude Code OAuth flow.",
}),
)
}
// Get user email for generating user_id metadata
const email = await claudeCodeOAuthManager.getEmail()
const model = this.getModel()
// Validate that the model ID is a valid ClaudeCodeModelId
const modelId = Object.hasOwn(claudeCodeModels, model.id)
? (model.id as ClaudeCodeModelId)
: claudeCodeDefaultModelId
// Generate user_id metadata in the format required by Claude Code API
const userId = generateUserId(email || undefined)
// Use maxTokens from model info for completion
const maxTokens = model.info.maxTokens ?? 16384
// Create streaming request using OAuth
// The system prompt is empty here since the prompt itself contains all context
// createStreamingMessage will still prepend the Claude Code branding
const stream = createStreamingMessage({
accessToken,
model: modelId,
systemPrompt: "", // Empty system prompt - the prompt text contains all necessary context
messages: [{ role: "user", content: prompt }],
maxTokens,
thinking: { type: "disabled" }, // No thinking for simple completions
metadata: {
user_id: userId,
},
})
// Collect all text chunks into a single response
let result = ""
for await (const chunk of stream) {
switch (chunk.type) {
case "text":
result += chunk.text
break
case "error":
throw new Error(chunk.error)
}
}
return result
}
}