Skip to content
This repository was archived by the owner on May 15, 2026. It is now read-only.
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
18 changes: 17 additions & 1 deletion src/api/providers/openrouter.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,6 +10,7 @@ import delay from "delay"
// Add custom interface for OpenRouter params
type OpenRouterChatCompletionParams = OpenAI.Chat.ChatCompletionCreateParams & {
transforms?: string[]
include_reasoning?: boolean
}

// Add custom interface for OpenRouter usage chunk
Expand DownExpand Up@@ -110,14 +111,23 @@ export class OpenRouterHandler implements ApiHandler, SingleCompletionHandler {
maxTokens = 8_192
break
}

let temperature = 0
switch (this.getModel().id) {
case "deepseek/deepseek-r1":
// Recommended temperature for DeepSeek reasoning models
temperature = 0.6
}

// https://openrouter.ai/docs/transforms
let fullResponseText = ""
const stream = await this.client.chat.completions.create({
model: this.getModel().id,
max_tokens: maxTokens,
temperature: 0,
temperature: temperature,
messages: openAiMessages,
stream: true,
include_reasoning: true,
// This way, the transforms field will only be included in the parameters when openRouterUseMiddleOutTransform is true.
...(this.options.openRouterUseMiddleOutTransform && { transforms: ["middle-out"] }),
} as OpenRouterChatCompletionParams)
Expand All@@ -137,6 +147,12 @@ export class OpenRouterHandler implements ApiHandler, SingleCompletionHandler {
}

