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

This commit is contained in:
Daniel 2025-12-04 15:53:10 -05:00 committed by GitHub
parent 63f6fecb1a
commit ae655c5d29
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 85 additions and 8 deletions

View file

@ -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,

View file

@ -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,

View file

@ -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")
})
})

View file

@ -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, "_")
}