Skip to content

Commit 06f98ca

Browse files
authored
Merge pull request RooCodeInc#1195 from RooVetGit/cte/openrouter-claude-thinking
Support Claude 3.7 Sonnet "Thinking" in OpenRouter
2 parents 17f0a5d + 69bcd2e commit 06f98ca

File tree

4 files changed

+59
-25
lines changed

4 files changed

+59
-25
lines changed

src/api/providers/openrouter.ts

Lines changed: 34 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ export class OpenRouterHandler implements ApiHandler, SingleCompletionHandler {
5252
...convertToOpenAiMessages(messages),
5353
]
5454

55+
const { id: modelId, info: modelInfo } = this.getModel()
56+
5557
// prompt caching: https://openrouter.ai/docs/prompt-caching
5658
// this is specifically for claude models (some models may 'support prompt caching' automatically without this)
5759
switch (true) {
@@ -95,10 +97,7 @@ export class OpenRouterHandler implements ApiHandler, SingleCompletionHandler {
9597
let topP: number | undefined = undefined
9698

9799
// Handle models based on deepseek-r1
98-
if (
99-
this.getModel().id.startsWith("deepseek/deepseek-r1") ||
100-
this.getModel().id === "perplexity/sonar-reasoning"
101-
) {
100+
if (modelId.startsWith("deepseek/deepseek-r1") || modelId === "perplexity/sonar-reasoning") {
102101
// Recommended temperature for DeepSeek reasoning models
103102
defaultTemperature = DEEP_SEEK_DEFAULT_TEMPERATURE
104103
// DeepSeek highly recommends using user instead of system role
@@ -107,24 +106,34 @@ export class OpenRouterHandler implements ApiHandler, SingleCompletionHandler {
107106
topP = 0.95
108107
}
109108

109+
let temperature = this.options.modelTemperature ?? defaultTemperature
110+
111+
// Anthropic "Thinking" models require a temperature of 1.0.
112+
if (modelInfo.thinking) {
113+
temperature = 1.0
114+
}
115+
110116
// https://openrouter.ai/docs/transforms
111117
let fullResponseText = ""
112-
const stream = await this.client.chat.completions.create({
113-
model: this.getModel().id,
114-
max_tokens: this.getModel().info.maxTokens,
115-
temperature: this.options.modelTemperature ?? defaultTemperature,
118+
119+
const completionParams: OpenRouterChatCompletionParams = {
120+
model: modelId,
121+
max_tokens: modelInfo.maxTokens,
122+
temperature,
116123
top_p: topP,
117124
messages: openAiMessages,
118125
stream: true,
119126
include_reasoning: true,
120127
// This way, the transforms field will only be included in the parameters when openRouterUseMiddleOutTransform is true.
121128
...(this.options.openRouterUseMiddleOutTransform && { transforms: ["middle-out"] }),
122-
} as OpenRouterChatCompletionParams)
129+
}
130+
131+
const stream = await this.client.chat.completions.create(completionParams)
123132

124133
let genId: string | undefined
125134

126135
for await (const chunk of stream as unknown as AsyncIterable<OpenAI.Chat.Completions.ChatCompletionChunk>) {
127-
// openrouter returns an error object instead of the openai sdk throwing an error
136+
// OpenRouter returns an error object instead of the OpenAI SDK throwing an error.
128137
if ("error" in chunk) {
129138
const error = chunk.error as { message?: string; code?: number }
130139
console.error(`OpenRouter API Error: ${error?.code} - ${error?.message}`)
@@ -136,19 +145,22 @@ export class OpenRouterHandler implements ApiHandler, SingleCompletionHandler {
136145
}
137146

138147
const delta = chunk.choices[0]?.delta
148+
139149
if ("reasoning" in delta && delta.reasoning) {
140150
yield {
141151
type: "reasoning",
142152
text: delta.reasoning,
143153
} as ApiStreamChunk
144154
}
155+
145156
if (delta?.content) {
146157
fullResponseText += delta.content
147158
yield {
148159
type: "text",
149160
text: delta.content,
150161
} as ApiStreamChunk
151162
}
163+
152164
// if (chunk.usage) {
153165
// yield {
154166
// type: "usage",
@@ -158,10 +170,12 @@ export class OpenRouterHandler implements ApiHandler, SingleCompletionHandler {
158170
// }
159171
}
160172

161-
// retry fetching generation details
173+
// Retry fetching generation details.
162174
let attempt = 0
175+
163176
while (attempt++ < 10) {
164177
await delay(200) // FIXME: necessary delay to ensure generation endpoint is ready
178+
165179
try {
166180
const response = await axios.get(`https://openrouter.ai/api/v1/generation?id=${genId}`, {
167181
headers: {
@@ -171,7 +185,7 @@ export class OpenRouterHandler implements ApiHandler, SingleCompletionHandler {
171185
})
172186

173187
const generation = response.data?.data
174-
console.log("OpenRouter generation details:", response.data)
188+
175189
yield {
176190
type: "usage",
177191
// cacheWriteTokens: 0,
@@ -182,20 +196,21 @@ export class OpenRouterHandler implements ApiHandler, SingleCompletionHandler {
182196
totalCost: generation?.total_cost || 0,
183197
fullResponseText,
184198
} as OpenRouterApiStreamUsageChunk
199+
185200
return
186201
} catch (error) {
187202
// ignore if fails
188203
console.error("Error fetching OpenRouter generation details:", error)
189204
}
190205
}
191206
}
192-
getModel(): { id: string; info: ModelInfo } {
207+
208+
getModel() {
193209
const modelId = this.options.openRouterModelId
194210
const modelInfo = this.options.openRouterModelInfo
195-
if (modelId && modelInfo) {
196-
return { id: modelId, info: modelInfo }
197-
}
198-
return { id: openRouterDefaultModelId, info: openRouterDefaultModelInfo }
211+
return modelId && modelInfo
212+
? { id: modelId, info: modelInfo }
213+
: { id: openRouterDefaultModelId, info: openRouterDefaultModelInfo }
199214
}
200215

201216
async completePrompt(prompt: string): Promise<string> {
@@ -218,6 +233,7 @@ export class OpenRouterHandler implements ApiHandler, SingleCompletionHandler {
218233
if (error instanceof Error) {
219234
throw new Error(`OpenRouter completion error: ${error.message}`)
220235
}
236+
221237
throw error
222238
}
223239
}
@@ -239,6 +255,7 @@ export async function getOpenRouterModels() {
239255
inputPrice: parseApiPrice(rawModel.pricing?.prompt),
240256
outputPrice: parseApiPrice(rawModel.pricing?.completion),
241257
description: rawModel.description,
258+
thinking: rawModel.id === "anthropic/claude-3.7-sonnet:thinking",
242259
}
243260

244261
// NOTE: this needs to be synced with api.ts/openrouter default model info.

src/shared/api.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,13 @@ export interface ModelInfo {
8989
cacheReadsPrice?: number
9090
description?: string
9191
reasoningEffort?: "low" | "medium" | "high"
92+
thinking?: boolean
93+
}
94+
95+
export const THINKING_BUDGET = {
96+
step: 1024,
97+
min: 1024,
98+
default: 8 * 1024,
9299
}
93100

94101
// Anthropic
@@ -106,6 +113,7 @@ export const anthropicModels = {
106113
outputPrice: 15.0, // $15 per million output tokens
107114
cacheWritesPrice: 3.75, // $3.75 per million tokens
108115
cacheReadsPrice: 0.3, // $0.30 per million tokens
116+
thinking: true,
109117
},
110118
"claude-3-5-sonnet-20241022": {
111119
maxTokens: 8192,

webview-ui/src/components/settings/ApiOptions.tsx

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import {
3333
unboundDefaultModelInfo,
3434
requestyDefaultModelId,
3535
requestyDefaultModelInfo,
36+
THINKING_BUDGET,
3637
} from "../../../../src/shared/api"
3738
import { ExtensionMessage } from "../../../../src/shared/ExtensionMessage"
3839

@@ -1270,12 +1271,20 @@ const ApiOptions = ({
12701271
</>
12711272
)}
12721273

1273-
{selectedProvider === "anthropic" && selectedModelId === "claude-3-7-sonnet-20250219" && (
1274+
{selectedModelInfo && selectedModelInfo.thinking && (
12741275
<div className="flex flex-col gap-2 mt-2">
12751276
<Checkbox
12761277
checked={!!anthropicThinkingBudget}
12771278
onChange={(checked) =>
1278-
setApiConfigurationField("anthropicThinking", checked ? 16_384 : undefined)
1279+
setApiConfigurationField(
1280+
"anthropicThinking",
1281+
checked
1282+
? Math.min(
1283+
THINKING_BUDGET.default,
1284+
selectedModelInfo.maxTokens ?? THINKING_BUDGET.default,
1285+
)
1286+
: undefined,
1287+
)
12791288
}>
12801289
Thinking?
12811290
</Checkbox>
@@ -1286,13 +1295,13 @@ const ApiOptions = ({
12861295
</div>
12871296
<div className="flex items-center gap-2">
12881297
<Slider
1289-
min={1024}
1290-
max={anthropicModels["claude-3-7-sonnet-20250219"].maxTokens - 1}
1291-
step={1024}
1298+
min={THINKING_BUDGET.min}
1299+
max={(selectedModelInfo.maxTokens ?? THINKING_BUDGET.default) - 1}
1300+
step={THINKING_BUDGET.step}
12921301
value={[anthropicThinkingBudget]}
12931302
onValueChange={(value) => setApiConfigurationField("anthropicThinking", value[0])}
12941303
/>
1295-
<div className="w-10">{anthropicThinkingBudget}</div>
1304+
<div className="w-12">{anthropicThinkingBudget}</div>
12961305
</div>
12971306
</>
12981307
)}

webview-ui/src/components/ui/slider.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ const Slider = React.forwardRef<
1111
ref={ref}
1212
className={cn("relative flex w-full touch-none select-none items-center", className)}
1313
{...props}>
14-
<SliderPrimitive.Track className="relative h-1 w-full grow overflow-hidden bg-primary/20">
15-
<SliderPrimitive.Range className="absolute h-full bg-primary" />
14+
<SliderPrimitive.Track className="relative w-full h-[8px] grow overflow-hidden bg-vscode-button-secondaryBackground border border-[#767676] dark:border-[#858585] rounded-sm">
15+
<SliderPrimitive.Range className="absolute h-full bg-vscode-button-background" />
1616
</SliderPrimitive.Track>
1717
<SliderPrimitive.Thumb className="block h-3 w-3 rounded-full border border-primary/50 bg-primary shadow transition-colors cursor-pointer focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50" />
1818
</SliderPrimitive.Root>

0 commit comments

Comments
 (0)