const delta = chunk.choices[0]?.delta
if ("reasoning" in delta && delta.reasoning) {
yield {
type: "reasoning",
text: delta.reasoning,
} as ApiStreamChunk
}
if (delta?.content) {
fullResponseText += delta.content
yield {
Expand Down
7 changes: 6 additions & 1 deletion src/api/transform/stream.ts
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
export type ApiStream = AsyncGenerator<ApiStreamChunk>
export type ApiStreamChunk = ApiStreamTextChunk | ApiStreamUsageChunk
export type ApiStreamChunk = ApiStreamTextChunk | ApiStreamUsageChunk | ApiStreamReasoningChunk

export interface ApiStreamTextChunk {
type: "text"
text: string
}

export interface ApiStreamReasoningChunk {
type: "reasoning"
text: string
}

export interface ApiStreamUsageChunk {
type: "usage"
inputTokens: number
Expand Down
7 changes: 6 additions & 1 deletion src/core/Cline.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -2219,7 +2219,7 @@ export class Cline {
}

/*
Seeing out of bounds is fine, it means that the next too call is being built up and ready to add to assistantMessageContent to present.
Seeing out of bounds is fine, it means that the next too call is being built up and ready to add to assistantMessageContent to present.
When you see the UI inactive during this, it means that a tool is breaking without presenting any UI. For example the write_to_file tool was breaking when relpath was undefined, and for invalid relpath it never presented UI.
*/
this.presentAssistantMessageLocked = false // this needs to be placed here, if not then calling this.presentAssistantMessage below would fail (sometimes) since it's locked
Expand DownExpand Up@@ -2391,9 +2391,14 @@ export class Cline {

const stream = this.attemptApiRequest(previousApiReqIndex) // yields only if the first chunk is successful, otherwise will allow the user to retry the request (most likely due to rate limit error, which gets thrown on the first chunk)
let assistantMessage = ""
let reasoningMessage = ""
try {
for await (const chunk of stream) {
switch (chunk.type) {
case "reasoning":
reasoningMessage += chunk.text
await this.say("reasoning", reasoningMessage, undefined, true)
break
case "usage":
inputTokens += chunk.inputTokens
outputTokens += chunk.outputTokens
Expand Down
2 changes: 2 additions & 0 deletions src/shared/ExtensionMessage.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -121,6 +121,7 @@ export interface ClineMessage {
text?: string
images?: string[]
partial?: boolean
reasoning?: string
}

export type ClineAsk =
Expand All@@ -142,6 +143,7 @@ export type ClineSay =
| "api_req_started"
| "api_req_finished"
| "text"
| "reasoning"
| "completion_result"
| "user_feedback"
| "user_feedback_diff"
Expand Down
17 changes: 17 additions & 0 deletions webview-ui/src/components/chat/ChatRow.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,6 +15,7 @@ import { vscode } from "../../utils/vscode"
import CodeAccordian, { removeLeadingNonAlphanumeric } from "../common/CodeAccordian"
import CodeBlock, { CODE_BLOCK_BG_COLOR } from "../common/CodeBlock"
import MarkdownBlock from "../common/MarkdownBlock"
import ReasoningBlock from "./ReasoningBlock"
import Thumbnails from "../common/Thumbnails"
import McpResourceRow from "../mcp/McpResourceRow"
import McpToolRow from "../mcp/McpToolRow"
Expand DownExpand Up@@ -79,6 +80,14 @@ export const ChatRowContent = ({
isStreaming,
}: ChatRowContentProps) => {
const { mcpServers, alwaysAllowMcp } = useExtensionState()
const [reasoningCollapsed, setReasoningCollapsed] = useState(false)

// Auto-collapse reasoning when new messages arrive
useEffect(() => {
if (!isLast && message.say === "reasoning") {
setReasoningCollapsed(true)
}
}, [isLast, message.say])
const [cost, apiReqCancelReason, apiReqStreamingFailedMessage] = useMemo(() => {
if (message.text != null && message.say === "api_req_started") {
const info: ClineApiReqInfo = JSON.parse(message.text)
Expand DownExpand Up@@ -472,6 +481,14 @@ export const ChatRowContent = ({
switch (message.type) {
case "say":
switch (message.say) {
case "reasoning":
return (
<ReasoningBlock
content={message.text || ""}
isCollapsed={reasoningCollapsed}
onToggleCollapse={() => setReasoningCollapsed(!reasoningCollapsed)}
/>
)
case "api_req_started":
return (
<>
Expand Down
70 changes: 70 additions & 0 deletions webview-ui/src/components/chat/ReasoningBlock.tsx
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
import React, { useEffect, useRef } from "react"
import { CODE_BLOCK_BG_COLOR } from "../common/CodeBlock"
import MarkdownBlock from "../common/MarkdownBlock"

interface ReasoningBlockProps {
content: string
isCollapsed?: boolean
onToggleCollapse?: () => void
autoHeight?: boolean
}

const ReasoningBlock: React.FC<ReasoningBlockProps> = ({
content,
isCollapsed = false,
onToggleCollapse,
autoHeight = false,
}) => {
const contentRef = useRef<HTMLDivElement>(null)

// Scroll to bottom when content updates
useEffect(() => {
if (contentRef.current && !isCollapsed) {
contentRef.current.scrollTop = contentRef.current.scrollHeight
}
}, [content, isCollapsed])

return (
<div
style={{
backgroundColor: CODE_BLOCK_BG_COLOR,
border: "1px solid var(--vscode-editorGroup-border)",
borderRadius: "3px",
overflow: "hidden",
}}>
<div
onClick={onToggleCollapse}
style={{
padding: "8px 12px",
cursor: "pointer",
userSelect: "none",
display: "flex",
alignItems: "center",
justifyContent: "space-between",
borderBottom: isCollapsed ? "none" : "1px solid var(--vscode-editorGroup-border)",
}}>
<span style={{ fontWeight: "bold" }}>Reasoning</span>
<span className={`codicon codicon-chevron-${isCollapsed ? "right" : "down"}`}></span>
</div>
{!isCollapsed && (
<div
ref={contentRef}
style={{
padding: "8px 12px",
maxHeight: autoHeight ? "none" : "160px",
overflowY: "auto",
}}>
<div
style={{
fontSize: "13px",
opacity: 0.9,
}}>
<MarkdownBlock markdown={content} />
</div>
</div>
)}
</div>
)
}

export default ReasoningBlock