Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
174 changes: 174 additions & 0 deletions src/api/providers/__tests__/deepseek.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,180 @@ describe("DeepSeekHandler", () => {
expect(usageChunks[0].cacheWriteTokens).toBe(8)
expect(usageChunks[0].cacheReadTokens).toBe(2)
})

it("should sanitize unwanted '极速模式' characters from response", async () => {
// Mock a response with unwanted characters
mockCreate.mockImplementationOnce(async (options) => {
if (!options.stream) {
return {
id: "test-completion",
choices: [
{
message: {
role: "assistant",
content: "Test response with 极速模式 unwanted characters",
refusal: null,
},
finish_reason: "stop",
index: 0,
},
],
usage: {
prompt_tokens: 10,
completion_tokens: 5,
total_tokens: 15,
},
}
}

// Return async iterator for streaming with unwanted characters
return {
[Symbol.asyncIterator]: async function* () {
yield {
choices: [
{
delta: {
content: "Here is 极速模式 some text with 极 unwanted 速 characters 模式",
},
index: 0,
},
],
usage: null,
}
yield {
choices: [
{
delta: {},
index: 0,
},
],
usage: {
prompt_tokens: 10,
completion_tokens: 5,
total_tokens: 15,
},
}
},
}
})

const stream = handler.createMessage(systemPrompt, messages)
const chunks: any[] = []
for await (const chunk of stream) {
chunks.push(chunk)
}

const textChunks = chunks.filter((chunk) => chunk.type === "text")
expect(textChunks).toHaveLength(1)
// The unwanted characters should be removed
expect(textChunks[0].text).toBe("Here is some text with unwanted characters")
expect(textChunks[0].text).not.toContain("极速模式")
expect(textChunks[0].text).not.toContain("极")
expect(textChunks[0].text).not.toContain("速")
expect(textChunks[0].text).not.toContain("模")
expect(textChunks[0].text).not.toContain("式")
})

it("should preserve legitimate Chinese text while removing artifacts", async () => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding more edge case tests:

  • Mixed English/Chinese content with legitimate uses of these characters
  • Performance impact with very large responses
  • Edge cases like "模式" at the beginning or end of strings
  • Test the space-based removal patterns with legitimate Chinese text

// Mock a response with both legitimate Chinese text and unwanted artifacts
mockCreate.mockImplementationOnce(async (options) => {
// Return async iterator for streaming
return {
[Symbol.asyncIterator]: async function* () {
yield {
choices: [
{
delta: {
content: "这是正常的中文文本极速模式,不应该被删除。File path: 极 test.txt",
},
index: 0,
},
],
usage: null,
}
yield {
choices: [
{
delta: {},
index: 0,
},
],
usage: {
prompt_tokens: 10,
completion_tokens: 5,
total_tokens: 15,
},
}
},
}
})

const stream = handler.createMessage(systemPrompt, messages)
const chunks: any[] = []
for await (const chunk of stream) {
chunks.push(chunk)
}

const textChunks = chunks.filter((chunk) => chunk.type === "text")
expect(textChunks).toHaveLength(1)
// Should remove "极速模式" phrase and isolated "极" between spaces
expect(textChunks[0].text).toBe("这是正常的中文文本,不应该被删除。File path: test.txt")
expect(textChunks[0].text).toContain("这是正常的中文文本")
expect(textChunks[0].text).not.toContain("极速模式")
// The isolated "极" between spaces should be removed
expect(textChunks[0].text).not.toContain(" 极 ")
})

it("should handle reasoning content with unwanted characters", async () => {
// Mock a response with reasoning content containing unwanted characters
mockCreate.mockImplementationOnce(async (options) => {
return {
[Symbol.asyncIterator]: async function* () {
yield {
choices: [
{
delta: {
content: "<think>Reasoning with 极速模式 artifacts</think>Regular text",
},
index: 0,
},
],
usage: null,
}
yield {
choices: [
{
delta: {},
index: 0,
},
],
usage: {
prompt_tokens: 10,
completion_tokens: 5,
total_tokens: 15,
},
}
},
}
})

const stream = handler.createMessage(systemPrompt, messages)
const chunks: any[] = []
for await (const chunk of stream) {
chunks.push(chunk)
}

// Check both reasoning and text chunks
const reasoningChunks = chunks.filter((chunk) => chunk.type === "reasoning")
const textChunks = chunks.filter((chunk) => chunk.type === "text")

if (reasoningChunks.length > 0) {
expect(reasoningChunks[0].text).not.toContain("极速模式")
}
if (textChunks.length > 0) {
expect(textChunks[0].text).not.toContain("极速模式")
}
})
})

