Skip to content

Conversation

@roomote
Copy link
Contributor

@roomote roomote bot commented Aug 6, 2025

This PR fixes the issue where LM Studio GPT-OSS models were showing raw model responses instead of properly formatted thinking blocks.

Problem

LM Studio GPT-OSS models use <thinking> tags for their reasoning content, but the current implementation only looks for <think> tags. This causes the thinking blocks to not be parsed correctly, resulting in the raw response being displayed.

Solution

  • Created a new MultiTagXmlMatcher utility that can handle multiple XML tag names
  • Updated the LM Studio handler to use MultiTagXmlMatcher with both ["think", "thinking"] tags
  • Added comprehensive tests for the new functionality

Testing

  • Added unit tests for MultiTagXmlMatcher covering various scenarios
  • Added specific tests in the LM Studio handler test suite for both <think> and <thinking> tag parsing
  • All tests pass successfully

Fixes #6750


Important

Adds support for <thinking> tags in LM Studio GPT-OSS models by introducing MultiTagXmlMatcher and updating LmStudioHandler.

  • Behavior:
    • Updates LmStudioHandler in lm-studio.ts to use MultiTagXmlMatcher for parsing both <think> and <thinking> tags.
    • Ensures reasoning content is correctly extracted and displayed.
  • Utilities:
    • Introduces MultiTagXmlMatcher in multi-tag-xml-matcher.ts to handle multiple XML tags.
  • Testing:
    • Adds unit tests for MultiTagXmlMatcher in multi-tag-xml-matcher.spec.ts.
    • Updates lmstudio.spec.ts to test <think> and <thinking> tag handling in LmStudioHandler.

This description was created by Ellipsis for 924a793. You can customize this summary. It will automatically update as commits are pushed.

…odels

- Created MultiTagXmlMatcher utility to handle multiple XML tag names
- Updated LM Studio handler to parse both <think> and <thinking> tags
- Added comprehensive tests for the new functionality
- Fixes #6750
@roomote roomote bot requested review from cte, jr and mrubens as code owners August 6, 2025 15:12
@dosubot dosubot bot added size:L This PR changes 100-499 lines, ignoring generated files. bug Something isn't working labels Aug 6, 2025
if (char === "<") {
// Emit any text before the tag
if (i > this.lastEmittedIndex) {
this.emit(false, this.buffer.substring(this.lastEmittedIndex, i))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When a '<' is encountered in TEXT state, the code unconditionally emits the preceding text. This can duplicate text that’s being collected as part of a matched tag. Consider emitting plain text only when depth is 0 (i.e. not inside a matching tag).

constructor(
private tagNames: string[],
private transform?: (chunks: XmlMatcherResult) => Result,
private position = 0,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The constructor parameter 'position' is declared but never used. Consider removing it if not needed.

Suggested change
private position = 0,

@FringeNet
Copy link

Does not use tags.

curl -X POST http://localhost:1234/v1/chat/completions   -H "Content-Type: application/json"   -d '{
    "model": "unsloth/gpt-oss-20b-GGUF/gpt-oss-20b-Q4_K_M.gguf",
    "messages": [{"role": "user", "content": "What is 1+1?"}],
    "reasoning_effort": "high"
  }'

Returns:

{
  "id": "chatcmpl-abxh306fwqfvcdb3zprm0d",
  "object": "chat.completion",
  "created": 1754493323,
  "model": "gpt-oss-20b@q4_k_m",
  "choices": [
    {
      "index": 0,
      "logprobs": null,
      "finish_reason": "stop",
      "message": {
        "role": "assistant",
        "content": "The user asks a simple math question: \"What is 1+1?\" The answer is 2. Provide the result and maybe mention basic addition. Probably just answer 2.\n\nWe should comply with policy: It's a direct factual answer. No disallowed content. We can provide short response.\\(1 + 1 = 2\\)."
      }
    }
  ],
  "usage": {
    "prompt_tokens": 74,
    "completion_tokens": 69,
    "total_tokens": 143
  },
  "stats": {},
  "system_fingerprint": "gpt-oss-20b@q4_k_m"

Copy link
Contributor Author

@roomote roomote bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reviewed my own code and found bugs I didn't know I wrote.


// If we're inside a matched tag, collect the content
if (this.depth > 0 && this.state === "TEXT" && i >= this.lastEmittedIndex) {
this.matchedContent += char
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this intentional? The character-by-character content collection in the matched tag could be inefficient for large responses. Consider buffering larger chunks for better performance:

Suggested change
this.matchedContent += char
// If we're inside a matched tag, collect the content
if (this.depth > 0 && this.state === "TEXT") {
const remainingContent = this.buffer.substring(i)
this.matchedContent += remainingContent
i = this.buffer.length - 1
}

this.lastEmittedIndex = i + 1
this.matchedContent = "" // Reset matched content
}
this.depth++
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we handle nested tags of the same type? The current implementation might not correctly parse cases like <think>outer <think>inner</think> outer</think>. The depth tracking seems to assume all matched tags are the same, but with multiple tag names, nested different tags would work while nested same tags might not.

const emptyBlocks = allResults.filter((r) => r.matched && r.data === "")
expect(emptyBlocks.length).toBeGreaterThan(0)
})
})
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we add tests for streaming behavior with partial chunks? For example, when a tag is split across chunks like receiving <thi in one chunk and nking>content</thinking> in the next. This would ensure the matcher handles real-world streaming scenarios correctly.

} else if (char !== "/" || this.currentTag.length > 0) {
this.currentTag += char
} else {
this.currentTag += char
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code appears to be duplicated. Could we simplify it to:

Suggested change
this.currentTag += char
} else {
this.currentTag += char
}

constructor(
private tagNames: string[],
private transform?: (chunks: XmlMatcherResult) => Result,
private position = 0,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the position parameter needed? It's accepted in the constructor but never used in the implementation, unlike the original XmlMatcher. If it's not needed, we could remove it to avoid confusion.

@@ -0,0 +1,141 @@
import { XmlMatcherResult } from "./xml-matcher"

/**
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we add JSDoc with usage examples? It would help developers understand how to use this with different tag combinations:

Suggested change
/**
/**
* A multi-tag XML matcher that can match multiple tag names.
* This is useful for handling different thinking tag formats from various models.
*
* @example
* // Match both <think> and <thinking> tags
* const matcher = new MultiTagXmlMatcher(['think', 'thinking'])
* const results = matcher.update('<think>Hello</think> world <thinking>Hi</thinking>')
*/

@hannesrudolph hannesrudolph added the Issue/PR - Triage New issue. Needs quick review to confirm validity and assign labels. label Aug 6, 2025
@daniel-lxs
Copy link
Member

Not a proper fix, closing for now

@daniel-lxs daniel-lxs closed this Aug 7, 2025
@github-project-automation github-project-automation bot moved this from New to Done in Roo Code Roadmap Aug 7, 2025
@github-project-automation github-project-automation bot moved this from Triage to Done in Roo Code Roadmap Aug 7, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working Issue/PR - Triage New issue. Needs quick review to confirm validity and assign labels. size:L This PR changes 100-499 lines, ignoring generated files.

Projects

Archived in project

Development

Successfully merging this pull request may close these issues.

[BUG] LM-Studio GPT-OSS Harmony Rendering

5 participants