Skip to content
This repository was archived by the owner on May 15, 2026. It is now read-only.

Commit 3f935af

Browse files
committed
refactor: remove all manual OpenAI message transforms from OpenRouter provider
- Remove buildOpenAiMessages(), extraBody.messages hack, and dummy messages - Wire reasoning_details through AI SDK natively via providerOptions.openrouter - Filter schema-invalid reasoning_details entries (malformed encrypted blocks) - Filter [REDACTED] from thinking UI stream (upstream provider behavior) - Remove unused imports: convertToOpenAiMessages, sanitizeGeminiMessages, consolidateReasoningDetails, convertToR1Format, addAnthropicCacheBreakpoints, addGeminiCacheBreakpoints - All models now use convertToAiSdkMessages() → streamText() natively - Prompt caching deferred (needs providerOptions.openrouter.cacheControl impl)
1 parent f3181ce commit 3f935af

6 files changed

Lines changed: 258 additions & 115 deletions

File tree

src/api/providers/__tests__/openrouter.spec.ts

Lines changed: 37 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1040,26 +1040,21 @@ describe("OpenRouterHandler", () => {
10401040
)
10411041
})
10421042

1043-
it("uses R1 format for DeepSeek R1 models (extraBody.messages)", async () => {
1043+
it("does not use R1 format for DeepSeek R1 models (uses standard AI SDK path)", async () => {
10441044
const handler = new OpenRouterHandler({
10451045
openRouterApiKey: "test-key",
10461046
openRouterModelId: "deepseek/deepseek-r1",
10471047
})
10481048
mockStreamResult()
10491049
await consumeGenerator(handler, "system prompt")
10501050

1051-
// R1 models should pass OpenAI messages via extraBody (including system as user message)
1052-
expect(mockCreateOpenRouter).toHaveBeenCalledWith(
1053-
expect.objectContaining({
1054-
extraBody: expect.objectContaining({
1055-
messages: expect.any(Array),
1056-
}),
1057-
}),
1058-
)
1051+
// R1 models should NOT pass extraBody.messages (R1 format conversion removed)
1052+
const providerCall = mockCreateOpenRouter.mock.calls[0][0]
1053+
expect(providerCall?.extraBody?.messages).toBeUndefined()
10591054

1060-
// System prompt should NOT be passed to streamText (it is in extraBody.messages)
1055+
// System prompt should be passed normally via streamText
10611056
const streamTextCall = mockStreamText.mock.calls[0][0]
1062-
expect(streamTextCall.system).toBeUndefined()
1057+
expect(streamTextCall.system).toBe("system prompt")
10631058
})
10641059

10651060
it("applies Anthropic beta headers for Anthropic models", async () => {
@@ -1089,22 +1084,21 @@ describe("OpenRouterHandler", () => {
10891084
expect(call.headers).toBeUndefined()
10901085
})
10911086

1092-
it("applies prompt caching for Anthropic models in caching set", async () => {
1087+
it("passes system prompt directly for Anthropic models (no caching transform)", async () => {
10931088
const handler = new OpenRouterHandler({
10941089
openRouterApiKey: "test-key",
10951090
openRouterModelId: "anthropic/claude-sonnet-4",
10961091
})
10971092
mockStreamResult()
10981093
await consumeGenerator(handler)
10991094

1100-
// Should have extraBody.messages with cache_control applied
1101-
expect(mockCreateOpenRouter).toHaveBeenCalledWith(
1102-
expect.objectContaining({
1103-
extraBody: expect.objectContaining({
1104-
messages: expect.arrayContaining([expect.objectContaining({ role: "system" })]),
1105-
}),
1106-
}),
1107-
)
1095+
// System prompt should be passed directly via streamText
1096+
const streamTextCall = mockStreamText.mock.calls[0][0]
1097+
expect(streamTextCall.system).toBe("test")
1098+
1099+
// Messages should be the converted AI SDK messages (no system-role message injected)
1100+
const systemMsgs = streamTextCall.messages.filter((m: any) => m.role === "system")
1101+
expect(systemMsgs).toHaveLength(0)
11081102
})
11091103

11101104
it("disables reasoning for Gemini 2.5 Pro when not explicitly configured", async () => {
@@ -1124,22 +1118,36 @@ describe("OpenRouterHandler", () => {
11241118
)
11251119
})
11261120

1127-
it("applies Gemini sanitization and encrypted block injection", async () => {
1121+
it("passes system prompt directly for Gemini models (no caching transform)", async () => {
11281122
const handler = new OpenRouterHandler({
11291123
openRouterApiKey: "test-key",
11301124
openRouterModelId: "google/gemini-2.5-flash",
11311125
})
11321126
mockStreamResult()
11331127
await consumeGenerator(handler)
11341128

1135-
// Gemini models should have extraBody.messages set (via buildOpenAiMessages)
1136-
expect(mockCreateOpenRouter).toHaveBeenCalledWith(
1137-
expect.objectContaining({
1138-
extraBody: expect.objectContaining({
1139-
messages: expect.any(Array),
1140-
}),
1141-
}),
1142-
)
1129+
// System prompt should be passed directly via streamText
1130+
const streamTextCall = mockStreamText.mock.calls[0][0]
1131+
expect(streamTextCall.system).toBe("test")
1132+
1133+
// No system-role message should be injected
1134+
const systemMsgs = streamTextCall.messages.filter((m: any) => m.role === "system")
1135+
expect(systemMsgs).toHaveLength(0)
1136+
})
1137+
1138+
it("does not use extraBody.messages for Gemini models outside caching set", async () => {
1139+
const handler = new OpenRouterHandler({
1140+
openRouterApiKey: "test-key",
1141+
openRouterModelId: "google/gemini-3-pro-preview",
1142+
})
1143+
mockStreamResult()
1144+
await consumeGenerator(handler)
1145+
1146+
// Non-caching Gemini models should go through the AI SDK natively
1147+
// (no extraBody.messages — reasoning_details are wired via providerOptions)
1148+
const callArgs = mockCreateOpenRouter.mock.calls[0]?.[0] ?? {}
1149+
const extraBody = callArgs.extraBody ?? {}
1150+
expect(extraBody.messages).toBeUndefined()
11431151
})
11441152

11451153
it("passes topP to completePrompt for R1 models", async () => {

src/api/providers/openrouter.ts

Lines changed: 7 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { Anthropic } from "@anthropic-ai/sdk"
2-
import OpenAI from "openai"
32
import { createOpenRouter } from "@openrouter/ai-sdk-provider"
43
import { streamText, generateText } from "ai"
54

@@ -9,7 +8,6 @@ import {
98
openRouterDefaultModelId,
109
openRouterDefaultModelInfo,
1110
OPENROUTER_DEFAULT_PROVIDER_NAME,
12-
OPEN_ROUTER_PROMPT_CACHING_MODELS,
1311
DEEP_SEEK_DEFAULT_TEMPERATURE,
1412
ApiProviderError,
1513
} from "@roo-code/types"
@@ -18,15 +16,7 @@ import { TelemetryService } from "@roo-code/telemetry"
1816
import type { ApiHandlerOptions } from "../../shared/api"
1917
import { calculateApiCostOpenAI } from "../../shared/cost"
2018

21-
import {
22-
convertToOpenAiMessages,
23-
sanitizeGeminiMessages,
24-
consolidateReasoningDetails,
25-
type ReasoningDetail,
26-
} from "../transform/openai-format"
27-
import { convertToR1Format } from "../transform/r1-format"
28-
import { addCacheBreakpoints as addAnthropicCacheBreakpoints } from "../transform/caching/anthropic"
29-
import { addCacheBreakpoints as addGeminiCacheBreakpoints } from "../transform/caching/gemini"
19+
import { type ReasoningDetail } from "../transform/openai-format"
3020
import { getModelParams } from "../transform/model-params"
3121
import { convertToAiSdkMessages, convertToolsForAiSdk, processAiSdkStreamPart } from "../transform/ai-sdk"
3222

@@ -77,17 +67,13 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
7767
private createOpenRouterProvider(options?: {
7868
reasoning?: { effort?: string; max_tokens?: number; exclude?: boolean }
7969
headers?: Record<string, string>
80-
openAiMessages?: OpenAI.Chat.ChatCompletionMessageParam[]
8170
}) {
8271
const apiKey = this.options.openRouterApiKey ?? "not-provided"
8372
const baseURL = this.options.openRouterBaseUrl || "https://openrouter.ai/api/v1"
8473
const extraBody: Record<string, unknown> = {}
8574
if (options?.reasoning) {
8675
extraBody.reasoning = options.reasoning
8776
}
88-
if (options?.openAiMessages) {
89-
extraBody.messages = options.openAiMessages
90-
}
9177
return createOpenRouter({
9278
apiKey,
9379
baseURL,
@@ -142,59 +128,6 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
142128
}
143129
}
144130

145-
private buildOpenAiMessages(
146-
systemPrompt: string,
147-
messages: Anthropic.Messages.MessageParam[],
148-
modelId: string,
149-
): OpenAI.Chat.ChatCompletionMessageParam[] | undefined {
150-
const isR1 = modelId.startsWith("deepseek/deepseek-r1") || modelId === "perplexity/sonar-reasoning"
151-
const isGemini = modelId.startsWith("google/gemini")
152-
const needsCaching = OPEN_ROUTER_PROMPT_CACHING_MODELS.has(modelId)
153-
if (!isR1 && !isGemini && !needsCaching) {
154-
return undefined
155-
}
156-
let openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[]
157-
if (isR1) {
158-
openAiMessages = convertToR1Format([{ role: "user", content: systemPrompt }, ...messages])
159-
} else {
160-
openAiMessages = [{ role: "system", content: systemPrompt }, ...convertToOpenAiMessages(messages)]
161-
}
162-
if (isGemini) {
163-
openAiMessages = sanitizeGeminiMessages(openAiMessages, modelId)
164-
openAiMessages = openAiMessages.map((msg) => {
165-
if (msg.role === "assistant") {
166-
const toolCalls = (msg as any).tool_calls as any[] | undefined
167-
const existingDetails = (msg as any).reasoning_details as any[] | undefined
168-
if (toolCalls && toolCalls.length > 0) {
169-
const hasEncrypted = existingDetails?.some((d) => d.type === "reasoning.encrypted") ?? false
170-
if (!hasEncrypted) {
171-
const fakeEncrypted = {
172-
type: "reasoning.encrypted",
173-
data: "skip_thought_signature_validator",
174-
id: toolCalls[0].id,
175-
format: "google-gemini-v1",
176-
index: 0,
177-
}
178-
return {
179-
...msg,
180-
reasoning_details: [...(existingDetails ?? []), fakeEncrypted],
181-
}
182-
}
183-
}
184-
}
185-
return msg
186-
})
187-
}
188-
if (needsCaching) {
189-
if (modelId.startsWith("google/")) {
190-
addGeminiCacheBreakpoints(systemPrompt, openAiMessages)
191-
} else {
192-
addAnthropicCacheBreakpoints(systemPrompt, openAiMessages)
193-
}
194-
}
195-
return openAiMessages
196-
}
197-
198131
override async *createMessage(
199132
systemPrompt: string,
200133
messages: Anthropic.Messages.MessageParam[],
@@ -216,12 +149,9 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
216149
? { "x-anthropic-beta": "fine-grained-tool-streaming-2025-05-14" }
217150
: undefined
218151

219-
const openAiMessages = this.buildOpenAiMessages(systemPrompt, messages, modelId)
220-
const openrouter = this.createOpenRouterProvider({ reasoning, headers, openAiMessages })
152+
const aiSdkMessages = convertToAiSdkMessages(messages)
221153

222-
const coreMessages = openAiMessages
223-
? convertToAiSdkMessages([{ role: "user", content: "." }])
224-
: convertToAiSdkMessages(messages)
154+
const openrouter = this.createOpenRouterProvider({ reasoning, headers })
225155

226156
const tools = convertToolsForAiSdk(metadata?.tools)
227157

@@ -250,8 +180,8 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
250180
try {
251181
const result = streamText({
252182
model: openrouter.chat(modelId),
253-
...(openAiMessages ? {} : { system: systemPrompt }),
254-
messages: coreMessages,
183+
system: systemPrompt,
184+
messages: aiSdkMessages,
255185
maxOutputTokens: maxTokens && maxTokens > 0 ? maxTokens : undefined,
256186
temperature,
257187
topP,
@@ -261,7 +191,7 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
261191
})
262192

263193
for await (const part of result.fullStream) {
264-
if (part.type === "reasoning-delta") {
194+
if (part.type === "reasoning-delta" && part.text !== "[REDACTED]") {
265195
accumulatedReasoningText += part.text
266196
}
267197
yield* processAiSdkStreamPart(part)
@@ -283,7 +213,7 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
283213
| undefined
284214

285215
if (providerReasoningDetails && providerReasoningDetails.length > 0) {
286-
this.currentReasoningDetails = consolidateReasoningDetails(providerReasoningDetails)
216+
this.currentReasoningDetails = providerReasoningDetails
287217
}
288218

289219
const usage = await result.usage

0 commit comments

Comments
 (0)