mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-15 23:31:04 +00:00
233 lines
7.3 KiB
TypeScript
233 lines
7.3 KiB
TypeScript
import { Anthropic } from "@anthropic-ai/sdk"
|
|
import OpenAI from "openai"
|
|
|
|
import {
|
|
type ModelInfo,
|
|
requestyDefaultModelId,
|
|
requestyDefaultModelInfo,
|
|
TOOL_PROTOCOL,
|
|
NATIVE_TOOL_DEFAULTS,
|
|
} from "@roo-code/types"
|
|
|
|
import type { ApiHandlerOptions, ModelRecord } from "../../shared/api"
|
|
import { resolveToolProtocol } from "../../utils/resolveToolProtocol"
|
|
import { calculateApiCostOpenAI } from "../../shared/cost"
|
|
|
|
import { convertToOpenAiMessages } from "../transform/openai-format"
|
|
import { ApiStream, ApiStreamUsageChunk } from "../transform/stream"
|
|
import { getModelParams } from "../transform/model-params"
|
|
import { AnthropicReasoningParams } from "../transform/reasoning"
|
|
|
|
import { DEFAULT_HEADERS } from "./constants"
|
|
import { getModels } from "./fetchers/modelCache"
|
|
import { BaseProvider } from "./base-provider"
|
|
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
|
|
import { toRequestyServiceUrl } from "../../shared/utils/requesty"
|
|
import { handleOpenAIError } from "./utils/openai-error-handler"
|
|
import { applyRouterToolPreferences } from "./utils/router-tool-preferences"
|
|
|
|
// Requesty usage includes an extra field for Anthropic use cases.
|
|
// Safely cast the prompt token details section to the appropriate structure.
|
|
interface RequestyUsage extends OpenAI.CompletionUsage {
|
|
prompt_tokens_details?: {
|
|
caching_tokens?: number
|
|
cached_tokens?: number
|
|
}
|
|
total_cost?: number
|
|
}
|
|
|
|
type RequestyChatCompletionParamsStreaming = OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming & {
|
|
requesty?: {
|
|
trace_id?: string
|
|
extra?: {
|
|
mode?: string
|
|
}
|
|
}
|
|
thinking?: AnthropicReasoningParams
|
|
}
|
|
|
|
type RequestyChatCompletionParams = OpenAI.Chat.ChatCompletionCreateParams & {
|
|
requesty?: {
|
|
trace_id?: string
|
|
extra?: {
|
|
mode?: string
|
|
}
|
|
}
|
|
thinking?: AnthropicReasoningParams
|
|
}
|
|
|
|
export class RequestyHandler extends BaseProvider implements SingleCompletionHandler {
|
|
protected options: ApiHandlerOptions
|
|
protected models: ModelRecord = {}
|
|
private client: OpenAI
|
|
private baseURL: string
|
|
private readonly providerName = "Requesty"
|
|
|
|
constructor(options: ApiHandlerOptions) {
|
|
super()
|
|
|
|
this.options = options
|
|
this.baseURL = toRequestyServiceUrl(options.requestyBaseUrl)
|
|
|
|
const apiKey = this.options.requestyApiKey ?? "not-provided"
|
|
|
|
this.client = new OpenAI({
|
|
baseURL: this.baseURL,
|
|
apiKey: apiKey,
|
|
defaultHeaders: DEFAULT_HEADERS,
|
|
})
|
|
}
|
|
|
|
public async fetchModel() {
|
|
this.models = await getModels({ provider: "requesty", baseUrl: this.baseURL })
|
|
return this.getModel()
|
|
}
|
|
|
|
override getModel() {
|
|
const id = this.options.requestyModelId ?? requestyDefaultModelId
|
|
const cachedInfo = this.models[id] ?? requestyDefaultModelInfo
|
|
|
|
// Merge native tool defaults for cached models that may lack these fields
|
|
// The order ensures that cached values (if present) override the defaults
|
|
let info: ModelInfo = { ...NATIVE_TOOL_DEFAULTS, ...cachedInfo }
|
|
|
|
// Apply tool preferences for models accessed through routers (OpenAI, Gemini)
|
|
info = applyRouterToolPreferences(id, info)
|
|
|
|
const params = getModelParams({
|
|
format: "anthropic",
|
|
modelId: id,
|
|
model: info,
|
|
settings: this.options,
|
|
})
|
|
|
|
return { id, info, ...params }
|
|
}
|
|
|
|
protected processUsageMetrics(usage: any, modelInfo?: ModelInfo): ApiStreamUsageChunk {
|
|
const requestyUsage = usage as RequestyUsage
|
|
const inputTokens = requestyUsage?.prompt_tokens || 0
|
|
const outputTokens = requestyUsage?.completion_tokens || 0
|
|
const cacheWriteTokens = requestyUsage?.prompt_tokens_details?.caching_tokens || 0
|
|
const cacheReadTokens = requestyUsage?.prompt_tokens_details?.cached_tokens || 0
|
|
const { totalCost } = modelInfo
|
|
? calculateApiCostOpenAI(modelInfo, inputTokens, outputTokens, cacheWriteTokens, cacheReadTokens)
|
|
: { totalCost: 0 }
|
|
|
|
return {
|
|
type: "usage",
|
|
inputTokens: inputTokens,
|
|
outputTokens: outputTokens,
|
|
cacheWriteTokens: cacheWriteTokens,
|
|
cacheReadTokens: cacheReadTokens,
|
|
totalCost: totalCost,
|
|
}
|
|
}
|
|
|
|
override async *createMessage(
|
|
systemPrompt: string,
|
|
messages: Anthropic.Messages.MessageParam[],
|
|
metadata?: ApiHandlerCreateMessageMetadata,
|
|
): ApiStream {
|
|
const {
|
|
id: model,
|
|
info,
|
|
maxTokens: max_tokens,
|
|
temperature,
|
|
reasoningEffort: reasoning_effort,
|
|
reasoning: thinking,
|
|
} = await this.fetchModel()
|
|
|
|
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
|
|
{ role: "system", content: systemPrompt },
|
|
...convertToOpenAiMessages(messages, { mergeToolResultText: true }),
|
|
]
|
|
|
|
// Map extended efforts to OpenAI Chat Completions-accepted values (omit unsupported)
|
|
const allowedEffort = (["low", "medium", "high"] as const).includes(reasoning_effort as any)
|
|
? (reasoning_effort as OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming["reasoning_effort"])
|
|
: undefined
|
|
|
|
// Check if native tool protocol is enabled
|
|
// IMPORTANT: Use metadata.toolProtocol if provided (task's locked protocol) for consistency
|
|
const toolProtocol = resolveToolProtocol(this.options, info, metadata?.toolProtocol)
|
|
const useNativeTools = toolProtocol === TOOL_PROTOCOL.NATIVE
|
|
|
|
const completionParams: RequestyChatCompletionParamsStreaming = {
|
|
messages: openAiMessages,
|
|
model,
|
|
max_tokens,
|
|
temperature,
|
|
...(allowedEffort && { reasoning_effort: allowedEffort }),
|
|
...(thinking && { thinking }),
|
|
stream: true,
|
|
stream_options: { include_usage: true },
|
|
requesty: { trace_id: metadata?.taskId, extra: { mode: metadata?.mode } },
|
|
...(useNativeTools && metadata?.tools && { tools: this.convertToolsForOpenAI(metadata.tools) }),
|
|
...(useNativeTools && metadata?.tool_choice && { tool_choice: metadata.tool_choice }),
|
|
}
|
|
|
|
let stream
|
|
try {
|
|
// With streaming params type, SDK returns an async iterable stream
|
|
stream = await this.client.chat.completions.create(completionParams)
|
|
} catch (error) {
|
|
throw handleOpenAIError(error, this.providerName)
|
|
}
|
|
let lastUsage: any = undefined
|
|
|
|
for await (const chunk of stream) {
|
|
const delta = chunk.choices[0]?.delta
|
|
|
|
if (delta?.content) {
|
|
yield { type: "text", text: delta.content }
|
|
}
|
|
|
|
if (delta && "reasoning_content" in delta && delta.reasoning_content) {
|
|
yield { type: "reasoning", text: (delta.reasoning_content as string | undefined) || "" }
|
|
}
|
|
|
|
// Handle native tool calls
|
|
if (delta && "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 (chunk.usage) {
|
|
lastUsage = chunk.usage
|
|
}
|
|
}
|
|
|
|
if (lastUsage) {
|
|
yield this.processUsageMetrics(lastUsage, info)
|
|
}
|
|
}
|
|
|
|
async completePrompt(prompt: string): Promise<string> {
|
|
const { id: model, maxTokens: max_tokens, temperature } = await this.fetchModel()
|
|
|
|
let openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [{ role: "system", content: prompt }]
|
|
|
|
const completionParams: RequestyChatCompletionParams = {
|
|
model,
|
|
max_tokens,
|
|
messages: openAiMessages,
|
|
temperature: temperature,
|
|
}
|
|
|
|
let response: OpenAI.Chat.ChatCompletion
|
|
try {
|
|
response = await this.client.chat.completions.create(completionParams)
|
|
} catch (error) {
|
|
throw handleOpenAIError(error, this.providerName)
|
|
}
|
|
return response.choices[0]?.message.content || ""
|
|
}
|
|
}
|