Skip to content
Merged
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
9 changes: 5 additions & 4 deletions src/api/providers/openrouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 & {
Expand Down Expand Up @@ -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,
Expand Down
9 changes: 5 additions & 4 deletions src/api/providers/roo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
Expand Down
59 changes: 59 additions & 0 deletions src/api/providers/utils/__tests__/sanitize-reasoning-id.spec.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 16 additions & 0 deletions src/api/providers/utils/sanitize-reasoning-id.ts
Original file line number Diff line number Diff line change
@@ -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, "_")
}
Loading