Browse Source

fix: sanitize reasoning_details IDs to remove invalid characters (#9839)

Daniel 1 month ago
parent
commit
ae655c5d29

+ 5 - 4
src/api/providers/openrouter.ts

@@ -29,6 +29,7 @@ import { BaseProvider } from "./base-provider"
 import type { ApiHandlerCreateMessageMetadata, SingleCompletionHandler } from "../index"
 import { handleOpenAIError } from "./utils/openai-error-handler"
 import { generateImageWithProvider, ImageGenerationResult } from "./utils/image-generation"
+import { sanitizeReasoningDetailId } from "./utils/sanitize-reasoning-id"
 
 // Add custom interface for OpenRouter params.
 type OpenRouterChatCompletionParams = OpenAI.Chat.ChatCompletionCreateParams & {
@@ -286,18 +287,18 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
 							if (detail.data !== undefined) {
 								existing.data = (existing.data || "") + detail.data
 							}
-							// Update other fields if provided
-							if (detail.id !== undefined) existing.id = detail.id
+							// Update other fields if provided - sanitize ID to remove invalid characters
+							if (detail.id !== undefined) existing.id = sanitizeReasoningDetailId(detail.id)
 							if (detail.format !== undefined) existing.format = detail.format
 							if (detail.signature !== undefined) existing.signature = detail.signature
 						} else {
-							// Start new reasoning detail accumulation
+							// Start new reasoning detail accumulation - sanitize ID to remove invalid characters
 							reasoningDetailsAccumulator.set(key, {
 								type: detail.type,
 								text: detail.text,
 								summary: detail.summary,
 								data: detail.data,
-								id: detail.id,
+								id: sanitizeReasoningDetailId(detail.id),
 								format: detail.format,
 								signature: detail.signature,
 								index,

+ 5 - 4
src/api/providers/roo.ts

@@ -19,6 +19,7 @@ import { MODEL_DEFAULTS } from "../providers/fetchers/roo"
 import { handleOpenAIError } from "./utils/openai-error-handler"
 import { generateImageWithProvider, generateImageWithImagesApi, ImageGenerationResult } from "./utils/image-generation"
 import { t } from "../../i18n"
+import { sanitizeReasoningDetailId } from "./utils/sanitize-reasoning-id"
 
 // Extend OpenAI's CompletionUsage to include Roo specific fields
 interface RooUsage extends OpenAI.CompletionUsage {
@@ -193,18 +194,18 @@ export class RooHandler extends BaseOpenAiCompatibleProvider<string> {
 								if (detail.data !== undefined) {
 									existing.data = (existing.data || "") + detail.data
 								}
-								// Update other fields if provided
-								if (detail.id !== undefined) existing.id = detail.id
+								// Update other fields if provided - sanitize ID to remove invalid characters
+								if (detail.id !== undefined) existing.id = sanitizeReasoningDetailId(detail.id)
 								if (detail.format !== undefined) existing.format = detail.format
 								if (detail.signature !== undefined) existing.signature = detail.signature
 							} else {
-								// Start new reasoning detail accumulation
+								// Start new reasoning detail accumulation - sanitize ID to remove invalid characters
 								reasoningDetailsAccumulator.set(key, {
 									type: detail.type,
 									text: detail.text,
 									summary: detail.summary,
 									data: detail.data,
-									id: detail.id,
+									id: sanitizeReasoningDetailId(detail.id),
 									format: detail.format,
 									signature: detail.signature,
 									index,

+ 59 - 0
src/api/providers/utils/__tests__/sanitize-reasoning-id.spec.ts

@@ -0,0 +1,59 @@
+import { sanitizeReasoningDetailId } from "../sanitize-reasoning-id"
+
+describe("sanitizeReasoningDetailId", () => {
+	it("should return null for null input", () => {
+		expect(sanitizeReasoningDetailId(null)).toBeNull()
+	})
+
+	it("should return undefined for undefined input", () => {
+		expect(sanitizeReasoningDetailId(undefined)).toBeUndefined()
+	})
+
+	it("should return empty string for empty string input", () => {
+		expect(sanitizeReasoningDetailId("")).toBe("")
+	})
+
+	it("should not modify IDs with only valid characters", () => {
+		expect(sanitizeReasoningDetailId("abc123")).toBe("abc123")
+		expect(sanitizeReasoningDetailId("test_id")).toBe("test_id")
+		expect(sanitizeReasoningDetailId("test-id")).toBe("test-id")
+		expect(sanitizeReasoningDetailId("ABC_123-test")).toBe("ABC_123-test")
+	})
+
+	it("should replace colons with underscores", () => {
+		expect(sanitizeReasoningDetailId("rs_033ca40017d1ad93016931b1d2bf7481a2969fd5c1835cb1d3:4")).toBe(
+			"rs_033ca40017d1ad93016931b1d2bf7481a2969fd5c1835cb1d3_4",
+		)
+	})
+
+	it("should replace multiple invalid characters", () => {
+		expect(sanitizeReasoningDetailId("test:1:2:3")).toBe("test_1_2_3")
+	})
+
+	it("should replace other special characters with underscores", () => {
+		expect(sanitizeReasoningDetailId("test@id")).toBe("test_id")
+		expect(sanitizeReasoningDetailId("test.id")).toBe("test_id")
+		expect(sanitizeReasoningDetailId("test#id")).toBe("test_id")
+		expect(sanitizeReasoningDetailId("test$id")).toBe("test_id")
+		expect(sanitizeReasoningDetailId("test%id")).toBe("test_id")
+		expect(sanitizeReasoningDetailId("test^id")).toBe("test_id")
+		expect(sanitizeReasoningDetailId("test&id")).toBe("test_id")
+		expect(sanitizeReasoningDetailId("test*id")).toBe("test_id")
+		expect(sanitizeReasoningDetailId("test+id")).toBe("test_id")
+		expect(sanitizeReasoningDetailId("test=id")).toBe("test_id")
+		expect(sanitizeReasoningDetailId("test id")).toBe("test_id")
+	})
+
+	it("should handle mixed valid and invalid characters", () => {
+		expect(sanitizeReasoningDetailId("rs_abc:1@2#3")).toBe("rs_abc_1_2_3")
+	})
+
+	it("should handle IDs starting with valid characters followed by invalid ones", () => {
+		expect(sanitizeReasoningDetailId("valid_start:invalid")).toBe("valid_start_invalid")
+	})
+
+	it("should handle consecutive invalid characters", () => {
+		expect(sanitizeReasoningDetailId("test::id")).toBe("test__id")
+		expect(sanitizeReasoningDetailId("test:::id")).toBe("test___id")
+	})
+})

+ 16 - 0
src/api/providers/utils/sanitize-reasoning-id.ts

@@ -0,0 +1,16 @@
+/**
+ * Sanitizes reasoning detail IDs to only contain allowed characters.
+ * The OpenAI Responses API only allows IDs containing letters, numbers, underscores, or dashes.
+ * This function replaces any invalid characters (like colons from IDs like "rs_xxx:4") with underscores.
+ *
+ * @param id - The original ID that may contain invalid characters
+ * @returns The sanitized ID with only allowed characters, or undefined if input is undefined/null
+ */
+export function sanitizeReasoningDetailId(id: string | null | undefined): string | null | undefined {
+	if (id === null || id === undefined) {
+		return id
+	}
+
+	// Replace any character that is not a letter, number, underscore, or dash with an underscore
+	return id.replace(/[^a-zA-Z0-9_-]/g, "_")
+}