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

Commit f3181ce

Browse files
committed
fix(openrouter): re-add model-specific handling for AI SDK migration
- Re-add DeepSeek R1 format via convertToR1Format with extraBody override - Re-add Gemini sanitization (sanitizeGeminiMessages) and encrypted block injection - Re-add Gemini 2.5 Pro reasoning exclusion when not explicitly configured - Re-add prompt caching (addAnthropicCacheBreakpoints, addGeminiCacheBreakpoints) - Re-add Anthropic beta headers (x-anthropic-beta: fine-grained-tool-streaming) - Re-add reasoning_details handling via consolidateReasoningDetails - Fix topP parameter passthrough to streamText() and generateText() - Fix duplicate toolCallIdToName in ai-sdk.ts from rebase - Update tests for new model-specific behavior and add 9 new tests
1 parent 2a6bb25 commit f3181ce

3 files changed

Lines changed: 345 additions & 155 deletions

File tree

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

Lines changed: 215 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,25 @@ vitest.mock("../fetchers/modelCache", () => ({
108108
excludedTools: ["existing_excluded"],
109109
includedTools: ["existing_included"],
110110
},
111+
"google/gemini-2.5-pro": {
112+
maxTokens: 65536,
113+
contextWindow: 1048576,
114+
supportsImages: true,
115+
supportsPromptCache: true,
116+
inputPrice: 1.25,
117+
outputPrice: 10,
118+
description: "Gemini 2.5 Pro",
119+
thinking: true,
120+
},
121+
"google/gemini-2.5-flash": {
122+
maxTokens: 65536,
123+
contextWindow: 1048576,
124+
supportsImages: true,
125+
supportsPromptCache: true,
126+
inputPrice: 0.15,
127+
outputPrice: 0.6,
128+
description: "Gemini 2.5 Flash",
129+
},
111130
})
112131
}),
113132
getModelsFromCache: vitest.fn().mockReturnValue(null),
@@ -275,7 +294,6 @@ describe("OpenRouterHandler", () => {
275294
// Verify streamText was called with correct parameters
276295
expect(mockStreamText).toHaveBeenCalledWith(
277296
expect.objectContaining({
278-
system: systemPrompt,
279297
messages: expect.any(Array),
280298
maxOutputTokens: 8192,
281299
temperature: 0,
@@ -516,7 +534,7 @@ describe("OpenRouterHandler", () => {
516534
expect(chunks[3]).toEqual({ type: "tool_call_end", id: "call_1" })
517535
})
518536

519-
it("handles complete tool call events", async () => {
537+
it("ignores tool-call events (handled by tool-input-start/delta/end)", async () => {
520538
const handler = new OpenRouterHandler(mockOptions)
521539

522540
const mockFullStream = (async function* () {
@@ -541,12 +559,10 @@ describe("OpenRouterHandler", () => {
541559
chunks.push(chunk)
542560
}
543561

544-
expect(chunks[0]).toEqual({
545-
type: "tool_call",
546-
id: "call_1",
547-
name: "read_file",
548-
arguments: '{"path":"test.ts"}',
549-
})
562+
// tool-call is intentionally ignored by processAiSdkStreamPart,
563+
// only usage chunk should be present
564+
expect(chunks).toHaveLength(1)
565+
expect(chunks[0]).toMatchObject({ type: "usage" })
550566
})
551567

552568
it("handles API errors gracefully", async () => {
@@ -715,11 +731,13 @@ describe("OpenRouterHandler", () => {
715731
// consume
716732
}
717733

718-
// Verify that createOpenRouter was NOT called with extraBody
719-
expect(mockCreateOpenRouter).toHaveBeenCalledWith({
720-
apiKey: "test-key",
721-
baseURL: "https://openrouter.ai/api/v1",
722-
})
734+
// Verify that createOpenRouter was called with correct base config
735+
expect(mockCreateOpenRouter).toHaveBeenCalledWith(
736+
expect.objectContaining({
737+
apiKey: "test-key",
738+
baseURL: "https://openrouter.ai/api/v1",
739+
}),
740+
)
723741

724742
// Verify that providerOptions is undefined when no provider routing
725743
expect(mockStreamText).toHaveBeenCalledWith(
@@ -852,10 +870,12 @@ describe("OpenRouterHandler", () => {
852870
// consume
853871
}
854872

855-
expect(mockCreateOpenRouter).toHaveBeenCalledWith({
856-
apiKey: "custom-key",
857-
baseURL: "https://custom.openrouter.ai/api/v1",
858-
})
873+
expect(mockCreateOpenRouter).toHaveBeenCalledWith(
874+
expect.objectContaining({
875+
apiKey: "custom-key",
876+
baseURL: "https://custom.openrouter.ai/api/v1",
877+
}),
878+
)
859879
})
860880

861881
it("uses default base URL when not specified", async () => {
@@ -877,10 +897,12 @@ describe("OpenRouterHandler", () => {
877897
// consume
878898
}
879899

880-
expect(mockCreateOpenRouter).toHaveBeenCalledWith({
881-
apiKey: "test-key",
882-
baseURL: "https://openrouter.ai/api/v1",
883-
})
900+
expect(mockCreateOpenRouter).toHaveBeenCalledWith(
901+
expect.objectContaining({
902+
apiKey: "test-key",
903+
baseURL: "https://openrouter.ai/api/v1",
904+
}),
905+
)
884906
})
885907
})
886908

@@ -964,4 +986,176 @@ describe("OpenRouterHandler", () => {
964986
expect(reasoningDetails).toBeUndefined()
965987
})
966988
})
989+
990+
describe("model-specific handling", () => {
991+
const mockStreamResult = () => {
992+
const mockFullStream = (async function* () {
993+
yield { type: "text-delta", text: "response", id: "1" }
994+
})()
995+
mockStreamText.mockReturnValue({
996+
fullStream: mockFullStream,
997+
usage: Promise.resolve({ inputTokens: 10, outputTokens: 20, totalTokens: 30 }),
998+
totalUsage: Promise.resolve({ inputTokens: 10, outputTokens: 20, totalTokens: 30 }),
999+
})
1000+
}
1001+
1002+
const consumeGenerator = async (
1003+
handler: any,
1004+
system = "test",
1005+
msgs: any[] = [{ role: "user", content: "test" }],
1006+
) => {
1007+
const generator = handler.createMessage(system, msgs)
1008+
for await (const _ of generator) {
1009+
// consume
1010+
}
1011+
}
1012+
1013+
it("passes topP for DeepSeek R1 models", async () => {
1014+
const handler = new OpenRouterHandler({
1015+
openRouterApiKey: "test-key",
1016+
openRouterModelId: "deepseek/deepseek-r1",
1017+
})
1018+
mockStreamResult()
1019+
await consumeGenerator(handler)
1020+
1021+
expect(mockStreamText).toHaveBeenCalledWith(
1022+
expect.objectContaining({
1023+
topP: 0.95,
1024+
}),
1025+
)
1026+
})
1027+
1028+
it("does not pass topP for non-R1 models", async () => {
1029+
const handler = new OpenRouterHandler({
1030+
openRouterApiKey: "test-key",
1031+
openRouterModelId: "openai/gpt-4o",
1032+
})
1033+
mockStreamResult()
1034+
await consumeGenerator(handler)
1035+
1036+
expect(mockStreamText).toHaveBeenCalledWith(
1037+
expect.objectContaining({
1038+
topP: undefined,
1039+
}),
1040+
)
1041+
})
1042+
1043+
it("uses R1 format for DeepSeek R1 models (extraBody.messages)", async () => {
1044+
const handler = new OpenRouterHandler({
1045+
openRouterApiKey: "test-key",
1046+
openRouterModelId: "deepseek/deepseek-r1",
1047+
})
1048+
mockStreamResult()
1049+
await consumeGenerator(handler, "system prompt")
1050+
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+
)
1059+
1060+
// System prompt should NOT be passed to streamText (it is in extraBody.messages)
1061+
const streamTextCall = mockStreamText.mock.calls[0][0]
1062+
expect(streamTextCall.system).toBeUndefined()
1063+
})
1064+
1065+
it("applies Anthropic beta headers for Anthropic models", async () => {
1066+
const handler = new OpenRouterHandler({
1067+
openRouterApiKey: "test-key",
1068+
openRouterModelId: "anthropic/claude-sonnet-4",
1069+
})
1070+
mockStreamResult()
1071+
await consumeGenerator(handler)
1072+
1073+
expect(mockCreateOpenRouter).toHaveBeenCalledWith(
1074+
expect.objectContaining({
1075+
headers: { "x-anthropic-beta": "fine-grained-tool-streaming-2025-05-14" },
1076+
}),
1077+
)
1078+
})
1079+
1080+
it("does not apply Anthropic beta headers for non-Anthropic models", async () => {
1081+
const handler = new OpenRouterHandler({
1082+
openRouterApiKey: "test-key",
1083+
openRouterModelId: "openai/gpt-4o",
1084+
})
1085+
mockStreamResult()
1086+
await consumeGenerator(handler)
1087+
1088+
const call = mockCreateOpenRouter.mock.calls[0][0]
1089+
expect(call.headers).toBeUndefined()
1090+
})
1091+
1092+
it("applies prompt caching for Anthropic models in caching set", async () => {
1093+
const handler = new OpenRouterHandler({
1094+
openRouterApiKey: "test-key",
1095+
openRouterModelId: "anthropic/claude-sonnet-4",
1096+
})
1097+
mockStreamResult()
1098+
await consumeGenerator(handler)
1099+
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+
)
1108+
})
1109+
1110+
it("disables reasoning for Gemini 2.5 Pro when not explicitly configured", async () => {
1111+
const handler = new OpenRouterHandler({
1112+
openRouterApiKey: "test-key",
1113+
openRouterModelId: "google/gemini-2.5-pro",
1114+
})
1115+
mockStreamResult()
1116+
await consumeGenerator(handler)
1117+
1118+
expect(mockCreateOpenRouter).toHaveBeenCalledWith(
1119+
expect.objectContaining({
1120+
extraBody: expect.objectContaining({
1121+
reasoning: { exclude: true },
1122+
}),
1123+
}),
1124+
)
1125+
})
1126+
1127+
it("applies Gemini sanitization and encrypted block injection", async () => {
1128+
const handler = new OpenRouterHandler({
1129+
openRouterApiKey: "test-key",
1130+
openRouterModelId: "google/gemini-2.5-flash",
1131+
})
1132+
mockStreamResult()
1133+
await consumeGenerator(handler)
1134+
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+
)
1143+
})
1144+
1145+
it("passes topP to completePrompt for R1 models", async () => {
1146+
const handler = new OpenRouterHandler({
1147+
openRouterApiKey: "test-key",
1148+
openRouterModelId: "deepseek/deepseek-r1",
1149+
})
1150+
mockGenerateText.mockResolvedValue({ text: "completion" })
1151+
1152+
await handler.completePrompt("test prompt")
1153+
1154+
expect(mockGenerateText).toHaveBeenCalledWith(
1155+
expect.objectContaining({
1156+
topP: 0.95,
1157+
}),
1158+
)
1159+
})
1160+
})
9671161
})

0 commit comments

Comments
 (0)