Skip to content

Commit fc48b10

Browse files
committed
fix: sanitize reasoning_details IDs to remove invalid characters
The OpenAI Responses API rejects IDs containing characters other than letters, numbers, underscores, or dashes. Some reasoning_details from providers (like Gemini) include colons in their IDs (e.g., rs_xxx:4). This fix: - Creates a sanitizeReasoningDetailId() utility function - Applies sanitization in both RooHandler and OpenRouterHandler - Adds comprehensive tests for the sanitization logic Fixes 400 error: Invalid 'input[x].id': Expected an ID that contains letters, numbers, underscores, or dashes
1 parent 29385e0 commit fc48b10

File tree

4 files changed

+85
-8
lines changed

4 files changed

+85
-8
lines changed

src/api/providers/openrouter.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import { BaseProvider } from "./base-provider"
2929
import type { ApiHandlerCreateMessageMetadata, SingleCompletionHandler } from "../index"
3030
import { handleOpenAIError } from "./utils/openai-error-handler"
3131
import { generateImageWithProvider, ImageGenerationResult } from "./utils/image-generation"
32+
import { sanitizeReasoningDetailId } from "./utils/sanitize-reasoning-id"
3233

3334
// Add custom interface for OpenRouter params.
3435
type OpenRouterChatCompletionParams = OpenAI.Chat.ChatCompletionCreateParams & {
@@ -286,18 +287,18 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
286287
if (detail.data !== undefined) {
287288
existing.data = (existing.data || "") + detail.data
288289
}
289-
// Update other fields if provided
290-
if (detail.id !== undefined) existing.id = detail.id
290+
// Update other fields if provided - sanitize ID to remove invalid characters
291+
if (detail.id !== undefined) existing.id = sanitizeReasoningDetailId(detail.id)
291292
if (detail.format !== undefined) existing.format = detail.format
292293
if (detail.signature !== undefined) existing.signature = detail.signature
293294
} else {
294-
// Start new reasoning detail accumulation
295+
// Start new reasoning detail accumulation - sanitize ID to remove invalid characters
295296
reasoningDetailsAccumulator.set(key, {
296297
type: detail.type,
297298
text: detail.text,
298299
summary: detail.summary,
299300
data: detail.data,
300-
id: detail.id,
301+
id: sanitizeReasoningDetailId(detail.id),
301302
format: detail.format,
302303
signature: detail.signature,
303304
index,

src/api/providers/roo.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { MODEL_DEFAULTS } from "../providers/fetchers/roo"
1919
import { handleOpenAIError } from "./utils/openai-error-handler"
2020
import { generateImageWithProvider, generateImageWithImagesApi, ImageGenerationResult } from "./utils/image-generation"
2121
import { t } from "../../i18n"
22+
import { sanitizeReasoningDetailId } from "./utils/sanitize-reasoning-id"
2223

2324
// Extend OpenAI's CompletionUsage to include Roo specific fields
2425
interface RooUsage extends OpenAI.CompletionUsage {
@@ -193,18 +194,18 @@ export class RooHandler extends BaseOpenAiCompatibleProvider<string> {
193194
if (detail.data !== undefined) {
194195
existing.data = (existing.data || "") + detail.data
195196
}
196-
// Update other fields if provided
197-
if (detail.id !== undefined) existing.id = detail.id
197+
// Update other fields if provided - sanitize ID to remove invalid characters
198+
if (detail.id !== undefined) existing.id = sanitizeReasoningDetailId(detail.id)
198199
if (detail.format !== undefined) existing.format = detail.format
199200
if (detail.signature !== undefined) existing.signature = detail.signature
200201
} else {
201-
// Start new reasoning detail accumulation
202+
// Start new reasoning detail accumulation - sanitize ID to remove invalid characters
202203
reasoningDetailsAccumulator.set(key, {
203204
type: detail.type,
204205
text: detail.text,
205206
summary: detail.summary,
206207
data: detail.data,
207-
id: detail.id,
208+
id: sanitizeReasoningDetailId(detail.id),
208209
format: detail.format,
209210
signature: detail.signature,
210211
index,
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { sanitizeReasoningDetailId } from "../sanitize-reasoning-id"
2+
3+
describe("sanitizeReasoningDetailId", () => {
4+
it("should return null for null input", () => {
5+
expect(sanitizeReasoningDetailId(null)).toBeNull()
6+
})
7+
8+
it("should return undefined for undefined input", () => {
9+
expect(sanitizeReasoningDetailId(undefined)).toBeUndefined()
10+
})
11+
12+
it("should return empty string for empty string input", () => {
13+
expect(sanitizeReasoningDetailId("")).toBe("")
14+
})
15+
16+
it("should not modify IDs with only valid characters", () => {
17+
expect(sanitizeReasoningDetailId("abc123")).toBe("abc123")
18+
expect(sanitizeReasoningDetailId("test_id")).toBe("test_id")
19+
expect(sanitizeReasoningDetailId("test-id")).toBe("test-id")
20+
expect(sanitizeReasoningDetailId("ABC_123-test")).toBe("ABC_123-test")
21+
})
22+
23+
it("should replace colons with underscores", () => {
24+
expect(sanitizeReasoningDetailId("rs_033ca40017d1ad93016931b1d2bf7481a2969fd5c1835cb1d3:4")).toBe(
25+
"rs_033ca40017d1ad93016931b1d2bf7481a2969fd5c1835cb1d3_4",
26+
)
27+
})
28+
29+
it("should replace multiple invalid characters", () => {
30+
expect(sanitizeReasoningDetailId("test:1:2:3")).toBe("test_1_2_3")
31+
})
32+
33+
it("should replace other special characters with underscores", () => {
34+
expect(sanitizeReasoningDetailId("test@id")).toBe("test_id")
35+
expect(sanitizeReasoningDetailId("test.id")).toBe("test_id")
36+
expect(sanitizeReasoningDetailId("test#id")).toBe("test_id")
37+
expect(sanitizeReasoningDetailId("test$id")).toBe("test_id")
38+
expect(sanitizeReasoningDetailId("test%id")).toBe("test_id")
39+
expect(sanitizeReasoningDetailId("test^id")).toBe("test_id")
40+
expect(sanitizeReasoningDetailId("test&id")).toBe("test_id")
41+
expect(sanitizeReasoningDetailId("test*id")).toBe("test_id")
42+
expect(sanitizeReasoningDetailId("test+id")).toBe("test_id")
43+
expect(sanitizeReasoningDetailId("test=id")).toBe("test_id")
44+
expect(sanitizeReasoningDetailId("test id")).toBe("test_id")
45+
})
46+
47+
it("should handle mixed valid and invalid characters", () => {
48+
expect(sanitizeReasoningDetailId("rs_abc:1@2#3")).toBe("rs_abc_1_2_3")
49+
})
50+
51+
it("should handle IDs starting with valid characters followed by invalid ones", () => {
52+
expect(sanitizeReasoningDetailId("valid_start:invalid")).toBe("valid_start_invalid")
53+
})
54+
55+
it("should handle consecutive invalid characters", () => {
56+
expect(sanitizeReasoningDetailId("test::id")).toBe("test__id")
57+
expect(sanitizeReasoningDetailId("test:::id")).toBe("test___id")
58+
})
59+
})
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/**
2+
* Sanitizes reasoning detail IDs to only contain allowed characters.
3+
* The OpenAI Responses API only allows IDs containing letters, numbers, underscores, or dashes.
4+
* This function replaces any invalid characters (like colons from IDs like "rs_xxx:4") with underscores.
5+
*
6+
* @param id - The original ID that may contain invalid characters
7+
* @returns The sanitized ID with only allowed characters, or undefined if input is undefined/null
8+
*/
9+
export function sanitizeReasoningDetailId(id: string | null | undefined): string | null | undefined {
10+
if (id === null || id === undefined) {
11+
return id
12+
}
13+
14+
// Replace any character that is not a letter, number, underscore, or dash with an underscore
15+
return id.replace(/[^a-zA-Z0-9_-]/g, "_")
16+
}

0 commit comments

Comments
 (0)