describe("processUsageMetrics", () => {
Expand Down
58 changes: 57 additions & 1 deletion src/api/providers/deepseek.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { deepSeekModels, deepSeekDefaultModelId } from "@roo-code/types"
import { Anthropic } from "@anthropic-ai/sdk"

import type { ApiHandlerOptions } from "../../shared/api"

import type { ApiStreamUsageChunk } from "../transform/stream"
import type { ApiStreamUsageChunk, ApiStream } from "../transform/stream"
import { getModelParams } from "../transform/model-params"
import type { ApiHandlerCreateMessageMetadata } from "../index"

import { OpenAiHandler } from "./openai"

Expand All @@ -26,6 +28,60 @@ export class DeepSeekHandler extends OpenAiHandler {
return { id, info, ...params }
}

override async *createMessage(
systemPrompt: string,
messages: Anthropic.Messages.MessageParam[],
metadata?: ApiHandlerCreateMessageMetadata,
): ApiStream {
// Get the stream from the parent class
const stream = super.createMessage(systemPrompt, messages, metadata)

// Process each chunk to remove unwanted characters
for await (const chunk of stream) {
if (chunk.type === "text" && chunk.text) {
// Sanitize the text content
chunk.text = this.sanitizeContent(chunk.text)
} else if (chunk.type === "reasoning" && chunk.text) {
// Also sanitize reasoning content
chunk.text = this.sanitizeContent(chunk.text)
}
yield chunk
}
}

/**
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be helpful to add a link to issue #7382 here and explain why these specific characters appear? Is this a known DeepSeek V3.1 bug or a configuration issue?

* Removes unwanted "极速模式" (speed mode) characters from the content.
* These characters appear to be injected by some DeepSeek V3.1 configurations,
* possibly from a Chinese language interface or prompt template.
* The sanitization preserves legitimate Chinese text while removing these artifacts.
*/
private sanitizeContent(content: string): string {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Performance consideration: We're applying 10 regex replacements sequentially on every chunk. For large responses, this could impact performance. Would it make sense to combine some patterns or use a single pass approach?

// First, try to remove the complete phrase "极速模式"
let sanitized = content.replace(/极速模式/g, "")

// Remove partial sequences like "模式" that might remain
sanitized = sanitized.replace(/模式(?![一-龿])/g, "")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The regex on line 63 (/模式(?![一-龿])/g) removes all occurrences of '模式' at the end of a string—even if part of a legitimate phrase (e.g. '常规模式'). Consider adding a negative lookbehind (similar to the other patterns) so that valid Chinese words aren’t unintentionally truncated.

Suggested change
sanitized = sanitized.replace(/(?![-龿])/g, "")
sanitized = sanitized.replace(/(?<![-龿])(?![-龿])/g, "")

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with ellipsis-dev bot here - this pattern will incorrectly remove '模式' from legitimate Chinese phrases. Should we add a negative lookbehind like the other patterns?


// Remove isolated occurrences of these characters when they appear
// between non-Chinese characters or at boundaries
// Using more specific patterns to avoid removing legitimate Chinese text
sanitized = sanitized.replace(/(?<![一-龿])极(?![一-龿])/g, "")
sanitized = sanitized.replace(/(?<![一-龿])速(?![一-龿])/g, "")
sanitized = sanitized.replace(/(?<![一-龿])模(?![一-龿])/g, "")
sanitized = sanitized.replace(/(?<![一-龿])式(?![一-龿])/g, "")

// Handle cases where these characters appear with spaces
sanitized = sanitized.replace(/\s+极\s*/g, " ")
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These space-based patterns might be too aggressive. They'll remove legitimate Chinese words when preceded by a space. For example, "这是 极好的" (This is excellent) would become "这是 好的". Could we make these patterns more specific to only target the artifacts?

sanitized = sanitized.replace(/\s+速\s*/g, " ")
sanitized = sanitized.replace(/\s+模\s*/g, " ")
sanitized = sanitized.replace(/\s+式\s*/g, " ")

// Clean up any resulting multiple spaces
sanitized = sanitized.replace(/\s+/g, " ").trim()

return sanitized
}

// Override to handle DeepSeek's usage metrics, including caching.
protected override processUsageMetrics(usage: any): ApiStreamUsageChunk {
return {
Expand Down
Loading