-
Notifications
You must be signed in to change notification settings - Fork 2.6k
fix: parse XML thinking and tool_call blocks in OpenRouter responses #6634
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -18,6 +18,7 @@ import { addCacheBreakpoints as addAnthropicCacheBreakpoints } from "../transfor | |||||||||||||||||||||||
| import { addCacheBreakpoints as addGeminiCacheBreakpoints } from "../transform/caching/gemini" | ||||||||||||||||||||||||
| import type { OpenRouterReasoningParams } from "../transform/reasoning" | ||||||||||||||||||||||||
| import { getModelParams } from "../transform/model-params" | ||||||||||||||||||||||||
| import { XmlMatcher } from "../../utils/xml-matcher" | ||||||||||||||||||||||||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I notice we're importing |
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| import { getModels } from "./fetchers/modelCache" | ||||||||||||||||||||||||
| import { getModelEndpoints } from "./fetchers/modelEndpointCache" | ||||||||||||||||||||||||
|
|
@@ -137,6 +138,7 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH | |||||||||||||||||||||||
| const stream = await this.client.chat.completions.create(completionParams) | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| let lastUsage: CompletionUsage | undefined = undefined | ||||||||||||||||||||||||
| let buffer = "" | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| for await (const chunk of stream) { | ||||||||||||||||||||||||
| // OpenRouter returns an error object instead of the OpenAI SDK throwing an error. | ||||||||||||||||||||||||
|
|
@@ -153,14 +155,77 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH | |||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| if (delta?.content) { | ||||||||||||||||||||||||
| yield { type: "text", text: delta.content } | ||||||||||||||||||||||||
| buffer += delta.content | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| // Process complete XML blocks | ||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider refactoring the inline XML parsing logic (lines 158–216) into a shared utility (or use the imported XmlMatcher) for improved readability and maintainability. This comment was generated because it violated a code review rule: irule_tTqpIuNs8DV0QFGj. |
||||||||||||||||||||||||
| let processed = true | ||||||||||||||||||||||||
| while (processed) { | ||||||||||||||||||||||||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For large responses with many XML blocks, this while loop with multiple regex matches could impact performance. Have we considered the performance implications? Perhaps we could optimize by combining the regex patterns or using a different parsing approach? |
||||||||||||||||||||||||
| processed = false | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| // Check for complete <think> blocks | ||||||||||||||||||||||||
| const thinkMatch = buffer.match(/^(.*?)<think>([\s\S]*?)<\/think>(.*)$/s) | ||||||||||||||||||||||||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The regex patterns assume well-formed XML. What happens if the model sends malformed XML like |
||||||||||||||||||||||||
| if (thinkMatch) { | ||||||||||||||||||||||||
| const [, before, content, after] = thinkMatch | ||||||||||||||||||||||||
| if (before) { | ||||||||||||||||||||||||
| yield { type: "text", text: before } | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| yield { type: "reasoning", text: content } | ||||||||||||||||||||||||
| buffer = after | ||||||||||||||||||||||||
| processed = true | ||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| // Check for complete <tool_call> blocks | ||||||||||||||||||||||||
| const toolMatch = buffer.match(/^(.*?)<tool_call>([\s\S]*?)<\/tool_call>(.*)$/s) | ||||||||||||||||||||||||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The logic for handling
Suggested change
|
||||||||||||||||||||||||
| if (toolMatch) { | ||||||||||||||||||||||||
| const [, before, content, after] = toolMatch | ||||||||||||||||||||||||
| if (before) { | ||||||||||||||||||||||||
| yield { type: "text", text: before } | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| yield { type: "text", text: `[Tool Call]: ${content}` } | ||||||||||||||||||||||||
| buffer = after | ||||||||||||||||||||||||
| processed = true | ||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| // Check if we have an incomplete tag at the end | ||||||||||||||||||||||||
| const incompleteTag = buffer.match(/^(.*?)(<(?:think|tool_call)[^>]*(?:>[\s\S]*)?)?$/s) | ||||||||||||||||||||||||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This buffer management logic for incomplete tags is complex. Could we add some inline comments explaining the different scenarios? For example:
This would improve maintainability for future developers (including myself in 5 minutes). |
||||||||||||||||||||||||
| if (incompleteTag && incompleteTag[2]) { | ||||||||||||||||||||||||
| // We have an incomplete tag, yield the text before it and keep the tag in buffer | ||||||||||||||||||||||||
| const [, before, tag] = incompleteTag | ||||||||||||||||||||||||
| if (before) { | ||||||||||||||||||||||||
| yield { type: "text", text: before } | ||||||||||||||||||||||||
| buffer = tag | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| break | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| // No tags found or incomplete, yield all content except potential start of a tag | ||||||||||||||||||||||||
| const tagStart = buffer.lastIndexOf("<") | ||||||||||||||||||||||||
| if (tagStart === -1) { | ||||||||||||||||||||||||
| // No < found, yield all | ||||||||||||||||||||||||
| if (buffer) { | ||||||||||||||||||||||||
| yield { type: "text", text: buffer } | ||||||||||||||||||||||||
| buffer = "" | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| } else if (tagStart > 0) { | ||||||||||||||||||||||||
| // Yield content before the < | ||||||||||||||||||||||||
| yield { type: "text", text: buffer.substring(0, tagStart) } | ||||||||||||||||||||||||
| buffer = buffer.substring(tagStart) | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| if (chunk.usage) { | ||||||||||||||||||||||||
| lastUsage = chunk.usage | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| // Process any remaining content in the buffer | ||||||||||||||||||||||||
| if (buffer) { | ||||||||||||||||||||||||
| yield { type: "text", text: buffer } | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| if (lastUsage) { | ||||||||||||||||||||||||
| yield { | ||||||||||||||||||||||||
| type: "usage", | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove unused import 'XmlMatcher' if it's not needed to avoid confusion.
This comment was generated because it violated a code review rule: irule_Vw7dJWzvznOJagxS.