mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-10-25 05:53:57 +00:00
feat: add DeepSeek V4 / R1 compatibility and enhance OpenAiHandler for reasoning details
This commit is contained in:
parent
aec3f1184c
commit
9dfc320bf0
3 changed files with 146 additions and 13 deletions
|
|
@ -1139,6 +1139,79 @@ describe("OpenAiHandler", () => {
|
|||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("DeepSeek V4 / R1 Compatibility", () => {
|
||||
it("should inject extra_body and reasoning_effort for DeepSeek R1 format", async () => {
|
||||
const r1Options: ApiHandlerOptions = {
|
||||
...mockOptions,
|
||||
openAiR1FormatEnabled: true,
|
||||
openAiModelId: "deepseek-reasoner",
|
||||
openAiBaseUrl: "https://api.deepseek.com/v1",
|
||||
openAiCustomModelInfo: {
|
||||
contextWindow: 64_000,
|
||||
supportsReasoningEffort: true,
|
||||
reasoningEffort: "xhigh",
|
||||
supportsPromptCache: false,
|
||||
},
|
||||
}
|
||||
const r1Handler = new OpenAiHandler(r1Options)
|
||||
const stream = r1Handler.createMessage("system", [{ role: "user", content: "test" }])
|
||||
|
||||
for await (const _ of stream) {
|
||||
}
|
||||
|
||||
expect(mockCreate).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
reasoning_effort: "max", // xhigh -> max for deepseek
|
||||
extra_body: {
|
||||
thinking: { type: "enabled" },
|
||||
},
|
||||
temperature: undefined, // disabled for thinking mode
|
||||
}),
|
||||
expect.any(Object),
|
||||
)
|
||||
|
||||
// T005 Verify preserveReasoning is enabled
|
||||
const model = r1Handler.getModel()
|
||||
expect(model.info.preserveReasoning).toBe(true)
|
||||
})
|
||||
|
||||
it("should NOT inject R1 parameters for standard models even if model ID contains deepseek but R1 format is disabled", async () => {
|
||||
const standardOptions: ApiHandlerOptions = {
|
||||
...mockOptions,
|
||||
openAiR1FormatEnabled: false,
|
||||
openAiModelId: "deepseek-chat", // standard model
|
||||
}
|
||||
const standardHandler = new OpenAiHandler(standardOptions)
|
||||
const stream = standardHandler.createMessage("system", [{ role: "user", content: "test" }])
|
||||
|
||||
for await (const _ of stream) {
|
||||
}
|
||||
|
||||
const callArgs = mockCreate.mock.calls[0][0]
|
||||
expect(callArgs).not.toHaveProperty("extra_body")
|
||||
expect(callArgs.temperature).toBeDefined()
|
||||
})
|
||||
|
||||
it("should use standard OpenAI protocol when R1 format is disabled", async () => {
|
||||
const standardOptions: ApiHandlerOptions = {
|
||||
...mockOptions,
|
||||
openAiR1FormatEnabled: false,
|
||||
openAiModelId: "gpt-4o",
|
||||
}
|
||||
const standardHandler = new OpenAiHandler(standardOptions)
|
||||
const stream = standardHandler.createMessage("system", [{ role: "user", content: "test" }])
|
||||
|
||||
for await (const _ of stream) {
|
||||
}
|
||||
|
||||
const callArgs = mockCreate.mock.calls[0][0]
|
||||
expect(callArgs).not.toHaveProperty("extra_body")
|
||||
expect(callArgs).not.toHaveProperty("reasoning_effort")
|
||||
// Standard model should have temperature 0 by default
|
||||
expect(callArgs.temperature).toBe(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("getOpenAiModels", () => {
|
||||
|
|
|
|||
|
|
@ -105,7 +105,9 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
|
|||
let convertedMessages
|
||||
|
||||
if (deepseekReasoner) {
|
||||
convertedMessages = convertToR1Format([{ role: "user", content: systemPrompt }, ...messages])
|
||||
convertedMessages = convertToR1Format([{ role: "user", content: systemPrompt }, ...messages], {
|
||||
mergeToolResultText: true,
|
||||
})
|
||||
} else {
|
||||
if (modelInfo.supportsPromptCache) {
|
||||
systemMessage = {
|
||||
|
|
@ -152,13 +154,32 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
|
|||
|
||||
const isGrokXAI = this._isGrokXAI(this.options.openAiBaseUrl)
|
||||
|
||||
const requestOptions: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = {
|
||||
const isDeepSeekV4 =
|
||||
deepseekReasoner &&
|
||||
(this.options.openAiBaseUrl?.includes("deepseek.com") || modelId.includes("deepseek"))
|
||||
|
||||
const requestOptions: any = {
|
||||
model: modelId,
|
||||
temperature: this.options.modelTemperature ?? (deepseekReasoner ? DEEP_SEEK_DEFAULT_TEMPERATURE : 0),
|
||||
temperature: deepseekReasoner
|
||||
? undefined
|
||||
: (this.options.modelTemperature ?? (deepseekReasoner ? DEEP_SEEK_DEFAULT_TEMPERATURE : 0)),
|
||||
messages: convertedMessages,
|
||||
stream: true as const,
|
||||
...(isGrokXAI ? {} : { stream_options: { include_usage: true } }),
|
||||
...(reasoning && reasoning),
|
||||
...(deepseekReasoner && {
|
||||
reasoning_effort: isDeepSeekV4
|
||||
? modelInfo.reasoningEffort === "xhigh"
|
||||
? "max"
|
||||
: "high"
|
||||
: (modelInfo.reasoningEffort as any),
|
||||
extra_body: {
|
||||
thinking: { type: "enabled" },
|
||||
},
|
||||
top_p: undefined,
|
||||
presence_penalty: undefined,
|
||||
frequency_penalty: undefined,
|
||||
}),
|
||||
tools: this.convertToolsForOpenAI(metadata?.tools),
|
||||
tool_choice: metadata?.tool_choice,
|
||||
parallel_tool_calls: metadata?.parallelToolCalls ?? true,
|
||||
|
|
@ -169,10 +190,10 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
|
|||
|
||||
let stream
|
||||
try {
|
||||
stream = await this.client.chat.completions.create(
|
||||
stream = (await this.client.chat.completions.create(
|
||||
requestOptions,
|
||||
isAzureAiInference ? { path: OPENAI_AZURE_AI_INFERENCE_PATH } : {},
|
||||
)
|
||||
)) as any
|
||||
} catch (error) {
|
||||
throw handleOpenAIError(error, this.providerName)
|
||||
}
|
||||
|
|
@ -221,15 +242,35 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
|
|||
yield this.processUsageMetrics(lastUsage, modelInfo)
|
||||
}
|
||||
} else {
|
||||
const requestOptions: OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming = {
|
||||
const isDeepSeekV4 =
|
||||
deepseekReasoner &&
|
||||
(this.options.openAiBaseUrl?.includes("deepseek.com") || modelId.includes("deepseek"))
|
||||
|
||||
const requestOptions: any = {
|
||||
model: modelId,
|
||||
messages: deepseekReasoner
|
||||
? convertToR1Format([{ role: "user", content: systemPrompt }, ...messages])
|
||||
? convertToR1Format([{ role: "user", content: systemPrompt }, ...messages], {
|
||||
mergeToolResultText: true,
|
||||
})
|
||||
: [systemMessage, ...convertToOpenAiMessages(messages)],
|
||||
// Tools are always present (minimum ALWAYS_AVAILABLE_TOOLS)
|
||||
tools: this.convertToolsForOpenAI(metadata?.tools),
|
||||
tool_choice: metadata?.tool_choice,
|
||||
parallel_tool_calls: metadata?.parallelToolCalls ?? true,
|
||||
...(deepseekReasoner && {
|
||||
reasoning_effort: isDeepSeekV4
|
||||
? modelInfo.reasoningEffort === "xhigh"
|
||||
? "max"
|
||||
: "high"
|
||||
: (modelInfo.reasoningEffort as any),
|
||||
extra_body: {
|
||||
thinking: { type: "enabled" },
|
||||
},
|
||||
temperature: undefined,
|
||||
top_p: undefined,
|
||||
presence_penalty: undefined,
|
||||
frequency_penalty: undefined,
|
||||
}),
|
||||
}
|
||||
|
||||
// Add max_tokens if needed
|
||||
|
|
@ -237,10 +278,10 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
|
|||
|
||||
let response
|
||||
try {
|
||||
response = await this.client.chat.completions.create(
|
||||
response = (await this.client.chat.completions.create(
|
||||
requestOptions,
|
||||
this._isAzureAiInference(modelUrl) ? { path: OPENAI_AZURE_AI_INFERENCE_PATH } : {},
|
||||
)
|
||||
)) as any
|
||||
} catch (error) {
|
||||
throw handleOpenAIError(error, this.providerName)
|
||||
}
|
||||
|
|
@ -289,7 +330,18 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
|
|||
settings: this.options,
|
||||
defaultTemperature: 0,
|
||||
})
|
||||
return { id, info, ...params }
|
||||
|
||||
const enabledR1Format = this.options.openAiR1FormatEnabled ?? false
|
||||
const deepseekReasoner = id.includes("deepseek-reasoner") || enabledR1Format
|
||||
|
||||
return {
|
||||
id,
|
||||
info: {
|
||||
...info,
|
||||
...(deepseekReasoner && { preserveReasoning: true }),
|
||||
},
|
||||
...params,
|
||||
}
|
||||
}
|
||||
|
||||
async completePrompt(prompt: string): Promise<string> {
|
||||
|
|
|
|||
|
|
@ -4498,8 +4498,11 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
|
|||
const [first, ...rest] = contentArray
|
||||
|
||||
// Check if this message has reasoning_details (OpenRouter format for Gemini 3, etc.)
|
||||
const msgWithDetails = msg
|
||||
if (msgWithDetails.reasoning_details && Array.isArray(msgWithDetails.reasoning_details)) {
|
||||
const msgWithDetails = msg as any
|
||||
if (
|
||||
(msgWithDetails.reasoning_details && Array.isArray(msgWithDetails.reasoning_details)) ||
|
||||
msgWithDetails.reasoning_content
|
||||
) {
|
||||
// Build the assistant message with reasoning_details
|
||||
let assistantContent: Anthropic.Messages.MessageParam["content"]
|
||||
|
||||
|
|
@ -4515,7 +4518,12 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
|
|||
cleanConversationHistory.push({
|
||||
role: "assistant",
|
||||
content: assistantContent,
|
||||
reasoning_details: msgWithDetails.reasoning_details,
|
||||
...(msgWithDetails.reasoning_details && {
|
||||
reasoning_details: msgWithDetails.reasoning_details,
|
||||
}),
|
||||
...(msgWithDetails.reasoning_content && {
|
||||
reasoning_content: msgWithDetails.reasoning_content,
|
||||
}),
|
||||
} as any)
|
||||
|
||||
continue
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue