mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-16 23:41:06 +00:00
334 lines
10 KiB
TypeScript
334 lines
10 KiB
TypeScript
import { Anthropic } from "@anthropic-ai/sdk"
|
|
import OpenAI from "openai"
|
|
|
|
import { rooDefaultModelId, getApiProtocol, type ImageGenerationApiMethod } from "@roo-code/types"
|
|
import { CloudService } from "@roo-code/cloud"
|
|
|
|
import type { ApiHandlerOptions, ModelRecord } from "../../shared/api"
|
|
import { ApiStream } from "../transform/stream"
|
|
import { getModelParams } from "../transform/model-params"
|
|
import { convertToOpenAiMessages } from "../transform/openai-format"
|
|
import type { RooReasoningParams } from "../transform/reasoning"
|
|
import { getRooReasoning } from "../transform/reasoning"
|
|
|
|
import type { ApiHandlerCreateMessageMetadata } from "../index"
|
|
import { BaseOpenAiCompatibleProvider } from "./base-openai-compatible-provider"
|
|
import { getModels, getModelsFromCache } from "../providers/fetchers/modelCache"
|
|
import { handleOpenAIError } from "./utils/openai-error-handler"
|
|
import { generateImageWithProvider, generateImageWithImagesApi, ImageGenerationResult } from "./utils/image-generation"
|
|
import { t } from "../../i18n"
|
|
|
|
import type { ModelInfo } from "@roo-code/types"
|
|
|
|
// Model-specific defaults that should be applied even when models come from API cache
|
|
const MODEL_DEFAULTS: Record<string, Partial<ModelInfo>> = {
|
|
"minimax/minimax-m2": {
|
|
defaultToolProtocol: "native",
|
|
},
|
|
"anthropic/claude-haiku-4.5": {
|
|
defaultToolProtocol: "native",
|
|
},
|
|
}
|
|
|
|
// Extend OpenAI's CompletionUsage to include Roo specific fields
|
|
interface RooUsage extends OpenAI.CompletionUsage {
|
|
cache_creation_input_tokens?: number
|
|
cost?: number
|
|
}
|
|
|
|
// Add custom interface for Roo params to support reasoning
|
|
type RooChatCompletionParams = OpenAI.Chat.ChatCompletionCreateParamsStreaming & {
|
|
reasoning?: RooReasoningParams
|
|
}
|
|
|
|
function getSessionToken(): string {
|
|
const token = CloudService.hasInstance() ? CloudService.instance.authService?.getSessionToken() : undefined
|
|
return token ?? "unauthenticated"
|
|
}
|
|
|
|
export class RooHandler extends BaseOpenAiCompatibleProvider<string> {
|
|
private fetcherBaseURL: string
|
|
|
|
constructor(options: ApiHandlerOptions) {
|
|
const sessionToken = getSessionToken()
|
|
|
|
let baseURL = process.env.ROO_CODE_PROVIDER_URL ?? "https://api.roocode.com/proxy"
|
|
|
|
// Ensure baseURL ends with /v1 for OpenAI client, but don't duplicate it
|
|
if (!baseURL.endsWith("/v1")) {
|
|
baseURL = `${baseURL}/v1`
|
|
}
|
|
|
|
// Always construct the handler, even without a valid token.
|
|
// The provider-proxy server will return 401 if authentication fails.
|
|
super({
|
|
...options,
|
|
providerName: "Roo Code Cloud",
|
|
baseURL, // Already has /v1 suffix
|
|
apiKey: sessionToken,
|
|
defaultProviderModelId: rooDefaultModelId,
|
|
providerModels: {},
|
|
defaultTemperature: 0.7,
|
|
})
|
|
|
|
// Load dynamic models asynchronously - strip /v1 from baseURL for fetcher
|
|
this.fetcherBaseURL = baseURL.endsWith("/v1") ? baseURL.slice(0, -3) : baseURL
|
|
this.loadDynamicModels(this.fetcherBaseURL, sessionToken).catch((error) => {
|
|
console.error("[RooHandler] Failed to load dynamic models:", error)
|
|
})
|
|
}
|
|
|
|
protected override createStream(
|
|
systemPrompt: string,
|
|
messages: Anthropic.Messages.MessageParam[],
|
|
metadata?: ApiHandlerCreateMessageMetadata,
|
|
requestOptions?: OpenAI.RequestOptions,
|
|
) {
|
|
const { id: model, info } = this.getModel()
|
|
|
|
// Get model parameters including reasoning
|
|
const params = getModelParams({
|
|
format: "openai",
|
|
modelId: model,
|
|
model: info,
|
|
settings: this.options,
|
|
defaultTemperature: this.defaultTemperature,
|
|
})
|
|
|
|
// Get Roo-specific reasoning parameters
|
|
const reasoning = getRooReasoning({
|
|
model: info,
|
|
reasoningBudget: params.reasoningBudget,
|
|
reasoningEffort: params.reasoningEffort,
|
|
settings: this.options,
|
|
})
|
|
|
|
const max_tokens = params.maxTokens ?? undefined
|
|
const temperature = params.temperature ?? this.defaultTemperature
|
|
|
|
const rooParams: RooChatCompletionParams = {
|
|
model,
|
|
max_tokens,
|
|
temperature,
|
|
messages: [{ role: "system", content: systemPrompt }, ...convertToOpenAiMessages(messages)],
|
|
stream: true,
|
|
stream_options: { include_usage: true },
|
|
...(reasoning && { reasoning }),
|
|
...(metadata?.tools && { tools: metadata.tools }),
|
|
...(metadata?.tool_choice && { tool_choice: metadata.tool_choice }),
|
|
}
|
|
|
|
try {
|
|
this.client.apiKey = getSessionToken()
|
|
return this.client.chat.completions.create(rooParams, requestOptions)
|
|
} catch (error) {
|
|
throw handleOpenAIError(error, this.providerName)
|
|
}
|
|
}
|
|
|
|
override async *createMessage(
|
|
systemPrompt: string,
|
|
messages: Anthropic.Messages.MessageParam[],
|
|
metadata?: ApiHandlerCreateMessageMetadata,
|
|
): ApiStream {
|
|
try {
|
|
const stream = await this.createStream(
|
|
systemPrompt,
|
|
messages,
|
|
metadata,
|
|
metadata?.taskId ? { headers: { "X-Roo-Task-ID": metadata.taskId } } : undefined,
|
|
)
|
|
|
|
let lastUsage: RooUsage | undefined = undefined
|
|
|
|
for await (const chunk of stream) {
|
|
const delta = chunk.choices[0]?.delta
|
|
|
|
if (delta) {
|
|
// Check for reasoning content (similar to OpenRouter)
|
|
if ("reasoning" in delta && delta.reasoning && typeof delta.reasoning === "string") {
|
|
yield {
|
|
type: "reasoning",
|
|
text: delta.reasoning,
|
|
}
|
|
}
|
|
|
|
// Also check for reasoning_content for backward compatibility
|
|
if ("reasoning_content" in delta && typeof delta.reasoning_content === "string") {
|
|
yield {
|
|
type: "reasoning",
|
|
text: delta.reasoning_content,
|
|
}
|
|
}
|
|
|
|
// Emit raw tool call chunks - NativeToolCallParser handles state management
|
|
if ("tool_calls" in delta && Array.isArray(delta.tool_calls)) {
|
|
for (const toolCall of delta.tool_calls) {
|
|
yield {
|
|
type: "tool_call_partial",
|
|
index: toolCall.index,
|
|
id: toolCall.id,
|
|
name: toolCall.function?.name,
|
|
arguments: toolCall.function?.arguments,
|
|
}
|
|
}
|
|
}
|
|
|
|
if (delta.content) {
|
|
yield {
|
|
type: "text",
|
|
text: delta.content,
|
|
}
|
|
}
|
|
}
|
|
|
|
if (chunk.usage) {
|
|
lastUsage = chunk.usage as RooUsage
|
|
}
|
|
}
|
|
|
|
if (lastUsage) {
|
|
// Check if the current model is marked as free
|
|
const model = this.getModel()
|
|
const isFreeModel = model.info.isFree ?? false
|
|
|
|
// Normalize input tokens based on protocol expectations:
|
|
// - OpenAI protocol expects TOTAL input tokens (cached + non-cached)
|
|
// - Anthropic protocol expects NON-CACHED input tokens (caches passed separately)
|
|
const modelId = model.id
|
|
const apiProtocol = getApiProtocol("roo", modelId)
|
|
|
|
const promptTokens = lastUsage.prompt_tokens || 0
|
|
const cacheWrite = lastUsage.cache_creation_input_tokens || 0
|
|
const cacheRead = lastUsage.prompt_tokens_details?.cached_tokens || 0
|
|
const nonCached = Math.max(0, promptTokens - cacheWrite - cacheRead)
|
|
|
|
const inputTokensForDownstream = apiProtocol === "anthropic" ? nonCached : promptTokens
|
|
|
|
yield {
|
|
type: "usage",
|
|
inputTokens: inputTokensForDownstream,
|
|
outputTokens: lastUsage.completion_tokens || 0,
|
|
cacheWriteTokens: cacheWrite,
|
|
cacheReadTokens: cacheRead,
|
|
totalCost: isFreeModel ? 0 : (lastUsage.cost ?? 0),
|
|
}
|
|
}
|
|
} catch (error) {
|
|
// Log streaming errors with context
|
|
console.error("[RooHandler] Error during message streaming:", {
|
|
error: error instanceof Error ? error.message : String(error),
|
|
stack: error instanceof Error ? error.stack : undefined,
|
|
modelId: this.options.apiModelId,
|
|
hasTaskId: Boolean(metadata?.taskId),
|
|
})
|
|
throw error
|
|
}
|
|
}
|
|
override async completePrompt(prompt: string): Promise<string> {
|
|
// Update API key before making request to ensure we use the latest session token
|
|
this.client.apiKey = getSessionToken()
|
|
return super.completePrompt(prompt)
|
|
}
|
|
|
|
private async loadDynamicModels(baseURL: string, apiKey?: string): Promise<void> {
|
|
try {
|
|
// Fetch models and cache them in the shared cache
|
|
await getModels({
|
|
provider: "roo",
|
|
baseUrl: baseURL,
|
|
apiKey,
|
|
})
|
|
} catch (error) {
|
|
// Enhanced error logging with more context
|
|
console.error("[RooHandler] Error loading dynamic models:", {
|
|
error: error instanceof Error ? error.message : String(error),
|
|
stack: error instanceof Error ? error.stack : undefined,
|
|
baseURL,
|
|
hasApiKey: Boolean(apiKey),
|
|
})
|
|
}
|
|
}
|
|
|
|
override getModel() {
|
|
const modelId = this.options.apiModelId || rooDefaultModelId
|
|
|
|
// Get models from shared cache
|
|
const models = getModelsFromCache("roo") || {}
|
|
const modelInfo = models[modelId]
|
|
|
|
// Get model-specific defaults if they exist
|
|
const modelDefaults = MODEL_DEFAULTS[modelId]
|
|
|
|
if (modelInfo) {
|
|
// Merge model-specific defaults with cached model info
|
|
const mergedInfo = modelDefaults ? { ...modelInfo, ...modelDefaults } : modelInfo
|
|
return { id: modelId, info: mergedInfo }
|
|
}
|
|
|
|
// Return the requested model ID even if not found, with fallback info.
|
|
const fallbackInfo = {
|
|
maxTokens: 16_384,
|
|
contextWindow: 262_144,
|
|
supportsImages: false,
|
|
supportsReasoningEffort: false,
|
|
supportsPromptCache: true,
|
|
supportsNativeTools: false,
|
|
inputPrice: 0,
|
|
outputPrice: 0,
|
|
isFree: false,
|
|
}
|
|
|
|
return {
|
|
id: modelId,
|
|
info: fallbackInfo,
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generate an image using Roo Code Cloud's image generation API
|
|
* @param prompt The text prompt for image generation
|
|
* @param model The model to use for generation
|
|
* @param inputImage Optional base64 encoded input image data URL
|
|
* @param apiMethod The API method to use (chat_completions or images_api)
|
|
* @returns The generated image data and format, or an error
|
|
*/
|
|
async generateImage(
|
|
prompt: string,
|
|
model: string,
|
|
inputImage?: string,
|
|
apiMethod?: ImageGenerationApiMethod,
|
|
): Promise<ImageGenerationResult> {
|
|
const sessionToken = getSessionToken()
|
|
|
|
if (!sessionToken || sessionToken === "unauthenticated") {
|
|
return {
|
|
success: false,
|
|
error: t("tools:generateImage.roo.authRequired"),
|
|
}
|
|
}
|
|
|
|
const baseURL = `${this.fetcherBaseURL}/v1`
|
|
|
|
// Use the specified API method, defaulting to chat_completions for backward compatibility
|
|
if (apiMethod === "images_api") {
|
|
return generateImageWithImagesApi({
|
|
baseURL,
|
|
authToken: sessionToken,
|
|
model,
|
|
prompt,
|
|
inputImage,
|
|
outputFormat: "png",
|
|
})
|
|
}
|
|
|
|
// Default to chat completions approach
|
|
return generateImageWithProvider({
|
|
baseURL,
|
|
authToken: sessionToken,
|
|
model,
|
|
prompt,
|
|
inputImage,
|
|
})
|
|
}
|
|
}
|